summary refs log tree commit diff
path: root/src/libnm-glib-aux
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2021-10-01 23:05:04 +0200
committerMichael Biebl <biebl@debian.org>2021-10-01 23:05:04 +0200
commite74c568b07b50b97873fb4ee1d776dedefbd54d6 (patch)
tree3469f17ea9af91f7ff169b890633bda68b0cf76e /src/libnm-glib-aux
parentbfe522304da217296e2a61040f58e35ec5d6f3f2 (diff)
New upstream version 1.32.12 upstream/1.32.12
Diffstat (limited to 'src/libnm-glib-aux')
-rw-r--r--src/libnm-glib-aux/meson.build27
-rw-r--r--src/libnm-glib-aux/nm-c-list.h180
-rw-r--r--src/libnm-glib-aux/nm-dbus-aux.c405
-rw-r--r--src/libnm-glib-aux/nm-dbus-aux.h218
-rw-r--r--src/libnm-glib-aux/nm-dedup-multi.c1065
-rw-r--r--src/libnm-glib-aux/nm-dedup-multi.h447
-rw-r--r--src/libnm-glib-aux/nm-default-glib-i18n-lib.h21
-rw-r--r--src/libnm-glib-aux/nm-default-glib-i18n-prog.h21
-rw-r--r--src/libnm-glib-aux/nm-default-glib.h75
-rw-r--r--src/libnm-glib-aux/nm-enum-utils.c368
-rw-r--r--src/libnm-glib-aux/nm-enum-utils.h32
-rw-r--r--src/libnm-glib-aux/nm-errno.c169
-rw-r--r--src/libnm-glib-aux/nm-errno.h171
-rw-r--r--src/libnm-glib-aux/nm-gassert-patch.h76
-rw-r--r--src/libnm-glib-aux/nm-glib.h710
-rw-r--r--src/libnm-glib-aux/nm-hash-utils.c286
-rw-r--r--src/libnm-glib-aux/nm-hash-utils.h445
-rw-r--r--src/libnm-glib-aux/nm-io-utils.c630
-rw-r--r--src/libnm-glib-aux/nm-io-utils.h63
-rw-r--r--src/libnm-glib-aux/nm-jansson.h30
-rw-r--r--src/libnm-glib-aux/nm-json-aux.c286
-rw-r--r--src/libnm-glib-aux/nm-json-aux.h380
-rw-r--r--src/libnm-glib-aux/nm-keyfile-aux.c487
-rw-r--r--src/libnm-glib-aux/nm-keyfile-aux.h61
-rw-r--r--src/libnm-glib-aux/nm-logging-base.c79
-rw-r--r--src/libnm-glib-aux/nm-logging-base.h28
-rw-r--r--src/libnm-glib-aux/nm-logging-fwd.h310
-rw-r--r--src/libnm-glib-aux/nm-logging-syslog.h60
-rw-r--r--src/libnm-glib-aux/nm-macros-internal.h1916
-rw-r--r--src/libnm-glib-aux/nm-obj.h66
-rw-r--r--src/libnm-glib-aux/nm-random-utils.c299
-rw-r--r--src/libnm-glib-aux/nm-random-utils.h11
-rw-r--r--src/libnm-glib-aux/nm-ref-string.c190
-rw-r--r--src/libnm-glib-aux/nm-ref-string.h176
-rw-r--r--src/libnm-glib-aux/nm-secret-utils.c178
-rw-r--r--src/libnm-glib-aux/nm-secret-utils.h273
-rw-r--r--src/libnm-glib-aux/nm-shared-utils.c6439
-rw-r--r--src/libnm-glib-aux/nm-shared-utils.h2995
-rw-r--r--src/libnm-glib-aux/nm-str-buf.h514
-rw-r--r--src/libnm-glib-aux/nm-test-utils.h2860
-rw-r--r--src/libnm-glib-aux/nm-time-utils.c328
-rw-r--r--src/libnm-glib-aux/nm-time-utils.h47
-rw-r--r--src/libnm-glib-aux/nm-uuid.c430
-rw-r--r--src/libnm-glib-aux/nm-uuid.h100
-rw-r--r--src/libnm-glib-aux/nm-value-type.h239
-rw-r--r--src/libnm-glib-aux/tests/meson.build55
-rw-r--r--src/libnm-glib-aux/tests/test-json-aux.c165
-rw-r--r--src/libnm-glib-aux/tests/test-shared-general.c1425
48 files changed, 25836 insertions, 0 deletions
diff --git a/src/libnm-glib-aux/meson.build b/src/libnm-glib-aux/meson.build
new file mode 100644
index 00000000..4f13fd0b
--- /dev/null
+++ b/src/libnm-glib-aux/meson.build
@@ -0,0 +1,27 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+libnm_glib_aux = static_library(
+  'nm-glib-aux',
+  sources: files(
+    'nm-dbus-aux.c',
+    'nm-dedup-multi.c',
+    'nm-enum-utils.c',
+    'nm-errno.c',
+    'nm-hash-utils.c',
+    'nm-io-utils.c',
+    'nm-json-aux.c',
+    'nm-keyfile-aux.c',
+    'nm-logging-base.c',
+    'nm-random-utils.c',
+    'nm-ref-string.c',
+    'nm-secret-utils.c',
+    'nm-shared-utils.c',
+    'nm-time-utils.c',
+    'nm-uuid.c',
+  ),
+  include_directories: [
+    src_inc,
+    top_inc,
+  ],
+  dependencies: glib_dep,
+)
diff --git a/src/libnm-glib-aux/nm-c-list.h b/src/libnm-glib-aux/nm-c-list.h
new file mode 100644
index 00000000..6dd3ac72
--- /dev/null
+++ b/src/libnm-glib-aux/nm-c-list.h
@@ -0,0 +1,180 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#ifndef __NM_C_LIST_H__
+#define __NM_C_LIST_H__
+
+#include "c-list/src/c-list.h"
+
+/*****************************************************************************/
+
+#define nm_c_list_contains_entry(list, what, member)   \
+    ({                                                 \
+        typeof(what) _what = (what);                   \
+                                                       \
+        _what &&c_list_contains(list, &_what->member); \
+    })
+
+/*****************************************************************************/
+
+typedef struct {
+    CList lst;
+    void *data;
+} NMCListElem;
+
+static inline NMCListElem *
+nm_c_list_elem_new_stale(void *data)
+{
+    NMCListElem *elem;
+
+    elem       = g_slice_new(NMCListElem);
+    elem->data = data;
+    return elem;
+}
+
+static inline gboolean
+nm_c_list_elem_free_full(NMCListElem *elem, GDestroyNotify free_fcn)
+{
+    if (!elem)
+        return FALSE;
+    c_list_unlink_stale(&elem->lst);
+    if (free_fcn)
+        free_fcn(elem->data);
+    g_slice_free(NMCListElem, elem);
+    return TRUE;
+}
+
+static inline gboolean
+nm_c_list_elem_free(NMCListElem *elem)
+{
+    return nm_c_list_elem_free_full(elem, NULL);
+}
+
+static inline void *
+nm_c_list_elem_free_steal(NMCListElem *elem)
+{
+    gpointer data;
+
+    if (!elem)
+        return NULL;
+    data = elem->data;
+    nm_c_list_elem_free_full(elem, NULL);
+    return data;
+}
+
+static inline void
+nm_c_list_elem_free_all(CList *head, GDestroyNotify free_fcn)
+{
+    NMCListElem *elem;
+
+    while ((elem = c_list_first_entry(head, NMCListElem, lst)))
+        nm_c_list_elem_free_full(elem, free_fcn);
+}
+
+#define nm_c_list_elem_find_first(head, arg, predicate) \
+    ({                                                  \
+        CList *const _head   = (head);                  \
+        NMCListElem *_result = NULL;                    \
+        NMCListElem *_elem;                             \
+                                                        \
+        c_list_for_each_entry (_elem, _head, lst) {     \
+            void *const arg = _elem->data;              \
+                                                        \
+            if (predicate) {                            \
+                _result = _elem;                        \
+                break;                                  \
+            }                                           \
+        }                                               \
+        _result;                                        \
+    })
+
+/**
+ * nm_c_list_elem_find_first_ptr:
+ * @head: the @CList head of a list containing #NMCListElem elements.
+ *   Note that the head is not itself part of the list.
+ * @needle: the needle pointer.
+ *
+ * Iterates the list and returns the first #NMCListElem with the matching @needle,
+ * using pointer equality.
+ *
+ * Returns: the found list element or %NULL if not found.
+ */
+static inline NMCListElem *
+nm_c_list_elem_find_first_ptr(CList *head, gconstpointer needle)
+{
+    return nm_c_list_elem_find_first(head, x, x == needle);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_c_list_move_before:
+ * @lst: the list element to which @elem will be prepended.
+ * @elem: the list element to move.
+ *
+ * This unlinks @elem from the current list and linkes it before
+ * @lst. This is like c_list_link_before(), except that @elem must
+ * be initialized and linked. Note that @elem may be linked in @lst
+ * or in another list. In both cases it gets moved.
+ *
+ * Returns: %TRUE if there were any changes. %FALSE if elem was already
+ *   linked at the right place.
+ */
+static inline gboolean
+nm_c_list_move_before(CList *lst, CList *elem)
+{
+    nm_assert(lst);
+    nm_assert(elem);
+
+    if (lst != elem && lst->prev != elem) {
+        c_list_unlink_stale(elem);
+        c_list_link_before(lst, elem);
+        return TRUE;
+    }
+    return FALSE;
+}
+#define nm_c_list_move_tail(lst, elem) nm_c_list_move_before(lst, elem)
+
+/**
+ * nm_c_list_move_after:
+ * @lst: the list element to which @elem will be prepended.
+ * @elem: the list element to move.
+ *
+ * This unlinks @elem from the current list and linkes it after
+ * @lst. This is like c_list_link_after(), except that @elem must
+ * be initialized and linked. Note that @elem may be linked in @lst
+ * or in another list. In both cases it gets moved.
+ *
+ * Returns: %TRUE if there were any changes. %FALSE if elem was already
+ *   linked at the right place.
+ */
+static inline gboolean
+nm_c_list_move_after(CList *lst, CList *elem)
+{
+    nm_assert(lst);
+    nm_assert(elem);
+
+    if (lst != elem && lst->next != elem) {
+        c_list_unlink_stale(elem);
+        c_list_link_after(lst, elem);
+        return TRUE;
+    }
+    return FALSE;
+}
+#define nm_c_list_move_front(lst, elem) nm_c_list_move_after(lst, elem)
+
+#define nm_c_list_free_all(lst, type, member, destroy_fcn)         \
+    G_STMT_START                                                   \
+    {                                                              \
+        CList *const _lst = (lst);                                 \
+        type *       _elem;                                        \
+                                                                   \
+        while ((_elem = c_list_first_entry(_lst, type, member))) { \
+            destroy_fcn(_elem);                                    \
+        }                                                          \
+    }                                                              \
+    G_STMT_END
+
+#endif /* __NM_C_LIST_H__ */
diff --git a/src/libnm-glib-aux/nm-dbus-aux.c b/src/libnm-glib-aux/nm-dbus-aux.c
new file mode 100644
index 00000000..f4f53a7d
--- /dev/null
+++ b/src/libnm-glib-aux/nm-dbus-aux.c
@@ -0,0 +1,405 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-dbus-aux.h"
+
+/*****************************************************************************/
+
+static void
+_nm_dbus_connection_call_get_name_owner_cb(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    gs_unref_variant GVariant *ret           = NULL;
+    gs_free_error GError *             error = NULL;
+    const char *                       owner = NULL;
+    gpointer                           orig_user_data;
+    NMDBusConnectionCallGetNameOwnerCb callback;
+
+    nm_utils_user_data_unpack(user_data, &orig_user_data, &callback);
+
+    ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error);
+    if (ret)
+        g_variant_get(ret, "(&s)", &owner);
+
+    callback(owner, error, orig_user_data);
+}
+
+void
+nm_dbus_connection_call_get_name_owner(GDBusConnection *                  dbus_connection,
+                                       const char *                       service_name,
+                                       int                                timeout_msec,
+                                       GCancellable *                     cancellable,
+                                       NMDBusConnectionCallGetNameOwnerCb callback,
+                                       gpointer                           user_data)
+{
+    nm_assert(callback);
+
+    g_dbus_connection_call(dbus_connection,
+                           DBUS_SERVICE_DBUS,
+                           DBUS_PATH_DBUS,
+                           DBUS_INTERFACE_DBUS,
+                           "GetNameOwner",
+                           g_variant_new("(s)", service_name),
+                           G_VARIANT_TYPE("(s)"),
+                           G_DBUS_CALL_FLAGS_NONE,
+                           timeout_msec,
+                           cancellable,
+                           _nm_dbus_connection_call_get_name_owner_cb,
+                           nm_utils_user_data_pack(user_data, callback));
+}
+
+/*****************************************************************************/
+
+static void
+_nm_dbus_connection_call_default_cb(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    gs_unref_variant GVariant *ret      = NULL;
+    gs_free_error GError *        error = NULL;
+    gpointer                      orig_user_data;
+    NMDBusConnectionCallDefaultCb callback;
+
+    nm_utils_user_data_unpack(user_data, &orig_user_data, &callback);
+
+    ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error);
+
+    nm_assert((!!ret) != (!!error));
+
+    callback(ret, error, orig_user_data);
+}
+
+void
+nm_dbus_connection_call_get_all(GDBusConnection *             dbus_connection,
+                                const char *                  bus_name,
+                                const char *                  object_path,
+                                const char *                  interface_name,
+                                int                           timeout_msec,
+                                GCancellable *                cancellable,
+                                NMDBusConnectionCallDefaultCb callback,
+                                gpointer                      user_data)
+{
+    nm_assert(callback);
+
+    g_dbus_connection_call(dbus_connection,
+                           bus_name,
+                           object_path,
+                           DBUS_INTERFACE_PROPERTIES,
+                           "GetAll",
+                           g_variant_new("(s)", interface_name),
+                           G_VARIANT_TYPE("(a{sv})"),
+                           G_DBUS_CALL_FLAGS_NONE,
+                           timeout_msec,
+                           cancellable,
+                           _nm_dbus_connection_call_default_cb,
+                           nm_utils_user_data_pack(user_data, callback));
+}
+
+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)
+{
+    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);
+}
+
+/*****************************************************************************/
+
+static void
+_nm_dbus_connection_call_get_managed_objects_cb(GObject *     source,
+                                                GAsyncResult *res,
+                                                gpointer      user_data)
+{
+    gs_unref_variant GVariant *ret      = NULL;
+    gs_unref_variant GVariant *arg      = NULL;
+    gs_free_error GError *        error = NULL;
+    gpointer                      orig_user_data;
+    NMDBusConnectionCallDefaultCb callback;
+
+    nm_utils_user_data_unpack(user_data, &orig_user_data, &callback);
+
+    ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error);
+
+    nm_assert((!!ret) != (!!error));
+
+    if (ret) {
+        nm_assert(g_variant_is_of_type(ret, G_VARIANT_TYPE("(a{oa{sa{sv}}})")));
+        arg = g_variant_get_child_value(ret, 0);
+    }
+
+    callback(arg, error, orig_user_data);
+}
+
+void
+nm_dbus_connection_call_get_managed_objects(GDBusConnection *             dbus_connection,
+                                            const char *                  bus_name,
+                                            const char *                  object_path,
+                                            GDBusCallFlags                flags,
+                                            int                           timeout_msec,
+                                            GCancellable *                cancellable,
+                                            NMDBusConnectionCallDefaultCb callback,
+                                            gpointer                      user_data)
+{
+    nm_assert(callback);
+
+    g_dbus_connection_call(dbus_connection,
+                           bus_name,
+                           object_path,
+                           DBUS_INTERFACE_OBJECT_MANAGER,
+                           "GetManagedObjects",
+                           NULL,
+                           G_VARIANT_TYPE("(a{oa{sa{sv}}})"),
+                           flags,
+                           timeout_msec,
+                           cancellable,
+                           _nm_dbus_connection_call_get_managed_objects_cb,
+                           nm_utils_user_data_pack(user_data, callback));
+}
+
+/*****************************************************************************/
+
+static void
+_call_finish_cb(GObject *     source,
+                GAsyncResult *result,
+                gpointer      user_data,
+                gboolean      return_void,
+                gboolean      strip_dbus_error)
+{
+    gs_unref_object GTask *task      = user_data;
+    gs_unref_variant GVariant *ret   = NULL;
+    GError *                   error = NULL;
+
+    nm_assert(G_IS_DBUS_CONNECTION(source));
+    nm_assert(G_IS_TASK(user_data));
+
+    ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
+    if (!ret) {
+        if (strip_dbus_error)
+            g_dbus_error_strip_remote_error(error);
+        g_task_return_error(task, error);
+        return;
+    }
+
+    if (!return_void)
+        g_task_return_pointer(task, g_steal_pointer(&ret), (GDestroyNotify) g_variant_unref);
+    else {
+        nm_assert(g_variant_is_of_type(ret, G_VARIANT_TYPE("()")));
+        g_task_return_boolean(task, TRUE);
+    }
+}
+
+/**
+ * nm_dbus_connection_call_finish_void_cb:
+ *
+ * A default callback to pass as callback to g_dbus_connection_call().
+ *
+ * - user_data must be a GTask, whose reference will be consumed by the
+ *   callback.
+ * - the return GVariant must be a empty tuple "()".
+ * - the GTask is returned either with error or TRUE boolean.
+ */
+void
+nm_dbus_connection_call_finish_void_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    _call_finish_cb(source, result, user_data, TRUE, FALSE);
+}
+
+/**
+ * nm_dbus_connection_call_finish_void_strip_dbus_error_cb:
+ *
+ * Like nm_dbus_connection_call_finish_void_cb(). The difference
+ * is that on error this will first call g_dbus_error_strip_remote_error() on the error.
+ */
+void
+nm_dbus_connection_call_finish_void_strip_dbus_error_cb(GObject *     source,
+                                                        GAsyncResult *result,
+                                                        gpointer      user_data)
+{
+    _call_finish_cb(source, result, user_data, TRUE, TRUE);
+}
+
+/**
+ * nm_dbus_connection_call_finish_variant_cb:
+ *
+ * A default callback to pass as callback to g_dbus_connection_call().
+ *
+ * - user_data must be a GTask, whose reference will be consumed by the
+ *   callback.
+ * - the GTask is returned either with error or with a pointer containing the GVariant.
+ */
+void
+nm_dbus_connection_call_finish_variant_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    _call_finish_cb(source, result, user_data, FALSE, FALSE);
+}
+
+/**
+ * nm_dbus_connection_call_finish_variant_strip_dbus_error_cb:
+ *
+ * Like nm_dbus_connection_call_finish_variant_strip_dbus_error_cb(). The difference
+ * is that on error this will first call g_dbus_error_strip_remote_error() on the error.
+ */
+void
+nm_dbus_connection_call_finish_variant_strip_dbus_error_cb(GObject *     source,
+                                                           GAsyncResult *result,
+                                                           gpointer      user_data)
+{
+    _call_finish_cb(source, result, user_data, FALSE, TRUE);
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    char *              bus_name;
+    char *              object_path;
+    char *              interface_name;
+    char *              method_name;
+    GVariant *          parameters;
+    GDBusConnection *   connection;
+    const GVariantType *reply_type;
+    int                 timeout_msec;
+} CallAsyncInfo;
+
+static void
+call_async_info_destroy(CallAsyncInfo *info)
+{
+    g_free(info->bus_name);
+    g_free(info->object_path);
+    g_free(info->interface_name);
+    g_free(info->method_name);
+    g_variant_unref(info->parameters);
+    nm_g_object_unref(info->connection);
+    g_free(info);
+}
+
+static void
+call_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    gs_unref_object GTask *task  = user_data;
+    GError *               error = NULL;
+    GVariant *             ret;
+
+    ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
+    if (!ret) {
+        g_task_return_error(task, error);
+        return;
+    }
+
+    g_task_return_pointer(task, ret, (GDestroyNotify) g_variant_unref);
+}
+
+static void
+call_bus_get_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    gs_unref_object GTask *task = user_data;
+    GCancellable *         cancellable;
+    CallAsyncInfo *        info;
+    GError *               error = NULL;
+
+    info             = g_task_get_task_data(task);
+    info->connection = g_bus_get_finish(result, &error);
+    cancellable      = g_task_get_cancellable(task);
+
+    if (!info->connection) {
+        g_task_return_error(task, g_steal_pointer(&error));
+        return;
+    }
+
+    g_dbus_connection_call(info->connection,
+                           info->bus_name,
+                           info->object_path,
+                           info->interface_name,
+                           info->method_name,
+                           info->parameters,
+                           info->reply_type,
+                           G_DBUS_CALL_FLAGS_NONE,
+                           info->timeout_msec,
+                           cancellable,
+                           call_cb,
+                           g_steal_pointer(&task));
+}
+
+void
+nm_dbus_call(GBusType            bus_type,
+             const char *        bus_name,
+             const char *        object_path,
+             const char *        interface_name,
+             const char *        method_name,
+             GVariant *          parameters,
+             const GVariantType *reply_type,
+             GCancellable *      cancellable,
+             int                 timeout_msec,
+             GAsyncReadyCallback callback,
+             gpointer            user_data)
+{
+    GTask *        task;
+    CallAsyncInfo *info;
+
+    info  = g_new(CallAsyncInfo, 1);
+    *info = (CallAsyncInfo){
+        .bus_name       = g_strdup(bus_name),
+        .object_path    = g_strdup(object_path),
+        .interface_name = g_strdup(interface_name),
+        .method_name    = g_strdup(method_name),
+        .parameters     = g_variant_ref_sink(parameters),
+        .reply_type     = reply_type,
+        .timeout_msec   = timeout_msec,
+    };
+
+    task = nm_g_task_new(NULL, cancellable, nm_dbus_call, callback, user_data);
+    g_task_set_task_data(task, info, (GDestroyNotify) call_async_info_destroy);
+
+    g_bus_get(bus_type, cancellable, call_bus_get_cb, task);
+}
+
+GVariant *
+nm_dbus_call_finish(GAsyncResult *result, GError **error)
+{
+    nm_assert(nm_g_task_is_valid(result, NULL, nm_dbus_call));
+
+    return g_task_propagate_pointer(G_TASK(result), error);
+}
+
+/*****************************************************************************/
+
+gboolean
+_nm_dbus_error_is(GError *error, ...)
+{
+    gs_free char *dbus_error = NULL;
+    const char *  name;
+    va_list       ap;
+
+    dbus_error = g_dbus_error_get_remote_error(error);
+    if (!dbus_error)
+        return FALSE;
+
+    va_start(ap, error);
+    while ((name = va_arg(ap, const char *))) {
+        if (nm_streq(dbus_error, name)) {
+            va_end(ap);
+            return TRUE;
+        }
+    }
+    va_end(ap);
+
+    return FALSE;
+}
diff --git a/src/libnm-glib-aux/nm-dbus-aux.h b/src/libnm-glib-aux/nm-dbus-aux.h
new file mode 100644
index 00000000..65a91311
--- /dev/null
+++ b/src/libnm-glib-aux/nm-dbus-aux.h
@@ -0,0 +1,218 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019 Red Hat, Inc.
+ */
+
+#ifndef __NM_DBUS_AUX_H__
+#define __NM_DBUS_AUX_H__
+
+#include "libnm-std-aux/nm-dbus-compat.h"
+
+/*****************************************************************************/
+
+static inline gboolean
+nm_clear_g_dbus_connection_signal(GDBusConnection *dbus_connection, guint *id)
+{
+    guint v;
+
+    if (id && (v = *id)) {
+        *id = 0;
+        g_dbus_connection_signal_unsubscribe(dbus_connection, v);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+typedef void (*NMDBusConnectionCallDefaultCb)(GVariant *result, GError *error, gpointer user_data);
+
+/*****************************************************************************/
+
+static inline void
+nm_dbus_connection_call_start_service_by_name(GDBusConnection *   dbus_connection,
+                                              const char *        name,
+                                              int                 timeout_msec,
+                                              GCancellable *      cancellable,
+                                              GAsyncReadyCallback callback,
+                                              gpointer            user_data)
+{
+    g_dbus_connection_call(dbus_connection,
+                           DBUS_SERVICE_DBUS,
+                           DBUS_PATH_DBUS,
+                           DBUS_INTERFACE_DBUS,
+                           "StartServiceByName",
+                           g_variant_new("(su)", name, 0u),
+                           G_VARIANT_TYPE("(u)"),
+                           G_DBUS_CALL_FLAGS_NONE,
+                           timeout_msec,
+                           cancellable,
+                           callback,
+                           user_data);
+}
+
+/*****************************************************************************/
+
+static inline guint
+nm_dbus_connection_signal_subscribe_name_owner_changed(GDBusConnection *   dbus_connection,
+                                                       const char *        service_name,
+                                                       GDBusSignalCallback callback,
+                                                       gpointer            user_data,
+                                                       GDestroyNotify      user_data_free_func)
+
+{
+    return g_dbus_connection_signal_subscribe(dbus_connection,
+                                              DBUS_SERVICE_DBUS,
+                                              DBUS_INTERFACE_DBUS,
+                                              "NameOwnerChanged",
+                                              DBUS_PATH_DBUS,
+                                              service_name,
+                                              G_DBUS_SIGNAL_FLAGS_NONE,
+                                              callback,
+                                              user_data,
+                                              user_data_free_func);
+}
+
+typedef void (*NMDBusConnectionCallGetNameOwnerCb)(const char *name_owner,
+                                                   GError *    error,
+                                                   gpointer    user_data);
+
+void nm_dbus_connection_call_get_name_owner(GDBusConnection *                  dbus_connection,
+                                            const char *                       service_name,
+                                            int                                timeout_msec,
+                                            GCancellable *                     cancellable,
+                                            NMDBusConnectionCallGetNameOwnerCb callback,
+                                            gpointer                           user_data);
+
+static inline guint
+nm_dbus_connection_signal_subscribe_properties_changed(GDBusConnection *   dbus_connection,
+                                                       const char *        bus_name,
+                                                       const char *        object_path,
+                                                       const char *        interface_name,
+                                                       GDBusSignalCallback callback,
+                                                       gpointer            user_data,
+                                                       GDestroyNotify      user_data_free_func)
+
+{
+    nm_assert(bus_name);
+
+    /* it seems that using a non-unique name causes problems that we get signals
+     * also from unrelated senders. Usually, you are anyway monitoring the name-owner,
+     * so you should have the unique name at hand.
+     *
+     * If not, investigate this, ensure that it works, and lift this restriction. */
+    nm_assert(g_dbus_is_unique_name(bus_name));
+
+    return g_dbus_connection_signal_subscribe(dbus_connection,
+                                              bus_name,
+                                              DBUS_INTERFACE_PROPERTIES,
+                                              "PropertiesChanged",
+                                              object_path,
+                                              interface_name,
+                                              G_DBUS_SIGNAL_FLAGS_NONE,
+                                              callback,
+                                              user_data,
+                                              user_data_free_func);
+}
+
+void nm_dbus_connection_call_get_all(GDBusConnection *             dbus_connection,
+                                     const char *                  bus_name,
+                                     const char *                  object_path,
+                                     const char *                  interface_name,
+                                     int                           timeout_msec,
+                                     GCancellable *                cancellable,
+                                     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(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,
+                                              DBUS_INTERFACE_OBJECT_MANAGER,
+                                              signal_name,
+                                              object_path,
+                                              NULL,
+                                              G_DBUS_SIGNAL_FLAGS_NONE,
+                                              callback,
+                                              user_data,
+                                              user_data_free_func);
+}
+
+void nm_dbus_connection_call_get_managed_objects(GDBusConnection *             dbus_connection,
+                                                 const char *                  bus_name,
+                                                 const char *                  object_path,
+                                                 GDBusCallFlags                flags,
+                                                 int                           timeout_msec,
+                                                 GCancellable *                cancellable,
+                                                 NMDBusConnectionCallDefaultCb callback,
+                                                 gpointer                      user_data);
+
+/*****************************************************************************/
+
+void
+nm_dbus_connection_call_finish_void_cb(GObject *source, GAsyncResult *result, gpointer user_data);
+
+void nm_dbus_connection_call_finish_void_strip_dbus_error_cb(GObject *     source,
+                                                             GAsyncResult *result,
+                                                             gpointer      user_data);
+
+void nm_dbus_connection_call_finish_variant_cb(GObject *     source,
+                                               GAsyncResult *result,
+                                               gpointer      user_data);
+
+void nm_dbus_connection_call_finish_variant_strip_dbus_error_cb(GObject *     source,
+                                                                GAsyncResult *result,
+                                                                gpointer      user_data);
+
+/*****************************************************************************/
+
+void nm_dbus_call(GBusType            bus_type,
+                  const char *        bus_name,
+                  const char *        object_path,
+                  const char *        interface_name,
+                  const char *        method_name,
+                  GVariant *          parameters,
+                  const GVariantType *reply_type,
+                  GCancellable *      cancellable,
+                  int                 timeout_msec,
+                  GAsyncReadyCallback callback,
+                  gpointer            user_data);
+
+GVariant *nm_dbus_call_finish(GAsyncResult *result, GError **error);
+
+/*****************************************************************************/
+
+gboolean _nm_dbus_error_is(GError *error, ...) G_GNUC_NULL_TERMINATED;
+
+#define nm_dbus_error_is(error, ...)                           \
+    ({                                                         \
+        GError *const _error = (error);                        \
+                                                               \
+        _error &&_nm_dbus_error_is(_error, __VA_ARGS__, NULL); \
+    })
+
+#define NM_DBUS_ERROR_NAME_UNKNOWN_METHOD "org.freedesktop.DBus.Error.UnknownMethod"
+
+/*****************************************************************************/
+
+#endif /* __NM_DBUS_AUX_H__ */
diff --git a/src/libnm-glib-aux/nm-dedup-multi.c b/src/libnm-glib-aux/nm-dedup-multi.c
new file mode 100644
index 00000000..f77bb3cd
--- /dev/null
+++ b/src/libnm-glib-aux/nm-dedup-multi.c
@@ -0,0 +1,1065 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-dedup-multi.h"
+
+#include "nm-hash-utils.h"
+#include "nm-c-list.h"
+
+/*****************************************************************************/
+
+typedef struct {
+    /* the stack-allocated lookup entry. It has a compatible
+     * memory layout with NMDedupMultiEntry and NMDedupMultiHeadEntry.
+     *
+     * It is recognizable by having lst_entries_sentinel.next set to NULL.
+     * Contrary to the other entries, which have lst_entries.next
+     * always non-NULL.
+     * */
+    CList                      lst_entries_sentinel;
+    const NMDedupMultiObj *    obj;
+    const NMDedupMultiIdxType *idx_type;
+    bool                       lookup_head;
+} LookupEntry;
+
+struct _NMDedupMultiIndex {
+    int         ref_count;
+    GHashTable *idx_entries;
+    GHashTable *idx_objs;
+};
+
+/*****************************************************************************/
+
+static void
+ASSERT_idx_type(const NMDedupMultiIdxType *idx_type)
+{
+    nm_assert(idx_type);
+#if NM_MORE_ASSERTS > 10
+    nm_assert(idx_type->klass);
+    nm_assert(idx_type->klass->idx_obj_id_hash_update);
+    nm_assert(idx_type->klass->idx_obj_id_equal);
+    nm_assert(!!idx_type->klass->idx_obj_partition_hash_update
+              == !!idx_type->klass->idx_obj_partition_equal);
+    nm_assert(idx_type->lst_idx_head.next);
+#endif
+}
+
+void
+nm_dedup_multi_idx_type_init(NMDedupMultiIdxType *idx_type, const NMDedupMultiIdxTypeClass *klass)
+{
+    nm_assert(idx_type);
+    nm_assert(klass);
+
+    *idx_type = (NMDedupMultiIdxType){
+        .klass        = klass,
+        .lst_idx_head = C_LIST_INIT(idx_type->lst_idx_head),
+    };
+
+    ASSERT_idx_type(idx_type);
+}
+
+/*****************************************************************************/
+
+static NMDedupMultiEntry *
+_entry_lookup_obj(const NMDedupMultiIndex *  self,
+                  const NMDedupMultiIdxType *idx_type,
+                  const NMDedupMultiObj *    obj)
+{
+    const LookupEntry stack_entry = {
+        .obj         = obj,
+        .idx_type    = idx_type,
+        .lookup_head = FALSE,
+    };
+
+    ASSERT_idx_type(idx_type);
+    return g_hash_table_lookup(self->idx_entries, &stack_entry);
+}
+
+static NMDedupMultiHeadEntry *
+_entry_lookup_head(const NMDedupMultiIndex *  self,
+                   const NMDedupMultiIdxType *idx_type,
+                   const NMDedupMultiObj *    obj)
+{
+    NMDedupMultiHeadEntry *head_entry;
+    const LookupEntry      stack_entry = {
+        .obj         = obj,
+        .idx_type    = idx_type,
+        .lookup_head = TRUE,
+    };
+
+    ASSERT_idx_type(idx_type);
+
+    if (!idx_type->klass->idx_obj_partition_equal) {
+        if (c_list_is_empty(&idx_type->lst_idx_head))
+            head_entry = NULL;
+        else {
+            nm_assert(c_list_length(&idx_type->lst_idx_head) == 1);
+            head_entry = c_list_entry(idx_type->lst_idx_head.next, NMDedupMultiHeadEntry, lst_idx);
+        }
+        nm_assert(head_entry == g_hash_table_lookup(self->idx_entries, &stack_entry));
+        return head_entry;
+    }
+
+    return g_hash_table_lookup(self->idx_entries, &stack_entry);
+}
+
+static void
+_entry_unpack(const NMDedupMultiEntry *   entry,
+              const NMDedupMultiIdxType **out_idx_type,
+              const NMDedupMultiObj **    out_obj,
+              gboolean *                  out_lookup_head)
+{
+    const NMDedupMultiHeadEntry *head_entry;
+    const LookupEntry *          lookup_entry;
+
+    nm_assert(entry);
+
+    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(LookupEntry, lst_entries_sentinel)
+                         == G_STRUCT_OFFSET(NMDedupMultiEntry, lst_entries));
+    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(NMDedupMultiEntry, lst_entries)
+                         == G_STRUCT_OFFSET(NMDedupMultiHeadEntry, lst_entries_head));
+    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(NMDedupMultiEntry, obj)
+                         == G_STRUCT_OFFSET(NMDedupMultiHeadEntry, idx_type));
+    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(NMDedupMultiEntry, is_head)
+                         == G_STRUCT_OFFSET(NMDedupMultiHeadEntry, is_head));
+
+    if (!entry->lst_entries.next) {
+        /* the entry is stack-allocated by _entry_lookup(). */
+        lookup_entry     = (LookupEntry *) entry;
+        *out_obj         = lookup_entry->obj;
+        *out_idx_type    = lookup_entry->idx_type;
+        *out_lookup_head = lookup_entry->lookup_head;
+    } else if (entry->is_head) {
+        head_entry = (NMDedupMultiHeadEntry *) entry;
+        nm_assert(!c_list_is_empty(&head_entry->lst_entries_head));
+        *out_obj =
+            c_list_entry(head_entry->lst_entries_head.next, NMDedupMultiEntry, lst_entries)->obj;
+        *out_idx_type    = head_entry->idx_type;
+        *out_lookup_head = TRUE;
+    } else {
+        *out_obj         = entry->obj;
+        *out_idx_type    = entry->head->idx_type;
+        *out_lookup_head = FALSE;
+    }
+
+    nm_assert(NM_IN_SET(*out_lookup_head, FALSE, TRUE));
+    ASSERT_idx_type(*out_idx_type);
+
+    /* for lookup of the head, we allow to omit object, but only
+     * if the idx_type does not partition the objects. Otherwise, we
+     * require a obj to compare. */
+    nm_assert(!*out_lookup_head || (*out_obj || !(*out_idx_type)->klass->idx_obj_partition_equal));
+
+    /* lookup of the object requires always an object. */
+    nm_assert(*out_lookup_head || *out_obj);
+}
+
+static guint
+_dict_idx_entries_hash(const NMDedupMultiEntry *entry)
+{
+    const NMDedupMultiIdxType *idx_type;
+    const NMDedupMultiObj *    obj;
+    gboolean                   lookup_head;
+    NMHashState                h;
+
+    _entry_unpack(entry, &idx_type, &obj, &lookup_head);
+
+    nm_hash_init(&h, 1914869417u);
+    if (idx_type->klass->idx_obj_partition_hash_update) {
+        nm_assert(obj);
+        idx_type->klass->idx_obj_partition_hash_update(idx_type, obj, &h);
+    }
+
+    if (!lookup_head)
+        idx_type->klass->idx_obj_id_hash_update(idx_type, obj, &h);
+
+    nm_hash_update_val(&h, idx_type);
+    return nm_hash_complete(&h);
+}
+
+static gboolean
+_dict_idx_entries_equal(const NMDedupMultiEntry *entry_a, const NMDedupMultiEntry *entry_b)
+{
+    const NMDedupMultiIdxType *idx_type_a, *idx_type_b;
+    const NMDedupMultiObj *    obj_a, *obj_b;
+    gboolean                   lookup_head_a, lookup_head_b;
+
+    _entry_unpack(entry_a, &idx_type_a, &obj_a, &lookup_head_a);
+    _entry_unpack(entry_b, &idx_type_b, &obj_b, &lookup_head_b);
+
+    if (idx_type_a != idx_type_b || lookup_head_a != lookup_head_b)
+        return FALSE;
+    if (!nm_dedup_multi_idx_type_partition_equal(idx_type_a, obj_a, obj_b))
+        return FALSE;
+    if (!lookup_head_a && !nm_dedup_multi_idx_type_id_equal(idx_type_a, obj_a, obj_b))
+        return FALSE;
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+_add(NMDedupMultiIndex *       self,
+     NMDedupMultiIdxType *     idx_type,
+     const NMDedupMultiObj *   obj,
+     NMDedupMultiEntry *       entry,
+     NMDedupMultiIdxMode       mode,
+     const NMDedupMultiEntry * entry_order,
+     NMDedupMultiHeadEntry *   head_existing,
+     const NMDedupMultiEntry **out_entry,
+     const NMDedupMultiObj **  out_obj_old)
+{
+    NMDedupMultiHeadEntry *head_entry;
+    const NMDedupMultiObj *obj_new, *obj_old;
+    gboolean               add_head_entry = FALSE;
+
+    nm_assert(self);
+    ASSERT_idx_type(idx_type);
+    nm_assert(obj);
+    nm_assert(NM_IN_SET(mode,
+                        NM_DEDUP_MULTI_IDX_MODE_PREPEND,
+                        NM_DEDUP_MULTI_IDX_MODE_PREPEND_FORCE,
+                        NM_DEDUP_MULTI_IDX_MODE_APPEND,
+                        NM_DEDUP_MULTI_IDX_MODE_APPEND_FORCE));
+    nm_assert(!head_existing || head_existing->idx_type == idx_type);
+    nm_assert(({
+        const NMDedupMultiHeadEntry *_h;
+        gboolean                     _ok = TRUE;
+        if (head_existing) {
+            _h = nm_dedup_multi_index_lookup_head(self, idx_type, obj);
+            if (head_existing == NM_DEDUP_MULTI_HEAD_ENTRY_MISSING)
+                _ok = (_h == NULL);
+            else
+                _ok = (_h == head_existing);
+        }
+        _ok;
+    }));
+
+    if (entry) {
+        gboolean changed = FALSE;
+
+        nm_dedup_multi_entry_set_dirty(entry, FALSE);
+
+        nm_assert(!head_existing || entry->head == head_existing);
+        nm_assert(!entry_order || entry_order->head == entry->head);
+        nm_assert(!entry_order || c_list_contains(&entry->lst_entries, &entry_order->lst_entries));
+        nm_assert(!entry_order || c_list_contains(&entry_order->lst_entries, &entry->lst_entries));
+
+        switch (mode) {
+        case NM_DEDUP_MULTI_IDX_MODE_PREPEND_FORCE:
+            if (entry_order) {
+                if (nm_c_list_move_before((CList *) &entry_order->lst_entries, &entry->lst_entries))
+                    changed = TRUE;
+            } else {
+                if (nm_c_list_move_front((CList *) &entry->head->lst_entries_head,
+                                         &entry->lst_entries))
+                    changed = TRUE;
+            }
+            break;
+        case NM_DEDUP_MULTI_IDX_MODE_APPEND_FORCE:
+            if (entry_order) {
+                if (nm_c_list_move_after((CList *) &entry_order->lst_entries, &entry->lst_entries))
+                    changed = TRUE;
+            } else {
+                if (nm_c_list_move_tail((CList *) &entry->head->lst_entries_head,
+                                        &entry->lst_entries))
+                    changed = TRUE;
+            }
+            break;
+        case NM_DEDUP_MULTI_IDX_MODE_PREPEND:
+        case NM_DEDUP_MULTI_IDX_MODE_APPEND:
+            break;
+        };
+
+        nm_assert(obj->klass == ((const NMDedupMultiObj *) entry->obj)->klass);
+        if (obj == entry->obj || obj->klass->obj_full_equal(obj, entry->obj)) {
+            NM_SET_OUT(out_entry, entry);
+            NM_SET_OUT(out_obj_old, nm_dedup_multi_obj_ref(entry->obj));
+            return changed;
+        }
+
+        obj_new = nm_dedup_multi_index_obj_intern(self, obj);
+
+        obj_old    = entry->obj;
+        entry->obj = obj_new;
+
+        NM_SET_OUT(out_entry, entry);
+        if (out_obj_old)
+            *out_obj_old = obj_old;
+        else
+            nm_dedup_multi_obj_unref(obj_old);
+        return TRUE;
+    }
+
+    if (idx_type->klass->idx_obj_partitionable
+        && !idx_type->klass->idx_obj_partitionable(idx_type, obj)) {
+        /* this object cannot be partitioned by this idx_type. */
+        nm_assert(!head_existing || head_existing == NM_DEDUP_MULTI_HEAD_ENTRY_MISSING);
+        NM_SET_OUT(out_entry, NULL);
+        NM_SET_OUT(out_obj_old, NULL);
+        return FALSE;
+    }
+
+    obj_new = nm_dedup_multi_index_obj_intern(self, obj);
+
+    if (!head_existing)
+        head_entry = _entry_lookup_head(self, idx_type, obj_new);
+    else if (head_existing == NM_DEDUP_MULTI_HEAD_ENTRY_MISSING)
+        head_entry = NULL;
+    else
+        head_entry = head_existing;
+
+    if (!head_entry) {
+        head_entry           = g_slice_new0(NMDedupMultiHeadEntry);
+        head_entry->is_head  = TRUE;
+        head_entry->idx_type = idx_type;
+        c_list_init(&head_entry->lst_entries_head);
+        c_list_link_tail(&idx_type->lst_idx_head, &head_entry->lst_idx);
+        add_head_entry = TRUE;
+    } else
+        nm_assert(c_list_contains(&idx_type->lst_idx_head, &head_entry->lst_idx));
+
+    if (entry_order) {
+        nm_assert(!add_head_entry);
+        nm_assert(entry_order->head == head_entry);
+        nm_assert(c_list_contains(&head_entry->lst_entries_head, &entry_order->lst_entries));
+        nm_assert(c_list_contains(&entry_order->lst_entries, &head_entry->lst_entries_head));
+    }
+
+    entry       = g_slice_new0(NMDedupMultiEntry);
+    entry->obj  = obj_new;
+    entry->head = head_entry;
+
+    switch (mode) {
+    case NM_DEDUP_MULTI_IDX_MODE_PREPEND:
+    case NM_DEDUP_MULTI_IDX_MODE_PREPEND_FORCE:
+        if (entry_order)
+            c_list_link_before((CList *) &entry_order->lst_entries, &entry->lst_entries);
+        else
+            c_list_link_front(&head_entry->lst_entries_head, &entry->lst_entries);
+        break;
+    default:
+        if (entry_order)
+            c_list_link_after((CList *) &entry_order->lst_entries, &entry->lst_entries);
+        else
+            c_list_link_tail(&head_entry->lst_entries_head, &entry->lst_entries);
+        break;
+    };
+
+    idx_type->len++;
+    head_entry->len++;
+
+    if (add_head_entry && !g_hash_table_add(self->idx_entries, head_entry))
+        nm_assert_not_reached();
+
+    if (!g_hash_table_add(self->idx_entries, entry))
+        nm_assert_not_reached();
+
+    NM_SET_OUT(out_entry, entry);
+    NM_SET_OUT(out_obj_old, NULL);
+    return TRUE;
+}
+
+gboolean
+nm_dedup_multi_index_add(NMDedupMultiIndex *                        self,
+                         NMDedupMultiIdxType *                      idx_type,
+                         /*const NMDedupMultiObj * */ gconstpointer obj,
+                         NMDedupMultiIdxMode                        mode,
+                         const NMDedupMultiEntry **                 out_entry,
+                         /* const NMDedupMultiObj ** */ gpointer    out_obj_old)
+{
+    NMDedupMultiEntry *entry;
+
+    g_return_val_if_fail(self, FALSE);
+    g_return_val_if_fail(idx_type, FALSE);
+    g_return_val_if_fail(obj, FALSE);
+    g_return_val_if_fail(NM_IN_SET(mode,
+                                   NM_DEDUP_MULTI_IDX_MODE_PREPEND,
+                                   NM_DEDUP_MULTI_IDX_MODE_PREPEND_FORCE,
+                                   NM_DEDUP_MULTI_IDX_MODE_APPEND,
+                                   NM_DEDUP_MULTI_IDX_MODE_APPEND_FORCE),
+                         FALSE);
+
+    entry = _entry_lookup_obj(self, idx_type, obj);
+    return _add(self, idx_type, obj, entry, mode, NULL, NULL, out_entry, out_obj_old);
+}
+
+/* nm_dedup_multi_index_add_full:
+ * @self: the index instance.
+ * @idx_type: the index handle for storing @obj.
+ * @obj: the NMDedupMultiObj instance to add.
+ * @mode: whether to append or prepend the new item. If @entry_order is given,
+ *   the entry will be sorted after/before, instead of appending/prepending to
+ *   the entire list. If a comparable object is already tracked, then it may
+ *   still be resorted by specifying one of the "FORCE" modes.
+ * @entry_order: if not NULL, the new entry will be sorted before or after @entry_order.
+ *   If given, @entry_order MUST be tracked by @self, and the object it points to MUST
+ *   be in the same partition tracked by @idx_type. That is, they must have the same
+ *   head_entry and it means, you must ensure that @entry_order and the created/modified
+ *   entry will share the same head.
+ * @entry_existing: if not NULL, it safes a hash lookup of the entry where the
+ *   object will be placed in. You can omit this, and it will be automatically
+ *   detected (at the expense of an additional hash lookup).
+ *   Basically, this is the result of nm_dedup_multi_index_lookup_obj(),
+ *   with the peculiarity that if you know that @obj is not yet tracked,
+ *   you may specify %NM_DEDUP_MULTI_ENTRY_MISSING.
+ * @head_existing: an optional argument to safe a lookup for the head. If specified,
+ *   it must be identical to nm_dedup_multi_index_lookup_head(), with the peculiarity
+ *   that if the head is not yet tracked, you may specify %NM_DEDUP_MULTI_HEAD_ENTRY_MISSING
+ * @out_entry: if give, return the added entry. This entry may have already exists (update)
+ *   or be newly created. If @obj is not partitionable according to @idx_type, @obj
+ *   is not to be added and it returns %NULL.
+ * @out_obj_old: if given, return the previously contained object. It only
+ *   returns a  object, if a matching entry was tracked previously, not if a
+ *   new entry was created. Note that when passing @out_obj_old you obtain a reference
+ *   to the boxed object and MUST return it with nm_dedup_multi_obj_unref().
+ *
+ * Adds and object to the index.
+ *
+ * Return: %TRUE if anything changed, %FALSE if nothing changed.
+ */
+gboolean
+nm_dedup_multi_index_add_full(NMDedupMultiIndex *                        self,
+                              NMDedupMultiIdxType *                      idx_type,
+                              /*const NMDedupMultiObj * */ gconstpointer obj,
+                              NMDedupMultiIdxMode                        mode,
+                              const NMDedupMultiEntry *                  entry_order,
+                              const NMDedupMultiEntry *                  entry_existing,
+                              const NMDedupMultiHeadEntry *              head_existing,
+                              const NMDedupMultiEntry **                 out_entry,
+                              /* const NMDedupMultiObj ** */ gpointer    out_obj_old)
+{
+    NMDedupMultiEntry *entry;
+
+    g_return_val_if_fail(self, FALSE);
+    g_return_val_if_fail(idx_type, FALSE);
+    g_return_val_if_fail(obj, FALSE);
+    g_return_val_if_fail(NM_IN_SET(mode,
+                                   NM_DEDUP_MULTI_IDX_MODE_PREPEND,
+                                   NM_DEDUP_MULTI_IDX_MODE_PREPEND_FORCE,
+                                   NM_DEDUP_MULTI_IDX_MODE_APPEND,
+                                   NM_DEDUP_MULTI_IDX_MODE_APPEND_FORCE),
+                         FALSE);
+
+    if (entry_existing == NULL)
+        entry = _entry_lookup_obj(self, idx_type, obj);
+    else if (entry_existing == NM_DEDUP_MULTI_ENTRY_MISSING) {
+        nm_assert(!_entry_lookup_obj(self, idx_type, obj));
+        entry = NULL;
+    } else {
+        nm_assert(entry_existing == _entry_lookup_obj(self, idx_type, obj));
+        entry = (NMDedupMultiEntry *) entry_existing;
+    }
+    return _add(self,
+                idx_type,
+                obj,
+                entry,
+                mode,
+                entry_order,
+                (NMDedupMultiHeadEntry *) head_existing,
+                out_entry,
+                out_obj_old);
+}
+
+/*****************************************************************************/
+
+static void
+_remove_entry(NMDedupMultiIndex *self, NMDedupMultiEntry *entry, gboolean *out_head_entry_removed)
+{
+    const NMDedupMultiObj *obj;
+    NMDedupMultiHeadEntry *head_entry;
+    NMDedupMultiIdxType *  idx_type;
+
+    nm_assert(self);
+    nm_assert(entry);
+    nm_assert(entry->obj);
+    nm_assert(entry->head);
+    nm_assert(!c_list_is_empty(&entry->lst_entries));
+    nm_assert(g_hash_table_lookup(self->idx_entries, entry) == entry);
+
+    head_entry = (NMDedupMultiHeadEntry *) entry->head;
+    obj        = entry->obj;
+
+    nm_assert(head_entry);
+    nm_assert(head_entry->len > 0);
+    nm_assert(g_hash_table_lookup(self->idx_entries, head_entry) == head_entry);
+
+    idx_type = (NMDedupMultiIdxType *) head_entry->idx_type;
+    ASSERT_idx_type(idx_type);
+
+    nm_assert(idx_type->len >= head_entry->len);
+    if (--head_entry->len > 0) {
+        nm_assert(idx_type->len > 1);
+        idx_type->len--;
+        head_entry = NULL;
+    }
+
+    NM_SET_OUT(out_head_entry_removed, head_entry != NULL);
+
+    if (!g_hash_table_remove(self->idx_entries, entry))
+        nm_assert_not_reached();
+
+    if (head_entry && !g_hash_table_remove(self->idx_entries, head_entry))
+        nm_assert_not_reached();
+
+    c_list_unlink_stale(&entry->lst_entries);
+    g_slice_free(NMDedupMultiEntry, entry);
+
+    if (head_entry) {
+        nm_assert(c_list_is_empty(&head_entry->lst_entries_head));
+        c_list_unlink_stale(&head_entry->lst_idx);
+        g_slice_free(NMDedupMultiHeadEntry, head_entry);
+    }
+
+    nm_dedup_multi_obj_unref(obj);
+}
+
+static guint
+_remove_head(NMDedupMultiIndex *    self,
+             NMDedupMultiHeadEntry *head_entry,
+             gboolean               remove_all /* otherwise just dirty ones */,
+             gboolean               mark_survivors_dirty)
+{
+    guint    n;
+    gboolean head_entry_removed;
+    CList *  iter_entry, *iter_entry_safe;
+
+    nm_assert(self);
+    nm_assert(head_entry);
+    nm_assert(head_entry->len > 0);
+    nm_assert(head_entry->len == c_list_length(&head_entry->lst_entries_head));
+    nm_assert(g_hash_table_lookup(self->idx_entries, head_entry) == head_entry);
+
+    n = 0;
+    c_list_for_each_safe (iter_entry, iter_entry_safe, &head_entry->lst_entries_head) {
+        NMDedupMultiEntry *entry;
+
+        entry = c_list_entry(iter_entry, NMDedupMultiEntry, lst_entries);
+        if (remove_all || entry->dirty) {
+            _remove_entry(self, entry, &head_entry_removed);
+            n++;
+            if (head_entry_removed)
+                break;
+        } else if (mark_survivors_dirty)
+            nm_dedup_multi_entry_set_dirty(entry, TRUE);
+    }
+
+    return n;
+}
+
+static guint
+_remove_idx_entry(NMDedupMultiIndex *  self,
+                  NMDedupMultiIdxType *idx_type,
+                  gboolean             remove_all /* otherwise just dirty ones */,
+                  gboolean             mark_survivors_dirty)
+{
+    guint  n;
+    CList *iter_idx, *iter_idx_safe;
+
+    nm_assert(self);
+    ASSERT_idx_type(idx_type);
+
+    n = 0;
+    c_list_for_each_safe (iter_idx, iter_idx_safe, &idx_type->lst_idx_head) {
+        n += _remove_head(self,
+                          c_list_entry(iter_idx, NMDedupMultiHeadEntry, lst_idx),
+                          remove_all,
+                          mark_survivors_dirty);
+    }
+    return n;
+}
+
+guint
+nm_dedup_multi_index_remove_entry(NMDedupMultiIndex *self, gconstpointer entry)
+{
+    g_return_val_if_fail(self, 0);
+
+    nm_assert(entry);
+
+    if (!((NMDedupMultiEntry *) entry)->is_head) {
+        _remove_entry(self, (NMDedupMultiEntry *) entry, NULL);
+        return 1;
+    }
+    return _remove_head(self, (NMDedupMultiHeadEntry *) entry, TRUE, FALSE);
+}
+
+guint
+nm_dedup_multi_index_remove_obj(NMDedupMultiIndex *                          self,
+                                NMDedupMultiIdxType *                        idx_type,
+                                /*const NMDedupMultiObj * */ gconstpointer   obj,
+                                /*const NMDedupMultiObj ** */ gconstpointer *out_obj)
+{
+    const NMDedupMultiEntry *entry;
+
+    entry = nm_dedup_multi_index_lookup_obj(self, idx_type, obj);
+    if (!entry) {
+        NM_SET_OUT(out_obj, NULL);
+        return 0;
+    }
+
+    /* since we are about to remove the object, we obviously pass
+     * a reference to @out_obj, the caller MUST unref the object,
+     * if he chooses to provide @out_obj. */
+    NM_SET_OUT(out_obj, nm_dedup_multi_obj_ref(entry->obj));
+
+    _remove_entry(self, (NMDedupMultiEntry *) entry, NULL);
+    return 1;
+}
+
+guint
+nm_dedup_multi_index_remove_head(NMDedupMultiIndex *                        self,
+                                 NMDedupMultiIdxType *                      idx_type,
+                                 /*const NMDedupMultiObj * */ gconstpointer obj)
+{
+    const NMDedupMultiHeadEntry *entry;
+
+    entry = nm_dedup_multi_index_lookup_head(self, idx_type, obj);
+    return entry ? _remove_head(self, (NMDedupMultiHeadEntry *) entry, TRUE, FALSE) : 0;
+}
+
+guint
+nm_dedup_multi_index_remove_idx(NMDedupMultiIndex *self, NMDedupMultiIdxType *idx_type)
+{
+    g_return_val_if_fail(self, 0);
+    g_return_val_if_fail(idx_type, 0);
+
+    return _remove_idx_entry(self, idx_type, TRUE, FALSE);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_dedup_multi_index_lookup_obj:
+ * @self: the index cache
+ * @idx_type: the lookup index type
+ * @obj: the object to lookup. This means the match is performed
+ *   according to NMDedupMultiIdxTypeClass's idx_obj_id_equal()
+ *   of @idx_type.
+ *
+ * Returns: the cache entry or %NULL if the entry wasn't found.
+ */
+const NMDedupMultiEntry *
+nm_dedup_multi_index_lookup_obj(const NMDedupMultiIndex *                  self,
+                                const NMDedupMultiIdxType *                idx_type,
+                                /*const NMDedupMultiObj * */ gconstpointer obj)
+{
+    g_return_val_if_fail(self, FALSE);
+    g_return_val_if_fail(idx_type, FALSE);
+    g_return_val_if_fail(obj, FALSE);
+
+    nm_assert(idx_type && idx_type->klass);
+    return _entry_lookup_obj(self, idx_type, obj);
+}
+
+/**
+ * nm_dedup_multi_index_lookup_head:
+ * @self: the index cache
+ * @idx_type: the lookup index type
+ * @obj: the object to lookup, of type "const NMDedupMultiObj *".
+ *   Depending on the idx_type, you *must* also provide a selector
+ *   object, even when looking up the list head. That is, because
+ *   the idx_type implementation may choose to partition the objects
+ *   in distinct list, so you need a selector object to know which
+ *   list head to lookup.
+ *
+ * Returns: the cache entry or %NULL if the entry wasn't found.
+ */
+const NMDedupMultiHeadEntry *
+nm_dedup_multi_index_lookup_head(const NMDedupMultiIndex *                  self,
+                                 const NMDedupMultiIdxType *                idx_type,
+                                 /*const NMDedupMultiObj * */ gconstpointer obj)
+{
+    g_return_val_if_fail(self, FALSE);
+    g_return_val_if_fail(idx_type, FALSE);
+
+    return _entry_lookup_head(self, idx_type, obj);
+}
+
+/*****************************************************************************/
+
+void
+nm_dedup_multi_index_dirty_set_head(NMDedupMultiIndex *                        self,
+                                    const NMDedupMultiIdxType *                idx_type,
+                                    /*const NMDedupMultiObj * */ gconstpointer obj)
+{
+    NMDedupMultiHeadEntry *head_entry;
+    CList *                iter_entry;
+
+    g_return_if_fail(self);
+    g_return_if_fail(idx_type);
+
+    head_entry = _entry_lookup_head(self, idx_type, obj);
+    if (!head_entry)
+        return;
+
+    c_list_for_each (iter_entry, &head_entry->lst_entries_head) {
+        NMDedupMultiEntry *entry;
+
+        entry = c_list_entry(iter_entry, NMDedupMultiEntry, lst_entries);
+        nm_dedup_multi_entry_set_dirty(entry, TRUE);
+    }
+}
+
+void
+nm_dedup_multi_index_dirty_set_idx(NMDedupMultiIndex *self, const NMDedupMultiIdxType *idx_type)
+{
+    CList *iter_idx, *iter_entry;
+
+    g_return_if_fail(self);
+    g_return_if_fail(idx_type);
+
+    c_list_for_each (iter_idx, &idx_type->lst_idx_head) {
+        NMDedupMultiHeadEntry *head_entry;
+
+        head_entry = c_list_entry(iter_idx, NMDedupMultiHeadEntry, lst_idx);
+        c_list_for_each (iter_entry, &head_entry->lst_entries_head) {
+            NMDedupMultiEntry *entry;
+
+            entry = c_list_entry(iter_entry, NMDedupMultiEntry, lst_entries);
+            nm_dedup_multi_entry_set_dirty(entry, TRUE);
+        }
+    }
+}
+
+/**
+ * nm_dedup_multi_index_dirty_remove_idx:
+ * @self: the index instance
+ * @idx_type: the index-type to select the objects.
+ * @mark_survivors_dirty: while the function removes all entries that are
+ *   marked as dirty, if @set_dirty is true, the surviving objects
+ *   will be marked dirty right away.
+ *
+ * Deletes all entries for @idx_type that are marked dirty. Only
+ * non-dirty objects survive. If @mark_survivors_dirty is set to TRUE, the survivors
+ * are marked as dirty right away.
+ *
+ * Returns: number of deleted entries.
+ */
+guint
+nm_dedup_multi_index_dirty_remove_idx(NMDedupMultiIndex *  self,
+                                      NMDedupMultiIdxType *idx_type,
+                                      gboolean             mark_survivors_dirty)
+{
+    g_return_val_if_fail(self, 0);
+    g_return_val_if_fail(idx_type, 0);
+
+    return _remove_idx_entry(self, idx_type, FALSE, mark_survivors_dirty);
+}
+
+/*****************************************************************************/
+
+static guint
+_dict_idx_objs_hash(const NMDedupMultiObj *obj)
+{
+    NMHashState h;
+
+    nm_hash_init(&h, 1748638583u);
+    obj->klass->obj_full_hash_update(obj, &h);
+    return nm_hash_complete(&h);
+}
+
+static gboolean
+_dict_idx_objs_equal(const NMDedupMultiObj *obj_a, const NMDedupMultiObj *obj_b)
+{
+    return obj_a == obj_b
+           || (obj_a->klass == obj_b->klass && obj_a->klass->obj_full_equal(obj_a, obj_b));
+}
+
+void
+nm_dedup_multi_index_obj_release(NMDedupMultiIndex *                         self,
+                                 /* const NMDedupMultiObj * */ gconstpointer obj)
+{
+    nm_assert(self);
+    nm_assert(obj);
+    nm_assert(g_hash_table_lookup(self->idx_objs, obj) == obj);
+    nm_assert(((const NMDedupMultiObj *) obj)->_multi_idx == self);
+
+    ((NMDedupMultiObj *) obj)->_multi_idx = NULL;
+    if (!g_hash_table_remove(self->idx_objs, obj))
+        nm_assert_not_reached();
+}
+
+gconstpointer
+nm_dedup_multi_index_obj_find(NMDedupMultiIndex *                         self,
+                              /* const NMDedupMultiObj * */ gconstpointer obj)
+{
+    g_return_val_if_fail(self, NULL);
+    g_return_val_if_fail(obj, NULL);
+
+    return g_hash_table_lookup(self->idx_objs, obj);
+}
+
+gconstpointer
+nm_dedup_multi_index_obj_intern(NMDedupMultiIndex *                         self,
+                                /* const NMDedupMultiObj * */ gconstpointer obj)
+{
+    const NMDedupMultiObj *obj_new = obj;
+    const NMDedupMultiObj *obj_old;
+
+    nm_assert(self);
+    nm_assert(obj_new);
+
+    if (obj_new->_multi_idx == self) {
+        nm_assert(g_hash_table_lookup(self->idx_objs, obj_new) == obj_new);
+        nm_dedup_multi_obj_ref(obj_new);
+        return obj_new;
+    }
+
+    obj_old = g_hash_table_lookup(self->idx_objs, obj_new);
+    nm_assert(obj_old != obj_new);
+
+    if (obj_old) {
+        nm_assert(obj_old->_multi_idx == self);
+        nm_dedup_multi_obj_ref(obj_old);
+        return obj_old;
+    }
+
+    if (nm_dedup_multi_obj_needs_clone(obj_new))
+        obj_new = nm_dedup_multi_obj_clone(obj_new);
+    else
+        obj_new = nm_dedup_multi_obj_ref(obj_new);
+
+    nm_assert(obj_new);
+    nm_assert(!obj_new->_multi_idx);
+
+    if (!g_hash_table_add(self->idx_objs, (gpointer) obj_new))
+        nm_assert_not_reached();
+
+    ((NMDedupMultiObj *) obj_new)->_multi_idx = self;
+    return obj_new;
+}
+
+void
+nm_dedup_multi_obj_unref(const NMDedupMultiObj *obj)
+{
+    if (obj) {
+        nm_assert(obj->_ref_count > 0);
+        nm_assert(obj->_ref_count != NM_OBJ_REF_COUNT_STACKINIT);
+
+again:
+        if (--(((NMDedupMultiObj *) obj)->_ref_count) <= 0) {
+            if (obj->_multi_idx) {
+                /* restore the ref-count to 1 and release the object first
+                 * from the index. Then, retry again to unref. */
+                ((NMDedupMultiObj *) obj)->_ref_count++;
+                nm_dedup_multi_index_obj_release(obj->_multi_idx, obj);
+                nm_assert(obj->_ref_count == 1);
+                nm_assert(!obj->_multi_idx);
+                goto again;
+            }
+
+            obj->klass->obj_destroy((NMDedupMultiObj *) obj);
+        }
+    }
+}
+
+gboolean
+nm_dedup_multi_obj_needs_clone(const NMDedupMultiObj *obj)
+{
+    nm_assert(obj);
+
+    if (obj->_multi_idx || obj->_ref_count == NM_OBJ_REF_COUNT_STACKINIT)
+        return TRUE;
+
+    if (obj->klass->obj_needs_clone && obj->klass->obj_needs_clone(obj))
+        return TRUE;
+
+    return FALSE;
+}
+
+const NMDedupMultiObj *
+nm_dedup_multi_obj_clone(const NMDedupMultiObj *obj)
+{
+    const NMDedupMultiObj *o;
+
+    nm_assert(obj);
+
+    o = obj->klass->obj_clone(obj);
+    nm_assert(o);
+    nm_assert(o->_ref_count == 1);
+    return o;
+}
+
+gconstpointer *
+nm_dedup_multi_objs_to_array_head(const NMDedupMultiHeadEntry *  head_entry,
+                                  NMDedupMultiFcnSelectPredicate predicate,
+                                  gpointer                       user_data,
+                                  guint *                        out_len)
+{
+    gconstpointer *result;
+    CList *        iter;
+    guint          i;
+
+    if (!head_entry) {
+        NM_SET_OUT(out_len, 0);
+        return NULL;
+    }
+
+    result = g_new(gconstpointer, head_entry->len + 1);
+    i      = 0;
+    c_list_for_each (iter, &head_entry->lst_entries_head) {
+        const NMDedupMultiObj *obj = c_list_entry(iter, NMDedupMultiEntry, lst_entries)->obj;
+
+        if (!predicate || predicate(obj, user_data)) {
+            nm_assert(i < head_entry->len);
+            result[i++] = obj;
+        }
+    }
+
+    if (i == 0) {
+        g_free(result);
+        NM_SET_OUT(out_len, 0);
+        return NULL;
+    }
+
+    nm_assert(i <= head_entry->len);
+    NM_SET_OUT(out_len, i);
+    result[i++] = NULL;
+    return result;
+}
+
+GPtrArray *
+nm_dedup_multi_objs_to_ptr_array_head(const NMDedupMultiHeadEntry *  head_entry,
+                                      NMDedupMultiFcnSelectPredicate predicate,
+                                      gpointer                       user_data)
+{
+    GPtrArray *result;
+    CList *    iter;
+
+    if (!head_entry)
+        return NULL;
+
+    result = g_ptr_array_new_full(head_entry->len, (GDestroyNotify) nm_dedup_multi_obj_unref);
+    c_list_for_each (iter, &head_entry->lst_entries_head) {
+        const NMDedupMultiObj *obj = c_list_entry(iter, NMDedupMultiEntry, lst_entries)->obj;
+
+        if (!predicate || predicate(obj, user_data))
+            g_ptr_array_add(result, (gpointer) nm_dedup_multi_obj_ref(obj));
+    }
+
+    if (result->len == 0) {
+        g_ptr_array_unref(result);
+        return NULL;
+    }
+    return result;
+}
+
+/**
+ * nm_dedup_multi_entry_reorder:
+ * @entry: the entry to reorder. It must not be NULL (and tracked in an index).
+ * @entry_order: (allow-none): an optional other entry. It MUST be in the same
+ *   list as entry. If given, @entry will be ordered after/before @entry_order.
+ *   If left at %NULL, @entry will be moved to the front/end of the list.
+ * @order_after: if @entry_order is given, %TRUE means to move @entry after
+ *   @entry_order (otherwise before).
+ *   If @entry_order is %NULL, %TRUE means to move @entry to the tail of the list
+ *   (otherwise the beginning). Note that "tail of the list" here means that @entry
+ *   will be linked before the head of the circular list.
+ *
+ * Returns: %TRUE, if anything was changed. Otherwise, @entry was already at the
+ * right place and nothing was done.
+ */
+gboolean
+nm_dedup_multi_entry_reorder(const NMDedupMultiEntry *entry,
+                             const NMDedupMultiEntry *entry_order,
+                             gboolean                 order_after)
+{
+    nm_assert(entry);
+
+    if (!entry_order) {
+        const NMDedupMultiHeadEntry *head_entry = entry->head;
+
+        if (order_after) {
+            if (nm_c_list_move_tail((CList *) &head_entry->lst_entries_head,
+                                    (CList *) &entry->lst_entries))
+                return TRUE;
+        } else {
+            if (nm_c_list_move_front((CList *) &head_entry->lst_entries_head,
+                                     (CList *) &entry->lst_entries))
+                return TRUE;
+        }
+    } else {
+        if (order_after) {
+            if (nm_c_list_move_after((CList *) &entry_order->lst_entries,
+                                     (CList *) &entry->lst_entries))
+                return TRUE;
+        } else {
+            if (nm_c_list_move_before((CList *) &entry_order->lst_entries,
+                                      (CList *) &entry->lst_entries))
+                return TRUE;
+        }
+    }
+
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+NMDedupMultiIndex *
+nm_dedup_multi_index_new(void)
+{
+    NMDedupMultiIndex *self;
+
+    self            = g_slice_new0(NMDedupMultiIndex);
+    self->ref_count = 1;
+    self->idx_entries =
+        g_hash_table_new((GHashFunc) _dict_idx_entries_hash, (GEqualFunc) _dict_idx_entries_equal);
+    self->idx_objs =
+        g_hash_table_new((GHashFunc) _dict_idx_objs_hash, (GEqualFunc) _dict_idx_objs_equal);
+    return self;
+}
+
+NMDedupMultiIndex *
+nm_dedup_multi_index_ref(NMDedupMultiIndex *self)
+{
+    g_return_val_if_fail(self, NULL);
+    g_return_val_if_fail(self->ref_count > 0, NULL);
+
+    self->ref_count++;
+    return self;
+}
+
+NMDedupMultiIndex *
+nm_dedup_multi_index_unref(NMDedupMultiIndex *self)
+{
+    GHashTableIter             iter;
+    const NMDedupMultiIdxType *idx_type;
+    NMDedupMultiEntry *        entry;
+    const NMDedupMultiObj *    obj;
+
+    g_return_val_if_fail(self, NULL);
+    g_return_val_if_fail(self->ref_count > 0, NULL);
+
+    if (--self->ref_count > 0)
+        return NULL;
+
+more:
+    g_hash_table_iter_init(&iter, self->idx_entries);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &entry, NULL)) {
+        if (entry->is_head)
+            idx_type = ((NMDedupMultiHeadEntry *) entry)->idx_type;
+        else
+            idx_type = entry->head->idx_type;
+        _remove_idx_entry(self, (NMDedupMultiIdxType *) idx_type, TRUE, FALSE);
+        goto more;
+    }
+
+    nm_assert(g_hash_table_size(self->idx_entries) == 0);
+
+    g_hash_table_iter_init(&iter, self->idx_objs);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &obj, NULL)) {
+        nm_assert(obj->_multi_idx == self);
+        ((NMDedupMultiObj *) obj)->_multi_idx = NULL;
+    }
+    g_hash_table_remove_all(self->idx_objs);
+
+    g_hash_table_unref(self->idx_entries);
+    g_hash_table_unref(self->idx_objs);
+
+    g_slice_free(NMDedupMultiIndex, self);
+    return NULL;
+}
diff --git a/src/libnm-glib-aux/nm-dedup-multi.h b/src/libnm-glib-aux/nm-dedup-multi.h
new file mode 100644
index 00000000..fb1cb863
--- /dev/null
+++ b/src/libnm-glib-aux/nm-dedup-multi.h
@@ -0,0 +1,447 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NM_DEDUP_MULTI_H__
+#define __NM_DEDUP_MULTI_H__
+
+#include "nm-obj.h"
+#include "libnm-std-aux/c-list-util.h"
+
+/*****************************************************************************/
+
+struct _NMHashState;
+
+typedef struct _NMDedupMultiObj          NMDedupMultiObj;
+typedef struct _NMDedupMultiObjClass     NMDedupMultiObjClass;
+typedef struct _NMDedupMultiIdxType      NMDedupMultiIdxType;
+typedef struct _NMDedupMultiIdxTypeClass NMDedupMultiIdxTypeClass;
+typedef struct _NMDedupMultiEntry        NMDedupMultiEntry;
+typedef struct _NMDedupMultiHeadEntry    NMDedupMultiHeadEntry;
+typedef struct _NMDedupMultiIndex        NMDedupMultiIndex;
+
+typedef enum _NMDedupMultiIdxMode {
+    NM_DEDUP_MULTI_IDX_MODE_PREPEND,
+
+    NM_DEDUP_MULTI_IDX_MODE_PREPEND_FORCE,
+
+    /* append new objects to the end of the list.
+     * If the object is already in the cache, don't move it. */
+    NM_DEDUP_MULTI_IDX_MODE_APPEND,
+
+    /* like NM_DEDUP_MULTI_IDX_MODE_APPEND, but if the object
+     * is already in the cache, move it to the end. */
+    NM_DEDUP_MULTI_IDX_MODE_APPEND_FORCE,
+} NMDedupMultiIdxMode;
+
+/*****************************************************************************/
+
+struct _NMDedupMultiObj {
+    union {
+        NMObjBaseInst               parent;
+        const NMDedupMultiObjClass *klass;
+    };
+    NMDedupMultiIndex *_multi_idx;
+    guint              _ref_count;
+};
+
+struct _NMDedupMultiObjClass {
+    NMObjBaseClass parent;
+
+    const NMDedupMultiObj *(*obj_clone)(const NMDedupMultiObj *obj);
+
+    gboolean (*obj_needs_clone)(const NMDedupMultiObj *obj);
+
+    void (*obj_destroy)(NMDedupMultiObj *obj);
+
+    /* the NMDedupMultiObj can be deduplicated. For that the obj_full_hash_update()
+     * and obj_full_equal() compare *all* fields of the object, even minor ones. */
+    void (*obj_full_hash_update)(const NMDedupMultiObj *obj, struct _NMHashState *h);
+    gboolean (*obj_full_equal)(const NMDedupMultiObj *obj_a, const NMDedupMultiObj *obj_b);
+};
+
+/*****************************************************************************/
+
+static inline const NMDedupMultiObj *
+nm_dedup_multi_obj_ref(const NMDedupMultiObj *obj)
+{
+    /* ref and unref accept const pointers. Objects is supposed to be shared
+     * and kept immutable. Disallowing to take/return a reference to a const
+     * NMPObject is cumbersome, because callers are precisely expected to
+     * keep a ref on the otherwise immutable object. */
+
+    nm_assert(obj);
+    nm_assert(obj->_ref_count != NM_OBJ_REF_COUNT_STACKINIT);
+    nm_assert(obj->_ref_count > 0);
+
+    ((NMDedupMultiObj *) obj)->_ref_count++;
+    return obj;
+}
+
+void                   nm_dedup_multi_obj_unref(const NMDedupMultiObj *obj);
+const NMDedupMultiObj *nm_dedup_multi_obj_clone(const NMDedupMultiObj *obj);
+gboolean               nm_dedup_multi_obj_needs_clone(const NMDedupMultiObj *obj);
+
+gconstpointer nm_dedup_multi_index_obj_intern(NMDedupMultiIndex *                         self,
+                                              /* const NMDedupMultiObj * */ gconstpointer obj);
+
+void nm_dedup_multi_index_obj_release(NMDedupMultiIndex *                         self,
+                                      /* const NMDedupMultiObj * */ gconstpointer obj);
+
+/* const NMDedupMultiObj * */ gconstpointer
+nm_dedup_multi_index_obj_find(NMDedupMultiIndex *                         self,
+                              /* const NMDedupMultiObj * */ gconstpointer obj);
+
+/*****************************************************************************/
+
+/* the NMDedupMultiIdxType is an access handle under which you can store and
+ * retrieve NMDedupMultiObj instances in NMDedupMultiIndex.
+ *
+ * The NMDedupMultiIdxTypeClass determines its behavior, but you can have
+ * multiple instances (of the same class).
+ *
+ * For example, NMIP4Config can have idx-type to put there all IPv4 Routes.
+ * This idx-type instance is private to the NMIP4Config instance. Basically,
+ * the NMIP4Config instance uses the idx-type to maintain an ordered list
+ * of routes in NMDedupMultiIndex.
+ *
+ * However, a NMDedupMultiIdxType may also partition the set of objects
+ * in multiple distinct lists. NMIP4Config doesn't do that (because instead
+ * of creating one idx-type for IPv4 and IPv6 routes, it just cretaes
+ * to distinct idx-types, one for each address family.
+ * This partitioning is used by NMPlatform to maintain a lookup index for
+ * routes by ifindex. As the ifindex is dynamic, it does not create an
+ * idx-type instance for each ifindex. Instead, it has one idx-type for
+ * all routes. But whenever accessing NMDedupMultiIndex with an NMDedupMultiObj,
+ * the partitioning NMDedupMultiIdxType takes into account the NMDedupMultiObj
+ * instance to associate it with the right list.
+ *
+ * Hence, a NMDedupMultiIdxEntry has a list of possibly multiple NMDedupMultiHeadEntry
+ * instances, which each is the head for a list of NMDedupMultiEntry instances.
+ * In the platform example, the NMDedupMultiHeadEntry partition the indexed objects
+ * by their ifindex. */
+struct _NMDedupMultiIdxType {
+    union {
+        NMObjBaseInst                   parent;
+        const NMDedupMultiIdxTypeClass *klass;
+    };
+
+    CList lst_idx_head;
+
+    guint len;
+};
+
+void nm_dedup_multi_idx_type_init(NMDedupMultiIdxType *           idx_type,
+                                  const NMDedupMultiIdxTypeClass *klass);
+
+struct _NMDedupMultiIdxTypeClass {
+    NMObjBaseClass parent;
+
+    void (*idx_obj_id_hash_update)(const NMDedupMultiIdxType *idx_type,
+                                   const NMDedupMultiObj *    obj,
+                                   struct _NMHashState *      h);
+    gboolean (*idx_obj_id_equal)(const NMDedupMultiIdxType *idx_type,
+                                 const NMDedupMultiObj *    obj_a,
+                                 const NMDedupMultiObj *    obj_b);
+
+    /* an NMDedupMultiIdxTypeClass which implements partitioning of the
+     * tracked objects, must implement the idx_obj_partition*() functions.
+     *
+     * idx_obj_partitionable() may return NULL if the object cannot be tracked.
+     * For example, a index for routes by ifindex, may not want to track any
+     * routes that don't have a valid ifindex. If the idx-type says that the
+     * object is not partitionable, it is never added to the NMDedupMultiIndex. */
+    gboolean (*idx_obj_partitionable)(const NMDedupMultiIdxType *idx_type,
+                                      const NMDedupMultiObj *    obj);
+    void (*idx_obj_partition_hash_update)(const NMDedupMultiIdxType *idx_type,
+                                          const NMDedupMultiObj *    obj,
+                                          struct _NMHashState *      h);
+    gboolean (*idx_obj_partition_equal)(const NMDedupMultiIdxType *idx_type,
+                                        const NMDedupMultiObj *    obj_a,
+                                        const NMDedupMultiObj *    obj_b);
+};
+
+static inline gboolean
+nm_dedup_multi_idx_type_id_equal(const NMDedupMultiIdxType *                 idx_type,
+                                 /* const NMDedupMultiObj * */ gconstpointer obj_a,
+                                 /* const NMDedupMultiObj * */ gconstpointer obj_b)
+{
+    nm_assert(idx_type);
+    return obj_a == obj_b || idx_type->klass->idx_obj_id_equal(idx_type, obj_a, obj_b);
+}
+
+static inline gboolean
+nm_dedup_multi_idx_type_partition_equal(const NMDedupMultiIdxType *                 idx_type,
+                                        /* const NMDedupMultiObj * */ gconstpointer obj_a,
+                                        /* const NMDedupMultiObj * */ gconstpointer obj_b)
+{
+    nm_assert(idx_type);
+    if (idx_type->klass->idx_obj_partition_equal) {
+        nm_assert(obj_a);
+        nm_assert(obj_b);
+        return obj_a == obj_b || idx_type->klass->idx_obj_partition_equal(idx_type, obj_a, obj_b);
+    }
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+struct _NMDedupMultiEntry {
+    /* this is the list of all entries that share the same head entry.
+     * All entries compare equal according to idx_obj_partition_equal(). */
+    CList lst_entries;
+
+    /* const NMDedupMultiObj * */ gconstpointer obj;
+
+    bool is_head;
+    bool dirty;
+
+    const NMDedupMultiHeadEntry *head;
+};
+
+struct _NMDedupMultiHeadEntry {
+    /* this is the list of all entries that share the same head entry.
+     * All entries compare equal according to idx_obj_partition_equal(). */
+    CList lst_entries_head;
+
+    const NMDedupMultiIdxType *idx_type;
+
+    bool is_head;
+
+    guint len;
+
+    CList lst_idx;
+};
+
+/*****************************************************************************/
+
+static inline gconstpointer
+nm_dedup_multi_entry_get_obj(const NMDedupMultiEntry *entry)
+{
+    /* convenience method that allows to skip the %NULL check on
+     * @entry. Think of the NULL-conditional operator ?. of C# */
+    return entry ? entry->obj : NULL;
+}
+
+/*****************************************************************************/
+
+static inline void
+nm_dedup_multi_entry_set_dirty(const NMDedupMultiEntry *entry, gboolean dirty)
+{
+    /* NMDedupMultiEntry is always exposed as a const object, because it is not
+     * supposed to be modified outside NMDedupMultiIndex API. Except the "dirty"
+     * flag. In C++ speak, it is a mutable field.
+     *
+     * Add this inline function, to cast-away constness and set the dirty flag. */
+    nm_assert(entry);
+    ((NMDedupMultiEntry *) entry)->dirty = dirty;
+}
+
+/*****************************************************************************/
+
+NMDedupMultiIndex *nm_dedup_multi_index_new(void);
+NMDedupMultiIndex *nm_dedup_multi_index_ref(NMDedupMultiIndex *self);
+NMDedupMultiIndex *nm_dedup_multi_index_unref(NMDedupMultiIndex *self);
+
+static inline void
+_nm_auto_unref_dedup_multi_index(NMDedupMultiIndex **v)
+{
+    if (*v)
+        nm_dedup_multi_index_unref(*v);
+}
+#define nm_auto_unref_dedup_multi_index nm_auto(_nm_auto_unref_dedup_multi_index)
+
+#define NM_DEDUP_MULTI_ENTRY_MISSING      ((const NMDedupMultiEntry *) GUINT_TO_POINTER(1))
+#define NM_DEDUP_MULTI_HEAD_ENTRY_MISSING ((const NMDedupMultiHeadEntry *) GUINT_TO_POINTER(1))
+
+gboolean nm_dedup_multi_index_add_full(NMDedupMultiIndex *                        self,
+                                       NMDedupMultiIdxType *                      idx_type,
+                                       /*const NMDedupMultiObj * */ gconstpointer obj,
+                                       NMDedupMultiIdxMode                        mode,
+                                       const NMDedupMultiEntry *                  entry_order,
+                                       const NMDedupMultiEntry *                  entry_existing,
+                                       const NMDedupMultiHeadEntry *              head_existing,
+                                       const NMDedupMultiEntry **                 out_entry,
+                                       /* const NMDedupMultiObj ** */ gpointer    out_obj_old);
+
+gboolean nm_dedup_multi_index_add(NMDedupMultiIndex *                        self,
+                                  NMDedupMultiIdxType *                      idx_type,
+                                  /*const NMDedupMultiObj * */ gconstpointer obj,
+                                  NMDedupMultiIdxMode                        mode,
+                                  const NMDedupMultiEntry **                 out_entry,
+                                  /* const NMDedupMultiObj ** */ gpointer    out_obj_old);
+
+const NMDedupMultiEntry *
+nm_dedup_multi_index_lookup_obj(const NMDedupMultiIndex *                  self,
+                                const NMDedupMultiIdxType *                idx_type,
+                                /*const NMDedupMultiObj * */ gconstpointer obj);
+
+const NMDedupMultiHeadEntry *
+nm_dedup_multi_index_lookup_head(const NMDedupMultiIndex *                  self,
+                                 const NMDedupMultiIdxType *                idx_type,
+                                 /*const NMDedupMultiObj * */ gconstpointer obj);
+
+guint nm_dedup_multi_index_remove_entry(NMDedupMultiIndex *self, gconstpointer entry);
+
+guint nm_dedup_multi_index_remove_obj(NMDedupMultiIndex *                          self,
+                                      NMDedupMultiIdxType *                        idx_type,
+                                      /*const NMDedupMultiObj * */ gconstpointer   obj,
+                                      /*const NMDedupMultiObj ** */ gconstpointer *out_obj);
+
+guint nm_dedup_multi_index_remove_head(NMDedupMultiIndex *                        self,
+                                       NMDedupMultiIdxType *                      idx_type,
+                                       /*const NMDedupMultiObj * */ gconstpointer obj);
+
+guint nm_dedup_multi_index_remove_idx(NMDedupMultiIndex *self, NMDedupMultiIdxType *idx_type);
+
+void nm_dedup_multi_index_dirty_set_head(NMDedupMultiIndex *                        self,
+                                         const NMDedupMultiIdxType *                idx_type,
+                                         /*const NMDedupMultiObj * */ gconstpointer obj);
+
+void nm_dedup_multi_index_dirty_set_idx(NMDedupMultiIndex *        self,
+                                        const NMDedupMultiIdxType *idx_type);
+
+guint nm_dedup_multi_index_dirty_remove_idx(NMDedupMultiIndex *  self,
+                                            NMDedupMultiIdxType *idx_type,
+                                            gboolean             mark_survivors_dirty);
+
+/*****************************************************************************/
+
+typedef struct _NMDedupMultiIter {
+    const CList *_head;
+    union {
+        const CList *_next;
+        const CList *_prev;
+    };
+    const NMDedupMultiEntry *current;
+} NMDedupMultiIter;
+
+static inline void
+nm_dedup_multi_iter_init(NMDedupMultiIter *iter, const NMDedupMultiHeadEntry *head)
+{
+    g_return_if_fail(iter);
+
+    if (head && !c_list_is_empty(&head->lst_entries_head)) {
+        iter->_head = &head->lst_entries_head;
+        iter->_next = head->lst_entries_head.next;
+    } else {
+        iter->_head = NULL;
+        iter->_next = NULL;
+    }
+    iter->current = NULL;
+}
+
+static inline void
+nm_dedup_multi_iter_init_reverse(NMDedupMultiIter *iter, const NMDedupMultiHeadEntry *head)
+{
+    g_return_if_fail(iter);
+
+    if (head && !c_list_is_empty(&head->lst_entries_head)) {
+        iter->_head = &head->lst_entries_head;
+        iter->_prev = head->lst_entries_head.prev;
+    } else {
+        iter->_head = NULL;
+        iter->_prev = NULL;
+    }
+    iter->current = NULL;
+}
+
+static inline gboolean
+nm_dedup_multi_iter_next(NMDedupMultiIter *iter)
+{
+    g_return_val_if_fail(iter, FALSE);
+
+    if (!iter->_next) {
+        iter->current = NULL;
+        return FALSE;
+    }
+
+    /* we always look ahead for the next. This way, the user
+     * may delete the current entry (but no other entries). */
+    iter->current = c_list_entry(iter->_next, NMDedupMultiEntry, lst_entries);
+    if (iter->_next->next == iter->_head)
+        iter->_next = NULL;
+    else
+        iter->_next = iter->_next->next;
+    return TRUE;
+}
+
+static inline gboolean
+nm_dedup_multi_iter_prev(NMDedupMultiIter *iter)
+{
+    g_return_val_if_fail(iter, FALSE);
+
+    if (!iter->_prev) {
+        iter->current = NULL;
+        return FALSE;
+    }
+
+    /* we always look ahead for the prev. This way, the user
+     * may delete the current entry (but no other entries). */
+    iter->current = c_list_entry(iter->_prev, NMDedupMultiEntry, lst_entries);
+    if (iter->_prev->prev == iter->_head)
+        iter->_prev = NULL;
+    else
+        iter->_prev = iter->_prev->prev;
+    return TRUE;
+}
+
+#define nm_dedup_multi_iter_for_each(iter, head_entry) \
+    for (nm_dedup_multi_iter_init((iter), (head_entry)); nm_dedup_multi_iter_next((iter));)
+
+/*****************************************************************************/
+
+typedef gboolean (*NMDedupMultiFcnSelectPredicate)(/* const NMDedupMultiObj * */ gconstpointer obj,
+                                                   gpointer user_data);
+
+gconstpointer *nm_dedup_multi_objs_to_array_head(const NMDedupMultiHeadEntry *  head_entry,
+                                                 NMDedupMultiFcnSelectPredicate predicate,
+                                                 gpointer                       user_data,
+                                                 guint *                        out_len);
+GPtrArray *    nm_dedup_multi_objs_to_ptr_array_head(const NMDedupMultiHeadEntry *  head_entry,
+                                                     NMDedupMultiFcnSelectPredicate predicate,
+                                                     gpointer                       user_data);
+
+static inline const NMDedupMultiEntry *
+nm_dedup_multi_head_entry_get_idx(const NMDedupMultiHeadEntry *head_entry, int idx)
+{
+    CList *iter;
+
+    if (head_entry) {
+        if (idx >= 0) {
+            c_list_for_each (iter, &head_entry->lst_entries_head) {
+                if (idx-- == 0)
+                    return c_list_entry(iter, NMDedupMultiEntry, lst_entries);
+            }
+        } else {
+            for (iter = head_entry->lst_entries_head.prev; iter != &head_entry->lst_entries_head;
+                 iter = iter->prev) {
+                if (++idx == 0)
+                    return c_list_entry(iter, NMDedupMultiEntry, lst_entries);
+            }
+        }
+    }
+    return NULL;
+}
+
+static inline void
+nm_dedup_multi_head_entry_sort(const NMDedupMultiHeadEntry *head_entry,
+                               CListSortCmp                 cmp,
+                               gconstpointer                user_data)
+{
+    if (head_entry) {
+        /* the head entry can be sorted directly without messing up the
+         * index to which it belongs. Of course, this does mess up any
+         * NMDedupMultiIter instances. */
+        c_list_sort((CList *) &head_entry->lst_entries_head, cmp, user_data);
+    }
+}
+
+gboolean nm_dedup_multi_entry_reorder(const NMDedupMultiEntry *entry,
+                                      const NMDedupMultiEntry *entry_order,
+                                      gboolean                 order_after);
+
+/*****************************************************************************/
+
+#endif /* __NM_DEDUP_MULTI_H__ */
diff --git a/src/libnm-glib-aux/nm-default-glib-i18n-lib.h b/src/libnm-glib-aux/nm-default-glib-i18n-lib.h
new file mode 100644
index 00000000..4b04da9e
--- /dev/null
+++ b/src/libnm-glib-aux/nm-default-glib-i18n-lib.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ */
+
+#ifndef __NM_DEFAULT_GLIB_I18N_LIB_H__
+#define __NM_DEFAULT_GLIB_I18N_LIB_H__
+
+/*****************************************************************************/
+
+#define _NETWORKMANAGER_COMPILATION_GLIB_I18N_LIB
+
+#include "libnm-glib-aux/nm-default-glib.h"
+
+#undef NETWORKMANAGER_COMPILATION
+#define NETWORKMANAGER_COMPILATION \
+    (NM_NETWORKMANAGER_COMPILATION_GLIB | NM_NETWORKMANAGER_COMPILATION_WITH_GLIB_I18N_LIB)
+
+/*****************************************************************************/
+
+#endif /* __NM_DEFAULT_GLIB_I18N_LIB_H__ */
diff --git a/src/libnm-glib-aux/nm-default-glib-i18n-prog.h b/src/libnm-glib-aux/nm-default-glib-i18n-prog.h
new file mode 100644
index 00000000..ebd31f96
--- /dev/null
+++ b/src/libnm-glib-aux/nm-default-glib-i18n-prog.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ */
+
+#ifndef __NM_DEFAULT_GLIB_I18N_PROG_H__
+#define __NM_DEFAULT_GLIB_I18N_PROG_H__
+
+/*****************************************************************************/
+
+#define _NETWORKMANAGER_COMPILATION_GLIB_I18N_PROG
+
+#include "libnm-glib-aux/nm-default-glib.h"
+
+#undef NETWORKMANAGER_COMPILATION
+#define NETWORKMANAGER_COMPILATION \
+    (NM_NETWORKMANAGER_COMPILATION_GLIB | NM_NETWORKMANAGER_COMPILATION_WITH_GLIB_I18N_PROG)
+
+/*****************************************************************************/
+
+#endif /* __NM_DEFAULT_GLIB_I18N_PROG_H__ */
diff --git a/src/libnm-glib-aux/nm-default-glib.h b/src/libnm-glib-aux/nm-default-glib.h
new file mode 100644
index 00000000..e5ccd47f
--- /dev/null
+++ b/src/libnm-glib-aux/nm-default-glib.h
@@ -0,0 +1,75 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ */
+
+#ifndef __NM_DEFAULT_GLIB_H__
+#define __NM_DEFAULT_GLIB_H__
+
+/*****************************************************************************/
+
+#include "libnm-std-aux/nm-default-std.h"
+
+#undef NETWORKMANAGER_COMPILATION
+#define NETWORKMANAGER_COMPILATION NM_NETWORKMANAGER_COMPILATION_WITH_GLIB
+
+/*****************************************************************************/
+
+#include <glib.h>
+
+#if defined(_NETWORKMANAGER_COMPILATION_GLIB_I18N_PROG)
+    #if defined(_NETWORKMANAGER_COMPILATION_GLIB_I18N_LIB)
+        #error Cannot define _NETWORKMANAGER_COMPILATION_GLIB_I18N_LIB and _NETWORKMANAGER_COMPILATION_GLIB_I18N_PROG together
+    #endif
+    #undef _NETWORKMANAGER_COMPILATION_GLIB_I18N_PROG
+    #include <glib/gi18n.h>
+#elif defined(_NETWORKMANAGER_COMPILATION_GLIB_I18N_LIB)
+    #undef _NETWORKMANAGER_COMPILATION_GLIB_I18N_LIB
+    #include <glib/gi18n-lib.h>
+#endif
+
+/*****************************************************************************/
+
+#if NM_MORE_ASSERTS == 0
+    #ifndef G_DISABLE_CAST_CHECKS
+        /* Unless compiling with G_DISABLE_CAST_CHECKS, glib performs type checking
+         * during G_VARIANT_TYPE() via g_variant_type_checked_(). This is not necessary
+         * because commonly this cast is needed during something like
+         *
+         *   g_variant_builder_init (&props, G_VARIANT_TYPE ("a{sv}"));
+         *
+         * Note that in if the variant type would be invalid, the check still
+         * wouldn't make the buggy code magically work. Instead of passing a
+         * bogus type string (bad), it would pass %NULL to g_variant_builder_init()
+         * (also bad).
+         *
+         * Also, a function like g_variant_builder_init() already validates
+         * the input type via something like
+         *
+         *   g_return_if_fail (g_variant_type_is_container (type));
+         *
+         * So, by having G_VARIANT_TYPE() also validate the type, we validate
+         * twice, whereas the first validation is rather pointless because it
+         * doesn't prevent the function to be called with invalid arguments.
+         *
+         * Just patch G_VARIANT_TYPE() to perform no check.
+         */
+        #undef G_VARIANT_TYPE
+        #define G_VARIANT_TYPE(type_string) ((const GVariantType *) (type_string))
+    #endif
+#endif
+
+/*****************************************************************************/
+
+#include "nm-gassert-patch.h"
+
+#include "libnm-std-aux/nm-std-aux.h"
+#include "libnm-std-aux/nm-std-utils.h"
+#include "libnm-glib-aux/nm-macros-internal.h"
+#include "libnm-glib-aux/nm-shared-utils.h"
+#include "libnm-glib-aux/nm-errno.h"
+#include "libnm-glib-aux/nm-hash-utils.h"
+
+/*****************************************************************************/
+
+#endif /* __NM_DEFAULT_GLIB_H__ */
diff --git a/src/libnm-glib-aux/nm-enum-utils.c b/src/libnm-glib-aux/nm-enum-utils.c
new file mode 100644
index 00000000..f97cdfcb
--- /dev/null
+++ b/src/libnm-glib-aux/nm-enum-utils.c
@@ -0,0 +1,368 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-enum-utils.h"
+#include "nm-str-buf.h"
+
+/*****************************************************************************/
+
+#define IS_FLAGS_SEPARATOR(ch) (NM_IN_SET((ch), ' ', '\t', ',', '\n', '\r'))
+
+static void
+_ASSERT_enum_values_info(GType type, const NMUtilsEnumValueInfo *value_infos)
+{
+#if NM_MORE_ASSERTS > 5
+    nm_auto_unref_gtypeclass GTypeClass *klass = NULL;
+    gs_unref_hashtable GHashTable *ht          = NULL;
+
+    klass = g_type_class_ref(type);
+
+    g_assert(G_IS_ENUM_CLASS(klass) || G_IS_FLAGS_CLASS(klass));
+
+    if (!value_infos)
+        return;
+
+    ht = g_hash_table_new(g_str_hash, g_str_equal);
+
+    for (; value_infos->nick; value_infos++) {
+        g_assert(value_infos->nick[0]);
+
+        /* duplicate nicks make no sense!! */
+        g_assert(!g_hash_table_contains(ht, value_infos->nick));
+        g_hash_table_add(ht, (gpointer) value_infos->nick);
+
+        if (G_IS_ENUM_CLASS(klass)) {
+            GEnumValue *enum_value;
+
+            enum_value = g_enum_get_value_by_nick(G_ENUM_CLASS(klass), value_infos->nick);
+            if (enum_value) {
+                /* we do allow specifying the same name via @value_infos and @type.
+                 * That might make sense, if @type comes from a library where older versions
+                 * of the library don't yet support the value. In this case, the caller can
+                 * provide the nick via @value_infos, to support the older library version.
+                 * And then, when actually running against a newer library version where
+                 * @type knows the nick, we have this situation.
+                 *
+                 * Another reason for specifying a nick both in @value_infos and @type,
+                 * is to specify an alias which is not used with highest preference. For
+                 * example, if you add an alias "disabled" for "none" (both numerically
+                 * equal), then the first alias in @value_infos will be preferred over
+                 * the name from @type. So, to still use "none" as preferred name, you may
+                 * explicitly specify the "none" alias in @value_infos before "disabled".
+                 *
+                 * However, what never is allowed, is to use a name (nick) to re-number
+                 * the value. That is, if both @value_infos and @type contain a particular
+                 * nick, their numeric values must agree as well.
+                 * Allowing this, would be very confusing, because the name would have a different
+                 * value from the regular GLib GEnum API.
+                 */
+                g_assert(enum_value->value == value_infos->value);
+            }
+        } else {
+            GFlagsValue *flags_value;
+
+            flags_value = g_flags_get_value_by_nick(G_FLAGS_CLASS(klass), value_infos->nick);
+            if (flags_value) {
+                /* see ENUM case above. */
+                g_assert(flags_value->value == (guint) value_infos->value);
+            }
+        }
+    }
+#endif
+}
+
+static gboolean
+_is_hex_string(const char *str, gboolean allow_sign)
+{
+    if (allow_sign && str[0] == '-')
+        str++;
+    return str[0] == '0' && str[1] == 'x' && str[2]
+           && NM_STRCHAR_ALL(&str[2], ch, g_ascii_isxdigit(ch));
+}
+
+static gboolean
+_is_dec_string(const char *str, gboolean allow_sign)
+{
+    if (allow_sign && str[0] == '-')
+        str++;
+    return str[0] && NM_STRCHAR_ALL(&str[0], ch, g_ascii_isdigit(ch));
+}
+
+static gboolean
+_enum_is_valid_enum_nick(const char *str)
+{
+    return str[0] && !NM_STRCHAR_ANY(str, ch, g_ascii_isspace(ch)) && !_is_dec_string(str, TRUE)
+           && !_is_hex_string(str, TRUE);
+}
+
+static gboolean
+_enum_is_valid_flags_nick(const char *str)
+{
+    return str[0] && !NM_STRCHAR_ANY(str, ch, IS_FLAGS_SEPARATOR(ch)) && !_is_dec_string(str, FALSE)
+           && !_is_hex_string(str, FALSE);
+}
+
+char *
+_nm_utils_enum_to_str_full(GType                       type,
+                           int                         value,
+                           const char *                flags_separator,
+                           const NMUtilsEnumValueInfo *value_infos)
+{
+    nm_auto_unref_gtypeclass GTypeClass *klass = NULL;
+
+    _ASSERT_enum_values_info(type, value_infos);
+
+    if (flags_separator
+        && (!flags_separator[0] || NM_STRCHAR_ANY(flags_separator, ch, !IS_FLAGS_SEPARATOR(ch))))
+        g_return_val_if_reached(NULL);
+
+    klass = g_type_class_ref(type);
+
+    if (G_IS_ENUM_CLASS(klass)) {
+        GEnumValue *enum_value;
+
+        for (; value_infos && value_infos->nick; value_infos++) {
+            if (value_infos->value == value)
+                return g_strdup(value_infos->nick);
+        }
+
+        enum_value = g_enum_get_value(G_ENUM_CLASS(klass), value);
+        if (!enum_value || !_enum_is_valid_enum_nick(enum_value->value_nick))
+            return g_strdup_printf("%d", value);
+        else
+            return g_strdup(enum_value->value_nick);
+    } else if (G_IS_FLAGS_CLASS(klass)) {
+        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));
+
+            if (uvalue == 0) {
+                if (value_infos->value != 0)
+                    continue;
+            } else {
+                if (!NM_FLAGS_ALL(uvalue, (unsigned) value_infos->value))
+                    continue;
+            }
+
+            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. */
+                goto flags_done;
+            }
+        }
+
+        do {
+            flags_value = g_flags_get_first_value(G_FLAGS_CLASS(klass), uvalue);
+            if (strbuf.len)
+                nm_str_buf_append(&strbuf, flags_separator);
+            if (!flags_value || !_enum_is_valid_flags_nick(flags_value->value_nick)) {
+                if (uvalue)
+                    nm_str_buf_append_printf(&strbuf, "0x%x", uvalue);
+                break;
+            }
+            nm_str_buf_append(&strbuf, flags_value->value_nick);
+            uvalue &= ~flags_value->value;
+        } while (uvalue);
+
+flags_done:
+        return nm_str_buf_finalize(&strbuf, NULL);
+    }
+
+    g_return_val_if_reached(NULL);
+}
+
+static const NMUtilsEnumValueInfo *
+_find_value_info(const NMUtilsEnumValueInfo *value_infos, const char *needle)
+{
+    if (value_infos) {
+        for (; value_infos->nick; value_infos++) {
+            if (nm_streq(needle, value_infos->nick))
+                return value_infos;
+        }
+    }
+    return NULL;
+}
+
+gboolean
+_nm_utils_enum_from_str_full(GType                       type,
+                             const char *                str,
+                             int *                       out_value,
+                             char **                     err_token,
+                             const NMUtilsEnumValueInfo *value_infos)
+{
+    nm_auto_unref_gtypeclass GTypeClass *klass     = NULL;
+    gboolean                             ret       = FALSE;
+    int                                  value     = 0;
+    gs_free char *                       str_clone = NULL;
+    char *                               s;
+    gint64                               v64;
+    const NMUtilsEnumValueInfo *         nick;
+
+    g_return_val_if_fail(str, FALSE);
+
+    _ASSERT_enum_values_info(type, value_infos);
+
+    s = nm_strdup_maybe_a(300, nm_str_skip_leading_spaces(str), &str_clone);
+    g_strchomp(s);
+
+    klass = g_type_class_ref(type);
+
+    if (G_IS_ENUM_CLASS(klass)) {
+        GEnumValue *enum_value;
+
+        G_STATIC_ASSERT(G_MAXINT < G_MAXINT64);
+        G_STATIC_ASSERT(G_MININT > G_MININT64);
+
+        if (s[0]) {
+            if (_is_hex_string(s, TRUE)) {
+                if (s[0] == '-') {
+                    v64 = _nm_utils_ascii_str_to_int64(&s[3],
+                                                       16,
+                                                       -((gint64) G_MAXINT),
+                                                       -((gint64) G_MININT),
+                                                       G_MAXINT64);
+                    if (v64 != G_MAXINT64) {
+                        value = (int) (-v64);
+                        ret   = TRUE;
+                    }
+                } else {
+                    v64 = _nm_utils_ascii_str_to_int64(&s[2], 16, G_MININT, G_MAXINT, G_MAXINT64);
+                    if (v64 != G_MAXINT64) {
+                        value = (int) v64;
+                        ret   = TRUE;
+                    }
+                }
+            } else if (_is_dec_string(s, TRUE)) {
+                v64 = _nm_utils_ascii_str_to_int64(s, 10, G_MININT, G_MAXINT, G_MAXINT64);
+                if (v64 != G_MAXINT64) {
+                    value = (int) v64;
+                    ret   = TRUE;
+                }
+            } else if ((nick = _find_value_info(value_infos, s))) {
+                value = nick->value;
+                ret   = TRUE;
+            } else if ((enum_value = g_enum_get_value_by_nick(G_ENUM_CLASS(klass), s))) {
+                value = enum_value->value;
+                ret   = TRUE;
+            }
+        }
+    } else if (G_IS_FLAGS_CLASS(klass)) {
+        GFlagsValue *flags_value;
+        unsigned     uvalue = 0;
+
+        ret = TRUE;
+        while (s[0]) {
+            char *s_end;
+
+            for (s_end = s; s_end[0]; s_end++) {
+                if (IS_FLAGS_SEPARATOR(s_end[0])) {
+                    s_end[0] = '\0';
+                    s_end++;
+                    break;
+                }
+            }
+
+            if (s[0]) {
+                if (_is_hex_string(s, FALSE)) {
+                    v64 = _nm_utils_ascii_str_to_int64(&s[2], 16, 0, G_MAXUINT, -1);
+                    if (v64 == -1) {
+                        ret = FALSE;
+                        break;
+                    }
+                    uvalue |= (unsigned) v64;
+                } else if (_is_dec_string(s, FALSE)) {
+                    v64 = _nm_utils_ascii_str_to_int64(s, 10, 0, G_MAXUINT, -1);
+                    if (v64 == -1) {
+                        ret = FALSE;
+                        break;
+                    }
+                    uvalue |= (unsigned) v64;
+                } else if ((nick = _find_value_info(value_infos, s)))
+                    uvalue |= (unsigned) nick->value;
+                else if ((flags_value = g_flags_get_value_by_nick(G_FLAGS_CLASS(klass), s)))
+                    uvalue |= flags_value->value;
+                else {
+                    ret = FALSE;
+                    break;
+                }
+            }
+
+            s = s_end;
+        }
+
+        value = (int) uvalue;
+    } else
+        g_return_val_if_reached(FALSE);
+
+    NM_SET_OUT(err_token, !ret && s[0] ? g_strdup(s) : NULL);
+    NM_SET_OUT(out_value, ret ? value : 0);
+    return ret;
+}
+
+const char **
+_nm_utils_enum_get_values(GType type, int from, int to)
+{
+    GTypeClass *klass;
+    GPtrArray * array;
+    int         i;
+    char        sbuf[64];
+
+    klass = g_type_class_ref(type);
+    array = g_ptr_array_new();
+
+    if (G_IS_ENUM_CLASS(klass)) {
+        GEnumClass *enum_class = G_ENUM_CLASS(klass);
+        GEnumValue *enum_value;
+
+        for (i = 0; i < enum_class->n_values; i++) {
+            enum_value = &enum_class->values[i];
+            if (enum_value->value >= from && enum_value->value <= to) {
+                if (_enum_is_valid_enum_nick(enum_value->value_nick))
+                    g_ptr_array_add(array, (gpointer) enum_value->value_nick);
+                else
+                    g_ptr_array_add(
+                        array,
+                        (gpointer) g_intern_string(nm_sprintf_buf(sbuf, "%d", enum_value->value)));
+            }
+        }
+    } else if (G_IS_FLAGS_CLASS(klass)) {
+        GFlagsClass *flags_class = G_FLAGS_CLASS(klass);
+        GFlagsValue *flags_value;
+
+        for (i = 0; i < flags_class->n_values; i++) {
+            flags_value = &flags_class->values[i];
+            if (flags_value->value >= (guint) from && flags_value->value <= (guint) to) {
+                if (_enum_is_valid_flags_nick(flags_value->value_nick))
+                    g_ptr_array_add(array, (gpointer) flags_value->value_nick);
+                else
+                    g_ptr_array_add(
+                        array,
+                        (gpointer) g_intern_string(
+                            nm_sprintf_buf(sbuf, "0x%x", (unsigned) flags_value->value)));
+            }
+        }
+    } else {
+        g_type_class_unref(klass);
+        g_ptr_array_free(array, TRUE);
+        g_return_val_if_reached(NULL);
+    }
+
+    g_type_class_unref(klass);
+    g_ptr_array_add(array, NULL);
+
+    return (const char **) g_ptr_array_free(array, FALSE);
+}
diff --git a/src/libnm-glib-aux/nm-enum-utils.h b/src/libnm-glib-aux/nm-enum-utils.h
new file mode 100644
index 00000000..89be54e7
--- /dev/null
+++ b/src/libnm-glib-aux/nm-enum-utils.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NM_ENUM_UTILS_H__
+#define __NM_ENUM_UTILS_H__
+
+/*****************************************************************************/
+
+typedef struct _NMUtilsEnumValueInfo {
+    /* currently, this is only used for _nm_utils_enum_from_str_full() to
+     * declare additional aliases for values. */
+    const char *nick;
+    int         value;
+} NMUtilsEnumValueInfo;
+
+char *   _nm_utils_enum_to_str_full(GType                       type,
+                                    int                         value,
+                                    const char *                sep,
+                                    const NMUtilsEnumValueInfo *value_infos);
+gboolean _nm_utils_enum_from_str_full(GType                       type,
+                                      const char *                str,
+                                      int *                       out_value,
+                                      char **                     err_token,
+                                      const NMUtilsEnumValueInfo *value_infos);
+
+const char **_nm_utils_enum_get_values(GType type, int from, int to);
+
+/*****************************************************************************/
+
+#endif /* __NM_ENUM_UTILS_H__ */
diff --git a/src/libnm-glib-aux/nm-errno.c b/src/libnm-glib-aux/nm-errno.c
new file mode 100644
index 00000000..0426a21d
--- /dev/null
+++ b/src/libnm-glib-aux/nm-errno.c
@@ -0,0 +1,169 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-errno.h"
+
+/*****************************************************************************/
+
+static NM_UTILS_LOOKUP_STR_DEFINE(
+    _geterror,
+#if 0
+    enum _NMErrno,
+#else
+    int,
+#endif
+    NM_UTILS_LOOKUP_DEFAULT(NULL),
+
+    NM_UTILS_LOOKUP_STR_ITEM(NME_ERRNO_SUCCESS, "NME_ERRNO_SUCCESS"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_ERRNO_OUT_OF_RANGE, "NME_ERRNO_OUT_OF_RANGE"),
+
+    NM_UTILS_LOOKUP_STR_ITEM(NME_UNSPEC, "NME_UNSPEC"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_BUG, "NME_BUG"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_NATIVE_ERRNO, "NME_NATIVE_ERRNO"),
+
+    NM_UTILS_LOOKUP_STR_ITEM(NME_NL_ATTRSIZE, "NME_NL_ATTRSIZE"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_NL_BAD_SOCK, "NME_NL_BAD_SOCK"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_NL_DUMP_INTR, "NME_NL_DUMP_INTR"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_NL_MSG_OVERFLOW, "NME_NL_MSG_OVERFLOW"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_NL_MSG_TOOSHORT, "NME_NL_MSG_TOOSHORT"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_NL_MSG_TRUNC, "NME_NL_MSG_TRUNC"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_NL_SEQ_MISMATCH, "NME_NL_SEQ_MISMATCH"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_NL_NOADDR, "NME_NL_NOADDR"),
+
+    NM_UTILS_LOOKUP_STR_ITEM(NME_PL_NOT_FOUND, "not-found"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_PL_EXISTS, "exists"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_PL_WRONG_TYPE, "wrong-type"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_PL_NOT_SLAVE, "not-slave"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_PL_NO_FIRMWARE, "no-firmware"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_PL_OPNOTSUPP, "not-supported"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_PL_NETLINK, "netlink"),
+    NM_UTILS_LOOKUP_STR_ITEM(NME_PL_CANT_SET_MTU, "cant-set-mtu"),
+
+    NM_UTILS_LOOKUP_ITEM_IGNORE(_NM_ERRNO_MININT),
+    NM_UTILS_LOOKUP_ITEM_IGNORE(_NM_ERRNO_RESERVED_LAST_PLUS_1), );
+
+/**
+ * nm_strerror():
+ * @nmerr: the NetworkManager specific errno to be converted
+ *   to string.
+ *
+ * NetworkManager specific error numbers reserve a range in "errno.h" with
+ * our own defines. For numbers that don't fall into this range, the numbers
+ * are identical to the common error numbers.
+ *
+ * Identical to strerror(), g_strerror(), nm_strerror_native() for error numbers
+ * that are not in the reserved range of NetworkManager specific errors.
+ *
+ * Returns: (transfer none): the string representation of the error number.
+ */
+const char *
+nm_strerror(int nmerr)
+{
+    const char *s;
+
+    nmerr = nm_errno(nmerr);
+
+    if (nmerr >= _NM_ERRNO_RESERVED_FIRST) {
+        s = _geterror(nmerr);
+        if (s)
+            return s;
+    }
+    return nm_strerror_native(nmerr);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_strerror_native_r:
+ * @errsv: the errno to convert to string.
+ * @buf: the output buffer where to write the string to.
+ * @buf_size: the length of buffer.
+ *
+ * This is like strerror_r(), with one difference: depending on the
+ * locale, the returned string is guaranteed to be valid UTF-8.
+ * Also, there is some confusion as to whether to use glibc's
+ * strerror_r() or the POXIX/XSI variant. This is abstracted
+ * by the function.
+ *
+ * Note that the returned buffer may also be a statically allocated
+ * buffer, and not the input buffer @buf. Consequently, the returned
+ * string may be longer than @buf_size.
+ *
+ * Returns: (transfer none): a NUL terminated error message. This is either a static
+ *   string (that is never freed), or the provided @buf argument.
+ */
+const char *
+nm_strerror_native_r(int errsv, char *buf, gsize buf_size)
+{
+    char *buf2;
+
+    nm_assert(buf);
+    nm_assert(buf_size > 0);
+
+#if (!defined(__GLIBC__) && !defined(__UCLIBC__)) || ((_POSIX_C_SOURCE >= 200112L) && !_GNU_SOURCE)
+    /* XSI-compliant */
+    {
+        int errno_saved = errno;
+
+        if (strerror_r(errsv, buf, buf_size) != 0) {
+            g_snprintf(buf, buf_size, "Unspecified errno %d", errsv);
+            errno = errno_saved;
+        }
+        buf2 = buf;
+    }
+#else
+    /* GNU-specific */
+    buf2 = strerror_r(errsv, buf, buf_size);
+#endif
+
+    /* like g_strerror(), ensure that the error message is UTF-8. */
+    if (!g_get_charset(NULL) && !g_utf8_validate(buf2, -1, NULL)) {
+        gs_free char *msg = NULL;
+
+        msg = g_locale_to_utf8(buf2, -1, NULL, NULL, NULL);
+        if (msg) {
+            g_strlcpy(buf, msg, buf_size);
+            buf2 = buf;
+        }
+    }
+
+    return buf2;
+}
+
+/**
+ * nm_strerror_native:
+ * @errsv: the errno integer from <errno.h>
+ *
+ * Like strerror(), but strerror() is not thread-safe and not guaranteed
+ * to be UTF-8.
+ *
+ * g_strerror() is a thread-safe variant of strerror(), however it caches
+ * all returned strings in a dictionary. That means, using this on untrusted
+ * error numbers can result in this cache to grow without limits.
+ *
+ * Instead, return a tread-local buffer. This way, it's thread-safe.
+ *
+ * There is a downside to this: subsequent calls of nm_strerror_native()
+ * overwrite the error message.
+ *
+ * Returns: (transfer none): the text representation of the error number.
+ */
+const char *
+nm_strerror_native(int errsv)
+{
+    static _nm_thread_local char *buf_static = NULL;
+    char *                        buf;
+
+    buf = buf_static;
+    if (G_UNLIKELY(!buf)) {
+        buf        = g_malloc(NM_STRERROR_BUFSIZE);
+        buf_static = buf;
+        nm_utils_thread_local_register_destroy(buf, g_free);
+    }
+
+    return nm_strerror_native_r(errsv, buf, NM_STRERROR_BUFSIZE);
+}
diff --git a/src/libnm-glib-aux/nm-errno.h b/src/libnm-glib-aux/nm-errno.h
new file mode 100644
index 00000000..62c8379f
--- /dev/null
+++ b/src/libnm-glib-aux/nm-errno.h
@@ -0,0 +1,171 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#ifndef __NM_ERRNO_H__
+#define __NM_ERRNO_H__
+
+#include <errno.h>
+
+/*****************************************************************************/
+
+enum _NMErrno {
+    _NM_ERRNO_MININT         = G_MININT,
+    _NM_ERRNO_MAXINT         = G_MAXINT,
+    _NM_ERRNO_RESERVED_FIRST = 100000,
+
+    /* when we cannot represent a number as positive number, we resort to this
+     * number. Basically, the values G_MININT, -NME_ERRNO_SUCCESS, NME_ERRNO_SUCCESS
+     * and G_MAXINT all map to the same value. */
+    NME_ERRNO_OUT_OF_RANGE = G_MAXINT,
+
+    /* Indicate that the original errno was zero. Zero denotes *no error*, but we know something
+     * went wrong and we want to report some error. This is a placeholder to mean, something
+     * was wrong, but errno was zero. */
+    NME_ERRNO_SUCCESS = G_MAXINT - 1,
+
+    /* an unspecified error. */
+    NME_UNSPEC = _NM_ERRNO_RESERVED_FIRST,
+
+    /* A bug, for example when an assertion failed.
+     * Should never happen. */
+    NME_BUG,
+
+    /* a native error number (from <errno.h>) cannot be mapped as
+     * an nm-error, because it is in the range [_NM_ERRNO_RESERVED_FIRST,
+     * _NM_ERRNO_RESERVED_LAST]. */
+    NME_NATIVE_ERRNO,
+
+    /* netlink errors. */
+    NME_NL_SEQ_MISMATCH,
+    NME_NL_MSG_TRUNC,
+    NME_NL_MSG_TOOSHORT,
+    NME_NL_DUMP_INTR,
+    NME_NL_ATTRSIZE,
+    NME_NL_BAD_SOCK,
+    NME_NL_NOADDR,
+    NME_NL_MSG_OVERFLOW,
+
+    /* platform errors. */
+    NME_PL_NOT_FOUND,
+    NME_PL_EXISTS,
+    NME_PL_WRONG_TYPE,
+    NME_PL_NOT_SLAVE,
+    NME_PL_NO_FIRMWARE,
+    NME_PL_OPNOTSUPP,
+    NME_PL_NETLINK,
+    NME_PL_CANT_SET_MTU,
+
+    _NM_ERRNO_RESERVED_LAST_PLUS_1,
+    _NM_ERRNO_RESERVED_LAST = _NM_ERRNO_RESERVED_LAST_PLUS_1 - 1,
+};
+
+/*****************************************************************************/
+
+/* When we receive an errno from a system function, we can safely assume
+ * that the error number is not negative. We rely on that, and possibly just
+ * "return -errsv;" to signal an error. We also rely on that, because libc
+ * is our trusted base: meaning, if it cannot even succeed at setting errno
+ * according to specification, all bets are off.
+ *
+ * This macro returns the input argument, and asserts that the error variable
+ * is positive.
+ *
+ * In a sense, the macro is related to nm_errno_native() function, but the difference
+ * is that this macro asserts that @errsv is positive, while nm_errno_native() coerces
+ * negative values to be non-negative. */
+#define NM_ERRNO_NATIVE(errsv)        \
+    ({                                \
+        const int _errsv_x = (errsv); \
+                                      \
+        nm_assert(_errsv_x > 0);      \
+        _errsv_x;                     \
+    })
+
+/* Normalize native errno.
+ *
+ * Our API may return native error codes (<errno.h>) as negative values. This function
+ * takes such an errno, and normalizes it to their positive value.
+ *
+ * The special values G_MININT and zero are coerced to NME_ERRNO_OUT_OF_RANGE and NME_ERRNO_SUCCESS
+ * respectively.
+ * Other values are coerced to their inverse.
+ * Other positive values are returned unchanged.
+ *
+ * Basically, this normalizes errsv to be positive (taking care of two pathological cases).
+ */
+static inline int
+nm_errno_native(int errsv)
+{
+    switch (errsv) {
+    case 0:
+        return NME_ERRNO_SUCCESS;
+    case G_MININT:
+        return NME_ERRNO_OUT_OF_RANGE;
+    default:
+        return errsv >= 0 ? errsv : -errsv;
+    }
+}
+
+/* Normalizes an nm-error to be positive.
+ *
+ * Various API returns negative error codes, and this function converts the negative
+ * value to its positive.
+ *
+ * Note that @nmerr is on the domain of NetworkManager specific error numbers,
+ * which is not the same as the native error numbers (errsv from <errno.h>). But
+ * as far as normalizing goes, nm_errno() does exactly the same remapping as
+ * nm_errno_native(). */
+static inline int
+nm_errno(int nmerr)
+{
+    return nm_errno_native(nmerr);
+}
+
+/* this maps a native errno to a (always non-negative) nm-error number.
+ *
+ * Note that nm-error numbers are embedded into the range of regular
+ * errno. The only difference is, that nm-error numbers reserve a
+ * range (_NM_ERRNO_RESERVED_FIRST, _NM_ERRNO_RESERVED_LAST) for their
+ * own purpose.
+ *
+ * That means, converting an errno to nm-error number means in
+ * most cases just returning itself.
+ * Only pathological cases need special handling:
+ *
+ *  - 0 is mapped to NME_ERRNO_SUCCESS;
+ *  - G_MININT is mapped to NME_ERRNO_OUT_OF_RANGE;
+ *  - values in the range of (+/-) [_NM_ERRNO_RESERVED_FIRST, _NM_ERRNO_RESERVED_LAST]
+ *    are mapped to NME_NATIVE_ERRNO
+ *  - all other values are their (positive) absolute value.
+ */
+static inline int
+nm_errno_from_native(int errsv)
+{
+    switch (errsv) {
+    case 0:
+        return NME_ERRNO_SUCCESS;
+    case G_MININT:
+        return NME_ERRNO_OUT_OF_RANGE;
+    default:
+        if (errsv < 0)
+            errsv = -errsv;
+        return G_UNLIKELY(errsv >= _NM_ERRNO_RESERVED_FIRST && errsv <= _NM_ERRNO_RESERVED_LAST)
+                   ? NME_NATIVE_ERRNO
+                   : errsv;
+    }
+}
+
+const char *nm_strerror(int nmerr);
+
+/*****************************************************************************/
+
+#define NM_STRERROR_BUFSIZE 1024
+
+const char *nm_strerror_native_r(int errsv, char *buf, gsize buf_size);
+const char *nm_strerror_native(int errsv);
+
+/*****************************************************************************/
+
+#endif /* __NM_ERRNO_H__ */
diff --git a/src/libnm-glib-aux/nm-gassert-patch.h b/src/libnm-glib-aux/nm-gassert-patch.h
new file mode 100644
index 00000000..bac8697c
--- /dev/null
+++ b/src/libnm-glib-aux/nm-gassert-patch.h
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ */
+
+#ifndef __NM_GASSERT_PATCH_H__
+#define __NM_GASSERT_PATCH_H__
+
+/*****************************************************************************/
+
+#if NM_MORE_ASSERTS == 0
+
+/* glib assertions (g_return_*(), g_assert*()) contain a textual representation
+ * of the checked statement. This part of the assertion blows up the size of the
+ * binary. Unless we compile a debug-build with NM_MORE_ASSERTS, drop these
+ * parts. Note that the failed assertion still prints the file and line where the
+ * assertion fails. That shall suffice. */
+
+static inline void
+_nm_g_return_if_fail_warning(const char *log_domain, const char *file, int line)
+{
+    char file_buf[256 + 15];
+
+    g_snprintf(file_buf, sizeof(file_buf), "((%s:%d))", file, line);
+    g_return_if_fail_warning(log_domain, file_buf, "<dropped>");
+}
+
+    #define g_return_if_fail_warning(log_domain, pretty_function, expression) \
+        _nm_g_return_if_fail_warning(log_domain, __FILE__, __LINE__)
+
+    #define g_assertion_message_expr(domain, file, line, func, expr) \
+        g_assertion_message_expr(domain, file, line, "<unknown-fcn>", (expr) ? "<dropped>" : NULL)
+
+    #undef g_return_val_if_reached
+    #define g_return_val_if_reached(val)                          \
+        G_STMT_START                                              \
+        {                                                         \
+            g_log(G_LOG_DOMAIN,                                   \
+                  G_LOG_LEVEL_CRITICAL,                           \
+                  "file %s: line %d (%s): should not be reached", \
+                  __FILE__,                                       \
+                  __LINE__,                                       \
+                  "<dropped>");                                   \
+            return (val);                                         \
+        }                                                         \
+        G_STMT_END
+
+    #undef g_return_if_reached
+    #define g_return_if_reached()                                 \
+        G_STMT_START                                              \
+        {                                                         \
+            g_log(G_LOG_DOMAIN,                                   \
+                  G_LOG_LEVEL_CRITICAL,                           \
+                  "file %s: line %d (%s): should not be reached", \
+                  __FILE__,                                       \
+                  __LINE__,                                       \
+                  "<dropped>");                                   \
+            return;                                               \
+        }                                                         \
+        G_STMT_END
+#endif
+
+/*****************************************************************************/
+
+#if NM_MORE_ASSERTS == 0
+    #define NM_ASSERT_G_RETURN_EXPR(expr) "<dropped>"
+    #define NM_ASSERT_NO_MSG              1
+
+#else
+    #define NM_ASSERT_G_RETURN_EXPR(expr) "" expr ""
+    #define NM_ASSERT_NO_MSG              0
+#endif
+
+/*****************************************************************************/
+
+#endif /* __NM_GASSERT_PATCH_H__ */
diff --git a/src/libnm-glib-aux/nm-glib.h b/src/libnm-glib-aux/nm-glib.h
new file mode 100644
index 00000000..66e5c16e
--- /dev/null
+++ b/src/libnm-glib-aux/nm-glib.h
@@ -0,0 +1,710 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2008 - 2018 Red Hat, Inc.
+ */
+
+#ifndef __NM_GLIB_H__
+#define __NM_GLIB_H__
+
+/*****************************************************************************/
+
+#ifndef __NM_MACROS_INTERNAL_H__
+    #error "nm-glib.h requires nm-macros-internal.h. Do not include this directly"
+#endif
+
+/*****************************************************************************/
+
+#ifdef __clang__
+
+    #undef G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+    #undef G_GNUC_END_IGNORE_DEPRECATIONS
+
+    #define G_GNUC_BEGIN_IGNORE_DEPRECATIONS \
+        _Pragma("clang diagnostic push")     \
+            _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"")
+
+    #define G_GNUC_END_IGNORE_DEPRECATIONS _Pragma("clang diagnostic pop")
+
+#endif
+
+/*****************************************************************************/
+
+static inline void
+__g_type_ensure(GType type)
+{
+#if !GLIB_CHECK_VERSION(2, 34, 0)
+    if (G_UNLIKELY(type == (GType) -1))
+        g_error("can't happen");
+#else
+    G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
+    g_type_ensure(type);
+    G_GNUC_END_IGNORE_DEPRECATIONS;
+#endif
+}
+#define g_type_ensure __g_type_ensure
+
+/*****************************************************************************/
+
+#if !GLIB_CHECK_VERSION(2, 34, 0)
+
+    #define g_clear_pointer(pp, destroy)                           \
+        G_STMT_START                                               \
+        {                                                          \
+            G_STATIC_ASSERT(sizeof *(pp) == sizeof(gpointer));     \
+            /* Only one access, please */                          \
+            gpointer *_pp = (gpointer *) (pp);                     \
+            gpointer  _p;                                          \
+            /* This assignment is needed to avoid a gcc warning */ \
+            GDestroyNotify _destroy = (GDestroyNotify) (destroy);  \
+                                                                   \
+            _p = *_pp;                                             \
+            if (_p) {                                              \
+                *_pp = NULL;                                       \
+                _destroy(_p);                                      \
+            }                                                      \
+        }                                                          \
+        G_STMT_END
+
+#endif
+
+/*****************************************************************************/
+
+#if !GLIB_CHECK_VERSION(2, 34, 0)
+
+    /* These are used to clean up the output of test programs; we can just let
+ * them no-op in older glib.
+ */
+    #define g_test_expect_message(log_domain, log_level, pattern)
+    #define g_test_assert_expected_messages()
+
+#else
+
+    /* We build with -DGLIB_MAX_ALLOWED_VERSION set to 2.32 to make sure we don't
+ * accidentally use new API that we shouldn't. But we don't want warnings for
+ * the APIs that we emulate above.
+ */
+
+    #define g_test_expect_message(domain, level, format...) \
+        G_STMT_START                                        \
+        {                                                   \
+            G_GNUC_BEGIN_IGNORE_DEPRECATIONS                \
+            g_test_expect_message(domain, level, format);   \
+            G_GNUC_END_IGNORE_DEPRECATIONS                  \
+        }                                                   \
+        G_STMT_END
+
+    #define g_test_assert_expected_messages_internal(domain, file, line, func)  \
+        G_STMT_START                                                            \
+        {                                                                       \
+            G_GNUC_BEGIN_IGNORE_DEPRECATIONS                                    \
+            g_test_assert_expected_messages_internal(domain, file, line, func); \
+            G_GNUC_END_IGNORE_DEPRECATIONS                                      \
+        }                                                                       \
+        G_STMT_END
+
+#endif
+
+/*****************************************************************************/
+
+#if GLIB_CHECK_VERSION(2, 35, 0)
+    /* For glib >= 2.36, g_type_init() is deprecated.
+ * But since 2.35.1 (7c42ab23b55c43ab96d0ac2124b550bf1f49c1ec) this function
+ * does nothing. Replace the call with empty statement. */
+    #define nm_g_type_init() \
+        G_STMT_START         \
+        {                    \
+            (void) 0;        \
+        }                    \
+        G_STMT_END
+#else
+    #define nm_g_type_init() \
+        G_STMT_START         \
+        {                    \
+            g_type_init();   \
+        }                    \
+        G_STMT_END
+#endif
+
+/*****************************************************************************/
+
+/* g_test_initialized() is only available since glib 2.36. */
+#if !GLIB_CHECK_VERSION(2, 36, 0)
+    #define g_test_initialized() (g_test_config_vars->test_initialized)
+#endif
+
+/*****************************************************************************/
+
+/* g_assert_cmpmem() is only available since glib 2.46. */
+#if !GLIB_CHECK_VERSION(2, 45, 7)
+    #define g_assert_cmpmem(m1, l1, m2, l2)                                                 \
+        G_STMT_START                                                                        \
+        {                                                                                   \
+            gconstpointer __m1 = m1, __m2 = m2;                                             \
+            int           __l1 = l1, __l2 = l2;                                             \
+            if (__l1 != __l2)                                                               \
+                g_assertion_message_cmpnum(G_LOG_DOMAIN,                                    \
+                                           __FILE__,                                        \
+                                           __LINE__,                                        \
+                                           G_STRFUNC,                                       \
+                                           #l1 " (len(" #m1 ")) == " #l2 " (len(" #m2 "))", \
+                                           __l1,                                            \
+                                           "==",                                            \
+                                           __l2,                                            \
+                                           'i');                                            \
+            else if (memcmp(__m1, __m2, __l1) != 0)                                         \
+                g_assertion_message(G_LOG_DOMAIN,                                           \
+                                    __FILE__,                                               \
+                                    __LINE__,                                               \
+                                    G_STRFUNC,                                              \
+                                    "assertion failed (" #m1 " == " #m2 ")");               \
+        }                                                                                   \
+        G_STMT_END
+#endif
+
+/*****************************************************************************/
+
+/* Rumtime check for glib version. First do a compile time check which
+ * (if satisfied) shortcuts the runtime check. */
+static inline gboolean
+nm_glib_check_version(guint major, guint minor, guint micro)
+{
+    return GLIB_CHECK_VERSION(major, minor, micro)
+           || ((glib_major_version > major)
+               || (glib_major_version == major && glib_minor_version > minor)
+               || (glib_major_version == major && glib_minor_version == minor
+                   && glib_micro_version < micro));
+}
+
+/*****************************************************************************/
+
+/* g_test_skip() is only available since glib 2.38. Add a compatibility wrapper. */
+static inline void
+__nmtst_g_test_skip(const char *msg)
+{
+#if GLIB_CHECK_VERSION(2, 38, 0)
+    G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+    g_test_skip(msg);
+    G_GNUC_END_IGNORE_DEPRECATIONS
+#else
+    g_debug("%s", msg);
+#endif
+}
+#define g_test_skip __nmtst_g_test_skip
+
+/*****************************************************************************/
+
+/* g_test_add_data_func_full() is only available since glib 2.34. Add a compatibility wrapper. */
+static inline void
+__g_test_add_data_func_full(const char *   testpath,
+                            gpointer       test_data,
+                            GTestDataFunc  test_func,
+                            GDestroyNotify data_free_func)
+{
+#if GLIB_CHECK_VERSION(2, 34, 0)
+    G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+    g_test_add_data_func_full(testpath, test_data, test_func, data_free_func);
+    G_GNUC_END_IGNORE_DEPRECATIONS
+#else
+    g_return_if_fail(testpath != NULL);
+    g_return_if_fail(testpath[0] == '/');
+    g_return_if_fail(test_func != NULL);
+
+    g_test_add_vtable(testpath,
+                      0,
+                      test_data,
+                      NULL,
+                      (GTestFixtureFunc) test_func,
+                      (GTestFixtureFunc) data_free_func);
+#endif
+}
+#define g_test_add_data_func_full __g_test_add_data_func_full
+
+/*****************************************************************************/
+
+static inline gboolean
+nm_g_hash_table_replace(GHashTable *hash, gpointer key, gpointer value)
+{
+    /* glib 2.40 added a return value indicating whether the key already existed
+     * (910191597a6c2e5d5d460e9ce9efb4f47d9cc63c). */
+#if GLIB_CHECK_VERSION(2, 40, 0)
+    return g_hash_table_replace(hash, key, value);
+#else
+    gboolean contained = g_hash_table_contains(hash, key);
+
+    g_hash_table_replace(hash, key, value);
+    return !contained;
+#endif
+}
+
+static inline gboolean
+nm_g_hash_table_insert(GHashTable *hash, gpointer key, gpointer value)
+{
+    /* glib 2.40 added a return value indicating whether the key already existed
+     * (910191597a6c2e5d5d460e9ce9efb4f47d9cc63c). */
+#if GLIB_CHECK_VERSION(2, 40, 0)
+    return g_hash_table_insert(hash, key, value);
+#else
+    gboolean contained = g_hash_table_contains(hash, key);
+
+    g_hash_table_insert(hash, key, value);
+    return !contained;
+#endif
+}
+
+static inline gboolean
+nm_g_hash_table_add(GHashTable *hash, gpointer key)
+{
+    /* glib 2.40 added a return value indicating whether the key already existed
+     * (910191597a6c2e5d5d460e9ce9efb4f47d9cc63c). */
+#if GLIB_CHECK_VERSION(2, 40, 0)
+    return g_hash_table_add(hash, key);
+#else
+    gboolean contained = g_hash_table_contains(hash, key);
+
+    g_hash_table_add(hash, key);
+    return !contained;
+#endif
+}
+
+/*****************************************************************************/
+
+#if !GLIB_CHECK_VERSION(2, 40, 0) || defined(NM_GLIB_COMPAT_H_TEST)
+static inline void
+_nm_g_ptr_array_insert(GPtrArray *array, int index_, gpointer data)
+{
+    g_return_if_fail(array);
+    g_return_if_fail(index_ >= -1);
+    g_return_if_fail(index_ <= (int) array->len);
+
+    g_ptr_array_add(array, data);
+
+    if (index_ != -1 && index_ != (int) (array->len - 1)) {
+        memmove(&(array->pdata[index_ + 1]),
+                &(array->pdata[index_]),
+                (array->len - index_ - 1) * sizeof(gpointer));
+        array->pdata[index_] = data;
+    }
+}
+#endif
+
+#if !GLIB_CHECK_VERSION(2, 40, 0)
+    #define g_ptr_array_insert(array, index, data)      \
+        G_STMT_START                                    \
+        {                                               \
+            _nm_g_ptr_array_insert(array, index, data); \
+        }                                               \
+        G_STMT_END
+#else
+    #define g_ptr_array_insert(array, index, data)  \
+        G_STMT_START                                \
+        {                                           \
+            G_GNUC_BEGIN_IGNORE_DEPRECATIONS        \
+            g_ptr_array_insert(array, index, data); \
+            G_GNUC_END_IGNORE_DEPRECATIONS          \
+        }                                           \
+        G_STMT_END
+#endif
+
+/*****************************************************************************/
+
+#if !GLIB_CHECK_VERSION(2, 40, 0)
+static inline gboolean
+_g_key_file_save_to_file(GKeyFile *key_file, const char *filename, GError **error)
+{
+    char *   contents;
+    gboolean success;
+    gsize    length;
+
+    g_return_val_if_fail(key_file != NULL, FALSE);
+    g_return_val_if_fail(filename != NULL, FALSE);
+    g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
+
+    contents = g_key_file_to_data(key_file, &length, NULL);
+    g_assert(contents != NULL);
+
+    success = g_file_set_contents(filename, contents, length, error);
+    g_free(contents);
+
+    return success;
+}
+    #define g_key_file_save_to_file(key_file, filename, error) \
+        _g_key_file_save_to_file(key_file, filename, error)
+#else
+    #define g_key_file_save_to_file(key_file, filename, error)             \
+        ({                                                                 \
+            gboolean _success;                                             \
+                                                                           \
+            G_GNUC_BEGIN_IGNORE_DEPRECATIONS                               \
+            _success = g_key_file_save_to_file(key_file, filename, error); \
+            G_GNUC_END_IGNORE_DEPRECATIONS                                 \
+            _success;                                                      \
+        })
+#endif
+
+/*****************************************************************************/
+
+#if GLIB_CHECK_VERSION(2, 36, 0)
+    #define g_credentials_get_unix_pid(creds, error)                                        \
+        ({                                                                                  \
+            G_GNUC_BEGIN_IGNORE_DEPRECATIONS(g_credentials_get_unix_pid)((creds), (error)); \
+            G_GNUC_END_IGNORE_DEPRECATIONS                                                  \
+        })
+#else
+    #define g_credentials_get_unix_pid(creds, error)                                          \
+        ({                                                                                    \
+            struct ucred *native_creds;                                                       \
+                                                                                              \
+            native_creds = g_credentials_get_native((creds), G_CREDENTIALS_TYPE_LINUX_UCRED); \
+            g_assert(native_creds);                                                           \
+            native_creds->pid;                                                                \
+        })
+#endif
+
+/*****************************************************************************/
+
+#if !GLIB_CHECK_VERSION(2, 40, 0) || defined(NM_GLIB_COMPAT_H_TEST)
+static inline gpointer *
+_nm_g_hash_table_get_keys_as_array(GHashTable *hash_table, guint *length)
+{
+    GHashTableIter iter;
+    gpointer       key, *ret;
+    guint          i = 0;
+
+    g_return_val_if_fail(hash_table, NULL);
+
+    ret = g_new0(gpointer, g_hash_table_size(hash_table) + 1);
+    g_hash_table_iter_init(&iter, hash_table);
+
+    while (g_hash_table_iter_next(&iter, &key, NULL))
+        ret[i++] = key;
+
+    ret[i] = NULL;
+
+    if (length)
+        *length = i;
+
+    return ret;
+}
+#endif
+#if !GLIB_CHECK_VERSION(2, 40, 0)
+    #define g_hash_table_get_keys_as_array(hash_table, length) \
+        ({ _nm_g_hash_table_get_keys_as_array(hash_table, length); })
+#else
+    #define g_hash_table_get_keys_as_array(hash_table, length)               \
+        ({                                                                   \
+            G_GNUC_BEGIN_IGNORE_DEPRECATIONS(g_hash_table_get_keys_as_array) \
+            ((hash_table), (length));                                        \
+            G_GNUC_END_IGNORE_DEPRECATIONS                                   \
+        })
+#endif
+
+/*****************************************************************************/
+
+#ifndef g_info
+    /* g_info was only added with 2.39.2 */
+    #define g_info(...) g_log(G_LOG_DOMAIN, G_LOG_LEVEL_INFO, __VA_ARGS__)
+#endif
+
+/*****************************************************************************/
+
+#ifdef g_steal_pointer
+    #undef g_steal_pointer
+#endif
+
+#define g_steal_pointer(pp)                                \
+    ({                                                     \
+        typeof(*(pp)) *const         _pp           = (pp); \
+        typeof(*_pp)                 _p            = *_pp; \
+        _nm_unused const void *const _p_type_check = _p;   \
+                                                           \
+        *_pp = NULL;                                       \
+        _p;                                                \
+    })
+
+/*****************************************************************************/
+
+static inline gboolean
+_nm_g_strv_contains(const char *const *strv, const char *str)
+{
+#if !GLIB_CHECK_VERSION(2, 44, 0)
+    g_return_val_if_fail(strv != NULL, FALSE);
+    g_return_val_if_fail(str != NULL, FALSE);
+
+    for (; *strv != NULL; strv++) {
+        if (g_str_equal(str, *strv))
+            return TRUE;
+    }
+
+    return FALSE;
+#else
+    G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+    return g_strv_contains(strv, str);
+    G_GNUC_END_IGNORE_DEPRECATIONS
+#endif
+}
+#define g_strv_contains _nm_g_strv_contains
+
+/*****************************************************************************/
+
+static inline GVariant *
+_nm_g_variant_new_take_string(char *string)
+{
+#if !GLIB_CHECK_VERSION(2, 36, 0)
+    GVariant *value;
+
+    g_return_val_if_fail(string != NULL, NULL);
+    g_return_val_if_fail(g_utf8_validate(string, -1, NULL), NULL);
+
+    value = g_variant_new_string(string);
+    g_free(string);
+    return value;
+#elif !GLIB_CHECK_VERSION(2, 38, 0)
+    GVariant *value;
+    GBytes *  bytes;
+
+    g_return_val_if_fail(string != NULL, NULL);
+    g_return_val_if_fail(g_utf8_validate(string, -1, NULL), NULL);
+
+    bytes = g_bytes_new_take(string, strlen(string) + 1);
+    value = g_variant_new_from_bytes(G_VARIANT_TYPE_STRING, bytes, TRUE);
+    g_bytes_unref(bytes);
+
+    return value;
+#else
+    G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+    return g_variant_new_take_string(string);
+    G_GNUC_END_IGNORE_DEPRECATIONS
+#endif
+}
+#define g_variant_new_take_string _nm_g_variant_new_take_string
+
+/*****************************************************************************/
+
+#if !GLIB_CHECK_VERSION(2, 38, 0)
+_nm_printf(1, 2) static inline GVariant *_nm_g_variant_new_printf(const char *format_string, ...)
+{
+    char *  string;
+    va_list ap;
+
+    g_return_val_if_fail(format_string, NULL);
+
+    va_start(ap, format_string);
+    string = g_strdup_vprintf(format_string, ap);
+    va_end(ap);
+
+    return g_variant_new_take_string(string);
+}
+    #define g_variant_new_printf(...) _nm_g_variant_new_printf(__VA_ARGS__)
+#else
+    #define g_variant_new_printf(...)               \
+        ({                                          \
+            GVariant *_v;                           \
+                                                    \
+            G_GNUC_BEGIN_IGNORE_DEPRECATIONS        \
+            _v = g_variant_new_printf(__VA_ARGS__); \
+            G_GNUC_END_IGNORE_DEPRECATIONS          \
+            _v;                                     \
+        })
+#endif
+
+/*****************************************************************************/
+
+/* Recent glib also casts the results to typeof(Obj), but only if
+ *
+ *  ( defined(g_has_typeof) && GLIB_VERSION_MAX_ALLOWED >= GLIB_VERSION_2_56 )
+ *
+ * Since we build NetworkManager with older GLIB_VERSION_MAX_ALLOWED, it's
+ * not taking effect.
+ *
+ * Override this. */
+#undef g_object_ref
+#undef g_object_ref_sink
+#define g_object_ref(Obj)      ((typeof(Obj)) g_object_ref(Obj))
+#define g_object_ref_sink(Obj) ((typeof(Obj)) g_object_ref_sink(Obj))
+
+/*****************************************************************************/
+
+#ifndef g_autofree
+    /* we still don't rely on recent glib to provide g_autofree. Hence, we continue
+ * to use our gs_* free macros that we took from libgsystem.
+ *
+ * To ease migration towards g_auto*, add a compat define for g_autofree. */
+    #define g_autofree gs_free
+#endif
+
+/*****************************************************************************/
+
+#if !GLIB_CHECK_VERSION(2, 47, 1)
+/* Older versions of g_value_unset() only allowed to unset a GValue which
+ * was initialized previously. This was relaxed ([1], [2], [3]).
+ *
+ * Our nm_auto_unset_gvalue macro requires to be able to call g_value_unset().
+ * Also, it is our general practice to allow for that. Add a compat implementation.
+ *
+ * [1] https://gitlab.gnome.org/GNOME/glib/commit/4b2d92a864f1505f1b08eb639d74293fa32681da
+ * [2] commit "Allow passing unset GValues to g_value_unset()"
+ * [3] https://bugzilla.gnome.org/show_bug.cgi?id=755766
+ */
+static inline void
+_nm_g_value_unset(GValue *value)
+{
+    g_return_if_fail(value);
+
+    if (value->g_type != 0)
+        g_value_unset(value);
+}
+    #define g_value_unset _nm_g_value_unset
+#endif
+
+/* G_PID_FORMAT was added only in 2.53.5. Define it ourself.
+ *
+ * If this was about "pid_t", we would check SIZEOF_PID_T, and set
+ * PRIi32/PRIi16, like systemd does. But it's actually about
+ * GPid, which glib typedefs as an "int".
+ *
+ * There is a test_gpid() that check that GPid is really a typedef
+ * for int. */
+#undef G_PID_FORMAT
+#define G_PID_FORMAT "i"
+
+/*****************************************************************************/
+
+/* G_SOURCE_FUNC was added in 2.57.2. */
+#undef G_SOURCE_FUNC
+#define G_SOURCE_FUNC(f) ((GSourceFunc) (void (*)(void)) (f))
+
+/*****************************************************************************/
+
+/* g_atomic_pointer_get() is implemented as a macro, and it is also used for
+ * (gsize *) arguments. However, that leads to compiler warnings in certain
+ * configurations. Work around it, by redefining the macro. */
+static inline gpointer
+_g_atomic_pointer_get(void **atomic)
+{
+    return g_atomic_pointer_get(atomic);
+}
+#undef g_atomic_pointer_get
+#define g_atomic_pointer_get(atomic)                                           \
+    ({                                                                         \
+        typeof(*atomic) *const _atomic = (atomic);                             \
+                                                                               \
+        /* g_atomic_pointer_get() is used by glib also for (gsize *) pointers,
+         * not only pointers to pointers. We thus don't enforce that (*atomic)
+         * is a pointer, but of suitable size/alignment. */ \
+                                                                               \
+        G_STATIC_ASSERT(sizeof(*_atomic) == sizeof(gpointer));                 \
+        G_STATIC_ASSERT(_nm_alignof(*_atomic) == _nm_alignof(gpointer));       \
+        (void) (0 ? (gpointer) * (_atomic) : NULL);                            \
+                                                                               \
+        (typeof(*_atomic)) _g_atomic_pointer_get((void **) _atomic);           \
+    })
+
+/* Reimplement g_atomic_pointer_set() macro too. Our variant does more type
+ * checks. */
+static inline void
+_g_atomic_pointer_set(void **atomic, void *newval)
+{
+    return g_atomic_pointer_set(atomic, newval);
+}
+#undef g_atomic_pointer_set
+#define g_atomic_pointer_set(atomic, newval)                        \
+    ({                                                              \
+        typeof(*atomic) *const         _atomic         = (atomic);  \
+        typeof(*_atomic) const         _newval         = (newval);  \
+        _nm_unused gconstpointer const _val_type_check = _newval;   \
+                                                                    \
+        (void) (0 ? (gpointer) * (_atomic) : NULL);                 \
+                                                                    \
+        _g_atomic_pointer_set((void **) _atomic, (void *) _newval); \
+    })
+
+/* Glib implements g_atomic_pointer_compare_and_exchange() as a macro.
+ * For one, to inline the atomic operation and also to perform some type checks
+ * on the arguments.
+ * Depending on compiler and glib version, glib passes the arguments as they
+ * are to __atomic_compare_exchange_n(). Some clang version don't accept const
+ * pointers there. Reimplement the macro to get that right, but with stronger
+ * type checks (as we use typeof()). Had one job. */
+static inline gboolean
+_g_atomic_pointer_compare_and_exchange(void **atomic, void *oldval, void *newval)
+{
+    return g_atomic_pointer_compare_and_exchange(atomic, oldval, newval);
+}
+#undef g_atomic_pointer_compare_and_exchange
+#define g_atomic_pointer_compare_and_exchange(atomic, oldval, newval) \
+    ({                                                                \
+        typeof(*atomic) *const         _atomic         = (atomic);    \
+        typeof(*_atomic) const         _oldval         = (oldval);    \
+        typeof(*_atomic) const         _newval         = (newval);    \
+        _nm_unused gconstpointer const _val_type_check = _oldval;     \
+                                                                      \
+        (void) (0 ? (gpointer) * (_atomic) : NULL);                   \
+                                                                      \
+        _g_atomic_pointer_compare_and_exchange((void **) _atomic,     \
+                                               (void *) _oldval,      \
+                                               (void *) _newval);     \
+    })
+
+/*****************************************************************************/
+
+#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
+
+/*****************************************************************************/
+
+__attribute__((
+    __deprecated__("Don't use g_cancellable_reset(). Create a new cancellable instead."))) void
+_nm_g_cancellable_reset(GCancellable *cancellable);
+
+#undef g_cancellable_reset
+#define g_cancellable_reset(cancellable) _nm_g_cancellable_reset(cancellable)
+
+/*****************************************************************************/
+
+/* Coverity gets confused by g_strstrip(g_strdup(foo)). Reimplement the macro
+ * in a way that hopefully works better to avoid the false positive. */
+#undef g_strstrip
+#define g_strstrip(str)           \
+    ({                            \
+        char *const _str = (str); \
+                                  \
+        g_strchug(_str);          \
+        g_strchomp(_str);         \
+        _str;                     \
+    })
+
+/*****************************************************************************/
+
+#endif /* __NM_GLIB_H__ */
diff --git a/src/libnm-glib-aux/nm-hash-utils.c b/src/libnm-glib-aux/nm-hash-utils.c
new file mode 100644
index 00000000..10fc097c
--- /dev/null
+++ b/src/libnm-glib-aux/nm-hash-utils.c
@@ -0,0 +1,286 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-hash-utils.h"
+
+#include <stdint.h>
+
+#include "nm-shared-utils.h"
+#include "nm-random-utils.h"
+
+/*****************************************************************************/
+
+#define HASH_KEY_SIZE       16u
+#define HASH_KEY_SIZE_GUINT ((HASH_KEY_SIZE + sizeof(guint) - 1) / sizeof(guint))
+
+G_STATIC_ASSERT(sizeof(guint) * HASH_KEY_SIZE_GUINT >= HASH_KEY_SIZE);
+
+static const guint8 *volatile global_seed = NULL;
+
+static const guint8 *
+_get_hash_key_init(void)
+{
+    /* the returned hash is aligned to guin64, hence, it is safe
+     * to use it as guint* or guint64* pointer. */
+    static union {
+        guint8  v8[HASH_KEY_SIZE];
+        guint   _align_as_uint;
+        guint32 _align_as_uint32;
+        guint64 _align_as_uint64;
+    } g_arr;
+    const guint8 *g;
+
+again:
+    g = g_atomic_pointer_get(&global_seed);
+    if (!G_UNLIKELY(g)) {
+        static gsize g_lock;
+        uint64_t     h;
+        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));
+
+        /* 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.
+         *
+         * 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 & G_MAXUINT));
+
+        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);
+    }
+
+    nm_assert(g == g_arr.v8);
+    return g;
+}
+
+#define _get_hash_key()                          \
+    ({                                           \
+        const guint8 *_g;                        \
+                                                 \
+        _g = g_atomic_pointer_get(&global_seed); \
+        if (G_UNLIKELY(!_g))                     \
+            _g = _get_hash_key_init();           \
+        _g;                                      \
+    })
+
+guint
+nm_hash_static(guint static_seed)
+{
+    /* 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 (like for nm_hash_complete()).
+     */
+    return ((*((const guint *) _get_hash_key())) ^ static_seed) ?: 3679500967u;
+}
+
+void
+nm_hash_siphash42_init(CSipHash *h, guint static_seed)
+{
+    const guint8 *g;
+    union {
+        guint64 _align_as_uint64;
+        guint   arr[HASH_KEY_SIZE_GUINT];
+    } seed;
+
+    nm_assert(h);
+
+    g = _get_hash_key();
+    memcpy(&seed, g, HASH_KEY_SIZE);
+    seed.arr[0] ^= static_seed;
+    c_siphash_init(h, (const guint8 *) &seed);
+}
+
+guint
+nm_hash_str(const char *str)
+{
+    NMHashState h;
+
+    if (!str)
+        return nm_hash_static(1867854211u);
+    nm_hash_init(&h, 1867854211u);
+    nm_hash_update_str(&h, str);
+    return nm_hash_complete(&h);
+}
+
+guint
+nm_str_hash(gconstpointer str)
+{
+    return nm_hash_str(str);
+}
+
+guint
+nm_hash_ptr(gconstpointer ptr)
+{
+    NMHashState h;
+
+    if (!ptr)
+        return nm_hash_static(2907677551u);
+    nm_hash_init(&h, 2907677551u);
+    nm_hash_update(&h, &ptr, sizeof(ptr));
+    return nm_hash_complete(&h);
+}
+
+guint
+nm_direct_hash(gconstpointer ptr)
+{
+    return nm_hash_ptr(ptr);
+}
+
+/*****************************************************************************/
+
+guint
+nm_pstr_hash(gconstpointer p)
+{
+    const char *const *s = p;
+
+    if (!s)
+        return nm_hash_static(101061439u);
+    return nm_hash_str(*s);
+}
+
+gboolean
+nm_pstr_equal(gconstpointer a, gconstpointer b)
+{
+    const char *const *s1 = a;
+    const char *const *s2 = b;
+
+    return (s1 == s2) || (s1 && s2 && nm_streq0(*s1, *s2));
+}
+
+guint
+nm_pint_hash(gconstpointer p)
+{
+    const int *s = p;
+
+    if (!s)
+        return nm_hash_static(298377461u);
+    return nm_hash_val(1208815757u, *s);
+}
+
+gboolean
+nm_pint_equal(gconstpointer a, gconstpointer b)
+{
+    const int *s1 = a;
+    const int *s2 = a;
+
+    return s1 == s2 || (s1 && s2 && *s1 == *s2);
+}
+
+guint
+nm_pdirect_hash(gconstpointer p)
+{
+    const void *const *s = p;
+
+    if (!s)
+        return nm_hash_static(1852748873u);
+    return nm_direct_hash(*s);
+}
+
+gboolean
+nm_pdirect_equal(gconstpointer a, gconstpointer b)
+{
+    const void *const *s1 = a;
+    const void *const *s2 = b;
+
+    return (s1 == s2) || (s1 && s2 && *s1 == *s2);
+}
+
+guint
+nm_ppdirect_hash(gconstpointer p)
+{
+    const void *const *const *s = p;
+
+    if (!s)
+        return nm_hash_static(396534869u);
+    if (!*s)
+        return nm_hash_static(1476102263u);
+    return nm_direct_hash(**s);
+}
+
+gboolean
+nm_ppdirect_equal(gconstpointer a, gconstpointer b)
+{
+    const void *const *const *s1 = a;
+    const void *const *const *s2 = b;
+
+    if (s1 == s2)
+        return TRUE;
+    if (!s1 || !s2)
+        return FALSE;
+
+    if (*s1 == *s2)
+        return TRUE;
+    if (!*s1 || !*s2)
+        return FALSE;
+
+    return **s1 == **s2;
+}
+
+/*****************************************************************************/
+
+guint
+nm_gbytes_hash(gconstpointer p)
+{
+    GBytes *      ptr = (GBytes *) p;
+    gconstpointer arr;
+    gsize         len;
+
+    arr = g_bytes_get_data(ptr, &len);
+    return nm_hash_mem(792701303u, arr, len);
+}
+
+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/src/libnm-glib-aux/nm-hash-utils.h b/src/libnm-glib-aux/nm-hash-utils.h
new file mode 100644
index 00000000..e79fb894
--- /dev/null
+++ b/src/libnm-glib-aux/nm-hash-utils.h
@@ -0,0 +1,445 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NM_HASH_UTILS_H__
+#define __NM_HASH_UTILS_H__
+
+#include "c-siphash/src/c-siphash.h"
+#include "nm-macros-internal.h"
+
+/*****************************************************************************/
+
+#define NM_HASH_SEED_16(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af) \
+    ((const guint8[16]){a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af})
+
+void nm_hash_siphash42_init(CSipHash *h, guint static_seed);
+
+/* Siphash24 of binary buffer @arr and @len, using the randomized seed from
+ * other NMHash functions.
+ *
+ * Note, that this is guaranteed to use siphash42 under the hood (contrary to
+ * all other NMHash API, which leave this undefined). That matters at the point,
+ * where the caller needs to be sure that a reasonably strong hashing algorithm
+ * is used.  (Yes, NMHash is all about siphash24, but otherwise that is not promised
+ * anywhere).
+ *
+ * Another difference is, that this returns guint64 (not guint like other NMHash functions).
+ *
+ * Another difference is, that this may also return zero (not like nm_hash_complete()).
+ *
+ * Then, why not use c_siphash_hash() directly? Because this also uses the randomized,
+ * per-run hash-seed like nm_hash_init(). So, you get siphash24 with a random
+ * seed (which is cached for the current run of the program).
+ */
+static inline guint64
+nm_hash_siphash42(guint static_seed, const void *ptr, gsize n)
+{
+    CSipHash h;
+
+    nm_hash_siphash42_init(&h, static_seed);
+    c_siphash_append(&h, ptr, n);
+    return c_siphash_finalize(&h);
+}
+
+/*****************************************************************************/
+
+struct _NMHashState {
+    CSipHash _state;
+};
+
+typedef struct _NMHashState NMHashState;
+
+guint nm_hash_static(guint static_seed);
+
+static inline void
+nm_hash_init(NMHashState *state, guint static_seed)
+{
+    nm_assert(state);
+
+    nm_hash_siphash42_init(&state->_state, static_seed);
+}
+
+static inline guint64
+nm_hash_complete_u64(NMHashState *state)
+{
+    nm_assert(state);
+
+    /* this returns the native u64 hash value. Note that this differs
+     * from nm_hash_complete() in two ways:
+     *
+     * - the type, guint64 vs. guint.
+     * - nm_hash_complete() never returns zero.
+     *
+     * In practice, nm_hash*() API is implemented via siphash24, so this returns
+     * the siphash24 value. But that is not guaranteed by the API, and if you need
+     * siphash24 directly, use c_siphash_*() and nm_hash_siphash42*() API. */
+    return c_siphash_finalize(&state->_state);
+}
+
+static inline guint
+nm_hash_complete(NMHashState *state)
+{
+    guint64 h;
+
+    h = nm_hash_complete_u64(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;
+}
+
+static inline void
+nm_hash_update(NMHashState *state, const void *ptr, gsize n)
+{
+    nm_assert(state);
+    nm_assert(n == 0 || ptr);
+
+    /* Note: the data passed in here might be sensitive data (secrets),
+     * that we should nm_explicit_bzero() afterwards. However, since
+     * we are using siphash24 with a random key, that is not really
+     * necessary. Something to keep in mind, if we ever move away from
+     * this hash implementation. */
+    c_siphash_append(&state->_state, ptr, n);
+}
+
+#define nm_hash_update_val(state, val)                \
+    G_STMT_START                                      \
+    {                                                 \
+        typeof(val) _val = (val);                     \
+                                                      \
+        nm_hash_update((state), &_val, sizeof(_val)); \
+    }                                                 \
+    G_STMT_END
+
+#define nm_hash_update_valp(state, val) nm_hash_update((state), (val), sizeof(*(val)))
+
+static inline void
+nm_hash_update_bool(NMHashState *state, bool val)
+{
+    nm_hash_update(state, &val, sizeof(val));
+}
+
+#define _NM_HASH_COMBINE_BOOLS_x_1(t, y) ((y) ? ((t) (1ull << 0)) : ((t) 0ull))
+#define _NM_HASH_COMBINE_BOOLS_x_2(t, y, ...) \
+    ((y) ? ((t) (1ull << 1)) : ((t) 0ull)) | _NM_HASH_COMBINE_BOOLS_x_1(t, __VA_ARGS__)
+#define _NM_HASH_COMBINE_BOOLS_x_3(t, y, ...) \
+    ((y) ? ((t) (1ull << 2)) : ((t) 0ull)) | _NM_HASH_COMBINE_BOOLS_x_2(t, __VA_ARGS__)
+#define _NM_HASH_COMBINE_BOOLS_x_4(t, y, ...) \
+    ((y) ? ((t) (1ull << 3)) : ((t) 0ull)) | _NM_HASH_COMBINE_BOOLS_x_3(t, __VA_ARGS__)
+#define _NM_HASH_COMBINE_BOOLS_x_5(t, y, ...) \
+    ((y) ? ((t) (1ull << 4)) : ((t) 0ull)) | _NM_HASH_COMBINE_BOOLS_x_4(t, __VA_ARGS__)
+#define _NM_HASH_COMBINE_BOOLS_x_6(t, y, ...) \
+    ((y) ? ((t) (1ull << 5)) : ((t) 0ull)) | _NM_HASH_COMBINE_BOOLS_x_5(t, __VA_ARGS__)
+#define _NM_HASH_COMBINE_BOOLS_x_7(t, y, ...) \
+    ((y) ? ((t) (1ull << 6)) : ((t) 0ull)) | _NM_HASH_COMBINE_BOOLS_x_6(t, __VA_ARGS__)
+#define _NM_HASH_COMBINE_BOOLS_x_8(t, y, ...) \
+    ((y) ? ((t) (1ull << 7)) : ((t) 0ull)) | _NM_HASH_COMBINE_BOOLS_x_7(t, __VA_ARGS__)
+#define _NM_HASH_COMBINE_BOOLS_x_9(t, y, ...) \
+    ((y) ? ((t) (1ull << 8)) : ((t) 0ull))    \
+        | (G_STATIC_ASSERT_EXPR(sizeof(t) >= 2), (_NM_HASH_COMBINE_BOOLS_x_8(t, __VA_ARGS__)))
+#define _NM_HASH_COMBINE_BOOLS_x_10(t, y, ...) \
+    ((y) ? ((t) (1ull << 9)) : ((t) 0ull)) | _NM_HASH_COMBINE_BOOLS_x_9(t, __VA_ARGS__)
+#define _NM_HASH_COMBINE_BOOLS_x_11(t, y, ...) \
+    ((y) ? ((t) (1ull << 10)) : ((t) 0ull)) | _NM_HASH_COMBINE_BOOLS_x_10(t, __VA_ARGS__)
+#define _NM_HASH_COMBINE_BOOLS_n2(t, n, ...) _NM_HASH_COMBINE_BOOLS_x_##n(t, __VA_ARGS__)
+#define _NM_HASH_COMBINE_BOOLS_n(t, n, ...)  _NM_HASH_COMBINE_BOOLS_n2(t, n, __VA_ARGS__)
+
+#define NM_HASH_COMBINE_BOOLS(type, ...) \
+    ((type) (_NM_HASH_COMBINE_BOOLS_n(type, NM_NARG(__VA_ARGS__), __VA_ARGS__)))
+
+#define nm_hash_update_bools(state, ...) \
+    nm_hash_update_val(state, NM_HASH_COMBINE_BOOLS(guint8, __VA_ARGS__))
+
+#define _NM_HASH_COMBINE_VALS_typ_x_1(y) typeof(y) _v1;
+#define _NM_HASH_COMBINE_VALS_typ_x_2(y, ...) \
+    typeof(y) _v2;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_1(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_3(y, ...) \
+    typeof(y) _v3;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_2(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_4(y, ...) \
+    typeof(y) _v4;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_3(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_5(y, ...) \
+    typeof(y) _v5;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_4(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_6(y, ...) \
+    typeof(y) _v6;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_5(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_7(y, ...) \
+    typeof(y) _v7;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_6(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_8(y, ...) \
+    typeof(y) _v8;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_7(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_9(y, ...) \
+    typeof(y) _v9;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_8(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_10(y, ...) \
+    typeof(y) _v10;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_9(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_11(y, ...) \
+    typeof(y) _v11;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_10(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_12(y, ...) \
+    typeof(y) _v12;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_11(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_13(y, ...) \
+    typeof(y) _v13;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_12(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_14(y, ...) \
+    typeof(y) _v14;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_13(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_15(y, ...) \
+    typeof(y) _v15;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_14(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_16(y, ...) \
+    typeof(y) _v16;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_15(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_17(y, ...) \
+    typeof(y) _v17;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_16(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_18(y, ...) \
+    typeof(y) _v18;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_17(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_19(y, ...) \
+    typeof(y) _v19;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_18(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_x_20(y, ...) \
+    typeof(y) _v20;                            \
+    _NM_HASH_COMBINE_VALS_typ_x_19(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_n2(n, ...) _NM_HASH_COMBINE_VALS_typ_x_##n(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_typ_n(n, ...)  _NM_HASH_COMBINE_VALS_typ_n2(n, __VA_ARGS__)
+
+#define _NM_HASH_COMBINE_VALS_val_x_1(y)      ._v1 = (y),
+#define _NM_HASH_COMBINE_VALS_val_x_2(y, ...) ._v2 = (y), _NM_HASH_COMBINE_VALS_val_x_1(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_3(y, ...) ._v3 = (y), _NM_HASH_COMBINE_VALS_val_x_2(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_4(y, ...) ._v4 = (y), _NM_HASH_COMBINE_VALS_val_x_3(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_5(y, ...) ._v5 = (y), _NM_HASH_COMBINE_VALS_val_x_4(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_6(y, ...) ._v6 = (y), _NM_HASH_COMBINE_VALS_val_x_5(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_7(y, ...) ._v7 = (y), _NM_HASH_COMBINE_VALS_val_x_6(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_8(y, ...) ._v8 = (y), _NM_HASH_COMBINE_VALS_val_x_7(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_9(y, ...) ._v9 = (y), _NM_HASH_COMBINE_VALS_val_x_8(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_10(y, ...) \
+    ._v10 = (y), _NM_HASH_COMBINE_VALS_val_x_9(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_11(y, ...) \
+    ._v11 = (y), _NM_HASH_COMBINE_VALS_val_x_10(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_12(y, ...) \
+    ._v12 = (y), _NM_HASH_COMBINE_VALS_val_x_11(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_13(y, ...) \
+    ._v13 = (y), _NM_HASH_COMBINE_VALS_val_x_12(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_14(y, ...) \
+    ._v14 = (y), _NM_HASH_COMBINE_VALS_val_x_13(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_15(y, ...) \
+    ._v15 = (y), _NM_HASH_COMBINE_VALS_val_x_14(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_16(y, ...) \
+    ._v16 = (y), _NM_HASH_COMBINE_VALS_val_x_15(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_17(y, ...) \
+    ._v17 = (y), _NM_HASH_COMBINE_VALS_val_x_16(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_18(y, ...) \
+    ._v18 = (y), _NM_HASH_COMBINE_VALS_val_x_17(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_19(y, ...) \
+    ._v19 = (y), _NM_HASH_COMBINE_VALS_val_x_18(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_x_20(y, ...) \
+    ._v20 = (y), _NM_HASH_COMBINE_VALS_val_x_19(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_n2(n, ...) _NM_HASH_COMBINE_VALS_val_x_##n(__VA_ARGS__)
+#define _NM_HASH_COMBINE_VALS_val_n(n, ...)  _NM_HASH_COMBINE_VALS_val_n2(n, __VA_ARGS__)
+
+/* NM_HASH_COMBINE_VALS() is faster then nm_hash_update_val() as it combines multiple
+ * calls to nm_hash_update() using a packed structure. */
+#define NM_HASH_COMBINE_VALS(var, ...)                                 \
+    const struct _nm_packed {                                          \
+        _NM_HASH_COMBINE_VALS_typ_n(NM_NARG(__VA_ARGS__), __VA_ARGS__) \
+    } var _nm_alignas(guint64) = {_NM_HASH_COMBINE_VALS_val_n(NM_NARG(__VA_ARGS__), __VA_ARGS__)}
+
+/* nm_hash_update_vals() is faster then nm_hash_update_val() as it combines multiple
+ * calls to nm_hash_update() using a packed structure. */
+#define nm_hash_update_vals(state, ...)               \
+    G_STMT_START                                      \
+    {                                                 \
+        NM_HASH_COMBINE_VALS(_val, __VA_ARGS__);      \
+                                                      \
+        nm_hash_update((state), &_val, sizeof(_val)); \
+    }                                                 \
+    G_STMT_END
+
+static inline void
+nm_hash_update_mem(NMHashState *state, const void *ptr, gsize n)
+{
+    /* This also hashes the length of the data. That means,
+     * hashing two consecutive binary fields (of arbitrary
+     * length), will hash differently. That is,
+     * [[1,1], []] differs from [[1],[1]].
+     *
+     * If you have a constant length (sizeof), use nm_hash_update()
+     * instead. */
+    nm_hash_update(state, &n, sizeof(n));
+    if (n > 0)
+        nm_hash_update(state, ptr, n);
+}
+
+static inline void
+nm_hash_update_str0(NMHashState *state, const char *str)
+{
+    if (str)
+        nm_hash_update_mem(state, str, strlen(str));
+    else {
+        gsize n = G_MAXSIZE;
+
+        nm_hash_update(state, &n, sizeof(n));
+    }
+}
+
+static inline void
+nm_hash_update_str(NMHashState *state, const char *str)
+{
+    nm_assert(str);
+    nm_hash_update(state, str, strlen(str) + 1);
+}
+
+#if _NM_CC_SUPPORT_GENERIC
+    /* Like nm_hash_update_str(), but restricted to arrays only. nm_hash_update_str() only works
+ * with a @str argument that cannot be NULL. If you have a string pointer, that is never NULL, use
+ * nm_hash_update() instead. */
+    #define nm_hash_update_strarr(state, str)                                \
+        (_Generic(&(str), const char(*)[sizeof(str)]                         \
+                  : nm_hash_update_str((state), (str)), char(*)[sizeof(str)] \
+                  : nm_hash_update_str((state), (str))))
+#else
+    #define nm_hash_update_strarr(state, str) nm_hash_update_str((state), (str))
+#endif
+
+guint nm_hash_ptr(gconstpointer ptr);
+guint nm_direct_hash(gconstpointer str);
+
+guint nm_hash_str(const char *str);
+guint nm_str_hash(gconstpointer str);
+
+#define nm_hash_val(static_seed, val)     \
+    ({                                    \
+        NMHashState _h;                   \
+                                          \
+        nm_hash_init(&_h, (static_seed)); \
+        nm_hash_update_val(&_h, (val));   \
+        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,
+ * that is, "const char *const*" types, using strcmp(). */
+
+guint nm_pstr_hash(gconstpointer p);
+
+gboolean nm_pstr_equal(gconstpointer a, gconstpointer b);
+
+/*****************************************************************************/
+
+/* nm_pint_*() are for hashing keys that are pointers to int values,
+ * that is, "const int *" types. */
+
+guint    nm_pint_hash(gconstpointer p);
+gboolean nm_pint_equal(gconstpointer a, gconstpointer b);
+
+G_STATIC_ASSERT(sizeof(int) == sizeof(guint32));
+#define nm_puint32_hash  nm_pint_hash
+#define nm_puint32_equal nm_pint_equal
+
+/*****************************************************************************/
+
+/* this hashes/compares the pointer value that we point to. Basically,
+ * (*((const void *const*) a) == *((const void *const*) b)). */
+
+guint nm_pdirect_hash(gconstpointer p);
+
+gboolean nm_pdirect_equal(gconstpointer a, gconstpointer b);
+
+/* this hashes/compares the direct pointer value by following pointers to
+ * pointers 2 times.
+ * (**((const void *const*const*) a) == **((const void *const*const*) b)). */
+
+guint nm_ppdirect_hash(gconstpointer p);
+
+gboolean nm_ppdirect_equal(gconstpointer a, gconstpointer b);
+
+/*****************************************************************************/
+
+guint nm_gbytes_hash(gconstpointer p);
+#define nm_gbytes_equal g_bytes_equal
+
+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
+ * the message that get logged. Logging pointer values directly defeats ASLR, so we should
+ * not do that. This returns a "unsigned long long" value that can be used
+ * instead.
+ *
+ * Note that there is a chance that two different pointer values hash to the same obfuscated
+ * value. So beware of that when reviewing logs. However, such a collision is very unlikely. */
+static inline guint64
+nm_hash_obfuscate_ptr(guint static_seed, gconstpointer val)
+{
+    NMHashState h;
+
+    nm_hash_init(&h, static_seed);
+    nm_hash_update_val(&h, val);
+    return nm_hash_complete_u64(&h);
+}
+
+/* if you want to log obfuscated pointer for a certain context (like, NMPRuleManager
+ * logging user-tags), then you are advised to use nm_hash_obfuscate_ptr() with your
+ * own, unique static-seed.
+ *
+ * However, for example the singleton constructors log the obfuscated pointer values
+ * for all singletons, so they must all be obfuscated with the same seed. So, this
+ * macro uses a particular static seed that should be used by when comparing pointer
+ * values in a global context. */
+#define NM_HASH_OBFUSCATE_PTR(ptr) (nm_hash_obfuscate_ptr(1678382159u, ptr))
+
+/* NM_HASH_OBFUSCATE_PTR_STR needs a buffer of at least this many bytes. */
+#define NM_HASH_OBFUSCATE_PTR_STR_BUF_SIZE 19
+
+#define NM_HASH_OBFUSCATE_PTR_STR(ptr, buf)                                                        \
+    ({                                                                                             \
+        gconstpointer _ptr = (ptr);                                                                \
+                                                                                                   \
+        _ptr ? nm_sprintf_buf(buf, "[" NM_HASH_OBFUSCATE_PTR_FMT "]", NM_HASH_OBFUSCATE_PTR(_ptr)) \
+             : "(null)";                                                                           \
+    })
+
+static inline const char *
+nm_hash_obfuscated_ptr_str(gconstpointer ptr, char buf[static 17])
+{
+    int l;
+
+    nm_assert(buf);
+    l = g_snprintf(buf, 17, NM_HASH_OBFUSCATE_PTR_FMT, NM_HASH_OBFUSCATE_PTR(ptr));
+    nm_assert(l < 17);
+    return buf;
+}
+
+#define nm_hash_obfuscated_ptr_str_a(ptr) (nm_hash_obfuscated_ptr_str((ptr), g_alloca(17)))
+
+/*****************************************************************************/
+
+#endif /* __NM_HASH_UTILS_H__ */
diff --git a/src/libnm-glib-aux/nm-io-utils.c b/src/libnm-glib-aux/nm-io-utils.c
new file mode 100644
index 00000000..87478a2d
--- /dev/null
+++ b/src/libnm-glib-aux/nm-io-utils.c
@@ -0,0 +1,630 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-io-utils.h"
+
+#include <sys/types.h>
+#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"
+
+/*****************************************************************************/
+
+_nm_printf(4, 5) static int _get_contents_error(GError **   error,
+                                                int         errsv,
+                                                int *       out_errsv,
+                                                const char *format,
+                                                ...)
+{
+    nm_assert(NM_ERRNO_NATIVE(errsv));
+
+    if (error) {
+        gs_free char *msg = NULL;
+        va_list       args;
+        char          bstrerr[NM_STRERROR_BUFSIZE];
+
+        va_start(args, format);
+        msg = g_strdup_vprintf(format, args);
+        va_end(args);
+        g_set_error(error,
+                    G_FILE_ERROR,
+                    g_file_error_from_errno(errsv),
+                    "%s: %s",
+                    msg,
+                    nm_strerror_native_r(errsv, bstrerr, sizeof(bstrerr)));
+    }
+
+    nm_assert(errsv > 0);
+    NM_SET_OUT(out_errsv, errsv);
+
+    return FALSE;
+}
+#define _get_contents_error_errno(error, out_errsv, ...)            \
+    ({                                                              \
+        int _errsv = (errno);                                       \
+                                                                    \
+        _get_contents_error(error, _errsv, out_errsv, __VA_ARGS__); \
+    })
+
+/**
+ * nm_utils_fd_get_contents:
+ * @fd: open file descriptor to read. The fd will not be closed,
+ *   but don't rely on its state afterwards.
+ * @close_fd: if %TRUE, @fd will be closed by the function.
+ *  Passing %TRUE here might safe a syscall for dup().
+ * @max_length: allocate at most @max_length bytes. If the
+ *   file is larger, reading will fail. Set to zero to use
+ *   a very large default.
+ *   WARNING: @max_length is here to avoid a crash for huge/unlimited files.
+ *   For example, stat(/sys/class/net/enp0s25/ifindex) gives a filesize of
+ *   4K, although the actual real is small. @max_length is the memory
+ *   allocated in the process of reading the file, thus it must be at least
+ *   the size reported by fstat.
+ *   If you set it to 1K, read will fail because fstat() claims the
+ *   file is larger.
+ * @flags: %NMUtilsFileGetContentsFlags for reading the file.
+ * @contents: the output buffer with the file read. It is always
+ *   NUL terminated. The buffer is at most @max_length long, including
+ *  the NUL byte. That is, it reads only files up to a length of
+ *  @max_length - 1 bytes.
+ * @length: optional output argument of the read file size.
+ * @out_errsv: (allow-none) (out): on error, a positive errno. or zero.
+ * @error:
+ *
+ *
+ * A reimplementation of g_file_get_contents() with a few differences:
+ *   - accepts an open fd, instead of a path name. This allows you to
+ *     use openat().
+ *   - limits the maximum filesize to max_length.
+ *
+ * Returns: TRUE on success.
+ */
+gboolean
+nm_utils_fd_get_contents(int                         fd,
+                         gboolean                    close_fd,
+                         gsize                       max_length,
+                         NMUtilsFileGetContentsFlags flags,
+                         char **                     contents,
+                         gsize *                     length,
+                         int *                       out_errsv,
+                         GError **                   error)
+{
+    nm_auto_close int fd_keeper = close_fd ? fd : -1;
+    struct stat       stat_buf;
+    gs_free char *    str          = NULL;
+    const bool        do_bzero_mem = NM_FLAGS_HAS(flags, NM_UTILS_FILE_GET_CONTENTS_FLAG_SECRET);
+    int               errsv;
+
+    g_return_val_if_fail(fd >= 0, FALSE);
+    g_return_val_if_fail(contents && !*contents, FALSE);
+    g_return_val_if_fail(!error || !*error, FALSE);
+
+    NM_SET_OUT(length, 0);
+
+    if (fstat(fd, &stat_buf) < 0)
+        return _get_contents_error_errno(error, out_errsv, "failure during fstat");
+
+    if (!max_length) {
+        /* default to a very large size, but not extreme */
+        max_length = 2 * 1024 * 1024;
+    }
+
+    if (stat_buf.st_size > 0 && S_ISREG(stat_buf.st_mode)) {
+        const gsize n_stat = stat_buf.st_size;
+        ssize_t     n_read;
+
+        if (n_stat > max_length - 1)
+            return _get_contents_error(error,
+                                       EMSGSIZE,
+                                       out_errsv,
+                                       "file too large (%zu+1 bytes with maximum %zu bytes)",
+                                       n_stat,
+                                       max_length);
+
+        str = g_try_malloc(n_stat + 1);
+        if (!str)
+            return _get_contents_error(error,
+                                       ENOMEM,
+                                       out_errsv,
+                                       "failure to allocate buffer of %zu+1 bytes",
+                                       n_stat);
+
+        n_read = nm_utils_fd_read_loop(fd, str, n_stat, TRUE);
+        if (n_read < 0) {
+            if (do_bzero_mem)
+                nm_explicit_bzero(str, n_stat);
+            return _get_contents_error(error,
+                                       -n_read,
+                                       out_errsv,
+                                       "error reading %zu bytes from file descriptor",
+                                       n_stat);
+        }
+        str[n_read] = '\0';
+
+        if (n_read < n_stat) {
+            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);
+    } else {
+        nm_auto_fclose FILE *f = NULL;
+        char                 buf[4096];
+        gsize                n_have, n_alloc;
+        int                  fd2;
+
+        if (fd_keeper >= 0)
+            fd2 = nm_steal_fd(&fd_keeper);
+        else {
+            fd2 = fcntl(fd, F_DUPFD_CLOEXEC, 0);
+            if (fd2 < 0)
+                return _get_contents_error_errno(error, out_errsv, "error during dup");
+        }
+
+        if (!(f = fdopen(fd2, "r"))) {
+            errsv = errno;
+            nm_close(fd2);
+            return _get_contents_error(error, errsv, out_errsv, "failure during fdopen");
+        }
+
+        n_have  = 0;
+        n_alloc = 0;
+
+        while (!feof(f)) {
+            gsize n_read;
+
+            n_read = fread(buf, 1, sizeof(buf), f);
+            errsv  = errno;
+            if (ferror(f)) {
+                if (do_bzero_mem)
+                    nm_explicit_bzero(buf, sizeof(buf));
+                return _get_contents_error(error, errsv, out_errsv, "error during fread");
+            }
+
+            if (n_have > G_MAXSIZE - 1 - n_read || n_have + n_read + 1 > max_length) {
+                if (do_bzero_mem)
+                    nm_explicit_bzero(buf, sizeof(buf));
+                return _get_contents_error(
+                    error,
+                    EMSGSIZE,
+                    out_errsv,
+                    "file stream too large (%zu+1 bytes with maximum %zu bytes)",
+                    (n_have > G_MAXSIZE - 1 - n_read) ? G_MAXSIZE : n_have + n_read,
+                    max_length);
+            }
+
+            if (n_have + n_read + 1 >= n_alloc) {
+                gsize old_n_alloc = n_alloc;
+
+                if (n_alloc != 0) {
+                    nm_assert(str);
+                    if (n_alloc >= max_length / 2)
+                        n_alloc = max_length;
+                    else
+                        n_alloc *= 2;
+                } else {
+                    nm_assert(!str);
+                    n_alloc = NM_MIN(n_read + 1, sizeof(buf));
+                }
+
+                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);
+                }
+            }
+
+            memcpy(str + n_have, buf, n_read);
+            n_have += n_read;
+        }
+
+        if (do_bzero_mem)
+            nm_explicit_bzero(buf, sizeof(buf));
+
+        if (n_alloc == 0)
+            str = g_new0(char, 1);
+        else {
+            str[n_have] = '\0';
+            if (n_have + 1 < n_alloc) {
+                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);
+            }
+        }
+
+        NM_SET_OUT(length, n_have);
+    }
+
+    *contents = g_steal_pointer(&str);
+    NM_SET_OUT(out_errsv, 0);
+    return TRUE;
+}
+
+/**
+ * nm_utils_file_get_contents:
+ * @dirfd: optional file descriptor to use openat(). If negative, use plain open().
+ * @filename: the filename to open. Possibly relative to @dirfd.
+ * @max_length: allocate at most @max_length bytes.
+ *   WARNING: see nm_utils_fd_get_contents() hint about @max_length.
+ * @flags: %NMUtilsFileGetContentsFlags for reading the file.
+ * @contents: the output buffer with the file read. It is always
+ *   NUL terminated. The buffer is at most @max_length long, including
+ *   the NUL byte. That is, it reads only files up to a length of
+ *   @max_length - 1 bytes.
+ * @length: optional output argument of the read file size.
+ * @out_errsv: (allow-none) (out): on error, a positive errno. or zero.
+ * @error:
+ *
+ * A reimplementation of g_file_get_contents() with a few differences:
+ *   - accepts an @dirfd to open @filename relative to that path via openat().
+ *   - limits the maximum filesize to max_length.
+ *   - uses O_CLOEXEC on internal file descriptor
+ *   - optionally returns the native errno on failure.
+ *
+ * Returns: TRUE on success.
+ */
+gboolean
+nm_utils_file_get_contents(int                         dirfd,
+                           const char *                filename,
+                           gsize                       max_length,
+                           NMUtilsFileGetContentsFlags flags,
+                           char **                     contents,
+                           gsize *                     length,
+                           int *                       out_errsv,
+                           GError **                   error)
+{
+    int fd;
+
+    g_return_val_if_fail(filename && filename[0], FALSE);
+    g_return_val_if_fail(contents && !*contents, FALSE);
+
+    NM_SET_OUT(length, 0);
+
+    if (dirfd >= 0) {
+        fd = openat(dirfd, filename, O_RDONLY | O_CLOEXEC);
+        if (fd < 0) {
+            return _get_contents_error_errno(error,
+                                             out_errsv,
+                                             "Failed to open file \"%s\" with openat",
+                                             filename);
+        }
+    } else {
+        fd = open(filename, O_RDONLY | O_CLOEXEC);
+        if (fd < 0) {
+            return _get_contents_error_errno(error,
+                                             out_errsv,
+                                             "Failed to open file \"%s\"",
+                                             filename);
+        }
+    }
+    return nm_utils_fd_get_contents(fd,
+                                    TRUE,
+                                    max_length,
+                                    flags,
+                                    contents,
+                                    length,
+                                    out_errsv,
+                                    error);
+}
+
+/*****************************************************************************/
+
+/*
+ * Copied from GLib's g_file_set_contents() et al., but allows
+ * specifying a mode for the new file and optionally the last access
+ * and last modification times.
+ */
+gboolean
+nm_utils_file_set_contents(const char *           filename,
+                           const char *           contents,
+                           gssize                 length,
+                           mode_t                 mode,
+                           const struct timespec *times,
+                           int *                  out_errsv,
+                           GError **              error)
+{
+    gs_free char *tmp_name = NULL;
+    struct stat   statbuf;
+    int           errsv;
+    gssize        s;
+    int           fd;
+
+    g_return_val_if_fail(filename, FALSE);
+    g_return_val_if_fail(contents || !length, FALSE);
+    g_return_val_if_fail(!error || !*error, FALSE);
+    g_return_val_if_fail(length >= -1, FALSE);
+
+    if (length == -1)
+        length = strlen(contents);
+
+    tmp_name = g_strdup_printf("%s.XXXXXX", filename);
+    fd       = g_mkstemp_full(tmp_name, O_RDWR | O_CLOEXEC, mode);
+    if (fd < 0) {
+        return _get_contents_error_errno(error, out_errsv, "failed to create file %s", tmp_name);
+    }
+
+    while (length > 0) {
+        s = write(fd, contents, length);
+        if (s < 0) {
+            errsv = NM_ERRNO_NATIVE(errno);
+            if (errsv == EINTR)
+                continue;
+
+            nm_close(fd);
+            unlink(tmp_name);
+            return _get_contents_error(error,
+                                       errsv,
+                                       out_errsv,
+                                       "failed to write to file %s",
+                                       tmp_name);
+        }
+
+        g_assert(s <= length);
+
+        contents += s;
+        length -= s;
+    }
+
+    /* If the final destination exists and is > 0 bytes, we want to sync the
+     * newly written file to ensure the data is on disk when we rename over
+     * the destination. Otherwise, if we get a system crash we can lose both
+     * the new and the old file on some filesystems. (I.E. those that don't
+     * guarantee the data is written to the disk before the metadata.)
+     */
+    if (lstat(filename, &statbuf) == 0 && statbuf.st_size > 0) {
+        if (fsync(fd) != 0) {
+            errsv = NM_ERRNO_NATIVE(errno);
+            nm_close(fd);
+            unlink(tmp_name);
+            return _get_contents_error(error, errsv, out_errsv, "failed to fsync %s", tmp_name);
+        }
+    }
+
+    if (times && futimens(fd, times) != 0) {
+        errsv = NM_ERRNO_NATIVE(errno);
+        nm_close(fd);
+        unlink(tmp_name);
+        return _get_contents_error(error,
+                                   errsv,
+                                   out_errsv,
+                                   "failed to set atime and mtime on %s",
+                                   tmp_name);
+    }
+
+    nm_close(fd);
+
+    if (rename(tmp_name, filename)) {
+        errsv = NM_ERRNO_NATIVE(errno);
+        unlink(tmp_name);
+        return _get_contents_error(error,
+                                   errsv,
+                                   out_errsv,
+                                   "failed rename %s to %s",
+                                   tmp_name,
+                                   filename);
+    }
+
+    return TRUE;
+}
+
+/**
+ * nm_utils_file_stat:
+ * @filename: the filename to stat.
+ * @out_st: (allow-none) (out): if given, this will be passed to stat().
+ *
+ * Just wraps stat() and gives the errno number as function result instead
+ * of setting the errno (though, errno is also set). It's only for convenience
+ * with
+ *
+ *    if (nm_utils_file_stat (filename, NULL) == -ENOENT) {
+ *    }
+ *
+ * Returns: 0 on success a negative errno on failure. */
+int
+nm_utils_file_stat(const char *filename, struct stat *out_st)
+{
+    struct stat st;
+
+    if (stat(filename, out_st ?: &st) != 0)
+        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;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    GSubprocess *subprocess;
+    GSource *    timeout_source;
+} SubprocessTerminateData;
+
+static void
+_subprocess_terminate_wait_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    SubprocessTerminateData *term_data = user_data;
+
+    g_subprocess_wait_finish(G_SUBPROCESS(source), result, NULL);
+
+    nm_clear_g_source_inst(&term_data->timeout_source);
+    g_object_unref(term_data->subprocess);
+    nm_g_slice_free(term_data);
+}
+
+static gboolean
+_subprocess_terminate_timeout_cb(gpointer user_data)
+{
+    SubprocessTerminateData *term_data = user_data;
+
+    nm_clear_g_source_inst(&term_data->timeout_source);
+    g_subprocess_send_signal(term_data->subprocess, SIGKILL);
+    return G_SOURCE_REMOVE;
+}
+
+void
+nm_g_subprocess_terminate_in_background(GSubprocess *subprocess, int timeout_msec_before_kill)
+{
+    SubprocessTerminateData *term_data;
+    GMainContext *           main_context;
+
+    nm_assert(timeout_msec_before_kill > 0);
+
+    /* The GSubprocess stays alive until the child is reaped (an internal reference is held).
+     *
+     * This function first sends SIGTERM to the process right away, and after a
+     * timeout "timeout_msec_before_kill" send a SIGKILL.
+     *
+     * Otherwise, it does nothing, it does not log, there is no notification when the process
+     * completes and there is no way to abort the thing.
+     *
+     * It honors the current g_main_context_get_thread_default(). */
+
+    if (!subprocess)
+        return;
+
+    g_return_if_fail(G_IS_SUBPROCESS(subprocess));
+
+    main_context = g_main_context_get_thread_default();
+
+    term_data  = g_slice_new(SubprocessTerminateData);
+    *term_data = (SubprocessTerminateData){
+        .subprocess     = g_object_ref(subprocess),
+        .timeout_source = NULL,
+    };
+
+    g_subprocess_send_signal(subprocess, SIGTERM);
+
+    g_subprocess_wait_async(subprocess, NULL, _subprocess_terminate_wait_cb, term_data);
+
+    term_data->timeout_source =
+        nm_g_source_attach(nm_g_timeout_source_new(timeout_msec_before_kill,
+                                                   G_PRIORITY_DEFAULT,
+                                                   _subprocess_terminate_timeout_cb,
+                                                   term_data,
+                                                   NULL),
+                           main_context);
+}
+
+/*****************************************************************************/
+
+char **
+nm_utils_find_mkstemp_files(const char *dirname, const char *filename)
+{
+    static const char letters[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+    DIR *             dir;
+    struct dirent *   entry;
+    GPtrArray *       arr = NULL;
+    gsize             l;
+
+    /* We write files with g_file_set_contents() and nm_utils_file_set_contents().
+     * These create temporary files using g_mkstemp_full(), with a random .XXXXXX suffix.
+     *
+     * If NetworkManager crashes while writing the file, then those temporary files are
+     * left over. We might want to find and delete such files.
+     *
+     * Beware: only delete such files if you are in full control about which files are
+     * supposed to be in the directory. For example, NetworkManager controls
+     * /var/lib/NetworkManager/timestamps files, and it thus takes the right to delete
+     * all files /var/lib/NetworkManager/timestamps.XXXXXX. That may not be appropriate
+     * in other cases! */
+
+    if (!dirname || !filename || !filename[0])
+        return NULL;
+
+    dir = opendir(dirname);
+    if (!dir)
+        return NULL;
+
+    l = strlen(filename);
+
+    while ((entry = readdir(dir))) {
+        const char *f = entry->d_name;
+        guint       i;
+
+        if (strncmp(f, filename, l) != 0)
+            goto next;
+        if (f[l] != '.')
+            goto next;
+        for (i = 1; i <= 6; i++) {
+            /* @letters is also what g_mkstemp_full() does! */
+            if (!memchr(letters, f[l + i], G_N_ELEMENTS(letters)))
+                goto next;
+        }
+        if (f[l + 7] != '\0')
+            goto next;
+
+        if (!arr)
+            arr = g_ptr_array_new();
+
+        g_ptr_array_add(arr, g_strdup(f));
+next:;
+    }
+
+    closedir(dir);
+
+    if (!arr)
+        return NULL;
+
+    g_ptr_array_add(arr, NULL);
+    return (char **) g_ptr_array_free(arr, FALSE);
+}
diff --git a/src/libnm-glib-aux/nm-io-utils.h b/src/libnm-glib-aux/nm-io-utils.h
new file mode 100644
index 00000000..31ff6d05
--- /dev/null
+++ b/src/libnm-glib-aux/nm-io-utils.h
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#ifndef __NM_IO_UTILS_H__
+#define __NM_IO_UTILS_H__
+
+#include "nm-macros-internal.h"
+
+/*****************************************************************************/
+
+/**
+ * NMUtilsFileGetContentsFlags:
+ * @NM_UTILS_FILE_GET_CONTENTS_FLAG_NONE: no flag
+ * @NM_UTILS_FILE_GET_CONTENTS_FLAG_SECRET: if present, ensure that no
+ *   data is left in memory. Essentially, it means to call nm_explicit_bzero()
+ *   to not leave key material on the heap (when reading secrets).
+ */
+typedef enum {
+    NM_UTILS_FILE_GET_CONTENTS_FLAG_NONE   = 0,
+    NM_UTILS_FILE_GET_CONTENTS_FLAG_SECRET = (1 << 0),
+} NMUtilsFileGetContentsFlags;
+
+gboolean nm_utils_fd_get_contents(int                         fd,
+                                  gboolean                    close_fd,
+                                  gsize                       max_length,
+                                  NMUtilsFileGetContentsFlags flags,
+                                  char **                     contents,
+                                  gsize *                     length,
+                                  int *                       out_errsv,
+                                  GError **                   error);
+
+gboolean nm_utils_file_get_contents(int                         dirfd,
+                                    const char *                filename,
+                                    gsize                       max_length,
+                                    NMUtilsFileGetContentsFlags flags,
+                                    char **                     contents,
+                                    gsize *                     length,
+                                    int *                       out_errsv,
+                                    GError **                   error);
+
+gboolean nm_utils_file_set_contents(const char *           filename,
+                                    const char *           contents,
+                                    gssize                 length,
+                                    mode_t                 mode,
+                                    const struct timespec *times,
+                                    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);
+
+void nm_g_subprocess_terminate_in_background(GSubprocess *subprocess, int timeout_msec_before_kill);
+
+char **nm_utils_find_mkstemp_files(const char *dirname, const char *filename);
+
+#endif /* __NM_IO_UTILS_H__ */
diff --git a/src/libnm-glib-aux/nm-jansson.h b/src/libnm-glib-aux/nm-jansson.h
new file mode 100644
index 00000000..6173a7ac
--- /dev/null
+++ b/src/libnm-glib-aux/nm-jansson.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#ifndef __NM_JANSSON_H__
+#define __NM_JANSSON_H__
+
+/* you need to include at least "config.h" first, possibly "nm-default.h". */
+
+#if WITH_JANSSON
+
+    #include <jansson.h>
+
+    /* Added in Jansson v2.8 */
+    #ifndef json_object_foreach_safe
+        #define json_object_foreach_safe(object, n, key, value)                         \
+            for (key = json_object_iter_key(json_object_iter(object)),                  \
+                n    = json_object_iter_next(object, json_object_key_to_iter(key));     \
+                 key && (value = json_object_iter_value(json_object_key_to_iter(key))); \
+                 key = json_object_iter_key(n),                                         \
+                n    = json_object_iter_next(object, json_object_key_to_iter(key)))
+    #endif
+
+NM_AUTO_DEFINE_FCN0(json_t *, _nm_auto_decref_json, json_decref);
+    #define nm_auto_decref_json nm_auto(_nm_auto_decref_json)
+
+#endif /* WITH_JANSON */
+
+#endif /* __NM_JANSSON_H__ */
diff --git a/src/libnm-glib-aux/nm-json-aux.c b/src/libnm-glib-aux/nm-json-aux.c
new file mode 100644
index 00000000..dc67d6d5
--- /dev/null
+++ b/src/libnm-glib-aux/nm-json-aux.c
@@ -0,0 +1,286 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2017 - 2019 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-json-aux.h"
+
+#include <dlfcn.h>
+
+/*****************************************************************************/
+
+/* If RTLD_DEEPBIND isn't available just ignore it. This can cause problems
+ * with jansson, json-glib, and cjson symbols clashing (and as such crashing the
+ * program). But that needs to be fixed by the json libraries, and it is by adding
+ * symbol versioning in recent versions. */
+#ifndef RTLD_DEEPBIND
+    #define RTLD_DEEPBIND 0
+#endif
+
+/*****************************************************************************/
+
+static void
+_gstr_append_string_len(GString *gstr, const char *str, gsize len)
+{
+    g_string_append_c(gstr, '\"');
+
+    while (len > 0) {
+        gsize       n;
+        const char *end;
+        gboolean    valid;
+
+        nm_assert(len > 0);
+
+        valid = g_utf8_validate(str, len, &end);
+
+        nm_assert(end && end >= str && end <= &str[len]);
+
+        if (end > str) {
+            const char *s;
+
+            for (s = str; s < end; s++) {
+                nm_assert(s[0] != '\0');
+
+                if (s[0] < 0x20) {
+                    const char *text;
+
+                    switch (s[0]) {
+                    case '\\':
+                        text = "\\\\";
+                        break;
+                    case '\"':
+                        text = "\\\"";
+                        break;
+                    case '\b':
+                        text = "\\b";
+                        break;
+                    case '\f':
+                        text = "\\f";
+                        break;
+                    case '\n':
+                        text = "\\n";
+                        break;
+                    case '\r':
+                        text = "\\r";
+                        break;
+                    case '\t':
+                        text = "\\t";
+                        break;
+                    default:
+                        g_string_append_printf(gstr, "\\u%04X", (guint) s[0]);
+                        continue;
+                    }
+                    g_string_append(gstr, text);
+                    continue;
+                }
+
+                if (NM_IN_SET(s[0], '\\', '\"'))
+                    g_string_append_c(gstr, '\\');
+                g_string_append_c(gstr, s[0]);
+            }
+        } else
+            nm_assert(!valid);
+
+        if (valid) {
+            nm_assert(end == &str[len]);
+            break;
+        }
+
+        nm_assert(end < &str[len]);
+
+        if (end[0] == '\0') {
+            /* there is a NUL byte in the string. Technically this is valid UTF-8, so we
+             * encode it there. However, this will likely result in a truncated string when
+             * parsing. */
+            g_string_append(gstr, "\\u0000");
+        } else {
+            /* the character is not valid UTF-8. There is nothing we can do about it, because
+             * JSON can only contain UTF-8 and even the escape sequences can only escape Unicode
+             * codepoints (but not binary).
+             *
+             * The argument is not a string (in any known encoding), hence we cannot represent
+             * it as a JSON string (which are unicode strings).
+             *
+             * Print an underscore instead of the invalid char :) */
+            g_string_append_c(gstr, '_');
+        }
+
+        n = str - end;
+        nm_assert(n < len);
+        n++;
+        str += n;
+        len -= n;
+    }
+
+    g_string_append_c(gstr, '\"');
+}
+
+void
+nm_json_gstr_append_string_len(GString *gstr, const char *str, gsize n)
+{
+    g_return_if_fail(gstr);
+
+    _gstr_append_string_len(gstr, str, n);
+}
+
+void
+nm_json_gstr_append_string(GString *gstr, const char *str)
+{
+    g_return_if_fail(gstr);
+
+    if (!str)
+        g_string_append(gstr, "null");
+    else
+        _gstr_append_string_len(gstr, str, strlen(str));
+}
+
+void
+nm_json_gstr_append_obj_name(GString *gstr, const char *key, char start_container)
+{
+    g_return_if_fail(gstr);
+    g_return_if_fail(key);
+
+    nm_json_gstr_append_string(gstr, key);
+
+    if (start_container != '\0') {
+        nm_assert(NM_IN_SET(start_container, '[', '{'));
+        g_string_append_printf(gstr, ": %c ", start_container);
+    } else
+        g_string_append(gstr, ": ");
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    NMJsonVt vt;
+    void *   dl_handle;
+} NMJsonVtInternal;
+
+static NMJsonVtInternal *
+_nm_json_vt_internal_load(void)
+{
+    NMJsonVtInternal *v;
+    const char *      soname;
+    void *            handle;
+
+    v = g_new0(NMJsonVtInternal, 1);
+
+#if WITH_JANSSON && defined(JANSSON_SONAME)
+    G_STATIC_ASSERT_EXPR(NM_STRLEN(JANSSON_SONAME) > 0);
+    nm_assert(strlen(JANSSON_SONAME) > 0);
+    soname = JANSSON_SONAME;
+#elif !WITH_JANSSON && !defined(JANSSON_SONAME)
+    soname = NULL;
+#else
+    #error "WITH_JANSON and JANSSON_SONAME are defined inconsistently."
+#endif
+
+    if (!soname)
+        return v;
+
+    handle = dlopen(soname,
+                    RTLD_LAZY | RTLD_LOCAL | RTLD_NODELETE
+#if !defined(ASAN_BUILD)
+                        | RTLD_DEEPBIND
+#endif
+                        | 0);
+    if (!handle)
+        return v;
+
+#define TRY_BIND_SYMBOL(symbol)              \
+    G_STMT_START                             \
+    {                                        \
+        void *_sym = dlsym(handle, #symbol); \
+                                             \
+        if (!_sym)                           \
+            goto fail_symbol;                \
+        v->vt.nm_##symbol = _sym;            \
+    }                                        \
+    G_STMT_END
+
+    TRY_BIND_SYMBOL(json_array);
+    TRY_BIND_SYMBOL(json_array_append_new);
+    TRY_BIND_SYMBOL(json_array_get);
+    TRY_BIND_SYMBOL(json_array_size);
+    TRY_BIND_SYMBOL(json_delete);
+    TRY_BIND_SYMBOL(json_dumps);
+    TRY_BIND_SYMBOL(json_false);
+    TRY_BIND_SYMBOL(json_integer);
+    TRY_BIND_SYMBOL(json_integer_value);
+    TRY_BIND_SYMBOL(json_loads);
+    TRY_BIND_SYMBOL(json_object);
+    TRY_BIND_SYMBOL(json_object_del);
+    TRY_BIND_SYMBOL(json_object_get);
+    TRY_BIND_SYMBOL(json_object_iter);
+    TRY_BIND_SYMBOL(json_object_iter_key);
+    TRY_BIND_SYMBOL(json_object_iter_next);
+    TRY_BIND_SYMBOL(json_object_iter_value);
+    TRY_BIND_SYMBOL(json_object_key_to_iter);
+    TRY_BIND_SYMBOL(json_object_set_new);
+    TRY_BIND_SYMBOL(json_object_size);
+    TRY_BIND_SYMBOL(json_string);
+    TRY_BIND_SYMBOL(json_string_value);
+    TRY_BIND_SYMBOL(json_true);
+
+    v->vt.loaded = TRUE;
+    v->dl_handle = handle;
+    return v;
+
+fail_symbol:
+    dlclose(&handle);
+    *v = (NMJsonVtInternal){};
+    return v;
+}
+
+const NMJsonVt *_nm_json_vt_ptr = NULL;
+
+const NMJsonVt *
+_nm_json_vt_init(void)
+{
+    NMJsonVtInternal *v;
+
+again:
+    v = g_atomic_pointer_get((gpointer *) &_nm_json_vt_ptr);
+    if (G_UNLIKELY(!v)) {
+        v = _nm_json_vt_internal_load();
+        if (!g_atomic_pointer_compare_and_exchange((gpointer *) &_nm_json_vt_ptr, NULL, v)) {
+            if (v->dl_handle)
+                dlclose(v->dl_handle);
+            g_free(v);
+            goto again;
+        }
+
+        /* we transfer ownership. */
+    }
+
+    nm_assert(v && v == g_atomic_pointer_get((gpointer *) &_nm_json_vt_ptr));
+    return &v->vt;
+}
+
+const NMJsonVt *
+nmtst_json_vt_reset(gboolean loaded)
+{
+    NMJsonVtInternal *v_old;
+    NMJsonVtInternal *v;
+
+    v_old = g_atomic_pointer_get((gpointer *) &_nm_json_vt_ptr);
+
+    if (!loaded) {
+        /* load a fake instance for testing. */
+        v = g_new0(NMJsonVtInternal, 1);
+    } else
+        v = _nm_json_vt_internal_load();
+
+    if (!g_atomic_pointer_compare_and_exchange((gpointer *) &_nm_json_vt_ptr, v_old, v))
+        g_assert_not_reached();
+
+    if (v_old) {
+        if (v_old->dl_handle)
+            dlclose(v_old->dl_handle);
+        g_free((gpointer *) v_old);
+    }
+
+    return v->vt.loaded ? &v->vt : NULL;
+}
diff --git a/src/libnm-glib-aux/nm-json-aux.h b/src/libnm-glib-aux/nm-json-aux.h
new file mode 100644
index 00000000..936e9146
--- /dev/null
+++ b/src/libnm-glib-aux/nm-json-aux.h
@@ -0,0 +1,380 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2017 - 2019 Red Hat, Inc.
+ */
+
+#ifndef __NM_JSON_AUX_H__
+#define __NM_JSON_AUX_H__
+
+#include "nm-value-type.h"
+
+/*****************************************************************************/
+
+static inline GString *
+nm_json_gstr_append_delimiter(GString *gstr)
+{
+    g_string_append(gstr, ", ");
+    return gstr;
+}
+
+void nm_json_gstr_append_string_len(GString *gstr, const char *str, gsize n);
+
+void nm_json_gstr_append_string(GString *gstr, const char *str);
+
+static inline void
+nm_json_gstr_append_bool(GString *gstr, gboolean v)
+{
+    g_string_append(gstr, v ? "true" : "false");
+}
+
+static inline void
+nm_json_gstr_append_int64(GString *gstr, gint64 v)
+{
+    g_string_append_printf(gstr, "%" G_GINT64_FORMAT, v);
+}
+
+static inline void
+nm_json_gstr_append_uint64(GString *gstr, guint64 v)
+{
+    g_string_append_printf(gstr, "%" G_GUINT64_FORMAT, v);
+}
+
+void nm_json_gstr_append_obj_name(GString *gstr, const char *key, char start_container);
+
+/*****************************************************************************/
+
+#define NM_JSON_REJECT_DUPLICATES 0x1
+
+typedef enum {
+    NM_JSON_OBJECT,
+    NM_JSON_ARRAY,
+    NM_JSON_STRING,
+    NM_JSON_INTEGER,
+    NM_JSON_REAL,
+    NM_JSON_TRUE,
+    NM_JSON_FALSE,
+    NM_JSON_NULL,
+} nm_json_type;
+
+typedef struct nm_json_t {
+    nm_json_type    type;
+    volatile size_t refcount;
+} nm_json_t;
+
+typedef long long nm_json_int_t;
+
+#define NM_JSON_ERROR_TEXT_LENGTH   160
+#define NM_JSON_ERROR_SOURCE_LENGTH 80
+
+typedef struct nm_json_error_t {
+    int  line;
+    int  column;
+    int  position;
+    char source[NM_JSON_ERROR_SOURCE_LENGTH];
+    char text[NM_JSON_ERROR_TEXT_LENGTH];
+} nm_json_error_t;
+
+typedef struct {
+    gboolean loaded;
+    char *(*nm_json_dumps)(const nm_json_t *json, size_t flags);
+    const char *(*nm_json_object_iter_key)(void *iter);
+    const char *(*nm_json_string_value)(const nm_json_t *json);
+    int (*nm_json_array_append_new)(nm_json_t *json, nm_json_t *value);
+    int (*nm_json_object_del)(nm_json_t *json, const char *key);
+    int (*nm_json_object_set_new)(nm_json_t *json, const char *key, nm_json_t *value);
+    nm_json_int_t (*nm_json_integer_value)(const nm_json_t *json);
+    nm_json_t *(*nm_json_array)(void);
+    nm_json_t *(*nm_json_array_get)(const nm_json_t *json, size_t index);
+    nm_json_t *(*nm_json_false)(void);
+    nm_json_t *(*nm_json_integer)(nm_json_int_t value);
+    nm_json_t *(*nm_json_loads)(const char *string, size_t flags, nm_json_error_t *error);
+    nm_json_t *(*nm_json_object)(void);
+    nm_json_t *(*nm_json_object_get)(const nm_json_t *json, const char *key);
+    nm_json_t *(*nm_json_object_iter_value)(void *);
+    nm_json_t *(*nm_json_string)(const char *value);
+    nm_json_t *(*nm_json_true)(void);
+    size_t (*nm_json_array_size)(const nm_json_t *json);
+    size_t (*nm_json_object_size)(const nm_json_t *json);
+    void (*nm_json_delete)(nm_json_t *json);
+    void *(*nm_json_object_iter)(nm_json_t *json);
+    void *(*nm_json_object_iter_next)(nm_json_t *json, void *iter);
+    void *(*nm_json_object_key_to_iter)(const char *key);
+} NMJsonVt;
+
+extern const NMJsonVt *_nm_json_vt_ptr;
+
+const NMJsonVt *_nm_json_vt_init(void);
+
+static inline const NMJsonVt *
+_nm_json_vt(void)
+{
+    const NMJsonVt *vt;
+
+    vt = g_atomic_pointer_get((gpointer *) &_nm_json_vt_ptr);
+    if (G_UNLIKELY(!vt)) {
+        vt = _nm_json_vt_init();
+        nm_assert(vt);
+    }
+    return vt;
+}
+
+static inline const NMJsonVt *
+nm_json_vt(void)
+{
+    const NMJsonVt *vt;
+
+    vt = _nm_json_vt();
+    return vt->loaded ? vt : NULL;
+}
+
+static inline const NMJsonVt *
+nm_json_vt_assert(void)
+{
+    const NMJsonVt *vt;
+
+    vt = _nm_json_vt();
+    nm_assert(vt->loaded);
+    return vt;
+}
+
+const NMJsonVt *nmtst_json_vt_reset(gboolean loaded);
+
+/*****************************************************************************/
+
+#define nm_json_boolean(vt, val) ((val) ? (vt)->nm_json_true() : (vt)->nm_json_false())
+
+static inline void
+nm_json_decref(const NMJsonVt *vt, nm_json_t *json)
+{
+    /* Our ref-counting is not threadsafe, unlike libjansson's. But we never
+     * share one json_t instance between threads, and if we would, we would very likely
+     * wrap a mutex around it. */
+    if (json && json->refcount != (size_t) -1 && --json->refcount == 0)
+        vt->nm_json_delete(json);
+}
+
+static inline void
+_nm_auto_decref_json(nm_json_t **p_json)
+{
+    if (*p_json && (*p_json)->refcount != (size_t) -1 && --(*p_json)->refcount == 0)
+        nm_json_vt()->nm_json_delete(*p_json);
+}
+
+#define nm_auto_decref_json nm_auto(_nm_auto_decref_json)
+
+/*****************************************************************************/
+
+/* the following are implemented as pure macros in jansson.h.
+ * They can be used directly, however, add a nm_json* variant,
+ * to make it explict we don't accidentally use jansson ABI. */
+
+#define nm_json_typeof(json)     ((json)->type)
+#define nm_json_is_object(json)  ((json) && nm_json_typeof(json) == NM_JSON_OBJECT)
+#define nm_json_is_array(json)   ((json) && nm_json_typeof(json) == NM_JSON_ARRAY)
+#define nm_json_is_string(json)  ((json) && nm_json_typeof(json) == NM_JSON_STRING)
+#define nm_json_is_integer(json) ((json) && nm_json_typeof(json) == NM_JSON_INTEGER)
+#define nm_json_is_real(json)    ((json) && nm_json_typeof(json) == NM_JSON_REAL)
+#define nm_json_is_number(json)  (nm_json_is_integer(json) || nm_json_is_real(json))
+#define nm_json_is_true(json)    ((json) && nm_json_typeof(json) == NM_JSON_TRUE)
+#define nm_json_is_false(json)   ((json) && nm_json_typeof(json) == NM_JSON_FALSE)
+#define nm_json_boolean_value    nm_json_is_true
+#define nm_json_is_boolean(json) (nm_json_is_true(json) || nm_json_is_false(json))
+#define nm_json_is_null(json)    ((json) && nm_json_typeof(json) == NM_JSON_NULL)
+
+#define nm_json_array_foreach(vt, array, index, value)                                           \
+    for (index = 0;                                                                              \
+         index < vt->nm_json_array_size(array) && (value = vt->nm_json_array_get(array, index)); \
+         index++)
+
+#define nm_json_object_foreach(vt, object, key, value)                                        \
+    for (key = vt->nm_json_object_iter_key(vt->nm_json_object_iter(object));                  \
+         key && (value = vt->nm_json_object_iter_value(vt->nm_json_object_key_to_iter(key))); \
+         key = vt->nm_json_object_iter_key(                                                   \
+             vt->nm_json_object_iter_next(object, vt->nm_json_object_key_to_iter(key))))
+
+/*****************************************************************************/
+
+static inline int
+nm_jansson_json_as_bool(const nm_json_t *elem, bool *out_val)
+{
+    if (!elem)
+        return 0;
+
+    if (!nm_json_is_boolean(elem))
+        return -EINVAL;
+
+    NM_SET_OUT(out_val, nm_json_boolean_value(elem));
+    return 1;
+}
+
+static inline int
+nm_jansson_json_as_int32(const NMJsonVt *vt, const nm_json_t *elem, gint32 *out_val)
+{
+    nm_json_int_t v;
+
+    if (!elem)
+        return 0;
+
+    if (!nm_json_is_integer(elem))
+        return -EINVAL;
+
+    v = vt->nm_json_integer_value(elem);
+    if (v < (gint64) G_MININT32 || v > (gint64) G_MAXINT32)
+        return -ERANGE;
+
+    NM_SET_OUT(out_val, v);
+    return 1;
+}
+
+static inline int
+nm_jansson_json_as_int(const NMJsonVt *vt, const nm_json_t *elem, int *out_val)
+{
+    nm_json_int_t v;
+
+    if (!elem)
+        return 0;
+
+    if (!nm_json_is_integer(elem))
+        return -EINVAL;
+
+    v = vt->nm_json_integer_value(elem);
+    if (v < (gint64) G_MININT || v > (gint64) G_MAXINT)
+        return -ERANGE;
+
+    NM_SET_OUT(out_val, v);
+    return 1;
+}
+
+static inline int
+nm_jansson_json_as_int64(const NMJsonVt *vt, const nm_json_t *elem, gint64 *out_val)
+{
+    nm_json_int_t v;
+
+    if (!elem)
+        return 0;
+
+    if (!nm_json_is_integer(elem))
+        return -EINVAL;
+
+    /* assert that this integer is signed. */
+    G_STATIC_ASSERT_EXPR(((nm_json_int_t) -1) < 0);
+
+    v = vt->nm_json_integer_value(elem);
+
+    if (sizeof(v) > sizeof(gint64)) {
+        if (v < G_MININT64 || v > G_MAXINT64)
+            return -ERANGE;
+    }
+
+    NM_SET_OUT(out_val, v);
+    return 1;
+}
+
+static inline int
+nm_jansson_json_as_uint64(const NMJsonVt *vt, const nm_json_t *elem, guint64 *out_val)
+{
+    nm_json_int_t v;
+
+    if (!elem)
+        return 0;
+
+    if (!nm_json_is_integer(elem))
+        return -EINVAL;
+
+    /* assert that this integer is signed. */
+    G_STATIC_ASSERT_EXPR(((nm_json_int_t) -1) < 0);
+
+    v = vt->nm_json_integer_value(elem);
+    if (v < 0)
+        return -ERANGE;
+
+    if (sizeof(v) > sizeof(gint64)) {
+        if (v > G_MAXUINT64)
+            return -ERANGE;
+    }
+
+    NM_SET_OUT(out_val, v);
+    return 1;
+}
+
+static inline int
+nm_jansson_json_as_string(const NMJsonVt *vt, const nm_json_t *elem, const char **out_val)
+{
+    if (!elem)
+        return 0;
+
+    if (!nm_json_is_string(elem))
+        return -EINVAL;
+
+    NM_SET_OUT(out_val, vt->nm_json_string_value(elem));
+    return 1;
+}
+
+/*****************************************************************************/
+
+#ifdef NM_VALUE_TYPE_DEFINE_FUNCTIONS
+
+static inline void
+nm_value_type_to_json(NMValueType value_type, GString *gstr, gconstpointer p_field)
+{
+    nm_assert(p_field);
+    nm_assert(gstr);
+
+    switch (value_type) {
+    case NM_VALUE_TYPE_BOOL:
+        nm_json_gstr_append_bool(gstr, *((const bool *) p_field));
+        return;
+    case NM_VALUE_TYPE_INT32:
+        nm_json_gstr_append_int64(gstr, *((const gint32 *) p_field));
+        return;
+    case NM_VALUE_TYPE_INT:
+        nm_json_gstr_append_int64(gstr, *((const int *) p_field));
+        return;
+    case NM_VALUE_TYPE_INT64:
+        nm_json_gstr_append_int64(gstr, *((const gint64 *) p_field));
+        return;
+    case NM_VALUE_TYPE_UINT64:
+        nm_json_gstr_append_uint64(gstr, *((const guint64 *) p_field));
+        return;
+    case NM_VALUE_TYPE_STRING:
+        nm_json_gstr_append_string(gstr, *((const char *const *) p_field));
+        return;
+    case NM_VALUE_TYPE_UNSPEC:
+        break;
+    }
+    nm_assert_not_reached();
+}
+
+static inline gboolean
+nm_value_type_from_json(const NMJsonVt * vt,
+                        NMValueType      value_type,
+                        const nm_json_t *elem,
+                        gpointer         out_val)
+{
+    switch (value_type) {
+    case NM_VALUE_TYPE_BOOL:
+        return (nm_jansson_json_as_bool(elem, out_val) > 0);
+    case NM_VALUE_TYPE_INT32:
+        return (nm_jansson_json_as_int32(vt, elem, out_val) > 0);
+    case NM_VALUE_TYPE_INT:
+        return (nm_jansson_json_as_int(vt, elem, out_val) > 0);
+    case NM_VALUE_TYPE_INT64:
+        return (nm_jansson_json_as_int64(vt, elem, out_val) > 0);
+    case NM_VALUE_TYPE_UINT64:
+        return (nm_jansson_json_as_uint64(vt, elem, out_val) > 0);
+
+    /* warning: this overwrites/leaks the previous value. You better have *out_val
+     * point to uninitialized memory or NULL. */
+    case NM_VALUE_TYPE_STRING:
+        return (nm_jansson_json_as_string(vt, elem, out_val) > 0);
+
+    case NM_VALUE_TYPE_UNSPEC:
+        break;
+    }
+    nm_assert_not_reached();
+    return FALSE;
+}
+
+#endif /* NM_VALUE_TYPE_DEFINE_FUNCTIONS */
+
+#endif /* __NM_JSON_AUX_H__ */
diff --git a/src/libnm-glib-aux/nm-keyfile-aux.c b/src/libnm-glib-aux/nm-keyfile-aux.c
new file mode 100644
index 00000000..9cda1cf7
--- /dev/null
+++ b/src/libnm-glib-aux/nm-keyfile-aux.c
@@ -0,0 +1,487 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-keyfile-aux.h"
+
+#include <syslog.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include "nm-io-utils.h"
+
+/*****************************************************************************/
+
+struct _NMKeyFileDB {
+    NMKeyFileDBLogFcn      log_fcn;
+    NMKeyFileDBGotDirtyFcn got_dirty_fcn;
+    gpointer               user_data;
+    const char *           group_name;
+    GKeyFile *             kf;
+    guint                  ref_count;
+
+    bool is_started : 1;
+    bool dirty : 1;
+    bool destroyed : 1;
+
+    bool groups_pruned : 1;
+
+    char filename[];
+};
+
+#define _NMLOG(self, syslog_level, fmt, ...)                                                   \
+    G_STMT_START                                                                               \
+    {                                                                                          \
+        NMKeyFileDB *_self = (self);                                                           \
+                                                                                               \
+        nm_assert(_self);                                                                      \
+        nm_assert(!_self->destroyed);                                                          \
+                                                                                               \
+        if (_self->log_fcn) {                                                                  \
+            _self->log_fcn(_self, (syslog_level), _self->user_data, "" fmt "", ##__VA_ARGS__); \
+        };                                                                                     \
+    }                                                                                          \
+    G_STMT_END
+
+#define _LOGD(...) _NMLOG(self, LOG_DEBUG, __VA_ARGS__)
+
+static gboolean
+_IS_KEY_FILE_DB(NMKeyFileDB *self, gboolean require_is_started, gboolean allow_destroyed)
+{
+    if (self == NULL)
+        return FALSE;
+    if (self->ref_count <= 0) {
+        nm_assert_not_reached();
+        return FALSE;
+    }
+    if (require_is_started && !self->is_started)
+        return FALSE;
+    if (!allow_destroyed && self->destroyed)
+        return FALSE;
+    return TRUE;
+}
+
+static GKeyFile *
+_key_file_new(void)
+{
+    GKeyFile *kf;
+
+    kf = g_key_file_new();
+    g_key_file_set_list_separator(kf, ',');
+    return kf;
+}
+
+/*****************************************************************************/
+
+NMKeyFileDB *
+nm_key_file_db_new(const char *           filename,
+                   const char *           group_name,
+                   NMKeyFileDBLogFcn      log_fcn,
+                   NMKeyFileDBGotDirtyFcn got_dirty_fcn,
+                   gpointer               user_data)
+{
+    NMKeyFileDB *self;
+    gsize        l_filename;
+    gsize        l_group;
+
+    g_return_val_if_fail(filename && filename[0], NULL);
+    g_return_val_if_fail(group_name && group_name[0], NULL);
+
+    l_filename = strlen(filename);
+    l_group    = strlen(group_name);
+
+    self                = g_malloc0(sizeof(NMKeyFileDB) + l_filename + 1 + l_group + 1);
+    self->ref_count     = 1;
+    self->log_fcn       = log_fcn;
+    self->got_dirty_fcn = got_dirty_fcn;
+    self->user_data     = user_data;
+    self->kf            = _key_file_new();
+    memcpy(self->filename, filename, l_filename + 1);
+    self->group_name = &self->filename[l_filename + 1];
+    memcpy((char *) self->group_name, group_name, l_group + 1);
+
+    return self;
+}
+
+NMKeyFileDB *
+nm_key_file_db_ref(NMKeyFileDB *self)
+{
+    if (!self)
+        return NULL;
+
+    g_return_val_if_fail(_IS_KEY_FILE_DB(self, FALSE, TRUE), NULL);
+
+    nm_assert(self->ref_count < G_MAXUINT);
+    self->ref_count++;
+    return self;
+}
+
+void
+nm_key_file_db_unref(NMKeyFileDB *self)
+{
+    if (!self)
+        return;
+
+    g_return_if_fail(_IS_KEY_FILE_DB(self, FALSE, TRUE));
+
+    if (--self->ref_count > 0)
+        return;
+
+    g_key_file_unref(self->kf);
+
+    g_free(self);
+}
+
+/* destroy() is like unref, but it also makes the instance unusable.
+ * All changes afterwards fail with an assertion.
+ *
+ * The point is that NMKeyFileDB is ref-counted in principle. But there
+ * is a primary owner who also provides the log_fcn().
+ *
+ * When the primary owner goes out of scope and gives up the reference, it does
+ * not want to receive any log notifications anymore.
+ *
+ * The way NMKeyFileDB is intended to be used is in a very strict context:
+ * NMSettings owns the NMKeyFileDB instance and receives logging notifications.
+ * It's also the last one to persist the data to disk. Afterwards, no other user
+ * is supposed to be around and do anything with NMKeyFileDB. But since NMKeyFileDB
+ * is ref-counted it's hard to ensure that this is truly honored. So we start
+ * asserting at that point.
+ */
+void
+nm_key_file_db_destroy(NMKeyFileDB *self)
+{
+    if (!self)
+        return;
+
+    g_return_if_fail(_IS_KEY_FILE_DB(self, FALSE, FALSE));
+    g_return_if_fail(!self->destroyed);
+
+    self->destroyed = TRUE;
+    nm_key_file_db_unref(self);
+}
+
+/*****************************************************************************/
+
+/* nm_key_file_db_start() is supposed to be called right away, after creating the
+ * instance.
+ *
+ * It's not done as separate step after nm_key_file_db_new(), because we want to log,
+ * and the log_fcn returns the self pointer (which we should not expose before
+ * nm_key_file_db_new() returns. */
+void
+nm_key_file_db_start(NMKeyFileDB *self)
+{
+    gs_free char *contents = NULL;
+    gsize         contents_len;
+    gs_free_error GError *error = NULL;
+
+    g_return_if_fail(_IS_KEY_FILE_DB(self, FALSE, FALSE));
+    g_return_if_fail(!self->is_started);
+
+    self->is_started = TRUE;
+
+    if (!nm_utils_file_get_contents(-1,
+                                    self->filename,
+                                    20 * 1024 * 1024,
+                                    NM_UTILS_FILE_GET_CONTENTS_FLAG_NONE,
+                                    &contents,
+                                    &contents_len,
+                                    NULL,
+                                    &error)) {
+        _LOGD("failed to read \"%s\": %s", self->filename, error->message);
+        return;
+    }
+
+    if (!g_key_file_load_from_data(self->kf,
+                                   contents,
+                                   contents_len,
+                                   G_KEY_FILE_KEEP_COMMENTS,
+                                   &error)) {
+        _LOGD("failed to load keyfile \"%s\": %s", self->filename, error->message);
+        return;
+    }
+
+    _LOGD("loaded keyfile-db for \"%s\"", self->filename);
+}
+
+/*****************************************************************************/
+
+const char *
+nm_key_file_db_get_filename(NMKeyFileDB *self)
+{
+    g_return_val_if_fail(_IS_KEY_FILE_DB(self, FALSE, TRUE), NULL);
+
+    return self->filename;
+}
+
+gboolean
+nm_key_file_db_is_dirty(NMKeyFileDB *self)
+{
+    g_return_val_if_fail(_IS_KEY_FILE_DB(self, FALSE, TRUE), FALSE);
+
+    return self->dirty;
+}
+
+/*****************************************************************************/
+
+char *
+nm_key_file_db_get_value(NMKeyFileDB *self, const char *key)
+{
+    g_return_val_if_fail(_IS_KEY_FILE_DB(self, TRUE, TRUE), NULL);
+
+    return g_key_file_get_value(self->kf, self->group_name, key, NULL);
+}
+
+char **
+nm_key_file_db_get_string_list(NMKeyFileDB *self, const char *key, gsize *out_len)
+{
+    g_return_val_if_fail(_IS_KEY_FILE_DB(self, TRUE, TRUE), NULL);
+
+    return g_key_file_get_string_list(self->kf, self->group_name, key, out_len, NULL);
+}
+
+/*****************************************************************************/
+
+static void
+_got_dirty(NMKeyFileDB *self, const char *key)
+{
+    nm_assert(_IS_KEY_FILE_DB(self, TRUE, FALSE));
+    nm_assert(!self->dirty);
+
+    _LOGD("updated entry for %s.%s", self->group_name, key);
+
+    self->dirty = TRUE;
+    if (self->got_dirty_fcn)
+        self->got_dirty_fcn(self, self->user_data);
+}
+
+/*****************************************************************************/
+
+void
+nm_key_file_db_remove_key(NMKeyFileDB *self, const char *key)
+{
+    gboolean got_dirty = FALSE;
+
+    g_return_if_fail(_IS_KEY_FILE_DB(self, TRUE, FALSE));
+
+    if (!key)
+        return;
+
+    if (!self->dirty) {
+        gs_free_error GError *error = NULL;
+
+        g_key_file_has_key(self->kf, self->group_name, key, &error);
+        got_dirty = (error != NULL);
+    }
+    g_key_file_remove_key(self->kf, self->group_name, key, NULL);
+
+    if (got_dirty)
+        _got_dirty(self, key);
+}
+
+void
+nm_key_file_db_set_value(NMKeyFileDB *self, const char *key, const char *value)
+{
+    gs_free char *old_value = NULL;
+    gboolean      got_dirty = FALSE;
+
+    g_return_if_fail(_IS_KEY_FILE_DB(self, TRUE, FALSE));
+    g_return_if_fail(key);
+
+    if (!value) {
+        nm_key_file_db_remove_key(self, key);
+        return;
+    }
+
+    if (!self->dirty) {
+        gs_free_error GError *error = NULL;
+
+        old_value = g_key_file_get_value(self->kf, self->group_name, key, &error);
+        if (error)
+            got_dirty = TRUE;
+    }
+
+    g_key_file_set_value(self->kf, self->group_name, key, value);
+
+    if (!self->dirty && !got_dirty) {
+        gs_free_error GError *error     = NULL;
+        gs_free char *        new_value = NULL;
+
+        new_value = g_key_file_get_value(self->kf, self->group_name, key, &error);
+        if (error || !new_value || !nm_streq0(old_value, new_value))
+            got_dirty = TRUE;
+    }
+
+    if (got_dirty)
+        _got_dirty(self, key);
+}
+
+void
+nm_key_file_db_set_string_list(NMKeyFileDB *      self,
+                               const char *       key,
+                               const char *const *value,
+                               gssize             len)
+{
+    gs_free char *old_value = NULL;
+    gboolean      got_dirty = FALSE;
+
+    g_return_if_fail(_IS_KEY_FILE_DB(self, TRUE, FALSE));
+    g_return_if_fail(key);
+
+    if (!value) {
+        nm_key_file_db_remove_key(self, key);
+        return;
+    }
+
+    if (!self->dirty) {
+        gs_free_error GError *error = NULL;
+
+        old_value = g_key_file_get_value(self->kf, self->group_name, key, &error);
+        if (error)
+            got_dirty = TRUE;
+    }
+
+    if (len < 0)
+        len = NM_PTRARRAY_LEN(value);
+
+    g_key_file_set_string_list(self->kf, self->group_name, key, value, len);
+
+    if (!self->dirty && !got_dirty) {
+        gs_free_error GError *error     = NULL;
+        gs_free char *        new_value = NULL;
+
+        new_value = g_key_file_get_value(self->kf, self->group_name, key, &error);
+        if (error || !new_value || !nm_streq0(old_value, new_value))
+            got_dirty = TRUE;
+    }
+
+    if (got_dirty)
+        _got_dirty(self, key);
+}
+
+/*****************************************************************************/
+
+void
+nm_key_file_db_to_file(NMKeyFileDB *self, gboolean force)
+{
+    gs_free_error GError *error = NULL;
+
+    g_return_if_fail(_IS_KEY_FILE_DB(self, TRUE, FALSE));
+
+    if (!force && !self->dirty)
+        return;
+
+    self->dirty = FALSE;
+
+    if (!g_key_file_save_to_file(self->kf, self->filename, &error)) {
+        _LOGD("failure to write keyfile \"%s\": %s", self->filename, error->message);
+    } else
+        _LOGD("write keyfile: \"%s\"", self->filename);
+}
+
+/*****************************************************************************/
+
+void
+nm_key_file_db_prune_tmp_files(NMKeyFileDB *self)
+{
+    gs_free char *     n_file   = NULL;
+    gs_free char *     n_dir    = NULL;
+    gs_strfreev char **tmpfiles = NULL;
+    gsize              i;
+
+    n_file = g_path_get_basename(self->filename);
+    n_dir  = g_path_get_dirname(self->filename);
+
+    tmpfiles = nm_utils_find_mkstemp_files(n_dir, n_file);
+    if (!tmpfiles)
+        return;
+
+    for (i = 0; tmpfiles[i]; i++) {
+        const char *  tmpfile   = tmpfiles[i];
+        gs_free char *full_file = NULL;
+        int           r;
+
+        full_file = g_strdup_printf("%s/%s", n_dir, tmpfile);
+
+        r = unlink(full_file);
+        if (r != 0) {
+            int errsv = errno;
+
+            if (errsv != ENOENT) {
+                _LOGD("prune left over temp file %s failed: %s",
+                      full_file,
+                      nm_strerror_native(errsv));
+            }
+            continue;
+        }
+
+        _LOGD("prune left over temp file %s", full_file);
+    }
+}
+
+/*****************************************************************************/
+
+void
+nm_key_file_db_prune(NMKeyFileDB *self,
+                     gboolean (*predicate)(const char *key, gpointer user_data),
+                     gpointer user_data)
+{
+    gs_strfreev char **   keys                 = NULL;
+    nm_auto_unref_keyfile GKeyFile *kf_to_free = NULL;
+    GKeyFile *                      kf_src     = NULL;
+    GKeyFile *                      kf_dst     = NULL;
+    guint                           k;
+
+    g_return_if_fail(_IS_KEY_FILE_DB(self, TRUE, FALSE));
+    nm_assert(predicate);
+
+    _LOGD("prune keyfile of old entries: \"%s\"", self->filename);
+
+    if (!self->groups_pruned) {
+        /* When we prune the first time, we swap the GKeyfile instance.
+         * The instance loaded from disk might have unrelated groups and
+         * comments. Let's get rid of them by creating a new instance.
+         *
+         * Otherwise, we know that self->kf only contains good keys,
+         * and at most we need to remove some of them. */
+        kf_to_free          = g_steal_pointer(&self->kf);
+        self->kf            = _key_file_new();
+        kf_src              = kf_to_free;
+        self->groups_pruned = TRUE;
+        self->dirty         = TRUE;
+    } else
+        kf_src = self->kf;
+    kf_dst = self->kf;
+
+    keys = g_key_file_get_keys(kf_src, self->group_name, NULL, NULL);
+    if (keys) {
+        for (k = 0; keys[k]; k++) {
+            const char *key = keys[k];
+            gboolean    keep;
+
+            keep = predicate(key, user_data);
+
+            if (!keep) {
+                if (kf_dst == kf_src) {
+                    g_key_file_remove_key(kf_dst, self->group_name, key, NULL);
+                    self->dirty = TRUE;
+                }
+                continue;
+            }
+
+            if (kf_dst != kf_src) {
+                gs_free char *value = NULL;
+
+                value = g_key_file_get_value(kf_src, self->group_name, key, NULL);
+                if (value)
+                    g_key_file_set_value(kf_dst, self->group_name, key, value);
+                else
+                    self->dirty = TRUE;
+            }
+        }
+    }
+}
diff --git a/src/libnm-glib-aux/nm-keyfile-aux.h b/src/libnm-glib-aux/nm-keyfile-aux.h
new file mode 100644
index 00000000..e756c57a
--- /dev/null
+++ b/src/libnm-glib-aux/nm-keyfile-aux.h
@@ -0,0 +1,61 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019 Red Hat, Inc.
+ */
+
+#ifndef __NM_KEYFILE_AUX_H__
+#define __NM_KEYFILE_AUX_H__
+
+/*****************************************************************************/
+
+typedef struct _NMKeyFileDB NMKeyFileDB;
+
+typedef void (*NMKeyFileDBLogFcn)(NMKeyFileDB *self,
+                                  int          syslog_level,
+                                  gpointer     user_data,
+                                  const char * fmt,
+                                  ...) G_GNUC_PRINTF(4, 5);
+
+typedef void (*NMKeyFileDBGotDirtyFcn)(NMKeyFileDB *self, gpointer user_data);
+
+NMKeyFileDB *nm_key_file_db_new(const char *           filename,
+                                const char *           group,
+                                NMKeyFileDBLogFcn      log_fcn,
+                                NMKeyFileDBGotDirtyFcn got_dirty_fcn,
+                                gpointer               user_data);
+
+void nm_key_file_db_start(NMKeyFileDB *self);
+
+NMKeyFileDB *nm_key_file_db_ref(NMKeyFileDB *self);
+void         nm_key_file_db_unref(NMKeyFileDB *self);
+
+void nm_key_file_db_destroy(NMKeyFileDB *self);
+
+const char *nm_key_file_db_get_filename(NMKeyFileDB *self);
+
+gboolean nm_key_file_db_is_dirty(NMKeyFileDB *self);
+
+char *nm_key_file_db_get_value(NMKeyFileDB *self, const char *key);
+
+char **nm_key_file_db_get_string_list(NMKeyFileDB *self, const char *key, gsize *out_len);
+
+void nm_key_file_db_remove_key(NMKeyFileDB *self, const char *key);
+
+void nm_key_file_db_set_value(NMKeyFileDB *self, const char *key, const char *value);
+
+void nm_key_file_db_set_string_list(NMKeyFileDB *      self,
+                                    const char *       key,
+                                    const char *const *value,
+                                    gssize             len);
+
+void nm_key_file_db_to_file(NMKeyFileDB *self, gboolean force);
+
+void nm_key_file_db_prune_tmp_files(NMKeyFileDB *self);
+
+void nm_key_file_db_prune(NMKeyFileDB *self,
+                          gboolean (*predicate)(const char *key, gpointer user_data),
+                          gpointer user_data);
+
+/*****************************************************************************/
+
+#endif /* __NM_KEYFILE_AUX_H__ */
diff --git a/src/libnm-glib-aux/nm-logging-base.c b/src/libnm-glib-aux/nm-logging-base.c
new file mode 100644
index 00000000..e11bd9f3
--- /dev/null
+++ b/src/libnm-glib-aux/nm-logging-base.c
@@ -0,0 +1,79 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-logging-base.h"
+
+#include <syslog.h>
+
+/*****************************************************************************/
+
+const LogLevelDesc nm_log_level_desc[_LOGL_N] = {
+    [LOGL_TRACE] =
+        {
+            "TRACE",
+            "<trace>",
+            LOG_DEBUG,
+            G_LOG_LEVEL_DEBUG,
+        },
+    [LOGL_DEBUG] =
+        {
+            "DEBUG",
+            "<debug>",
+            LOG_DEBUG,
+            G_LOG_LEVEL_DEBUG,
+        },
+    [LOGL_INFO] =
+        {
+            "INFO",
+            "<info>",
+            LOG_INFO,
+            G_LOG_LEVEL_INFO,
+        },
+    [LOGL_WARN] =
+        {
+            "WARN",
+            "<warn>",
+            LOG_WARNING,
+            G_LOG_LEVEL_MESSAGE,
+        },
+    [LOGL_ERR] =
+        {
+            "ERR",
+            "<error>",
+            LOG_ERR,
+            G_LOG_LEVEL_MESSAGE,
+        },
+    [_LOGL_OFF] =
+        {
+            "OFF",
+            NULL,
+            0,
+            0,
+        },
+    [_LOGL_KEEP] =
+        {
+            "KEEP",
+            NULL,
+            0,
+            0,
+        },
+};
+
+gboolean
+_nm_log_parse_level(const char *level, NMLogLevel *out_level)
+{
+    int i;
+
+    if (!level)
+        return FALSE;
+
+    for (i = 0; i < (int) G_N_ELEMENTS(nm_log_level_desc); i++) {
+        if (!g_ascii_strcasecmp(nm_log_level_desc[i].name, level)) {
+            NM_SET_OUT(out_level, i);
+            return TRUE;
+        }
+    }
+
+    return FALSE;
+}
diff --git a/src/libnm-glib-aux/nm-logging-base.h b/src/libnm-glib-aux/nm-logging-base.h
new file mode 100644
index 00000000..136f0c04
--- /dev/null
+++ b/src/libnm-glib-aux/nm-logging-base.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NM_LOGGING_BASE_H__
+#define __NM_LOGGING_BASE_H__
+
+#include "nm-logging-fwd.h"
+
+typedef struct {
+    const char *name;
+    const char *level_str;
+
+    /* nm-logging uses syslog internally. Note that the three most-verbose syslog levels
+     * are LOG_DEBUG, LOG_INFO and LOG_NOTICE. Journal already highlights LOG_NOTICE
+     * as special.
+     *
+     * On the other hand, we have three levels LOGL_TRACE, LOGL_DEBUG and LOGL_INFO,
+     * which are regular messages not to be highlighted. For that reason, we must map
+     * LOGL_TRACE and LOGL_DEBUG both to syslog level LOG_DEBUG. */
+    int syslog_level;
+
+    GLogLevelFlags g_log_level;
+} LogLevelDesc;
+
+extern const LogLevelDesc nm_log_level_desc[_LOGL_N];
+
+gboolean _nm_log_parse_level(const char *level, NMLogLevel *out_level);
+
+#endif /* __NM_LOGGING_BASE_H__ */
diff --git a/src/libnm-glib-aux/nm-logging-fwd.h b/src/libnm-glib-aux/nm-logging-fwd.h
new file mode 100644
index 00000000..0ede91b2
--- /dev/null
+++ b/src/libnm-glib-aux/nm-logging-fwd.h
@@ -0,0 +1,310 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2006 - 2018 Red Hat, Inc.
+ * Copyright (C) 2006 - 2008 Novell, Inc.
+ */
+
+#ifndef __NM_LOGGING_FWD_H__
+#define __NM_LOGGING_FWD_H__
+
+/* Log domains */
+
+typedef enum { /*< skip >*/
+               LOGD_NONE       = 0LL,
+               LOGD_PLATFORM   = (1LL << 0), /* Platform services */
+               LOGD_RFKILL     = (1LL << 1),
+               LOGD_ETHER      = (1LL << 2),
+               LOGD_WIFI       = (1LL << 3),
+               LOGD_BT         = (1LL << 4),
+               LOGD_MB         = (1LL << 5), /* mobile broadband */
+               LOGD_DHCP4      = (1LL << 6),
+               LOGD_DHCP6      = (1LL << 7),
+               LOGD_PPP        = (1LL << 8),
+               LOGD_WIFI_SCAN  = (1LL << 9),
+               LOGD_IP4        = (1LL << 10),
+               LOGD_IP6        = (1LL << 11),
+               LOGD_AUTOIP4    = (1LL << 12),
+               LOGD_DNS        = (1LL << 13),
+               LOGD_VPN        = (1LL << 14),
+               LOGD_SHARING    = (1LL << 15), /* Connection sharing/dnsmasq */
+               LOGD_SUPPLICANT = (1LL << 16), /* Wi-Fi and 802.1x */
+               LOGD_AGENTS     = (1LL << 17), /* Secret agents */
+               LOGD_SETTINGS   = (1LL << 18), /* Settings */
+               LOGD_SUSPEND    = (1LL << 19), /* Suspend/Resume */
+               LOGD_CORE       = (1LL << 20), /* Core daemon and policy stuff */
+               LOGD_DEVICE     = (1LL << 21), /* Device state and activation */
+               LOGD_OLPC       = (1LL << 22),
+               LOGD_INFINIBAND = (1LL << 23),
+               LOGD_FIREWALL   = (1LL << 24),
+               LOGD_ADSL       = (1LL << 25),
+               LOGD_BOND       = (1LL << 26),
+               LOGD_VLAN       = (1LL << 27),
+               LOGD_BRIDGE     = (1LL << 28),
+               LOGD_DBUS_PROPS = (1LL << 29),
+               LOGD_TEAM       = (1LL << 30),
+               LOGD_CONCHECK   = (1LL << 31),
+               LOGD_DCB        = (1LL << 32), /* Data Center Bridging */
+               LOGD_DISPATCH   = (1LL << 33),
+               LOGD_AUDIT      = (1LL << 34),
+               LOGD_SYSTEMD    = (1LL << 35),
+               LOGD_VPN_PLUGIN = (1LL << 36),
+               LOGD_PROXY      = (1LL << 37),
+
+               __LOGD_MAX,
+               LOGD_ALL     = (((__LOGD_MAX - 1LL) << 1) - 1LL),
+               LOGD_DEFAULT = LOGD_ALL & ~(LOGD_DBUS_PROPS | LOGD_WIFI_SCAN | LOGD_VPN_PLUGIN | 0),
+
+               /* aliases: */
+               LOGD_DHCP = LOGD_DHCP4 | LOGD_DHCP6,
+               LOGD_IP   = LOGD_IP4 | LOGD_IP6,
+
+#define LOGD_DHCPX(is_ipv4) ((is_ipv4) ? LOGD_DHCP4 : LOGD_DHCP6)
+#define LOGD_IPX(is_ipv4)   ((is_ipv4) ? LOGD_IP4 : LOGD_IP6)
+
+} NMLogDomain;
+
+/* Log levels */
+typedef enum { /*< skip >*/
+               LOGL_TRACE,
+               LOGL_DEBUG,
+               LOGL_INFO,
+               LOGL_WARN,
+               LOGL_ERR,
+
+               _LOGL_N_REAL, /* the number of actual logging levels */
+
+               _LOGL_OFF = _LOGL_N_REAL, /* special logging level that is always disabled. */
+               _LOGL_KEEP, /* special logging level to indicate that the logging level should not be changed. */
+
+               _LOGL_N, /* the number of logging levels including "OFF" */
+} NMLogLevel;
+
+gboolean _nm_log_enabled_impl(gboolean mt_require_locking, NMLogLevel level, NMLogDomain domain);
+
+void _nm_log_impl(const char *file,
+                  guint       line,
+                  const char *func,
+                  gboolean    mt_require_locking,
+                  NMLogLevel  level,
+                  NMLogDomain domain,
+                  int         error,
+                  const char *ifname,
+                  const char *con_uuid,
+                  const char *fmt,
+                  ...) _nm_printf(10, 11);
+
+#define _nm_log_full(...) _nm_log_impl(__VA_ARGS__)
+
+static inline NMLogLevel
+nm_log_level_from_syslog(int syslog_level)
+{
+    switch (syslog_level) {
+    case 0 /* LOG_EMERG */:
+        return LOGL_ERR;
+    case 1 /* LOG_ALERT */:
+        return LOGL_ERR;
+    case 2 /* LOG_CRIT */:
+        return LOGL_ERR;
+    case 3 /* LOG_ERR */:
+        return LOGL_ERR;
+    case 4 /* LOG_WARNING */:
+        return LOGL_WARN;
+    case 5 /* LOG_NOTICE */:
+        return LOGL_INFO;
+    case 6 /* LOG_INFO */:
+        return LOGL_DEBUG;
+    case 7 /* LOG_DEBUG */:
+        return LOGL_TRACE;
+    default:
+        return syslog_level >= 0 ? LOGL_TRACE : LOGL_ERR;
+    }
+}
+
+static inline int
+nm_log_level_to_syslog(NMLogLevel nm_level)
+{
+    switch (nm_level) {
+    case LOGL_ERR:
+        return 3; /* LOG_ERR */
+    case LOGL_WARN:
+        return 4; /* LOG_WARN */
+    case LOGL_INFO:
+        return 5; /* LOG_NOTICE */
+    case LOGL_DEBUG:
+        return 6; /* LOG_INFO */
+    case LOGL_TRACE:
+        return 7; /* LOG_DEBUG */
+    default:
+        return 0; /* LOG_EMERG */
+    }
+}
+
+/*****************************************************************************/
+
+struct timespec;
+
+/* this function must be implemented to handle the notification when
+ * the first monotonic-timestamp is fetched. */
+extern void _nm_utils_monotonic_timestamp_initialized(const struct timespec *tp,
+                                                      gint64                 offset_sec,
+                                                      gboolean               is_boottime);
+
+/*****************************************************************************/
+
+#define _LOGL_TRACE LOGL_TRACE
+#define _LOGL_DEBUG LOGL_DEBUG
+#define _LOGL_INFO  LOGL_INFO
+#define _LOGL_WARN  LOGL_WARN
+#define _LOGL_ERR   LOGL_ERR
+
+/* This is the default definition of _NMLOG_ENABLED(). Special implementations
+ * might want to undef this and redefine it. */
+#define _NMLOG_ENABLED(level) (nm_logging_enabled((level), (_NMLOG_DOMAIN)))
+
+#define _LOGT(...) _NMLOG(_LOGL_TRACE, __VA_ARGS__)
+#define _LOGD(...) _NMLOG(_LOGL_DEBUG, __VA_ARGS__)
+#define _LOGI(...) _NMLOG(_LOGL_INFO, __VA_ARGS__)
+#define _LOGW(...) _NMLOG(_LOGL_WARN, __VA_ARGS__)
+#define _LOGE(...) _NMLOG(_LOGL_ERR, __VA_ARGS__)
+
+#define _LOGT_ENABLED(...) _NMLOG_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)
+#define _LOGD_ENABLED(...) _NMLOG_ENABLED(_LOGL_DEBUG, ##__VA_ARGS__)
+#define _LOGI_ENABLED(...) _NMLOG_ENABLED(_LOGL_INFO, ##__VA_ARGS__)
+#define _LOGW_ENABLED(...) _NMLOG_ENABLED(_LOGL_WARN, ##__VA_ARGS__)
+#define _LOGE_ENABLED(...) _NMLOG_ENABLED(_LOGL_ERR, ##__VA_ARGS__)
+
+#define _LOGT_err(errsv, ...) _NMLOG_err(errsv, _LOGL_TRACE, __VA_ARGS__)
+#define _LOGD_err(errsv, ...) _NMLOG_err(errsv, _LOGL_DEBUG, __VA_ARGS__)
+#define _LOGI_err(errsv, ...) _NMLOG_err(errsv, _LOGL_INFO, __VA_ARGS__)
+#define _LOGW_err(errsv, ...) _NMLOG_err(errsv, _LOGL_WARN, __VA_ARGS__)
+#define _LOGE_err(errsv, ...) _NMLOG_err(errsv, _LOGL_ERR, __VA_ARGS__)
+
+/* _LOGT() and _LOGt() both log with level TRACE, but the latter is disabled by default,
+ * unless building with --with-more-logging. */
+#if NM_MORE_LOGGING
+    #define _LOGt_ENABLED(...)    _NMLOG_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)
+    #define _LOGt(...)            _NMLOG(_LOGL_TRACE, __VA_ARGS__)
+    #define _LOGt_err(errsv, ...) _NMLOG_err(errsv, _LOGL_TRACE, __VA_ARGS__)
+#else
+    /* still call the logging macros to get compile time checks, but they will be optimized out. */
+    #define _LOGt_ENABLED(...) (FALSE && (_NMLOG_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)))
+    #define _LOGt(...)                            \
+        G_STMT_START                              \
+        {                                         \
+            if (FALSE) {                          \
+                _NMLOG(_LOGL_TRACE, __VA_ARGS__); \
+            }                                     \
+        }                                         \
+        G_STMT_END
+    #define _LOGt_err(errsv, ...)                            \
+        G_STMT_START                                         \
+        {                                                    \
+            if (FALSE) {                                     \
+                _NMLOG_err(errsv, _LOGL_TRACE, __VA_ARGS__); \
+            }                                                \
+        }                                                    \
+        G_STMT_END
+#endif
+
+/*****************************************************************************/
+
+/* Some implementation define a second set of logging macros, for a separate
+ * use. As with the _LOGD() macro family above, the exact implementation
+ * depends on the file that uses them.
+ * Still, it encourages a common pattern to have the common set of macros
+ * like _LOG2D(), _LOG2I(), etc. and have _LOG2t() which by default
+ * is disabled at compile time. */
+
+#define _NMLOG2_ENABLED(level) (nm_logging_enabled((level), (_NMLOG2_DOMAIN)))
+
+#define _LOG2T(...) _NMLOG2(_LOGL_TRACE, __VA_ARGS__)
+#define _LOG2D(...) _NMLOG2(_LOGL_DEBUG, __VA_ARGS__)
+#define _LOG2I(...) _NMLOG2(_LOGL_INFO, __VA_ARGS__)
+#define _LOG2W(...) _NMLOG2(_LOGL_WARN, __VA_ARGS__)
+#define _LOG2E(...) _NMLOG2(_LOGL_ERR, __VA_ARGS__)
+
+#define _LOG2T_ENABLED(...) _NMLOG2_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)
+#define _LOG2D_ENABLED(...) _NMLOG2_ENABLED(_LOGL_DEBUG, ##__VA_ARGS__)
+#define _LOG2I_ENABLED(...) _NMLOG2_ENABLED(_LOGL_INFO, ##__VA_ARGS__)
+#define _LOG2W_ENABLED(...) _NMLOG2_ENABLED(_LOGL_WARN, ##__VA_ARGS__)
+#define _LOG2E_ENABLED(...) _NMLOG2_ENABLED(_LOGL_ERR, ##__VA_ARGS__)
+
+#define _LOG2T_err(errsv, ...) _NMLOG2_err(errsv, _LOGL_TRACE, __VA_ARGS__)
+#define _LOG2D_err(errsv, ...) _NMLOG2_err(errsv, _LOGL_DEBUG, __VA_ARGS__)
+#define _LOG2I_err(errsv, ...) _NMLOG2_err(errsv, _LOGL_INFO, __VA_ARGS__)
+#define _LOG2W_err(errsv, ...) _NMLOG2_err(errsv, _LOGL_WARN, __VA_ARGS__)
+#define _LOG2E_err(errsv, ...) _NMLOG2_err(errsv, _LOGL_ERR, __VA_ARGS__)
+
+#if NM_MORE_LOGGING
+    #define _LOG2t_ENABLED(...)    _NMLOG2_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)
+    #define _LOG2t(...)            _NMLOG2(_LOGL_TRACE, __VA_ARGS__)
+    #define _LOG2t_err(errsv, ...) _NMLOG2_err(errsv, _LOGL_TRACE, __VA_ARGS__)
+#else
+    /* still call the logging macros to get compile time checks, but they will be optimized out. */
+    #define _LOG2t_ENABLED(...) (FALSE && (_NMLOG2_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)))
+    #define _LOG2t(...)                            \
+        G_STMT_START                               \
+        {                                          \
+            if (FALSE) {                           \
+                _NMLOG2(_LOGL_TRACE, __VA_ARGS__); \
+            }                                      \
+        }                                          \
+        G_STMT_END
+    #define _LOG2t_err(errsv, ...)                            \
+        G_STMT_START                                          \
+        {                                                     \
+            if (FALSE) {                                      \
+                _NMLOG2_err(errsv, _LOGL_TRACE, __VA_ARGS__); \
+            }                                                 \
+        }                                                     \
+        G_STMT_END
+#endif
+
+#define _NMLOG3_ENABLED(level) (nm_logging_enabled((level), (_NMLOG3_DOMAIN)))
+
+#define _LOG3T(...) _NMLOG3(_LOGL_TRACE, __VA_ARGS__)
+#define _LOG3D(...) _NMLOG3(_LOGL_DEBUG, __VA_ARGS__)
+#define _LOG3I(...) _NMLOG3(_LOGL_INFO, __VA_ARGS__)
+#define _LOG3W(...) _NMLOG3(_LOGL_WARN, __VA_ARGS__)
+#define _LOG3E(...) _NMLOG3(_LOGL_ERR, __VA_ARGS__)
+
+#define _LOG3T_ENABLED(...) _NMLOG3_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)
+#define _LOG3D_ENABLED(...) _NMLOG3_ENABLED(_LOGL_DEBUG, ##__VA_ARGS__)
+#define _LOG3I_ENABLED(...) _NMLOG3_ENABLED(_LOGL_INFO, ##__VA_ARGS__)
+#define _LOG3W_ENABLED(...) _NMLOG3_ENABLED(_LOGL_WARN, ##__VA_ARGS__)
+#define _LOG3E_ENABLED(...) _NMLOG3_ENABLED(_LOGL_ERR, ##__VA_ARGS__)
+
+#define _LOG3T_err(errsv, ...) _NMLOG3_err(errsv, _LOGL_TRACE, __VA_ARGS__)
+#define _LOG3D_err(errsv, ...) _NMLOG3_err(errsv, _LOGL_DEBUG, __VA_ARGS__)
+#define _LOG3I_err(errsv, ...) _NMLOG3_err(errsv, _LOGL_INFO, __VA_ARGS__)
+#define _LOG3W_err(errsv, ...) _NMLOG3_err(errsv, _LOGL_WARN, __VA_ARGS__)
+#define _LOG3E_err(errsv, ...) _NMLOG3_err(errsv, _LOGL_ERR, __VA_ARGS__)
+
+#if NM_MORE_LOGGING
+    #define _LOG3t_ENABLED(...)    _NMLOG3_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)
+    #define _LOG3t(...)            _NMLOG3(_LOGL_TRACE, __VA_ARGS__)
+    #define _LOG3t_err(errsv, ...) _NMLOG3_err(errsv, _LOGL_TRACE, __VA_ARGS__)
+#else
+    /* still call the logging macros to get compile time checks, but they will be optimized out. */
+    #define _LOG3t_ENABLED(...) (FALSE && (_NMLOG3_ENABLED(_LOGL_TRACE, ##__VA_ARGS__)))
+    #define _LOG3t(...)                            \
+        G_STMT_START                               \
+        {                                          \
+            if (FALSE) {                           \
+                _NMLOG3(_LOGL_TRACE, __VA_ARGS__); \
+            }                                      \
+        }                                          \
+        G_STMT_END
+    #define _LOG3t_err(errsv, ...)                            \
+        G_STMT_START                                          \
+        {                                                     \
+            if (FALSE) {                                      \
+                _NMLOG3_err(errsv, _LOGL_TRACE, __VA_ARGS__); \
+            }                                                 \
+        }                                                     \
+        G_STMT_END
+#endif
+
+/*****************************************************************************/
+
+#endif /* __NM_LOGGING_FWD_H__ */
diff --git a/src/libnm-glib-aux/nm-logging-syslog.h b/src/libnm-glib-aux/nm-logging-syslog.h
new file mode 100644
index 00000000..1d23086a
--- /dev/null
+++ b/src/libnm-glib-aux/nm-logging-syslog.h
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2016 Red Hat, Inc.
+ */
+
+#ifndef __NM_LOGGING_SYSLOG_H__
+#define __NM_LOGGING_SYSLOG_H__
+
+#include <syslog.h>
+
+static inline int
+nm_utils_syslog_coerce_from_nm(int syslog_level)
+{
+    /* NetworkManager uses internally NMLogLevel levels. When spawning
+     * the VPN plugin, it maps those levels to syslog levels as follows:
+     *
+     *  LOGL_INFO = LOG_NOTICE,
+     *  LOGL_DEBUG = LOG_INFO,
+     *  LOGL_TRACE = LOG_DEBUG,
+     *
+     * However, when actually printing to syslog, we don't want to print messages
+     * with LOGL_INFO level as LOG_NOTICE, because they are *not* to be highlighted.
+     *
+     * In other words: NetworkManager has 3 levels that should not require highlighting:
+     * LOGL_INFO, LOGL_DEBUG, LOGL_TRACE. syslog on the other hand has only LOG_INFO and LOG_DEBUG.
+     *
+     * So, coerce those values before printing to syslog. When you receive the syslog_level
+     * from NetworkManager, instead of calling
+     *   syslog(syslog_level, ...)
+     * you should call
+     *   syslog(nm_utils_syslog_coerce_from_nm(syslog_level), ...)
+     */
+    switch (syslog_level) {
+    case LOG_INFO:
+        return LOG_DEBUG;
+    case LOG_NOTICE:
+        return LOG_INFO;
+    default:
+        return syslog_level;
+    }
+}
+
+static inline const char *
+nm_utils_syslog_to_str(int syslog_level)
+{
+    /* Maps the levels the same way as NetworkManager's nm-logging.c does */
+    if (syslog_level >= LOG_DEBUG)
+        return "<trace>";
+    if (syslog_level >= LOG_INFO)
+        return "<debug>";
+    if (syslog_level >= LOG_NOTICE)
+        return "<info>";
+    if (syslog_level >= LOG_WARNING)
+        return "<warn>";
+    return "<error>";
+}
+
+/*****************************************************************************/
+
+#endif /* __NM_LOGGING_SYSLOG_H__ */
diff --git a/src/libnm-glib-aux/nm-macros-internal.h b/src/libnm-glib-aux/nm-macros-internal.h
new file mode 100644
index 00000000..f2d81e1c
--- /dev/null
+++ b/src/libnm-glib-aux/nm-macros-internal.h
@@ -0,0 +1,1916 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2012 Colin Walters <walters@verbum.org>.
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#ifndef __NM_MACROS_INTERNAL_H__
+#define __NM_MACROS_INTERNAL_H__
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+
+#include <gio/gio.h>
+
+/*****************************************************************************/
+
+/* most of our code is single-threaded with a mainloop. Hence, we usually don't need
+ * any thread-safety. Sometimes, we do need thread-safety (nm-logging), but we can
+ * avoid locking if we are on the main-thread by:
+ *
+ *   - modifications of shared data is done infrequently and only from the
+ *     main-thread (nm_logging_setup())
+ *   - read-only access is done frequently (nm_logging_enabled())
+ *     - from the main-thread, we can do that without locking (because
+ *       all modifications are also done on the main thread.
+ *     - from other threads, we need locking. But this is expected to be
+ *       done infrequently too. Important is the lock-free fast-path on the
+ *       main-thread.
+ *
+ * By defining NM_THREAD_SAFE_ON_MAIN_THREAD you indicate that this code runs
+ * on the main-thread. It is by default defined to "1". If you have code that
+ * is also used on another thread, redefine the define to 0 (to opt in into
+ * the slow-path).
+ */
+#define NM_THREAD_SAFE_ON_MAIN_THREAD 1
+
+/*****************************************************************************/
+
+#include "nm-glib.h"
+
+/*****************************************************************************/
+
+#define nm_offsetofend(t, m) (G_STRUCT_OFFSET(t, m) + sizeof(((t *) NULL)->m))
+
+/*****************************************************************************/
+
+#define gs_free            nm_auto_g_free
+#define gs_unref_object    nm_auto_unref_object
+#define gs_unref_variant   nm_auto_unref_variant
+#define gs_unref_array     nm_auto_unref_array
+#define gs_unref_ptrarray  nm_auto_unref_ptrarray
+#define gs_unref_hashtable nm_auto_unref_hashtable
+#define gs_unref_bytes     nm_auto_unref_bytes
+#define gs_strfreev        nm_auto_strfreev
+#define gs_free_error      nm_auto_free_error
+
+/*****************************************************************************/
+
+NM_AUTO_DEFINE_FCN_VOID0(void *, _nm_auto_g_free, g_free);
+#define nm_auto_g_free nm_auto(_nm_auto_g_free)
+
+NM_AUTO_DEFINE_FCN_VOID0(GObject *, _nm_auto_unref_object, g_object_unref);
+#define nm_auto_unref_object nm_auto(_nm_auto_unref_object)
+
+NM_AUTO_DEFINE_FCN0(GVariant *, _nm_auto_unref_variant, g_variant_unref);
+#define nm_auto_unref_variant nm_auto(_nm_auto_unref_variant)
+
+NM_AUTO_DEFINE_FCN0(GArray *, _nm_auto_unref_array, g_array_unref);
+#define nm_auto_unref_array nm_auto(_nm_auto_unref_array)
+
+NM_AUTO_DEFINE_FCN0(GPtrArray *, _nm_auto_unref_ptrarray, g_ptr_array_unref);
+#define nm_auto_unref_ptrarray nm_auto(_nm_auto_unref_ptrarray)
+
+NM_AUTO_DEFINE_FCN0(GHashTable *, _nm_auto_unref_hashtable, g_hash_table_unref);
+#define nm_auto_unref_hashtable nm_auto(_nm_auto_unref_hashtable)
+
+NM_AUTO_DEFINE_FCN0(GSList *, _nm_auto_free_slist, g_slist_free);
+#define nm_auto_free_slist nm_auto(_nm_auto_free_slist)
+
+NM_AUTO_DEFINE_FCN0(GBytes *, _nm_auto_unref_bytes, g_bytes_unref);
+#define nm_auto_unref_bytes nm_auto(_nm_auto_unref_bytes)
+
+NM_AUTO_DEFINE_FCN0(char **, _nm_auto_strfreev, g_strfreev);
+#define nm_auto_strfreev nm_auto(_nm_auto_strfreev)
+
+NM_AUTO_DEFINE_FCN0(GError *, _nm_auto_free_error, g_error_free);
+#define nm_auto_free_error nm_auto(_nm_auto_free_error)
+
+NM_AUTO_DEFINE_FCN0(GKeyFile *, _nm_auto_unref_keyfile, g_key_file_unref);
+#define nm_auto_unref_keyfile nm_auto(_nm_auto_unref_keyfile)
+
+NM_AUTO_DEFINE_FCN0(GVariantIter *, _nm_auto_free_variant_iter, g_variant_iter_free);
+#define nm_auto_free_variant_iter nm_auto(_nm_auto_free_variant_iter)
+
+NM_AUTO_DEFINE_FCN0(GVariantBuilder *, _nm_auto_unref_variant_builder, g_variant_builder_unref);
+#define nm_auto_unref_variant_builder nm_auto(_nm_auto_unref_variant_builder)
+
+#define nm_auto_clear_variant_builder nm_auto(g_variant_builder_clear)
+
+NM_AUTO_DEFINE_FCN0(GList *, _nm_auto_free_list, g_list_free);
+#define nm_auto_free_list nm_auto(_nm_auto_free_list)
+
+NM_AUTO_DEFINE_FCN0(GChecksum *, _nm_auto_checksum_free, g_checksum_free);
+#define nm_auto_free_checksum nm_auto(_nm_auto_checksum_free)
+
+#define nm_auto_unset_gvalue nm_auto(g_value_unset)
+
+NM_AUTO_DEFINE_FCN_VOID0(void *, _nm_auto_unref_gtypeclass, g_type_class_unref);
+#define nm_auto_unref_gtypeclass nm_auto(_nm_auto_unref_gtypeclass)
+
+NM_AUTO_DEFINE_FCN0(GByteArray *, _nm_auto_unref_bytearray, g_byte_array_unref);
+#define nm_auto_unref_bytearray nm_auto(_nm_auto_unref_bytearray)
+
+static inline void
+_nm_auto_free_gstring(GString **str)
+{
+    if (*str)
+        g_string_free(*str, TRUE);
+}
+#define nm_auto_free_gstring nm_auto(_nm_auto_free_gstring)
+
+NM_AUTO_DEFINE_FCN0(GSource *, _nm_auto_unref_gsource, g_source_unref);
+#define nm_auto_unref_gsource nm_auto(_nm_auto_unref_gsource)
+
+NM_AUTO_DEFINE_FCN0(guint, _nm_auto_remove_source, g_source_remove);
+#define nm_auto_remove_source nm_auto(_nm_auto_remove_source)
+
+NM_AUTO_DEFINE_FCN0(GIOChannel *, _nm_auto_unref_io_channel, g_io_channel_unref);
+#define nm_auto_unref_io_channel nm_auto(_nm_auto_unref_io_channel)
+
+NM_AUTO_DEFINE_FCN0(GMainLoop *, _nm_auto_unref_gmainloop, g_main_loop_unref);
+#define nm_auto_unref_gmainloop nm_auto(_nm_auto_unref_gmainloop)
+
+NM_AUTO_DEFINE_FCN0(GOptionContext *, _nm_auto_free_option_context, g_option_context_free);
+#define nm_auto_free_option_context nm_auto(_nm_auto_free_option_context)
+
+static inline void
+_nm_auto_freev(gpointer ptr)
+{
+    gpointer **p = ptr;
+    gpointer * _ptr;
+
+    if (*p) {
+        for (_ptr = *p; *_ptr; _ptr++)
+            g_free(*_ptr);
+        g_free(*p);
+    }
+}
+/* g_free a NULL terminated array of pointers, with also freeing each
+ * pointer with g_free(). It essentially does the same as
+ * gs_strfreev / g_strfreev(), but not restricted to strv arrays. */
+#define nm_auto_freev nm_auto(_nm_auto_freev)
+
+/*****************************************************************************/
+
+#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
+
+/* 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__
+
+/*****************************************************************************/
+
+#if defined(__GNUC__)
+    #define _NM_PRAGMA_WARNING_DO(warning) G_STRINGIFY(GCC diagnostic ignored warning)
+#elif defined(__clang__)
+    #define _NM_PRAGMA_WARNING_DO(warning) G_STRINGIFY(clang diagnostic ignored warning)
+#endif
+
+/* you can only suppress a specific warning that the compiler
+ * understands. Otherwise you will get another compiler warning
+ * about invalid pragma option.
+ * It's not that bad however, because gcc and clang often have the
+ * same name for the same warning. */
+
+#if defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))
+    #define NM_PRAGMA_WARNING_DISABLE(warning) \
+        _Pragma("GCC diagnostic push") _Pragma(_NM_PRAGMA_WARNING_DO(warning))
+#elif defined(__clang__)
+    #define NM_PRAGMA_WARNING_DISABLE(warning)                         \
+        _Pragma("clang diagnostic push")                               \
+            _Pragma(_NM_PRAGMA_WARNING_DO("-Wunknown-warning-option")) \
+                _Pragma(_NM_PRAGMA_WARNING_DO(warning))
+#else
+    #define NM_PRAGMA_WARNING_DISABLE(warning)
+#endif
+
+#if defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))
+    #define NM_PRAGMA_WARNING_REENABLE _Pragma("GCC diagnostic pop")
+#elif defined(__clang__)
+    #define NM_PRAGMA_WARNING_REENABLE _Pragma("clang diagnostic pop")
+#else
+    #define NM_PRAGMA_WARNING_REENABLE
+#endif
+
+/*****************************************************************************/
+
+/**
+ * NM_G_ERROR_MSG:
+ * @error: (allow-none): the #GError instance
+ *
+ * All functions must follow the convention that when they
+ * return a failure, they must also set the GError to a valid
+ * message. For external API however, we want to be extra
+ * careful before accessing the error instance. Use NM_G_ERROR_MSG()
+ * which is safe to use on NULL.
+ *
+ * Returns: the error message.
+ **/
+static inline const char *
+NM_G_ERROR_MSG(GError *error)
+{
+    return error ? (error->message ?: "(null)") : "(no-error)";
+}
+
+/*****************************************************************************/
+
+#ifndef _NM_CC_SUPPORT_AUTO_TYPE
+    #if (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9)))
+        #define _NM_CC_SUPPORT_AUTO_TYPE 1
+    #else
+        #define _NM_CC_SUPPORT_AUTO_TYPE 0
+    #endif
+#endif
+
+#ifndef _NM_CC_SUPPORT_GENERIC
+    /* In the meantime, NetworkManager requires C11 and _Generic() should always be available.
+     * However, shared/nm-utils may also be used in VPN/applet, which possibly did not yet
+     * bump the C standard requirement. Leave this for the moment, but eventually we can
+     * drop it.
+     *
+     * Technically, gcc 4.9 already has some support for _Generic(). But there seems
+     * to be issues with propagating "const char *[5]" to "const char **". Only assume
+     * we have _Generic() since gcc 5. */
+    #if (defined(__GNUC__) && __GNUC__ >= 5) || (defined(__clang__))
+        #define _NM_CC_SUPPORT_GENERIC 1
+    #else
+        #define _NM_CC_SUPPORT_GENERIC 0
+    #endif
+#endif
+
+#if _NM_CC_SUPPORT_AUTO_TYPE
+    #define _nm_auto_type __auto_type
+#endif
+
+#if _NM_CC_SUPPORT_GENERIC
+    #define _NM_CONSTCAST_FULL_1(type, obj_expr, obj) \
+        (_Generic ((obj_expr), \
+               const void        *const: ((const type *) (obj)), \
+               const void        *     : ((const type *) (obj)), \
+                     void        *const: ((      type *) (obj)), \
+                     void        *     : ((      type *) (obj)), \
+               const type        *const: ((const type *) (obj)), \
+               const type        *     : ((const type *) (obj)), \
+                     type        *const: ((      type *) (obj)), \
+                     type        *     : ((      type *) (obj))))
+    #define _NM_CONSTCAST_FULL_2(type, obj_expr, obj, alias_type2) \
+        (_Generic ((obj_expr), \
+               const void        *const: ((const type *) (obj)), \
+               const void        *     : ((const type *) (obj)), \
+                     void        *const: ((      type *) (obj)), \
+                     void        *     : ((      type *) (obj)), \
+               const alias_type2 *const: ((const type *) (obj)), \
+               const alias_type2 *     : ((const type *) (obj)), \
+                     alias_type2 *const: ((      type *) (obj)), \
+                     alias_type2 *     : ((      type *) (obj)), \
+               const type        *const: ((const type *) (obj)), \
+               const type        *     : ((const type *) (obj)), \
+                     type        *const: ((      type *) (obj)), \
+                     type        *     : ((      type *) (obj))))
+    #define _NM_CONSTCAST_FULL_3(type, obj_expr, obj, alias_type2, alias_type3) \
+        (_Generic ((obj_expr), \
+               const void        *const: ((const type *) (obj)), \
+               const void        *     : ((const type *) (obj)), \
+                     void        *const: ((      type *) (obj)), \
+                     void        *     : ((      type *) (obj)), \
+               const alias_type2 *const: ((const type *) (obj)), \
+               const alias_type2 *     : ((const type *) (obj)), \
+                     alias_type2 *const: ((      type *) (obj)), \
+                     alias_type2 *     : ((      type *) (obj)), \
+               const alias_type3 *const: ((const type *) (obj)), \
+               const alias_type3 *     : ((const type *) (obj)), \
+                     alias_type3 *const: ((      type *) (obj)), \
+                     alias_type3 *     : ((      type *) (obj)), \
+               const type        *const: ((const type *) (obj)), \
+               const type        *     : ((const type *) (obj)), \
+                     type        *const: ((      type *) (obj)), \
+                     type        *     : ((      type *) (obj))))
+    #define _NM_CONSTCAST_FULL_4(type, obj_expr, obj, alias_type2, alias_type3, alias_type4) \
+        (_Generic ((obj_expr), \
+               const void        *const: ((const type *) (obj)), \
+               const void        *     : ((const type *) (obj)), \
+                     void        *const: ((      type *) (obj)), \
+                     void        *     : ((      type *) (obj)), \
+               const alias_type2 *const: ((const type *) (obj)), \
+               const alias_type2 *     : ((const type *) (obj)), \
+                     alias_type2 *const: ((      type *) (obj)), \
+                     alias_type2 *     : ((      type *) (obj)), \
+               const alias_type3 *const: ((const type *) (obj)), \
+               const alias_type3 *     : ((const type *) (obj)), \
+                     alias_type3 *const: ((      type *) (obj)), \
+                     alias_type3 *     : ((      type *) (obj)), \
+               const alias_type4 *const: ((const type *) (obj)), \
+               const alias_type4 *     : ((const type *) (obj)), \
+                     alias_type4 *const: ((      type *) (obj)), \
+                     alias_type4 *     : ((      type *) (obj)), \
+               const type        *const: ((const type *) (obj)), \
+               const type        *     : ((const type *) (obj)), \
+                     type        *const: ((      type *) (obj)), \
+                     type        *     : ((      type *) (obj))))
+    #define _NM_CONSTCAST_FULL_x(type, obj_expr, obj, n, ...) \
+        (_NM_CONSTCAST_FULL_##n(type, obj_expr, obj, ##__VA_ARGS__))
+    #define _NM_CONSTCAST_FULL_y(type, obj_expr, obj, n, ...) \
+        (_NM_CONSTCAST_FULL_x(type, obj_expr, obj, n, ##__VA_ARGS__))
+    #define NM_CONSTCAST_FULL(type, obj_expr, obj, ...) \
+        (_NM_CONSTCAST_FULL_y(type, obj_expr, obj, NM_NARG(dummy, ##__VA_ARGS__), ##__VA_ARGS__))
+#else
+    #define NM_CONSTCAST_FULL(type, obj_expr, obj, ...) ((type *) (obj))
+#endif
+
+#define NM_CONSTCAST(type, obj, ...) NM_CONSTCAST_FULL(type, (obj), (obj), ##__VA_ARGS__)
+
+#if _NM_CC_SUPPORT_GENERIC
+    #define NM_UNCONST_PTR(type, arg) \
+        _Generic((arg), const type * : ((type *) (arg)), type * : ((type *) (arg)))
+#else
+    #define NM_UNCONST_PTR(type, arg) ((type *) (arg))
+#endif
+
+#if _NM_CC_SUPPORT_GENERIC
+    #define NM_UNCONST_PPTR(type, arg) \
+        _Generic ((arg), \
+              const type *     *: ((type **) (arg)), \
+                    type *     *: ((type **) (arg)), \
+              const type *const*: ((type **) (arg)), \
+                    type *const*: ((type **) (arg)))
+#else
+    #define NM_UNCONST_PPTR(type, arg) ((type **) (arg))
+#endif
+
+#define NM_GOBJECT_CAST(type, obj, is_check, ...)                     \
+    ({                                                                \
+        const void *_obj = (obj);                                     \
+                                                                      \
+        nm_assert(_obj || (is_check(_obj)));                          \
+        NM_CONSTCAST_FULL(type, (obj), _obj, GObject, ##__VA_ARGS__); \
+    })
+
+#define NM_GOBJECT_CAST_NON_NULL(type, obj, is_check, ...)            \
+    ({                                                                \
+        const void *_obj = (obj);                                     \
+                                                                      \
+        nm_assert(is_check(_obj));                                    \
+        NM_CONSTCAST_FULL(type, (obj), _obj, GObject, ##__VA_ARGS__); \
+    })
+
+#define NM_ENSURE_NOT_NULL(ptr)   \
+    ({                            \
+        typeof(ptr) _ptr = (ptr); \
+                                  \
+        nm_assert(_ptr != NULL);  \
+        _ptr;                     \
+    })
+
+#if _NM_CC_SUPPORT_GENERIC
+    /* returns @value, if the type of @value matches @type.
+     * This requires support for C11 _Generic(). If no support is
+     * present, this returns @value directly.
+     *
+     * It's useful to check the let the compiler ensure that @value is
+     * of a certain type. */
+    #define _NM_ENSURE_TYPE(type, value) (_Generic((value), type : (value)))
+    #define _NM_ENSURE_TYPE_CONST(type, value)               \
+        (_Generic((value), const type                        \
+                  : ((const type) (value)), const type const \
+                  : ((const type) (value)), type             \
+                  : ((const type) (value)), type const       \
+                  : ((const type) (value))))
+#else
+    #define _NM_ENSURE_TYPE(type, value)       (value)
+    #define _NM_ENSURE_TYPE_CONST(type, value) ((const type) (value))
+#endif
+
+#if _NM_CC_SUPPORT_GENERIC && (!defined(__clang__) || __clang_major__ > 3)
+    #define NM_STRUCT_OFFSET_ENSURE_TYPE(type, container, field) \
+        (_Generic((&(((container *) NULL)->field))[0], type : G_STRUCT_OFFSET(container, field)))
+#else
+    #define NM_STRUCT_OFFSET_ENSURE_TYPE(type, container, field) G_STRUCT_OFFSET(container, field)
+#endif
+
+/* Casts (arg) to (type**), but also having a compile time check that
+ * the arg is some sort of pointer to a pointer.
+ *
+ * The only purpose of this macro is some additional compile time safety,
+ * that the argument is a pointer to pointer. But then it will C cast any kind
+ * of such argument. */
+#define NM_CAST_PPTR(type, arg)                                   \
+    ({                                                            \
+        typeof(*(arg)) *const        _arg  = (arg);               \
+        typeof(*_arg)                _arg2 = _arg ? *_arg : NULL; \
+        _nm_unused const void *const _arg3 = _arg2;               \
+                                                                  \
+        (type **) _arg;                                           \
+    })
+
+#if _NM_CC_SUPPORT_GENERIC
+    /* these macros cast (value) to
+     *  - "const char **"      (for "MC", mutable-const)
+     *  - "const char *const*" (for "CC", const-const)
+     * The point is to do this cast, but only accepting pointers
+     * that are compatible already.
+     *
+     * The problem is, if you add a function like g_strdupv(), the input
+     * argument is not modified (CC), but you want to make it work also
+     * for "char **". C doesn't allow this form of casting (for good reasons),
+     * so the function makes a choice like g_strdupv(char**). That means,
+     * every time you want to call it with a const argument, you need to
+     * explicitly cast it.
+     *
+     * These macros do the cast, but they only accept a compatible input
+     * type, otherwise they will fail compilation.
+     */
+    #define NM_CAST_STRV_MC(value) \
+        (_Generic ((value), \
+               const char *     *: (const char *     *) (value), \
+                     char *     *: (const char *     *) (value), \
+                           void *: (const char *     *) (value)))
+    #define NM_CAST_STRV_CC(value) \
+        (_Generic ((value), \
+               const char *const*: (const char *const*) (value), \
+               const char *     *: (const char *const*) (value), \
+                     char *const*: (const char *const*) (value), \
+                     char *     *: (const char *const*) (value), \
+                     const void *: (const char *const*) (value), \
+                           void *: (const char *const*) (value), \
+               const char *const*const: (const char *const*) (value), \
+               const char *     *const: (const char *const*) (value), \
+                     char *const*const: (const char *const*) (value), \
+                     char *     *const: (const char *const*) (value), \
+                     const void *const: (const char *const*) (value), \
+                           void *const: (const char *const*) (value)))
+#else
+    #define NM_CAST_STRV_MC(value) ((const char **) (value))
+    #define NM_CAST_STRV_CC(value) ((const char *const *) (value))
+#endif
+
+#if _NM_CC_SUPPORT_GENERIC
+    #define NM_PROPAGATE_CONST(test_expr, ptr) \
+        (_Generic ((test_expr), \
+               const typeof (*(test_expr)) *: ((const typeof (*(ptr)) *) (ptr)), \
+                                     default: (_Generic ((test_expr), \
+                                                         typeof (*(test_expr)) *: (ptr)))))
+#else
+    #define NM_PROPAGATE_CONST(test_expr, ptr) (ptr)
+#endif
+
+/* with the way it is implemented, the caller may or may not pass a trailing
+ * ',' and it will work. However, this makes the macro unsuitable for initializing
+ * an array. */
+#define NM_MAKE_STRV(...)                                                                     \
+    ((const char *const[(sizeof(((const char *const[]){__VA_ARGS__})) / sizeof(const char *)) \
+                        + 1]){__VA_ARGS__})
+
+/*****************************************************************************/
+
+/* NM_CACHED_QUARK() returns the GQuark for @string, but caches
+ * it in a static variable to speed up future lookups.
+ *
+ * @string must be a string literal.
+ */
+#define NM_CACHED_QUARK(string)                                                \
+    ({                                                                         \
+        static GQuark _nm_cached_quark = 0;                                    \
+                                                                               \
+        (G_LIKELY(_nm_cached_quark != 0)                                       \
+             ? _nm_cached_quark                                                \
+             : (_nm_cached_quark = g_quark_from_static_string("" string ""))); \
+    })
+
+/* NM_CACHED_QUARK_FCN() is essentially the same as G_DEFINE_QUARK
+ * with two differences:
+ * - @string must be a quoted string-literal
+ * - @fcn must be the full function name, while G_DEFINE_QUARK() appends
+ *   "_quark" to the function name.
+ * Both properties of G_DEFINE_QUARK() are non favorable, because you can no
+ * longer grep for string/fcn -- unless you are aware that you are searching
+ * for G_DEFINE_QUARK() and omit quotes / append _quark(). With NM_CACHED_QUARK_FCN(),
+ * ctags/cscope can locate the use of @fcn (though it doesn't recognize that
+ * NM_CACHED_QUARK_FCN() defines it).
+ */
+#define NM_CACHED_QUARK_FCN(string, fcn) \
+    GQuark fcn(void)                     \
+    {                                    \
+        return NM_CACHED_QUARK(string);  \
+    }                                    \
+    _NM_DUMMY_STRUCT_FOR_TRAILING_SEMICOLON
+
+/*****************************************************************************/
+
+static inline GString *
+nm_gstring_prepare(GString **l)
+{
+    if (*l)
+        g_string_set_size(*l, 0);
+    else
+        *l = g_string_sized_new(30);
+    return *l;
+}
+
+static inline GString *
+nm_gstring_add_space_delimiter(GString *str)
+{
+    if (str->len > 0)
+        g_string_append_c(str, ' ');
+    return str;
+}
+
+static inline gboolean
+nm_str_is_empty(const char *str)
+{
+    /* %NULL is also accepted, and also "empty". */
+    return !str || !str[0];
+}
+
+static inline const char *
+nm_str_not_empty(const char *str)
+{
+    return !nm_str_is_empty(str) ? str : NULL;
+}
+
+static inline char *
+nm_strdup_not_empty(const char *str)
+{
+    return !nm_str_is_empty(str) ? g_strdup(str) : NULL;
+}
+
+static inline char *
+nm_str_realloc(char *str)
+{
+    gs_free char *s = str;
+
+    /* Returns a new clone of @str and frees @str. The point is that @str
+     * possibly points to a larger chunck of memory. We want to freshly allocate
+     * a buffer.
+     *
+     * We could use realloc(), but that might not do anything or leave
+     * @str in its memory pool for chunks of a different size (bad for
+     * fragmentation).
+     *
+     * This is only useful when we want to keep the buffer around for a long
+     * time and want to re-allocate a more optimal buffer. */
+
+    return g_strdup(s);
+}
+
+/*****************************************************************************/
+
+#define NM_PRINT_FMT_QUOTED2(cond, prefix, str, str_else) \
+    (cond) ? (prefix) : "", (cond) ? (str) : (str_else)
+#define NM_PRINT_FMT_QUOTED(cond, prefix, str, suffix, str_else) \
+    (cond) ? (prefix) : "", (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)")
+
+/*****************************************************************************/
+
+/* redefine assertions to use g_assert*() */
+#undef _nm_assert_call
+#undef _nm_assert_call_not_reached
+#define _nm_assert_call(cond)         g_assert(cond)
+#define _nm_assert_call_not_reached() g_assert_not_reached()
+
+/* 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_FULL(suffix, ...)                     \
+    typedef enum {                                                              \
+        PROP_0##suffix,                                                         \
+        __VA_ARGS__ _PROPERTY_ENUMS_LAST##suffix,                               \
+    } _PropertyEnums##suffix;                                                   \
+    static GParamSpec *obj_properties##suffix[_PROPERTY_ENUMS_LAST##suffix] = { \
+        NULL,                                                                   \
+    }
+
+#define NM_GOBJECT_PROPERTIES_DEFINE_NOTIFY(suffix, obj_type)                                 \
+    static inline void _nm_gobject_notify_together_impl##suffix(                              \
+        obj_type *                    obj,                                                    \
+        guint                         n,                                                      \
+        const _PropertyEnums##suffix *props)                                                  \
+    {                                                                                         \
+        GObject *const gobj        = (GObject *) obj;                                         \
+        GParamSpec *   pspec_first = NULL;                                                    \
+        gboolean       frozen      = FALSE;                                                   \
+                                                                                              \
+        nm_assert(G_IS_OBJECT(obj));                                                          \
+        nm_assert(n > 0);                                                                     \
+                                                                                              \
+        while (n-- > 0) {                                                                     \
+            const _PropertyEnums##suffix prop = *props++;                                     \
+            GParamSpec *                 pspec;                                               \
+                                                                                              \
+            if (prop == PROP_0##suffix)                                                       \
+                continue;                                                                     \
+                                                                                              \
+            nm_assert((gsize) prop < G_N_ELEMENTS(obj_properties##suffix));                   \
+            pspec = obj_properties##suffix[prop];                                             \
+            nm_assert(pspec);                                                                 \
+                                                                                              \
+            if (!frozen) {                                                                    \
+                if (!pspec_first) {                                                           \
+                    pspec_first = pspec;                                                      \
+                    continue;                                                                 \
+                }                                                                             \
+                frozen = TRUE;                                                                \
+                g_object_freeze_notify(gobj);                                                 \
+                g_object_notify_by_pspec(gobj, pspec_first);                                  \
+            }                                                                                 \
+            g_object_notify_by_pspec(gobj, pspec);                                            \
+        }                                                                                     \
+                                                                                              \
+        if (frozen)                                                                           \
+            g_object_thaw_notify(gobj);                                                       \
+        else if (pspec_first)                                                                 \
+            g_object_notify_by_pspec(gobj, pspec_first);                                      \
+    }                                                                                         \
+                                                                                              \
+    _nm_unused static inline void _notify##suffix(obj_type *obj, _PropertyEnums##suffix prop) \
+    {                                                                                         \
+        _nm_gobject_notify_together_impl##suffix(obj, 1, &prop);                              \
+    }                                                                                         \
+    _NM_DUMMY_STRUCT_FOR_TRAILING_SEMICOLON
+
+#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_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_full(, obj, __VA_ARGS__)
+
+/*****************************************************************************/
+
+#define _NM_GET_PRIVATE(self, type, is_check, ...) \
+    (&(NM_GOBJECT_CAST_NON_NULL(type, (self), is_check, ##__VA_ARGS__)->_priv))
+#if _NM_CC_SUPPORT_AUTO_TYPE
+    #define _NM_GET_PRIVATE_PTR(self, type, is_check, ...)                       \
+        ({                                                                       \
+            _nm_auto_type _self_get_private =                                    \
+                NM_GOBJECT_CAST_NON_NULL(type, (self), is_check, ##__VA_ARGS__); \
+                                                                                 \
+            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)
+#endif
+
+/*****************************************************************************/
+
+static inline gpointer
+nm_g_object_ref(gpointer obj)
+{
+    /* g_object_ref() doesn't accept NULL. */
+    if (obj)
+        g_object_ref(obj);
+    return obj;
+}
+#define nm_g_object_ref(obj) ((typeof(obj)) nm_g_object_ref(obj))
+
+static inline void
+nm_g_object_unref(gpointer obj)
+{
+    /* g_object_unref() doesn't accept NULL. Usually, we workaround that
+     * by using g_clear_object(), but sometimes that is not convenient
+     * (for example as destroy function for a hash table that can contain
+     * NULL values). */
+    if (obj)
+        g_object_unref(obj);
+}
+
+/* Assigns GObject @obj to destination @pp, and takes an additional ref.
+ * The previous value of @pp is unrefed.
+ *
+ * It makes sure to first increase the ref-count of @obj, and handles %NULL
+ * @obj correctly.
+ * */
+#define nm_g_object_ref_set(pp, obj)                   \
+    ({                                                 \
+        typeof(*(pp)) *const _pp  = (pp);              \
+        typeof(*_pp) const   _obj = (obj);             \
+        typeof(*_pp)         _p;                       \
+        gboolean             _changed = FALSE;         \
+                                                       \
+        nm_assert(!_pp || !*_pp || G_IS_OBJECT(*_pp)); \
+        nm_assert(!_obj || G_IS_OBJECT(_obj));         \
+                                                       \
+        if (_pp && ((_p = *_pp) != _obj)) {            \
+            nm_g_object_ref(_obj);                     \
+            *_pp = _obj;                               \
+            nm_g_object_unref(_p);                     \
+            _changed = TRUE;                           \
+        }                                              \
+        _changed;                                      \
+    })
+
+#define nm_g_object_ref_set_take(pp, obj)              \
+    ({                                                 \
+        typeof(*(pp)) *const _pp  = (pp);              \
+        typeof(*_pp) const   _obj = (obj);             \
+        typeof(*_pp)         _p;                       \
+        gboolean             _changed = FALSE;         \
+                                                       \
+        nm_assert(!_pp || !*_pp || G_IS_OBJECT(*_pp)); \
+        nm_assert(!_obj || G_IS_OBJECT(_obj));         \
+                                                       \
+        if (_pp && ((_p = *_pp) != _obj)) {            \
+            *_pp = _obj;                               \
+            nm_g_object_unref(_p);                     \
+            _changed = TRUE;                           \
+        } else                                         \
+            nm_g_object_unref(_obj);                   \
+        _changed;                                      \
+    })
+
+/* basically, replaces
+ *   g_clear_pointer (&location, g_free)
+ * with
+ *   nm_clear_g_free (&location)
+ *
+ * Another advantage is that by using a macro and typeof(), it is more
+ * typesafe and gives you for example a compiler warning when pp is a const
+ * pointer or points to a const-pointer.
+ */
+#define nm_clear_g_free(pp) nm_clear_pointer(pp, g_free)
+
+/* Our nm_clear_pointer() is more typesafe than g_clear_pointer() and
+ * should be preferred.
+ *
+ * For g_clear_object() that is not the case (because g_object_unref()
+ * anyway takes a void pointer). So using g_clear_object() is fine.
+ *
+ * Still have a nm_clear_g_object() because that returns a boolean
+ * indication whether anything was cleared. */
+#define nm_clear_g_object(pp) nm_clear_pointer(pp, g_object_unref)
+
+/**
+ * nm_clear_error:
+ * @err: a pointer to pointer to a #GError.
+ *
+ * This is like g_clear_error(). The only difference is
+ * that this is an inline function.
+ */
+static inline void
+nm_clear_error(GError **err)
+{
+    if (err && *err) {
+        g_error_free(*err);
+        *err = NULL;
+    }
+}
+
+static inline void
+nm_g_error_free(GError *err)
+{
+    if (err)
+        g_error_free(err);
+}
+
+/* Patch g_clear_error() to use nm_clear_error(), which is inlineable
+ * and visible to the compiler. For example gs_free_error attribute only
+ * frees the error after checking that it's not %NULL. So, in many cases
+ * the compiler knows that gs_free_error has no effect and can optimize
+ * the call away. By making g_clear_error() inlineable, we give the compiler
+ * more chance to detect that the function actually has no effect. */
+#define g_clear_error(ptr) nm_clear_error(ptr)
+
+static inline gboolean
+nm_clear_g_source(guint *id)
+{
+    guint v;
+
+    if (id && (v = *id)) {
+        *id = 0;
+        g_source_remove(v);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+static inline gboolean
+nm_clear_g_signal_handler(gpointer self, gulong *id)
+{
+    gulong v;
+
+    if (id && (v = *id)) {
+        *id = 0;
+        g_signal_handler_disconnect(self, v);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+static inline gboolean
+nm_clear_g_variant(GVariant **variant)
+{
+    GVariant *v;
+
+    if (variant && (v = *variant)) {
+        *variant = NULL;
+        g_variant_unref(v);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+static inline gboolean
+nm_clear_g_string(GString **ptr)
+{
+    GString *s;
+
+    if (ptr && (s = *ptr)) {
+        *ptr = NULL;
+        g_string_free(s, TRUE);
+    };
+    return FALSE;
+}
+
+static inline gboolean
+nm_clear_g_cancellable(GCancellable **cancellable)
+{
+    GCancellable *v;
+
+    if (cancellable && (v = *cancellable)) {
+        *cancellable = NULL;
+        g_cancellable_cancel(v);
+        g_object_unref(v);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+/* If @cancellable_id is not 0, clear it and call g_cancellable_disconnect().
+ * @cancellable may be %NULL, if there is nothing to disconnect.
+ *
+ * It's like nm_clear_g_signal_handler(), except that it uses g_cancellable_disconnect()
+ * instead of g_signal_handler_disconnect().
+ *
+ * Note the warning in glib documentation about dead-lock and what g_cancellable_disconnect()
+ * actually does. */
+static inline gboolean
+nm_clear_g_cancellable_disconnect(GCancellable *cancellable, gulong *cancellable_id)
+{
+    gulong id;
+
+    if (cancellable_id && (id = *cancellable_id) != 0) {
+        *cancellable_id = 0;
+        g_cancellable_disconnect(cancellable, id);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+static inline const char *
+nm_dbus_path_not_empty(const char *str)
+{
+    nm_assert(!str || str[0] == '/');
+    return !str || (str[0] == '/' && str[1] == '\0') ? NULL : str;
+}
+
+/*****************************************************************************/
+
+/* GVariantType is basically a C string. But G_VARIANT_TYPE() is not suitable
+ * to initialize a static variable (because it evaluates a function check that
+ * the string is valid). Add an alternative macro that does the plain cast.
+ *
+ * Here you loose the assertion check that G_VARIANT_TYPE() to ensure the
+ * string is valid. */
+#define NM_G_VARIANT_TYPE(fmt) ((const GVariantType *) ("" fmt ""))
+
+static inline GVariant *
+nm_g_variant_ref(GVariant *v)
+{
+    if (v)
+        g_variant_ref(v);
+    return v;
+}
+
+static inline GVariant *
+nm_g_variant_ref_sink(GVariant *v)
+{
+    if (v)
+        g_variant_ref_sink(v);
+    return v;
+}
+
+static inline void
+nm_g_variant_unref(GVariant *v)
+{
+    if (v)
+        g_variant_unref(v);
+}
+
+static inline GVariant *
+nm_g_variant_take_ref(GVariant *v)
+{
+    if (v)
+        g_variant_take_ref(v);
+    return v;
+}
+
+/*****************************************************************************/
+
+#define NM_DIV_ROUND_UP(x, y)     \
+    ({                            \
+        const typeof(x) _x = (x); \
+        const typeof(y) _y = (y); \
+                                  \
+        (_x / _y + !!(_x % _y));  \
+    })
+
+/*****************************************************************************/
+
+#define NM_UTILS_LOOKUP_DEFAULT(v)      return (v)
+#define NM_UTILS_LOOKUP_DEFAULT_WARN(v) g_return_val_if_reached(v)
+#define NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT(v) \
+    {                                        \
+        nm_assert_not_reached();             \
+        return (v);                          \
+    }
+#define NM_UTILS_LOOKUP_ITEM(v, n) \
+    (void) 0;                      \
+case v:                            \
+    return (n);                    \
+    (void) 0
+#define NM_UTILS_LOOKUP_STR_ITEM(v, n) NM_UTILS_LOOKUP_ITEM(v, "" n "")
+#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(fcn_name, lookup_type, result_type, unknown_val, ...) \
+    result_type fcn_name(lookup_type val)                                            \
+    {                                                                                \
+        switch (val) {                                                               \
+            (void) 0, __VA_ARGS__(void) 0;                                           \
+        };                                                                           \
+        {                                                                            \
+            unknown_val;                                                             \
+        }                                                                            \
+    }                                                                                \
+    _NM_DUMMY_STRUCT_FOR_TRAILING_SEMICOLON
+
+#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__)
+
+/* Call the string-lookup-table function @fcn_name. If the function returns
+ * %NULL, the numeric index is converted to string using a alloca() buffer.
+ * Beware: this macro uses alloca(). */
+#define NM_UTILS_LOOKUP_STR_A(fcn_name, idx)                         \
+    ({                                                               \
+        typeof(idx) _idx = (idx);                                    \
+        const char *_s;                                              \
+                                                                     \
+        _s = fcn_name(_idx);                                         \
+        if (!_s) {                                                   \
+            _s = g_alloca(30);                                       \
+                                                                     \
+            g_snprintf((char *) _s, 30, "(%lld)", (long long) _idx); \
+        }                                                            \
+        _s;                                                          \
+    })
+
+/*****************************************************************************/
+
+/* check if @flags has exactly one flag (@check) set. You should call this
+ * only with @check being a compile time constant and a power of two. */
+#define NM_FLAGS_HAS(flags, check)                                       \
+    (G_STATIC_ASSERT_EXPR((check) > 0 && ((check) & ((check) -1)) == 0), \
+     NM_FLAGS_ANY((flags), (check)))
+
+#define NM_FLAGS_ANY(flags, check) (((flags) & (check)) != 0)
+
+#define NM_FLAGS_ALL(flags, check)            \
+    ({                                        \
+        const typeof(check) _check = (check); \
+                                              \
+        (((flags) & (_check)) == (_check));   \
+    })
+
+#define NM_FLAGS_SET(flags, val)              \
+    ({                                        \
+        const typeof(flags) _flags = (flags); \
+        const typeof(flags) _val   = (val);   \
+                                              \
+        _flags | _val;                        \
+    })
+
+#define NM_FLAGS_UNSET(flags, val)            \
+    ({                                        \
+        const typeof(flags) _flags = (flags); \
+        const typeof(flags) _val   = (val);   \
+                                              \
+        _flags &(~_val);                      \
+    })
+
+#define NM_FLAGS_ASSIGN(flags, val, assign)           \
+    ({                                                \
+        const typeof(flags) _flags = (flags);         \
+        const typeof(flags) _val   = (val);           \
+                                                      \
+        (assign) ? _flags | (_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)                                                          \
+    return_type versioned_func                        args_typed;                               \
+    _nm_externally_visible return_type versioned_func args_typed                                \
+    {                                                                                           \
+        return orig_func args;                                                                  \
+    }                                                                                           \
+    return_type orig_func args_typed;                                                           \
+    __asm__(".symver " G_STRINGIFY(versioned_func) ", " G_STRINGIFY(orig_func) "@" G_STRINGIFY( \
+        version))
+
+#define NM_BACKPORT_SYMBOL(version, return_type, func, args_typed, args) \
+    _NM_BACKPORT_SYMBOL_IMPL(version, return_type, func, _##func##_##version, args_typed, args)
+
+/*****************************************************************************/
+
+/* mirrors g_ascii_isspace() and what we consider spaces in general. */
+#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)                          \
+    ({                                                           \
+        typeof(*(str)) *             _str_sls        = (str);    \
+        _nm_unused const char *const _str_type_check = _str_sls; \
+                                                                 \
+        if (_str_sls) {                                          \
+            while (g_ascii_isspace(_str_sls[0]))                 \
+                _str_sls++;                                      \
+        }                                                        \
+        _str_sls;                                                \
+    })
+
+static inline char *
+nm_strstrip(char *str)
+{
+    /* g_strstrip doesn't like NULL. */
+    return str ? g_strstrip(str) : NULL;
+}
+
+static inline const char *
+nm_strstrip_avoid_copy(const char *str, char **str_free)
+{
+    gsize l;
+    char *s;
+
+    nm_assert(str_free && !*str_free);
+
+    if (!str)
+        return NULL;
+
+    str = nm_str_skip_leading_spaces(str);
+    l   = strlen(str);
+    if (l == 0 || !g_ascii_isspace(str[l - 1]))
+        return str;
+    while (l > 0 && g_ascii_isspace(str[l - 1]))
+        l--;
+
+    s = g_new(char, l + 1);
+    memcpy(s, str, l);
+    s[l]      = '\0';
+    *str_free = s;
+    return s;
+}
+
+#define nm_strstrip_avoid_copy_a(alloca_maxlen, str, out_str_free)                                \
+    ({                                                                                            \
+        const char *_str_ssac          = (str);                                                   \
+        char **     _out_str_free_ssac = (out_str_free);                                          \
+                                                                                                  \
+        G_STATIC_ASSERT_EXPR((alloca_maxlen) > 0);                                                \
+                                                                                                  \
+        nm_assert(_out_str_free_ssac || ((alloca_maxlen) > (str ? strlen(str) : 0u)));            \
+        nm_assert(!_out_str_free_ssac || !*_out_str_free_ssac);                                   \
+                                                                                                  \
+        if (_str_ssac) {                                                                          \
+            _str_ssac = nm_str_skip_leading_spaces(_str_ssac);                                    \
+            if (_str_ssac[0] != '\0') {                                                           \
+                gsize _l = strlen(_str_ssac);                                                     \
+                                                                                                  \
+                if (g_ascii_isspace(_str_ssac[--_l])) {                                           \
+                    while (_l > 0 && g_ascii_isspace(_str_ssac[_l - 1])) {                        \
+                        _l--;                                                                     \
+                    }                                                                             \
+                    _str_ssac = nm_strndup_a((alloca_maxlen), _str_ssac, _l, _out_str_free_ssac); \
+                }                                                                                 \
+            }                                                                                     \
+        }                                                                                         \
+                                                                                                  \
+        _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().
+ *
+ * Like strcmp(), this function is not forgiving to accept %NULL. */
+static inline int
+nm_strcmp_p(gconstpointer a, gconstpointer b)
+{
+    const char *s1 = *((const char **) a);
+    const char *s2 = *((const char **) b);
+
+    return strcmp(s1, s2);
+}
+
+/*****************************************************************************/
+
+static inline int
+_NM_IN_STRSET_ASCII_CASE_op_streq(const char *x, const char *s)
+{
+    return s && g_ascii_strcasecmp(x, s) == 0;
+}
+
+#define NM_IN_STRSET_ASCII_CASE(x, ...)                     \
+    _NM_IN_STRSET_EVAL_N(||,                                \
+                         _NM_IN_STRSET_ASCII_CASE_op_streq, \
+                         x,                                 \
+                         NM_NARG(__VA_ARGS__),              \
+                         __VA_ARGS__)
+
+#define NM_STR_HAS_SUFFIX_ASCII_CASE(str, suffix)                                               \
+    ({                                                                                          \
+        const char *const _str_has_suffix = (str);                                              \
+        size_t            _l;                                                                   \
+                                                                                                \
+        nm_assert(strlen(suffix) == NM_STRLEN(suffix));                                         \
+                                                                                                \
+        (_str_has_suffix && ((_l = strlen(_str_has_suffix)) >= NM_STRLEN(suffix))               \
+         && (g_ascii_strcasecmp(&_str_has_suffix[_l - NM_STRLEN(suffix)], "" suffix "") == 0)); \
+    })
+
+#define NM_STR_HAS_SUFFIX_ASCII_CASE_WITH_MORE(str, suffix)                                     \
+    ({                                                                                          \
+        const char *const _str_has_suffix = (str);                                              \
+        size_t            _l;                                                                   \
+                                                                                                \
+        nm_assert(strlen(suffix) == NM_STRLEN(suffix));                                         \
+                                                                                                \
+        (_str_has_suffix && ((_l = strlen(_str_has_suffix)) > NM_STRLEN(suffix))                \
+         && (g_ascii_strcasecmp(&_str_has_suffix[_l - NM_STRLEN(suffix)], "" suffix "") == 0)); \
+    })
+
+/*****************************************************************************/
+
+#define nm_g_slice_free(ptr) g_slice_free(typeof(*(ptr)), ptr)
+
+/*****************************************************************************/
+
+/* like g_memdup(). The difference is that the @size argument is of type
+ * gsize, while g_memdup() has type guint. Since, the size of container types
+ * like GArray is guint as well, this means trying to g_memdup() an
+ * array,
+ *    g_memdup (array->data, array->len * sizeof (ElementType))
+ * will lead to integer overflow, if there are more than G_MAXUINT/sizeof(ElementType)
+ * bytes. That seems unnecessarily dangerous to me.
+ * nm_memdup() avoids that, because its size argument is always large enough
+ * to contain all data that a GArray can hold.
+ *
+ * Another minor difference to g_memdup() is that the glib version also
+ * returns %NULL if @data is %NULL. E.g. g_memdup(NULL, 1)
+ * gives %NULL, but nm_memdup(NULL, 1) crashes. I think that
+ * is desirable, because @size MUST be correct at all times. @size
+ * may be zero, but one must not claim to have non-zero bytes when
+ * passing a %NULL @data pointer.
+ */
+static inline gpointer
+nm_memdup(gconstpointer data, gsize size)
+{
+    gpointer p;
+
+    if (size == 0)
+        return NULL;
+    p = g_malloc(size);
+    memcpy(p, data, size);
+    return p;
+}
+
+#define nm_malloc_maybe_a(alloca_maxlen, bytes, to_free)  \
+    ({                                                    \
+        const gsize       _bytes   = (bytes);             \
+        typeof(to_free)   _to_free = (to_free);           \
+        typeof(*_to_free) _ptr;                           \
+                                                          \
+        G_STATIC_ASSERT_EXPR((alloca_maxlen) <= 500u);    \
+        G_STATIC_ASSERT_EXPR((alloca_maxlen) > 0u);       \
+        nm_assert(_to_free && !*_to_free);                \
+                                                          \
+        if (G_LIKELY(_bytes <= (alloca_maxlen))) {        \
+            _ptr = _bytes > 0u ? g_alloca(_bytes) : NULL; \
+        } else {                                          \
+            _ptr      = g_malloc(_bytes);                 \
+            *_to_free = _ptr;                             \
+        };                                                \
+                                                          \
+        _ptr;                                             \
+    })
+
+#define nm_malloc0_maybe_a(alloca_maxlen, bytes, to_free) \
+    ({                                                    \
+        const gsize       _bytes   = (bytes);             \
+        typeof(to_free)   _to_free = (to_free);           \
+        typeof(*_to_free) _ptr;                           \
+                                                          \
+        G_STATIC_ASSERT_EXPR((alloca_maxlen) <= 500u);    \
+        G_STATIC_ASSERT_EXPR((alloca_maxlen) > 0u);       \
+        nm_assert(_to_free && !*_to_free);                \
+                                                          \
+        if (G_LIKELY(_bytes <= (alloca_maxlen))) {        \
+            if (_bytes > 0u) {                            \
+                _ptr = g_alloca(_bytes);                  \
+                memset(_ptr, 0, _bytes);                  \
+            } else                                        \
+                _ptr = NULL;                              \
+        } else {                                          \
+            _ptr      = g_malloc0(_bytes);                \
+            *_to_free = _ptr;                             \
+        };                                                \
+                                                          \
+        _ptr;                                             \
+    })
+
+#define nm_memdup_maybe_a(alloca_maxlen, data, size, to_free)                 \
+    ({                                                                        \
+        const gsize          _size       = (size);                            \
+        typeof(to_free)      _to_free_md = (to_free);                         \
+        typeof(*_to_free_md) _ptr_md     = NULL;                              \
+                                                                              \
+        nm_assert(_to_free_md && !*_to_free_md);                              \
+                                                                              \
+        if (_size > 0u) {                                                     \
+            _ptr_md = nm_malloc_maybe_a((alloca_maxlen), _size, _to_free_md); \
+            memcpy(_ptr_md, (data), _size);                                   \
+        }                                                                     \
+                                                                              \
+        _ptr_md;                                                              \
+    })
+
+static inline char *
+_nm_strndup_a_step(char *s, const char *str, gsize len)
+{
+    NM_PRAGMA_WARNING_DISABLE("-Wstringop-truncation");
+    NM_PRAGMA_WARNING_DISABLE("-Wstringop-overflow");
+    if (len > 0)
+        strncpy(s, str, len);
+    s[len] = '\0';
+    return s;
+    NM_PRAGMA_WARNING_REENABLE;
+    NM_PRAGMA_WARNING_REENABLE;
+}
+
+/* Similar to g_strndup(), however, if the string (including the terminating
+ * NUL char) fits into alloca_maxlen, this will alloca() the memory.
+ *
+ * It's a mix of strndup() and strndupa(), but deciding based on @alloca_maxlen
+ * which one to use.
+ *
+ * In case malloc() is necessary, @out_str_free will be set (this string
+ * must be freed afterwards). It is permissible to pass %NULL as @out_str_free,
+ * if you ensure that len < alloca_maxlen.
+ *
+ * Note that just like g_strndup(), this always returns a buffer with @len + 1
+ * bytes, even if strlen(@str) is shorter than that (NUL terminated early). We fill
+ * the buffer with strncpy(), which means, that @str is copied up to the first
+ * NUL character and then filled with NUL characters. */
+#define nm_strndup_a(alloca_maxlen, str, len, out_str_free)        \
+    ({                                                             \
+        const gsize       _alloca_maxlen_snd = (alloca_maxlen);    \
+        const char *const _str_snd           = (str);              \
+        const gsize       _len_snd           = (len);              \
+        char **const      _out_str_free_snd  = (out_str_free);     \
+        char *            _s_snd;                                  \
+                                                                   \
+        G_STATIC_ASSERT_EXPR((alloca_maxlen) <= 300);              \
+                                                                   \
+        if (_out_str_free_snd && _len_snd >= _alloca_maxlen_snd) { \
+            _s_snd             = g_malloc(_len_snd + 1);           \
+            *_out_str_free_snd = _s_snd;                           \
+        } else {                                                   \
+            g_assert(_len_snd < _alloca_maxlen_snd);               \
+            _s_snd = g_alloca(_len_snd + 1);                       \
+        }                                                          \
+        _nm_strndup_a_step(_s_snd, _str_snd, _len_snd);            \
+    })
+
+#define nm_strdup_maybe_a(alloca_maxlen, str, out_str_free)               \
+    ({                                                                    \
+        const char *const _str_snd = (str);                               \
+                                                                          \
+        (char *) nm_memdup_maybe_a(alloca_maxlen,                         \
+                                   _str_snd,                              \
+                                   _str_snd ? strlen(_str_snd) + 1u : 0u, \
+                                   out_str_free);                         \
+    })
+
+/*****************************************************************************/
+
+/* generic macro to convert an int to a (heap allocated) string.
+ *
+ * Usually, an inline function nm_strdup_int64() would be enough. However,
+ * that cannot be used for guint64. So, we would also need nm_strdup_uint64().
+ * This causes subtle error potential, because the caller needs to ensure to
+ * use the right one (and compiler isn't going to help as it silently casts).
+ *
+ * Instead, this generic macro is supposed to handle all integers correctly. */
+#if _NM_CC_SUPPORT_GENERIC
+    #define nm_strdup_int(val)                                                       \
+        _Generic((val), char                                                         \
+                 : g_strdup_printf("%d", (int) (val)),                               \
+                                                                                     \
+                   signed char                                                       \
+                 : g_strdup_printf("%d", (signed) (val)), signed short               \
+                 : g_strdup_printf("%d", (signed) (val)), signed                     \
+                 : g_strdup_printf("%d", (signed) (val)), signed long                \
+                 : g_strdup_printf("%ld", (signed long) (val)), signed long long     \
+                 : g_strdup_printf("%lld", (signed long long) (val)),                \
+                                                                                     \
+                   unsigned char                                                     \
+                 : g_strdup_printf("%u", (unsigned) (val)), unsigned short           \
+                 : g_strdup_printf("%u", (unsigned) (val)), unsigned                 \
+                 : g_strdup_printf("%u", (unsigned) (val)), unsigned long            \
+                 : g_strdup_printf("%lu", (unsigned long) (val)), unsigned long long \
+                 : g_strdup_printf("%llu", (unsigned long long) (val)))
+#else
+    #define nm_strdup_int(val)                                        \
+        ((sizeof(val) == sizeof(guint64) && ((typeof(val)) -1) > 0)   \
+             ? g_strdup_printf("%" G_GUINT64_FORMAT, (guint64) (val)) \
+             : g_strdup_printf("%" G_GINT64_FORMAT, (gint64) (val)))
+#endif
+
+/*****************************************************************************/
+
+static inline guint
+nm_encode_version(guint major, guint minor, guint micro)
+{
+    /* analog to the preprocessor macro NM_ENCODE_VERSION(). */
+    return (major << 16) | (minor << 8) | micro;
+}
+
+static inline void
+nm_decode_version(guint version, guint *major, guint *minor, guint *micro)
+{
+    *major = (version & 0xFFFF0000u) >> 16;
+    *minor = (version & 0x0000FF00u) >> 8;
+    *micro = (version & 0x000000FFu);
+}
+
+/*****************************************************************************/
+
+/* taken from systemd's DECIMAL_STR_MAX()
+ *
+ * Returns the number of chars needed to format variables of the
+ * specified type as a decimal string. Adds in extra space for a
+ * negative '-' prefix (hence works correctly on signed
+ * types). Includes space for the trailing NUL. */
+#define NM_DECIMAL_STR_MAX(type) \
+    (2                           \
+     + (sizeof(type) <= 1   ? 3  \
+        : sizeof(type) <= 2 ? 5  \
+        : sizeof(type) <= 4 ? 10 \
+        : sizeof(type) <= 8 ? 20 \
+                            : sizeof(int[-2 * (sizeof(type) > 8)])))
+
+/*****************************************************************************/
+
+/* if @str is NULL, return "(null)". Otherwise, allocate a buffer using
+ * alloca() of and fill it with @str. @str will be quoted with double quote.
+ * If @str is longer then @trunc_at, the string is truncated and the closing
+ * quote is instead '^' to indicate truncation.
+ *
+ * Thus, the maximum stack allocated buffer will be @trunc_at+3. The maximum
+ * buffer size must be a constant and not larger than 300. */
+#define nm_strquote_a(trunc_at, str)                                     \
+    ({                                                                   \
+        const char *const _str = (str);                                  \
+                                                                         \
+        (_str ? ({                                                       \
+            const gsize _trunc_at     = (trunc_at);                      \
+            const gsize _strlen_trunc = NM_MIN(strlen(_str), _trunc_at); \
+            char *      _buf;                                            \
+                                                                         \
+            G_STATIC_ASSERT_EXPR((trunc_at) <= 300);                     \
+                                                                         \
+            _buf    = g_alloca(_strlen_trunc + 3);                       \
+            _buf[0] = '"';                                               \
+            memcpy(&_buf[1], _str, _strlen_trunc);                       \
+            _buf[_strlen_trunc + 1] = _str[_strlen_trunc] ? '^' : '"';   \
+            _buf[_strlen_trunc + 2] = '\0';                              \
+            _buf;                                                        \
+        })                                                               \
+              : "(null)");                                               \
+    })
+
+#define nm_sprintf_buf(buf, format, ...)                                                    \
+    ({                                                                                      \
+        char *_buf = (buf);                                                                 \
+        int   _buf_len;                                                                     \
+                                                                                            \
+        /* some static assert trying to ensure that the buffer is statically allocated.
+         * It disallows a buffer size of sizeof(gpointer) to catch that. */     \
+        G_STATIC_ASSERT(G_N_ELEMENTS(buf) == sizeof(buf) && sizeof(buf) != sizeof(char *)); \
+        _buf_len = g_snprintf(_buf, sizeof(buf), "" format "", ##__VA_ARGS__);              \
+        nm_assert(_buf_len < sizeof(buf));                                                  \
+        _buf;                                                                               \
+    })
+
+#define nm_vsprintf_buf_or_alloc(format, va_last_arg, sbuf_stack, out_sbuf_heap, out_len) \
+    ({                                                                                    \
+        const char *const _format        = (format);                                      \
+        char *const       _sbuf_stack    = (sbuf_stack);                                  \
+        char **const      _out_sbuf_heap = (out_sbuf_heap);                               \
+        gsize *const      _out_len       = (out_len);                                     \
+        const char *      _msg;                                                           \
+        va_list           _va_args;                                                       \
+        int               _l;                                                             \
+                                                                                          \
+        G_STATIC_ASSERT_EXPR(G_N_ELEMENTS(sbuf_stack) > sizeof(_sbuf_stack));             \
+                                                                                          \
+        va_start(_va_args, va_last_arg);                                                  \
+        _l = g_vsnprintf(_sbuf_stack, sizeof(sbuf_stack), _format, _va_args);             \
+        va_end(_va_args);                                                                 \
+                                                                                          \
+        nm_assert(_l >= 0 && _l < G_MAXINT);                                              \
+                                                                                          \
+        if ((gsize) _l >= sizeof(sbuf_stack)) {                                           \
+            const gsize _l2 = ((gsize) _l) + 1u;                                          \
+            char *      _sbuf_heap;                                                       \
+                                                                                          \
+            /* Don't use g_strdup_vprintf() here either, because that also needs
+             * to first determine the length (which is commonly does by printing
+             * to a stack allocated buffer of size 1. We already know the required
+             * size. */          \
+                                                                                          \
+            _sbuf_heap = g_malloc(_l2);                                                   \
+                                                                                          \
+            va_start(_va_args, va_last_arg);                                              \
+            _l = g_vsnprintf(_sbuf_heap, _l2, _format, _va_args);                         \
+            va_end(_va_args);                                                             \
+                                                                                          \
+            nm_assert(_l >= 0 && ((gsize) _l) == _l2 - 1u);                               \
+                                                                                          \
+            _msg            = _sbuf_heap;                                                 \
+            *_out_sbuf_heap = _sbuf_heap;                                                 \
+        } else {                                                                          \
+            _msg            = _sbuf_stack;                                                \
+            *_out_sbuf_heap = NULL;                                                       \
+        }                                                                                 \
+                                                                                          \
+        nm_assert(strlen(_msg) == (gsize) _l);                                            \
+        NM_SET_OUT(_out_len, (gsize) _l);                                                 \
+                                                                                          \
+        _msg;                                                                             \
+    })
+
+/* it is "unsafe" because @bufsize must not be a constant expression and
+ * there is no check at compiletime. Regardless of that, the buffer size
+ * must not be larger than 300 bytes, as this gets stack allocated. */
+#define nm_sprintf_buf_unsafe_a(bufsize, format, ...)                       \
+    ({                                                                      \
+        char *          _buf;                                               \
+        int             _buf_len;                                           \
+        typeof(bufsize) _bufsize = (bufsize);                               \
+                                                                            \
+        nm_assert(_bufsize <= 300);                                         \
+                                                                            \
+        _buf     = g_alloca(_bufsize);                                      \
+        _buf_len = g_snprintf(_buf, _bufsize, "" format "", ##__VA_ARGS__); \
+        nm_assert(_buf_len >= 0 && _buf_len < _bufsize);                    \
+        _buf;                                                               \
+    })
+
+#define nm_sprintf_bufa(bufsize, format, ...)                      \
+    ({                                                             \
+        G_STATIC_ASSERT_EXPR((bufsize) <= 300);                    \
+        nm_sprintf_buf_unsafe_a((bufsize), format, ##__VA_ARGS__); \
+    })
+
+/* aims to alloca() a buffer and fill it with printf(format, name).
+ * Note that format must not contain any format specifier except
+ * "%s".
+ * If the resulting string would be too large for stack allocation,
+ * it allocates a buffer with g_malloc() and assigns it to *p_val_to_free. */
+#define nm_construct_name_a(format, name, p_val_to_free)                                   \
+    ({                                                                                     \
+        const char *const _name          = (name);                                         \
+        char **const      _p_val_to_free = (p_val_to_free);                                \
+        const gsize       _name_len      = strlen(_name);                                  \
+        char *            _buf2;                                                           \
+                                                                                           \
+        nm_assert(_p_val_to_free && !*_p_val_to_free);                                     \
+        if (NM_STRLEN(format) <= 290 && _name_len < (gsize) (290 - NM_STRLEN(format)))     \
+            _buf2 = nm_sprintf_buf_unsafe_a(NM_STRLEN(format) + _name_len, format, _name); \
+        else {                                                                             \
+            _buf2           = g_strdup_printf(format, _name);                              \
+            *_p_val_to_free = _buf2;                                                       \
+        }                                                                                  \
+        (const char *) _buf2;                                                              \
+    })
+
+/*****************************************************************************/
+
+#define nm_va_args_one_ptr(last)           \
+    ({                                     \
+        va_list  _va_args;                 \
+        gpointer _ptr;                     \
+                                           \
+        va_start(_va_args, (last));        \
+        _ptr = va_arg(_va_args, gpointer); \
+        va_end(_va_args);                  \
+        _ptr;                              \
+    })
+
+/*****************************************************************************/
+
+#ifdef _G_BOOLEAN_EXPR
+    /* g_assert() uses G_LIKELY(), which in turn uses _G_BOOLEAN_EXPR().
+ * As glib's implementation uses a local variable _g_boolean_var_,
+ * we cannot do
+ *   g_assert (some_macro ());
+ * where some_macro() itself expands to ({g_assert(); ...}).
+ * In other words, you cannot have a g_assert() inside a g_assert()
+ * without getting a -Werror=shadow failure.
+ *
+ * Workaround that by re-defining _G_BOOLEAN_EXPR()
+ **/
+    #undef _G_BOOLEAN_EXPR
+    #define _G_BOOLEAN_EXPR(expr) NM_BOOLEAN_EXPR(expr)
+#endif
+
+/*****************************************************************************/
+
+#define NM_PID_T_INVAL ((pid_t) -1)
+
+/*****************************************************************************/
+
+NM_AUTO_DEFINE_FCN_VOID0(GMutex *, _nm_auto_unlock_g_mutex, g_mutex_unlock);
+
+#define nm_auto_unlock_g_mutex nm_auto(_nm_auto_unlock_g_mutex)
+
+#define _NM_G_MUTEX_LOCKED(lock, uniq)                                      \
+    _nm_unused nm_auto_unlock_g_mutex GMutex *NM_UNIQ_T(nm_lock, uniq) = ({ \
+        GMutex *const _lock = (lock);                                       \
+                                                                            \
+        g_mutex_lock(_lock);                                                \
+        _lock;                                                              \
+    })
+
+#define NM_G_MUTEX_LOCKED(lock) _NM_G_MUTEX_LOCKED(lock, NM_UNIQ)
+
+/*****************************************************************************/
+
+#endif /* __NM_MACROS_INTERNAL_H__ */
diff --git a/src/libnm-glib-aux/nm-obj.h b/src/libnm-glib-aux/nm-obj.h
new file mode 100644
index 00000000..2062f661
--- /dev/null
+++ b/src/libnm-glib-aux/nm-obj.h
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NM_OBJ_H__
+#define __NM_OBJ_H__
+
+/*****************************************************************************/
+
+#define NM_OBJ_REF_COUNT_STACKINIT (G_MAXINT)
+
+typedef struct _NMObjBaseInst  NMObjBaseInst;
+typedef struct _NMObjBaseClass NMObjBaseClass;
+
+struct _NMObjBaseInst {
+    /* The first field of NMObjBaseInst is compatible with GObject.
+     * Basically, NMObjBaseInst is an abstract base type of GTypeInstance.
+     *
+     * If you do it right, you may derive a type of NMObjBaseInst as a proper GTypeInstance.
+     * That involves allocating a GType for it, which can be inconvenient because
+     * a GType is dynamically created (and the class can no longer be immutable
+     * memory).
+     *
+     * Even if your implementation of NMObjBaseInst is not a full fledged GType(Instance),
+     * you still can use GTypeInstances in the same context as you can decide based on the
+     * NMObjBaseClass with what kind of object you are dealing with.
+     *
+     * Basically, the only thing NMObjBaseInst gives you is access to an
+     * NMObjBaseClass instance.
+     */
+    union {
+        const NMObjBaseClass *klass;
+        GTypeInstance         g_type_instance;
+    };
+};
+
+struct _NMObjBaseClass {
+    /* NMObjBaseClass is the base class of all NMObjBaseInst implementations.
+     * Note that it is also an abstract super class of GTypeInstance, that means
+     * you may implement a NMObjBaseClass as a subtype of GTypeClass.
+     *
+     * For that to work, you must properly set the GTypeClass instance (and its
+     * GType).
+     *
+     * Note that to implement a NMObjBaseClass that is *not* a GTypeClass, you wouldn't
+     * set the GType. Hence, this field is only useful for type implementations that actually
+     * extend GTypeClass.
+     *
+     * In a way it is wrong that NMObjBaseClass has the GType member, because it is
+     * a base class of GTypeClass and doesn't necessarily use the GType. However,
+     * it is here so that G_TYPE_CHECK_INSTANCE_TYPE() and friends work correctly
+     * on any NMObjectClass. That means, while not necessary, it is convenient that
+     * a NMObjBaseClass has all members of GTypeClass.
+     * Also note that usually you have only one instance of a certain type, so this
+     * wastes just a few bytes for the unneeded GType.
+     */
+    union {
+        GType      g_type;
+        GTypeClass g_type_class;
+    };
+};
+
+/*****************************************************************************/
+
+#endif /* __NM_OBJ_H__ */
diff --git a/src/libnm-glib-aux/nm-random-utils.c b/src/libnm-glib-aux/nm-random-utils.c
new file mode 100644
index 00000000..b055bc3f
--- /dev/null
+++ b/src/libnm-glib-aux/nm-random-utils.c
@@ -0,0 +1,299 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-random-utils.h"
+
+#include <fcntl.h>
+#include <sys/auxv.h>
+#include <sys/syscall.h>
+
+#if USE_SYS_RANDOM_H
+    #include <sys/random.h>
+#else
+    #include <linux/random.h>
+#endif
+
+#include "nm-shared-utils.h"
+#include "nm-time-utils.h"
+
+/*****************************************************************************/
+
+#if !defined(SYS_getrandom) && defined(__NR_getrandom)
+    #define SYS_getrandom __NR_getrandom
+#endif
+
+#ifndef GRND_NONBLOCK
+    #define GRND_NONBLOCK 0x01
+#endif
+
+#ifndef GRND_INSECURE
+    #define GRND_INSECURE 0x04
+#endif
+
+#if !HAVE_GETRANDOM && defined(SYS_getrandom)
+static int
+getrandom(void *buf, size_t buflen, unsigned flags)
+{
+    return syscall(SYS_getrandom, buf, buflen, flags);
+}
+    #undef HAVE_GETRANDOM
+    #define HAVE_GETRANDOM 1
+#endif
+
+/*****************************************************************************/
+
+typedef struct _nm_packed {
+    uintptr_t heap_ptr;
+    uintptr_t stack_ptr;
+    gint64    now_bootime;
+    gint64    now_real;
+    pid_t     pid;
+    pid_t     ppid;
+    pid_t     tid;
+    guint32   grand[16];
+    guint8    auxval[16];
+    guint8    getrandom_buf[20];
+} BadRandSeed;
+
+typedef struct _nm_packed {
+    guint64 counter;
+    union {
+        guint8 full[NM_UTILS_CHECKSUM_LENGTH_SHA256];
+        struct {
+            guint8 half_1[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2];
+            guint8 half_2[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2];
+        };
+    } sha_digest;
+    union {
+        guint8  u8[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2];
+        guint32 u32[((NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2) + 3) / 4];
+    } rand_vals;
+    GRand *rand;
+} BadRandState;
+
+static void
+_bad_random_init_seed(BadRandSeed *seed)
+{
+    const guint8 *p_at_random;
+    int           seed_idx;
+    GRand *       rand;
+
+    /* g_rand_new() reads /dev/urandom, but we already noticed that
+     * /dev/urandom fails to give us good randomness (which is why
+     * we hit the "bad randomness" code path). So this may not be as
+     * good as we wish, but let's hope that it it does something smart
+     * to give some extra entropy... */
+    rand = g_rand_new();
+
+    /* Get some seed material from a GRand. */
+    for (seed_idx = 0; seed_idx < (int) G_N_ELEMENTS(seed->grand); seed_idx++)
+        seed->grand[seed_idx] = g_rand_int(rand);
+
+    /* Add an address from the heap and stack, maybe ASLR helps a bit? */
+    seed->heap_ptr  = (uintptr_t) ((gpointer) rand);
+    seed->stack_ptr = (uintptr_t) ((gpointer) &rand);
+
+    g_rand_free(rand);
+
+    /* Add the per-process, random number. */
+    p_at_random = ((gpointer) getauxval(AT_RANDOM));
+    if (p_at_random) {
+        G_STATIC_ASSERT(sizeof(seed->auxval) == 16);
+        memcpy(&seed->auxval, p_at_random, 16);
+    }
+
+#if HAVE_GETRANDOM
+    {
+        ssize_t r;
+
+        /* This is likely to fail, because we already failed a moment earlier. Still, give
+         * it a try. */
+        r = getrandom(seed->getrandom_buf,
+                      sizeof(seed->getrandom_buf),
+                      GRND_INSECURE | GRND_NONBLOCK);
+        (void) r;
+    }
+#endif
+
+    seed->now_bootime = nm_utils_clock_gettime_nsec(CLOCK_BOOTTIME);
+    seed->now_real    = g_get_real_time();
+    seed->pid         = getpid();
+    seed->ppid        = getppid();
+    seed->tid         = nm_utils_gettid();
+}
+
+static void
+_bad_random_bytes(guint8 *buf, gsize n)
+{
+    nm_auto_free_checksum GChecksum *sum = g_checksum_new(G_CHECKSUM_SHA256);
+
+    nm_assert(n > 0);
+
+    /* We are in the fallback code path, where getrandom() (and /dev/urandom) failed
+     * to give us good randomness. Try our best.
+     *
+     * Our ability to get entropy for the CPRNG is very limited and thus the overall
+     * result will not be good randomness. See _bad_random_init_seed().
+     *
+     * Once we have some seed material, we combine GRand (which is not a cryptographically
+     * secure PRNG) with some iterative sha256 hashing. It would be nice if we had
+     * easy access to chacha20, but it's probably more cumbersome to fork those
+     * implementations than hack a bad CPRNG by using sha256 hashing. After all, this
+     * is fallback code to get *some* randomness. And with the inability to get a good
+     * seed, the CPRNG is not going to give us truly good randomness. */
+
+    {
+        static BadRandState gl_state;
+        static GRand *      gl_rand;
+        static GMutex       gl_mutex;
+        NM_G_MUTEX_LOCKED(&gl_mutex);
+
+        if (G_UNLIKELY(!gl_rand)) {
+            union {
+                BadRandSeed d_seed;
+                guint32     d_u32[(sizeof(BadRandSeed) + 3) / 4];
+            } data = {
+                .d_u32 = {0},
+            };
+
+            _bad_random_init_seed(&data.d_seed);
+
+            gl_rand = g_rand_new_with_seed_array(data.d_u32, G_N_ELEMENTS(data.d_u32));
+
+            g_checksum_update(sum, (const guchar *) &data, sizeof(data));
+            nm_utils_checksum_get_digest(sum, gl_state.sha_digest.full);
+        }
+
+        while (TRUE) {
+            int i;
+
+            gl_state.counter++;
+            for (i = 0; i < G_N_ELEMENTS(gl_state.rand_vals.u32); i++)
+                gl_state.rand_vals.u32[i] = g_rand_int(gl_rand);
+            g_checksum_reset(sum);
+            g_checksum_update(sum, (const guchar *) &gl_state, sizeof(gl_state));
+            nm_utils_checksum_get_digest(sum, gl_state.sha_digest.full);
+
+            /* gl_state.sha_digest.full and gl_state.rand_vals contain now our
+             * random values, but they are also the state for the next iteration.
+             * We must not directly expose that state to the caller, so XOR the values.
+             *
+             * That means, per iteration we can generate 16 bytes of randomness. That
+             * is for example required to generate a random UUID. */
+            for (i = 0; i < (int) (NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2); i++) {
+                nm_assert(n > 0);
+                buf[0] = gl_state.sha_digest.half_1[i] ^ gl_state.sha_digest.half_2[i]
+                         ^ gl_state.rand_vals.u8[i];
+                buf++;
+                n--;
+                if (n == 0)
+                    return;
+            }
+        }
+    }
+}
+
+/**
+ * nm_utils_random_bytes:
+ * @p: the buffer to fill
+ * @n: the number of bytes to write to @p.
+ *
+ * Uses getrandom() or reads /dev/urandom to fill the buffer
+ * with random data. If all fails, as last fallback it uses
+ * GRand to fill the buffer with pseudo random numbers.
+ * The function always succeeds in writing some random numbers
+ * to the buffer. The return value of FALSE indicates that the
+ * obtained bytes are probably not of good randomness.
+ *
+ * Returns: whether the written bytes are good. If you
+ * don't require good randomness, you can ignore the return
+ * value.
+ *
+ * Note that if calling getrandom() fails because there is not enough
+ * entropy (at early boot), the function will read /dev/urandom.
+ * Which of course, still has low entropy, and cause kernel to log
+ * a warning.
+ */
+gboolean
+nm_utils_random_bytes(void *p, size_t n)
+{
+    int      fd;
+    int      r;
+    gboolean has_high_quality = TRUE;
+    guint8 * buf              = p;
+
+    g_return_val_if_fail(p, FALSE);
+    g_return_val_if_fail(n > 0, FALSE);
+
+#if HAVE_GETRANDOM
+    {
+        static gboolean have_syscall = TRUE;
+
+        if (have_syscall) {
+            ssize_t r2;
+            int     errsv;
+
+            r2 = getrandom(buf, n, GRND_NONBLOCK);
+            if (r2 >= 0) {
+                if ((size_t) r2 == n)
+                    return TRUE;
+
+                /* no or partial read. There is not enough entropy.
+                 * Fill the rest reading with the fallback code and remember
+                 * that some bits are not high quality. */
+                nm_assert((size_t) r2 < n);
+                buf += r2;
+                n -= r2;
+
+                /* At this point, we don't want to read /dev/urandom, because
+                 * the entropy pool is low (early boot?), and asking for more
+                 * entropy causes kernel messages to be logged.
+                 *
+                 * Note that we fall back to _bad_random_bytes(), which (among others) seeds
+                 * itself with g_rand_new(). That also will read /dev/urandom, but as
+                 * we do that only once, we don't care. But in general, we are here in
+                 * a situation where we want to avoid reading /dev/urandom too much. */
+                goto out_bad_random;
+            }
+            errsv = errno;
+            if (errsv == ENOSYS) {
+                /* no support for getrandom(). We don't know whether
+                 * we /dev/urandom will give us good quality. Assume yes. */
+                have_syscall = FALSE;
+            } else if (errsv == EAGAIN) {
+                /* No entropy. We avoid reading /dev/urandom. */
+                goto out_bad_random;
+            } else {
+                /* Unknown error, likely no entropy. We'll read /dev/urandom below, but we don't
+                 * have high-quality randomness. */
+                has_high_quality = FALSE;
+            }
+        }
+    }
+#endif
+
+fd_open:
+    fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOCTTY);
+    if (fd < 0) {
+        if (errno == EINTR)
+            goto fd_open;
+        goto out_bad_random;
+    }
+    r = nm_utils_fd_read_loop_exact(fd, buf, n, TRUE);
+    nm_close(fd);
+    if (r >= 0)
+        return has_high_quality;
+
+out_bad_random:
+    /* we failed to fill the bytes reading from /dev/urandom.
+     * Fill the bits using our pseudo random numbers.
+     *
+     * We don't have good quality.
+     */
+    _bad_random_bytes(buf, n);
+    return FALSE;
+}
diff --git a/src/libnm-glib-aux/nm-random-utils.h b/src/libnm-glib-aux/nm-random-utils.h
new file mode 100644
index 00000000..d0eae103
--- /dev/null
+++ b/src/libnm-glib-aux/nm-random-utils.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NM_RANDOM_UTILS_H__
+#define __NM_RANDOM_UTILS_H__
+
+gboolean nm_utils_random_bytes(void *p, size_t n);
+
+#endif /* __NM_RANDOM_UTILS_H__ */
diff --git a/src/libnm-glib-aux/nm-ref-string.c b/src/libnm-glib-aux/nm-ref-string.c
new file mode 100644
index 00000000..93e97c79
--- /dev/null
+++ b/src/libnm-glib-aux/nm-ref-string.c
@@ -0,0 +1,190 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-ref-string.h"
+
+/*****************************************************************************/
+
+G_LOCK_DEFINE_STATIC(gl_lock);
+static GHashTable *gl_hash;
+
+/*****************************************************************************/
+
+static void
+_ref_string_get(const NMRefString *rstr, const char **out_str, gsize *out_len)
+{
+    if (rstr->len == G_MAXSIZE) {
+        *out_len = rstr->_priv_lookup.l_len;
+        *out_str = rstr->_priv_lookup.l_str;
+    } else {
+        *out_len = rstr->len;
+        *out_str = rstr->str;
+    }
+}
+
+static guint
+_ref_string_hash(gconstpointer ptr)
+{
+    const char *cstr;
+    gsize       len;
+
+    _ref_string_get(ptr, &cstr, &len);
+    return nm_hash_mem(1463435489u, cstr, len);
+}
+
+static gboolean
+_ref_string_equal(gconstpointer ptr_a, gconstpointer ptr_b)
+{
+    const char *cstr_a;
+    const char *cstr_b;
+    gsize       len_a;
+    gsize       len_b;
+
+    _ref_string_get(ptr_a, &cstr_a, &len_a);
+    _ref_string_get(ptr_b, &cstr_b, &len_b);
+
+    /* memcmp() accepts "n=0" argument, but it's not clear whether in that case
+     * all pointers must still be valid. The input pointer might be provided by
+     * the user via nm_ref_string_new_len(), and for len=0 we want to allow
+     * also invalid pointers. Hence, this extra "len_a==0" check. */
+    return len_a == len_b && (len_a == 0 || (memcmp(cstr_a, cstr_b, len_a) == 0));
+}
+
+/*****************************************************************************/
+
+void
+_nm_assert_nm_ref_string(NMRefString *rstr)
+{
+    int r;
+
+    nm_assert(rstr);
+
+    if (NM_MORE_ASSERTS > 0) {
+        r = g_atomic_int_get(&rstr->_ref_count);
+        nm_assert(r > 0);
+        nm_assert(r < G_MAXINT);
+    }
+
+    nm_assert(rstr->str[rstr->len] == '\0');
+
+    if (NM_MORE_ASSERTS > 10) {
+        G_LOCK(gl_lock);
+        r = g_atomic_int_get(&rstr->_ref_count);
+        nm_assert(r > 0);
+        nm_assert(r < G_MAXINT);
+
+        nm_assert(rstr == g_hash_table_lookup(gl_hash, rstr));
+        G_UNLOCK(gl_lock);
+    }
+}
+
+/**
+ * nm_ref_string_new_len:
+ * @cstr: the string to intern. Must contain @len bytes.
+ *   If @len is zero, @cstr may be %NULL. Note that it is
+ *   acceptable that the string contains a NUL character
+ *   within the first @len bytes. That is, the string is
+ *   not treated as a NUL terminated string, but as binary.
+ *   Also, contrary to strncpy(), this will read all the
+ *   first @len bytes. It won't stop at the first NUL.
+ * @len: the length of the string (usually there is no NUL character
+ *   within the first @len bytes, but that would be acceptable as well
+ *   to add binary data).
+ *
+ * Note that the resulting NMRefString instance will always be NUL terminated
+ * (at position @len).
+ *
+ * Note that NMRefString are always interned/deduplicated. If such a string
+ * already exists, the existing instance will be referred and returned.
+ *
+ *
+ * Since all NMRefString are shared and interned, you may use
+ * pointer equality to compare them. Note that if a NMRefString contains
+ * a NUL character (meaning, if
+ *
+ *    strlen (nm_ref_string_get_str (str)) != nm_ref_string_get_len (str)
+ *
+ * ), then pointer in-equality does not mean that the NUL terminated strings
+ * are also unequal. In other words, for strings that contain NUL characters,
+ *
+ *    if (str1 != str2)
+ *       assert (!nm_streq0 (nm_ref_string_get_str (str1), nm_ref_string_get_str (str2)));
+ *
+ * might not hold!
+ *
+ *
+ * NMRefString is thread-safe.
+ *
+ * Returns: (transfer full): the interned string. This is
+ *   never %NULL, but note that %NULL is also a valid NMRefString.
+ *   The result must be unrefed with nm_ref_string_unref().
+ */
+NMRefString *
+nm_ref_string_new_len(const char *cstr, gsize len)
+{
+    NMRefString *rstr;
+
+    /* @len cannot be close to G_MAXSIZE. For one, that would mean our call
+     * to malloc() below overflows. Also, we use G_MAXSIZE as special length
+     * to indicate using _priv_lookup. */
+    nm_assert(len < G_MAXSIZE - G_STRUCT_OFFSET(NMRefString, str) - 1u);
+
+    G_LOCK(gl_lock);
+
+    if (G_UNLIKELY(!gl_hash)) {
+        gl_hash = g_hash_table_new_full(_ref_string_hash, _ref_string_equal, g_free, NULL);
+        rstr    = NULL;
+    } else {
+        NMRefString rr_lookup = {
+            .len = G_MAXSIZE,
+            ._priv_lookup =
+                {
+                    .l_len = len,
+                    .l_str = cstr,
+                },
+        };
+
+        rstr = g_hash_table_lookup(gl_hash, &rr_lookup);
+    }
+
+    if (rstr) {
+        nm_assert(({
+            int r = g_atomic_int_get(&rstr->_ref_count);
+
+            (r >= 0 && r < G_MAXINT);
+        }));
+        g_atomic_int_inc(&rstr->_ref_count);
+    } else {
+        rstr = g_malloc((G_STRUCT_OFFSET(NMRefString, str) + 1u) + len);
+        if (len > 0)
+            memcpy((char *) rstr->str, cstr, len);
+        ((char *) rstr->str)[len] = '\0';
+        *((gsize *) &rstr->len)   = len;
+        rstr->_ref_count          = 1;
+
+        if (!g_hash_table_add(gl_hash, rstr))
+            nm_assert_not_reached();
+    }
+
+    G_UNLOCK(gl_lock);
+
+    return rstr;
+}
+
+void
+_nm_ref_string_unref_slow_path(NMRefString *rstr)
+{
+    G_LOCK(gl_lock);
+
+    nm_assert(g_hash_table_lookup(gl_hash, rstr) == rstr);
+
+    if (G_LIKELY(g_atomic_int_dec_and_test(&rstr->_ref_count))) {
+        if (!g_hash_table_remove(gl_hash, rstr))
+            nm_assert_not_reached();
+    }
+
+    G_UNLOCK(gl_lock);
+}
+
+/*****************************************************************************/
diff --git a/src/libnm-glib-aux/nm-ref-string.h b/src/libnm-glib-aux/nm-ref-string.h
new file mode 100644
index 00000000..df37d583
--- /dev/null
+++ b/src/libnm-glib-aux/nm-ref-string.h
@@ -0,0 +1,176 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NM_REF_STRING_H__
+#define __NM_REF_STRING_H__
+
+/*****************************************************************************/
+
+typedef struct _NMRefString {
+    const gsize len;
+    union {
+        struct {
+            volatile int _ref_count;
+            const char   str[];
+        };
+        struct {
+            /* This union field is only used during lookup by external string.
+             * In that case, len will be set to G_MAXSIZE, and the actual len/str values
+             * are set in _priv_lookup. */
+            gsize       l_len;
+            const char *l_str;
+        } _priv_lookup;
+    };
+} NMRefString;
+
+/*****************************************************************************/
+
+void _nm_assert_nm_ref_string(NMRefString *rstr);
+
+static inline void
+nm_assert_nm_ref_string(NMRefString *rstr)
+{
+#if NM_MORE_ASSERTS
+    _nm_assert_nm_ref_string(rstr);
+#endif
+}
+
+/*****************************************************************************/
+
+NMRefString *nm_ref_string_new_len(const char *cstr, gsize len);
+
+static inline NMRefString *
+nm_ref_string_new(const char *cstr)
+{
+    return cstr ? nm_ref_string_new_len(cstr, strlen(cstr)) : NULL;
+}
+
+/*****************************************************************************/
+
+static inline NMRefString *
+nm_ref_string_ref(NMRefString *rstr)
+{
+    if (rstr) {
+        nm_assert_nm_ref_string(rstr);
+        g_atomic_int_inc(&rstr->_ref_count);
+    }
+    return rstr;
+}
+
+void _nm_ref_string_unref_slow_path(NMRefString *rstr);
+
+static inline void
+nm_ref_string_unref(NMRefString *rstr)
+{
+    int r;
+
+    if (!rstr)
+        return;
+
+    nm_assert_nm_ref_string(rstr);
+
+    /* fast-path: first try to decrement the ref-count without bringing it
+     * to zero. */
+    r = rstr->_ref_count;
+    if (G_LIKELY(r > 1 && g_atomic_int_compare_and_exchange(&rstr->_ref_count, r, r - 1)))
+        return;
+
+    _nm_ref_string_unref_slow_path(rstr);
+}
+
+NM_AUTO_DEFINE_FCN_VOID(NMRefString *, _nm_auto_ref_string, nm_ref_string_unref);
+#define nm_auto_ref_string nm_auto(_nm_auto_ref_string)
+
+/*****************************************************************************/
+
+static inline const char *
+nm_ref_string_get_str(NMRefString *rstr)
+{
+    return rstr ? rstr->str : NULL;
+}
+
+static inline gsize
+nm_ref_string_get_len(NMRefString *rstr)
+{
+    return rstr ? rstr->len : 0u;
+}
+
+static inline gboolean
+nm_ref_string_equal(NMRefString *a, NMRefString *b)
+{
+    return a == b;
+}
+
+static inline int
+nm_ref_string_cmp(NMRefString *a, NMRefString *b)
+{
+    NM_CMP_SELF(a, b);
+
+    /* It would be cheaper to first compare by length. But this
+     * way we get a nicer, ASCIIbethical sort order. */
+    NM_CMP_DIRECT_MEMCMP(a->str, b->str, NM_MIN(a->len, b->len));
+    NM_CMP_DIRECT(a->len, b->len);
+    return nm_assert_unreachable_val(0);
+}
+
+#define NM_CMP_DIRECT_REF_STRING(a, b) NM_CMP_RETURN_DIRECT(nm_ref_string_cmp((a), (b)))
+
+static inline gboolean
+nm_ref_string_equal_str(NMRefString *rstr, const char *str)
+{
+    if (!str)
+        return (!!rstr);
+
+    if (!rstr)
+        return FALSE;
+
+    return rstr->len == strlen(str) && (rstr->str == str || memcmp(rstr->str, str, rstr->len) == 0);
+}
+
+static inline gboolean
+NM_IS_REF_STRING(NMRefString *rstr)
+{
+    if (rstr)
+        nm_assert_nm_ref_string(rstr);
+
+    /* Technically, %NULL is also a valid NMRefString (according to nm_ref_string_new(),
+     * nm_ref_string_get_str() and nm_ref_string_unref()). However, NM_IS_REF_STRING()
+     * does not think so. If callers want to allow %NULL, they need to check
+     * separately. */
+    return !!rstr;
+}
+
+static inline NMRefString *
+NM_REF_STRING_UPCAST(const char *str)
+{
+    NMRefString *rstr;
+
+    if (!str)
+        return NULL;
+
+    rstr = (gpointer) (((char *) str) - G_STRUCT_OFFSET(NMRefString, str));
+    nm_assert_nm_ref_string(rstr);
+    return rstr;
+}
+
+static inline gboolean
+nm_ref_string_reset_str(NMRefString **ptr, const char *str)
+{
+    nm_auto_ref_string NMRefString *rstr = NULL;
+    gsize                           l;
+
+    nm_assert(ptr);
+
+    if (!str)
+        return nm_clear_pointer(ptr, nm_ref_string_unref);
+
+    l = strlen(str);
+
+    if ((*ptr) && (*ptr)->len == l && ((*ptr)->str == str || memcmp((*ptr)->str, str, l) == 0))
+        return FALSE;
+
+    rstr = *ptr;
+    *ptr = nm_ref_string_new_len(str, l);
+    return TRUE;
+}
+
+#endif /* __NM_REF_STRING_H__ */
diff --git a/src/libnm-glib-aux/nm-secret-utils.c b/src/libnm-glib-aux/nm-secret-utils.c
new file mode 100644
index 00000000..c764b6e5
--- /dev/null
+++ b/src/libnm-glib-aux/nm-secret-utils.c
@@ -0,0 +1,178 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ * Copyright (C) 2015 - 2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#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 (G_UNLIKELY(n == 0))
+        return;
+
+    nm_assert(s);
+
+#if defined(HAVE_DECL_EXPLICIT_BZERO) && HAVE_DECL_EXPLICIT_BZERO
+    explicit_bzero(s, n);
+#else
+    {
+        volatile guint8 *p = s;
+
+        memset(s, '\0', n);
+        while (n-- > 0)
+            *(p++) = '\0';
+    }
+#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 *
+nm_secret_strchomp(char *secret)
+{
+    gsize len;
+
+    g_return_val_if_fail(secret, NULL);
+
+    /* it's actually identical to g_strchomp(). However,
+     * the glib function does not document, that it clears the
+     * memory. For @secret, we don't only want to truncate trailing
+     * spaces, we want to overwrite them with NUL. */
+
+    len = strlen(secret);
+    while (len--) {
+        if (g_ascii_isspace((guchar) secret[len]))
+            secret[len] = '\0';
+        else
+            break;
+    }
+
+    return secret;
+}
+
+/*****************************************************************************/
+
+GBytes *
+nm_secret_copy_to_gbytes(gconstpointer mem, gsize mem_len)
+{
+    NMSecretBuf *b;
+
+    if (mem_len == 0)
+        return g_bytes_new_static("", 0);
+
+    nm_assert(mem);
+
+    /* NUL terminate the buffer.
+     *
+     * The entire buffer is already malloc'ed and likely has some room for padding.
+     * Thus, in many situations, this additional byte will cause no overhead in
+     * practice.
+     *
+     * Even if it causes an overhead, do it just for safety. Yes, the returned
+     * bytes is not a NUL terminated string and no user must rely on this. Do
+     * not treat binary data as NUL terminated strings, unless you know what
+     * you are doing. Anyway, defensive FTW.
+     */
+
+    b = nm_secret_buf_new(mem_len + 1);
+    memcpy(b->bin, mem, mem_len);
+    b->bin[mem_len] = 0;
+    return nm_secret_buf_to_gbytes_take(b, mem_len);
+}
+
+/*****************************************************************************/
+
+NMSecretBuf *
+nm_secret_buf_new(gsize len)
+{
+    NMSecretBuf *secret;
+
+    nm_assert(len > 0);
+
+    secret                      = g_malloc(sizeof(NMSecretBuf) + len);
+    *((gsize *) &(secret->len)) = len;
+    return secret;
+}
+
+static void
+_secret_buf_free(gpointer user_data)
+{
+    NMSecretBuf *secret = user_data;
+
+    nm_assert(secret);
+    nm_assert(secret->len > 0);
+
+    nm_explicit_bzero(secret->bin, secret->len);
+    g_free(user_data);
+}
+
+GBytes *
+nm_secret_buf_to_gbytes_take(NMSecretBuf *secret, gssize actual_len)
+{
+    nm_assert(secret);
+    nm_assert(secret->len > 0);
+    nm_assert(actual_len == -1 || (actual_len >= 0 && actual_len <= secret->len));
+    return g_bytes_new_with_free_func(secret->bin,
+                                      actual_len >= 0 ? (gsize) actual_len : secret->len,
+                                      _secret_buf_free,
+                                      secret);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_utils_memeqzero_secret:
+ * @data: the data pointer to check (may be %NULL if @length is zero).
+ * @length: the number of bytes to check.
+ *
+ * Checks that all bytes are zero. This always takes the same amount
+ * of time to prevent timing attacks.
+ *
+ * Returns: whether all bytes are zero.
+ */
+gboolean
+nm_utils_memeqzero_secret(gconstpointer data, gsize length)
+{
+    const guint8 *const key = data;
+    volatile guint8     acc = 0;
+    gsize               i;
+
+    for (i = 0; i < length; i++) {
+        acc |= key[i];
+        asm volatile("" : "=r"(acc) : "0"(acc));
+    }
+    return 1 & ((acc - 1) >> 8);
+}
diff --git a/src/libnm-glib-aux/nm-secret-utils.h b/src/libnm-glib-aux/nm-secret-utils.h
new file mode 100644
index 00000000..ac279635
--- /dev/null
+++ b/src/libnm-glib-aux/nm-secret-utils.h
@@ -0,0 +1,273 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#ifndef __NM_SECRET_UTILS_H__
+#define __NM_SECRET_UTILS_H__
+
+#include "nm-macros-internal.h"
+
+/*****************************************************************************/
+
+void nm_explicit_bzero(void *s, gsize n);
+
+/*****************************************************************************/
+
+char *nm_secret_strchomp(char *secret);
+
+/*****************************************************************************/
+
+void nm_free_secret(char *secret);
+
+NM_AUTO_DEFINE_FCN0(char *, _nm_auto_free_secret, nm_free_secret);
+/**
+ * nm_auto_free_secret:
+ *
+ * Call g_free() on a variable location when it goes out of scope.
+ * Also, previously, calls memset(loc, 0, strlen(loc)) to clear out
+ * the secret.
+ */
+#define nm_auto_free_secret nm_auto(_nm_auto_free_secret)
+
+/*****************************************************************************/
+
+GBytes *nm_secret_copy_to_gbytes(gconstpointer mem, gsize mem_len);
+
+/*****************************************************************************/
+
+/* NMSecretPtr is a pair of malloc'ed data pointer and the length of the
+ * data. The purpose is to use it in combination with nm_auto_clear_secret_ptr
+ * which ensures that the data pointer (with all len bytes) is cleared upon
+ * cleanup. */
+typedef struct {
+    gsize len;
+
+    /* the data pointer. This pointer must be allocated with malloc (at least
+     * when used with nm_secret_ptr_clear()). */
+    union {
+        char *  str;
+        void *  ptr;
+        guint8 *bin;
+    };
+} NMSecretPtr;
+
+static inline void
+nm_secret_ptr_bzero(NMSecretPtr *secret)
+{
+    if (secret) {
+        if (secret->len > 0) {
+            if (secret->ptr)
+                nm_explicit_bzero(secret->ptr, secret->len);
+        }
+    }
+}
+
+#define nm_auto_bzero_secret_ptr nm_auto(nm_secret_ptr_bzero)
+
+static inline void
+nm_secret_ptr_clear(NMSecretPtr *secret)
+{
+    if (secret) {
+        if (secret->len > 0) {
+            if (secret->ptr)
+                nm_explicit_bzero(secret->ptr, secret->len);
+            secret->len = 0;
+        }
+        nm_clear_g_free(&secret->ptr);
+    }
+}
+
+#define nm_auto_clear_secret_ptr nm_auto(nm_secret_ptr_clear)
+
+#define NM_SECRET_PTR_INIT() \
+    ((const NMSecretPtr){    \
+        .len = 0,            \
+        .ptr = NULL,         \
+    })
+
+#define NM_SECRET_PTR_STATIC(_len) \
+    ((const NMSecretPtr){          \
+        .len = _len,               \
+        .ptr = ((guint8[_len]){}), \
+    })
+
+#define NM_SECRET_PTR_ARRAY(_arr)                      \
+    ((const NMSecretPtr){                              \
+        .len = G_N_ELEMENTS(_arr) * sizeof((_arr)[0]), \
+        .ptr = &((_arr)[0]),                           \
+    })
+
+static inline void
+nm_secret_ptr_clear_static(const NMSecretPtr *secret)
+{
+    if (secret) {
+        if (secret->len > 0) {
+            nm_assert(secret->ptr);
+            nm_explicit_bzero(secret->ptr, secret->len);
+        }
+    }
+}
+
+#define nm_auto_clear_static_secret_ptr nm_auto(nm_secret_ptr_clear_static)
+
+static inline void
+nm_secret_ptr_move(NMSecretPtr *dst, NMSecretPtr *src)
+{
+    if (dst && dst != src) {
+        *dst     = *src;
+        src->len = 0;
+        src->ptr = NULL;
+    }
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    const gsize len;
+    union {
+        char   str[0];
+        guint8 bin[0];
+    };
+} NMSecretBuf;
+
+static inline void
+_nm_auto_free_secret_buf(NMSecretBuf **ptr)
+{
+    NMSecretBuf *b = *ptr;
+
+    if (b) {
+        nm_assert(b->len > 0);
+        nm_explicit_bzero(b->bin, b->len);
+        g_free(b);
+    }
+}
+#define nm_auto_free_secret_buf nm_auto(_nm_auto_free_secret_buf)
+
+NMSecretBuf *nm_secret_buf_new(gsize len);
+
+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 instead 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 instead 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/src/libnm-glib-aux/nm-shared-utils.c b/src/libnm-glib-aux/nm-shared-utils.c
new file mode 100644
index 00000000..81852aea
--- /dev/null
+++ b/src/libnm-glib-aux/nm-shared-utils.c
@@ -0,0 +1,6439 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2016 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-shared-utils.h"
+
+#include <pwd.h>
+#include <arpa/inet.h>
+#include <poll.h>
+#include <fcntl.h>
+#include <sys/syscall.h>
+#include <glib-unix.h>
+#include <net/if.h>
+#include <net/ethernet.h>
+#include <pthread.h>
+
+#include "c-list/src/c-list.h"
+#include "nm-errno.h"
+#include "nm-str-buf.h"
+
+G_STATIC_ASSERT(sizeof(NMEtherAddr) == 6);
+G_STATIC_ASSERT(_nm_alignof(NMEtherAddr) == 1);
+
+G_STATIC_ASSERT(sizeof(NMUtilsNamedEntry) == sizeof(const char *));
+G_STATIC_ASSERT(G_STRUCT_OFFSET(NMUtilsNamedValue, value_ptr) == sizeof(const char *));
+
+/*****************************************************************************/
+
+const char _nm_hexchar_table_lower[16] = "0123456789abcdef";
+const char _nm_hexchar_table_upper[16] = "0123456789ABCDEF";
+
+const void *const _NM_PTRARRAY_EMPTY[1] = {NULL};
+
+/*****************************************************************************/
+
+const NMIPAddr nm_ip_addr_zero = {};
+
+/* this initializes a struct in_addr/in6_addr and allows for untrusted
+ * arguments (like unsuitable @addr_family or @src_len). It's almost safe
+ * in the sense that it verifies input arguments strictly. Also, it
+ * uses memcpy() to access @src, so alignment is not an issue.
+ *
+ * Only potential pitfalls:
+ *
+ * - it allows for @addr_family to be AF_UNSPEC. If that is the case (and the
+ *   caller allows for that), the caller MUST provide @out_addr_family.
+ * - when setting @dst to an IPv4 address, the trailing bytes are not touched.
+ *   Meaning, if @dst is an NMIPAddr union, only the first bytes will be set.
+ *   If that matter to you, clear @dst before. */
+gboolean
+nm_ip_addr_set_from_untrusted(int           addr_family,
+                              gpointer      dst,
+                              gconstpointer src,
+                              gsize         src_len,
+                              int *         out_addr_family)
+{
+    nm_assert(dst);
+
+    switch (addr_family) {
+    case AF_UNSPEC:
+        if (!out_addr_family) {
+            /* when the callers allow undefined @addr_family, they must provide
+             * an @out_addr_family argument. */
+            nm_assert_not_reached();
+            return FALSE;
+        }
+        switch (src_len) {
+        case sizeof(struct in_addr):
+            addr_family = AF_INET;
+            break;
+        case sizeof(struct in6_addr):
+            addr_family = AF_INET6;
+            break;
+        default:
+            return FALSE;
+        }
+        break;
+    case AF_INET:
+        if (src_len != sizeof(struct in_addr))
+            return FALSE;
+        break;
+    case AF_INET6:
+        if (src_len != sizeof(struct in6_addr))
+            return FALSE;
+        break;
+    default:
+        /* when the callers allow undefined @addr_family, they must provide
+         * an @out_addr_family argument. */
+        nm_assert(out_addr_family);
+        return FALSE;
+    }
+
+    nm_assert(src);
+
+    memcpy(dst, src, src_len);
+    NM_SET_OUT(out_addr_family, addr_family);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+G_STATIC_ASSERT(ETH_ALEN == sizeof(struct ether_addr));
+G_STATIC_ASSERT(ETH_ALEN == 6);
+
+/*****************************************************************************/
+
+/**
+ * nm_utils_inet6_is_token:
+ * @in6addr: the AF_INET6 address structure
+ *
+ * Checks if only the bottom 64bits of the address are set.
+ *
+ * Return value: %TRUE or %FALSE
+ */
+gboolean
+_nm_utils_inet6_is_token(const struct in6_addr *in6addr)
+{
+    if (in6addr->s6_addr[0] || in6addr->s6_addr[1] || in6addr->s6_addr[2] || in6addr->s6_addr[3]
+        || in6addr->s6_addr[4] || in6addr->s6_addr[5] || in6addr->s6_addr[6] || in6addr->s6_addr[7])
+        return FALSE;
+
+    if (in6addr->s6_addr[8] || in6addr->s6_addr[9] || in6addr->s6_addr[10] || in6addr->s6_addr[11]
+        || in6addr->s6_addr[12] || in6addr->s6_addr[13] || in6addr->s6_addr[14]
+        || in6addr->s6_addr[15])
+        return TRUE;
+
+    return FALSE;
+}
+
+/**
+ * nm_utils_ipv6_addr_set_interface_identifier:
+ * @addr: output token encoded as %in6_addr
+ * @iid: %NMUtilsIPv6IfaceId interface identifier
+ *
+ * Converts the %NMUtilsIPv6IfaceId to an %in6_addr (suitable for use
+ * with Linux platform). This only copies the lower 8 bytes, ignoring
+ * the /64 network prefix which is expected to be all-zero for a valid
+ * token.
+ */
+void
+nm_utils_ipv6_addr_set_interface_identifier(struct in6_addr *addr, const NMUtilsIPv6IfaceId iid)
+{
+    memcpy(addr->s6_addr + 8, &iid.id_u8, 8);
+}
+
+/**
+ * nm_utils_ipv6_interface_identifier_get_from_addr:
+ * @iid: output %NMUtilsIPv6IfaceId interface identifier set from the token
+ * @addr: token encoded as %in6_addr
+ *
+ * Converts the %in6_addr encoded token (as used by Linux platform) to
+ * the interface identifier.
+ */
+void
+nm_utils_ipv6_interface_identifier_get_from_addr(NMUtilsIPv6IfaceId *   iid,
+                                                 const struct in6_addr *addr)
+{
+    memcpy(iid, addr->s6_addr + 8, 8);
+}
+
+/**
+ * nm_utils_ipv6_interface_identifier_get_from_token:
+ * @iid: output %NMUtilsIPv6IfaceId interface identifier set from the token
+ * @token: token encoded as string
+ *
+ * Converts the %in6_addr encoded token (as used in ip6 settings) to
+ * the interface identifier.
+ *
+ * Returns: %TRUE if the @token is a valid token, %FALSE otherwise
+ */
+gboolean
+nm_utils_ipv6_interface_identifier_get_from_token(NMUtilsIPv6IfaceId *iid, const char *token)
+{
+    struct in6_addr i6_token;
+
+    g_return_val_if_fail(token, FALSE);
+
+    if (!inet_pton(AF_INET6, token, &i6_token))
+        return FALSE;
+
+    if (!_nm_utils_inet6_is_token(&i6_token))
+        return FALSE;
+
+    nm_utils_ipv6_interface_identifier_get_from_addr(iid, &i6_token);
+    return TRUE;
+}
+
+/**
+ * nm_utils_inet6_interface_identifier_to_token:
+ * @iid: %NMUtilsIPv6IfaceId interface identifier
+ * @buf: the destination buffer of at least %NM_UTILS_INET_ADDRSTRLEN
+ *   bytes.
+ *
+ * Converts the interface identifier to a string token.
+ *
+ * Returns: the input buffer filled with the id as string.
+ */
+const char *
+nm_utils_inet6_interface_identifier_to_token(NMUtilsIPv6IfaceId iid,
+                                             char               buf[static INET6_ADDRSTRLEN])
+{
+    struct in6_addr i6_token = {.s6_addr = {
+                                    0,
+                                }};
+
+    nm_assert(buf);
+    nm_utils_ipv6_addr_set_interface_identifier(&i6_token, iid);
+    return _nm_utils_inet6_ntop(&i6_token, buf);
+}
+
+/*****************************************************************************/
+
+pid_t
+nm_utils_gettid(void)
+{
+    return (pid_t) syscall(SYS_gettid);
+}
+
+/* Used for asserting that this function is called on the main-thread.
+ * The main-thread is determined by remembering the thread-id
+ * of when the function was called the first time.
+ *
+ * When forking, the thread-id is again reset upon first call. */
+gboolean
+_nm_assert_on_main_thread(void)
+{
+    G_LOCK_DEFINE_STATIC(lock);
+    static pid_t seen_tid;
+    static pid_t seen_pid;
+    pid_t        tid;
+    pid_t        pid;
+    gboolean     success = FALSE;
+
+    tid = nm_utils_gettid();
+    nm_assert(tid != 0);
+
+    G_LOCK(lock);
+
+    if (G_LIKELY(tid == seen_tid)) {
+        /* we don't care about false positives (when the process forked, and the thread-id
+         * is accidentally re-used) . It's for assertions only. */
+        success = TRUE;
+    } else {
+        pid = getpid();
+        nm_assert(pid != 0);
+
+        if (seen_tid == 0 || seen_pid != pid) {
+            /* either this is the first time we call the function, or the process
+             * forked. In both cases, remember the thread-id. */
+            seen_tid = tid;
+            seen_pid = pid;
+            success  = TRUE;
+        }
+    }
+
+    G_UNLOCK(lock);
+
+    return success;
+}
+
+/*****************************************************************************/
+
+void
+nm_utils_strbuf_append_c(char **buf, gsize *len, char c)
+{
+    switch (*len) {
+    case 0:
+        return;
+    case 1:
+        (*buf)[0] = '\0';
+        *len      = 0;
+        (*buf)++;
+        return;
+    default:
+        (*buf)[0] = c;
+        (*buf)[1] = '\0';
+        (*len)--;
+        (*buf)++;
+        return;
+    }
+}
+
+void
+nm_utils_strbuf_append_bin(char **buf, gsize *len, gconstpointer str, gsize str_len)
+{
+    switch (*len) {
+    case 0:
+        return;
+    case 1:
+        if (str_len == 0) {
+            (*buf)[0] = '\0';
+            return;
+        }
+        (*buf)[0] = '\0';
+        *len      = 0;
+        (*buf)++;
+        return;
+    default:
+        if (str_len == 0) {
+            (*buf)[0] = '\0';
+            return;
+        }
+        if (str_len >= *len) {
+            memcpy(*buf, str, *len - 1);
+            (*buf)[*len - 1] = '\0';
+            *buf             = &(*buf)[*len];
+            *len             = 0;
+        } else {
+            memcpy(*buf, str, str_len);
+            *buf      = &(*buf)[str_len];
+            (*buf)[0] = '\0';
+            *len -= str_len;
+        }
+        return;
+    }
+}
+
+void
+nm_utils_strbuf_append_str(char **buf, gsize *len, const char *str)
+{
+    gsize src_len;
+
+    switch (*len) {
+    case 0:
+        return;
+    case 1:
+        if (!str || !*str) {
+            (*buf)[0] = '\0';
+            return;
+        }
+        (*buf)[0] = '\0';
+        *len      = 0;
+        (*buf)++;
+        return;
+    default:
+        if (!str || !*str) {
+            (*buf)[0] = '\0';
+            return;
+        }
+        src_len = g_strlcpy(*buf, str, *len);
+        if (src_len >= *len) {
+            *buf = &(*buf)[*len];
+            *len = 0;
+        } else {
+            *buf = &(*buf)[src_len];
+            *len -= src_len;
+        }
+        return;
+    }
+}
+
+void
+nm_utils_strbuf_append(char **buf, gsize *len, const char *format, ...)
+{
+    char *  p = *buf;
+    va_list args;
+    int     retval;
+
+    if (*len == 0)
+        return;
+
+    va_start(args, format);
+    retval = g_vsnprintf(p, *len, format, args);
+    va_end(args);
+
+    if ((gsize) retval >= *len) {
+        *buf = &p[*len];
+        *len = 0;
+    } else {
+        *buf = &p[retval];
+        *len -= retval;
+    }
+}
+
+/**
+ * nm_utils_strbuf_seek_end:
+ * @buf: the input/output buffer
+ * @len: the input/output length of the buffer.
+ *
+ * Commonly, one uses nm_utils_strbuf_append*(), to incrementally
+ * append strings to the buffer. However, sometimes we need to use
+ * existing API to write to the buffer.
+ * After doing so, we want to adjust the buffer counter.
+ * Essentially,
+ *
+ *   g_snprintf (buf, len, ...);
+ *   nm_utils_strbuf_seek_end (&buf, &len);
+ *
+ * is almost the same as
+ *
+ *   nm_utils_strbuf_append (&buf, &len, ...);
+ *
+ * The only difference is the behavior when the string got truncated:
+ * nm_utils_strbuf_append() will recognize that and set the remaining
+ * length to zero.
+ *
+ * In general, the behavior is:
+ *
+ *  - if *len is zero, do nothing
+ *  - if the buffer contains a NUL byte within the first *len characters,
+ *    the buffer is pointed to the NUL byte and len is adjusted. In this
+ *    case, the remaining *len is always >= 1.
+ *    In particular, that is also the case if the NUL byte is at the very last
+ *    position ((*buf)[*len -1]). That happens, when the previous operation
+ *    either fit the string exactly into the buffer or the string was truncated
+ *    by g_snprintf(). The difference cannot be determined.
+ *  - if the buffer contains no NUL bytes within the first *len characters,
+ *    write NUL at the last position, set *len to zero, and point *buf past
+ *    the NUL byte. This would happen with
+ *
+ *       strncpy (buf, long_str, len);
+ *       nm_utils_strbuf_seek_end (&buf, &len).
+ *
+ *    where strncpy() does truncate the string and not NUL terminate it.
+ *    nm_utils_strbuf_seek_end() would then NUL terminate it.
+ */
+void
+nm_utils_strbuf_seek_end(char **buf, gsize *len)
+{
+    gsize l;
+    char *end;
+
+    nm_assert(len);
+    nm_assert(buf && *buf);
+
+    if (*len <= 1) {
+        if (*len == 1 && (*buf)[0])
+            goto truncate;
+        return;
+    }
+
+    end = memchr(*buf, 0, *len);
+    if (end) {
+        l = end - *buf;
+        nm_assert(l < *len);
+
+        *buf = end;
+        *len -= l;
+        return;
+    }
+
+truncate:
+    /* hm, no NUL character within len bytes.
+     * Just NUL terminate the array and consume them
+     * all. */
+    *buf += *len;
+    (*buf)[-1] = '\0';
+    *len       = 0;
+    return;
+}
+
+/*****************************************************************************/
+
+GBytes *
+nm_gbytes_get_empty(void)
+{
+    static GBytes *bytes = NULL;
+    GBytes *       b;
+
+again:
+    b = g_atomic_pointer_get(&bytes);
+    if (G_UNLIKELY(!b)) {
+        b = g_bytes_new_static("", 0);
+        if (!g_atomic_pointer_compare_and_exchange(&bytes, NULL, b)) {
+            g_bytes_unref(b);
+            goto again;
+        }
+    }
+    return b;
+}
+
+GBytes *
+nm_g_bytes_new_from_str(const char *str)
+{
+    gsize l;
+
+    if (!str)
+        return NULL;
+
+    /* the returned array is guaranteed to have a trailing '\0'
+     * character *after* the length. */
+
+    l = strlen(str);
+    return g_bytes_new_take(nm_memdup(str, l + 1u), l);
+}
+
+/**
+ * nm_utils_gbytes_equal_mem:
+ * @bytes: (allow-none): a #GBytes array to compare. Note that
+ *   %NULL is treated like an #GBytes array of length zero.
+ * @mem_data: the data pointer with @mem_len bytes
+ * @mem_len: the length of the data pointer
+ *
+ * Returns: %TRUE if @bytes contains the same data as @mem_data. As a
+ *   special case, a %NULL @bytes is treated like an empty array.
+ */
+gboolean
+nm_utils_gbytes_equal_mem(GBytes *bytes, gconstpointer mem_data, gsize mem_len)
+{
+    gconstpointer p;
+    gsize         l;
+
+    if (!bytes) {
+        /* as a special case, let %NULL GBytes compare identical
+         * to an empty array. */
+        return (mem_len == 0);
+    }
+
+    p = g_bytes_get_data(bytes, &l);
+    return l == mem_len
+           && (mem_len == 0 /* allow @mem_data to be %NULL */
+               || memcmp(p, mem_data, mem_len) == 0);
+}
+
+GVariant *
+nm_utils_gbytes_to_variant_ay(GBytes *bytes)
+{
+    const guint8 *p = NULL;
+    gsize         l = 0;
+
+    if (!bytes) {
+        /* for convenience, accept NULL to return an empty variant */
+    } else
+        p = g_bytes_get_data(bytes, &l);
+
+    return nm_g_variant_new_ay(p, l);
+}
+
+/*****************************************************************************/
+
+#define _variant_singleton_get(create_variant)                                       \
+    ({                                                                               \
+        static GVariant *_singleton = NULL;                                          \
+        GVariant *       _v;                                                         \
+                                                                                     \
+        while (TRUE) {                                                               \
+            _v = g_atomic_pointer_get(&_singleton);                                  \
+            if (G_UNLIKELY(!_v)) {                                                   \
+                _v = (create_variant);                                               \
+                nm_assert(_v);                                                       \
+                nm_assert(g_variant_is_floating(_v));                                \
+                g_variant_ref_sink(_v);                                              \
+                if (!g_atomic_pointer_compare_and_exchange(&_singleton, NULL, _v)) { \
+                    g_variant_unref(_v);                                             \
+                    continue;                                                        \
+                }                                                                    \
+            }                                                                        \
+            break;                                                                   \
+        }                                                                            \
+        _v;                                                                          \
+    })
+
+GVariant *
+nm_g_variant_singleton_u_0(void)
+{
+    return _variant_singleton_get(g_variant_new_uint32(0));
+}
+
+GVariant *
+nm_g_variant_singleton_b(gboolean value)
+{
+    return value ? _variant_singleton_get(g_variant_new_boolean(TRUE))
+                 : _variant_singleton_get(g_variant_new_boolean(FALSE));
+}
+
+GVariant *
+nm_g_variant_singleton_s_empty(void)
+{
+    return _variant_singleton_get(g_variant_new_string(""));
+}
+
+static GVariant *
+_variant_singleton_get_array_init(GVariant **p_singleton, const char *variant_type)
+{
+    GVariant *v;
+
+    v = g_variant_new_array(G_VARIANT_TYPE(variant_type), NULL, 0);
+    g_variant_ref_sink(v);
+
+    if (G_LIKELY(g_atomic_pointer_compare_and_exchange(p_singleton, NULL, v)))
+        return v;
+
+    g_variant_unref(v);
+
+    return g_atomic_pointer_get(p_singleton);
+}
+
+#define _variant_singleton_get_array(variant_type)                                   \
+    ({                                                                               \
+        static GVariant *_singleton = NULL;                                          \
+        GVariant *       _v;                                                         \
+                                                                                     \
+        _v = g_atomic_pointer_get(&_singleton);                                      \
+        if (G_UNLIKELY(!_v)) {                                                       \
+            _v = _variant_singleton_get_array_init(&_singleton, "" variant_type ""); \
+            nm_assert(_v);                                                           \
+        }                                                                            \
+        nm_assert(g_variant_is_of_type(_v, G_VARIANT_TYPE("a" variant_type "")));    \
+        _v;                                                                          \
+    })
+
+GVariant *
+nm_g_variant_singleton_aLsvI(void)
+{
+    return _variant_singleton_get_array("{sv}");
+}
+
+GVariant *
+nm_g_variant_singleton_aLsaLsvII(void)
+{
+    return _variant_singleton_get_array("{sa{sv}}");
+}
+
+GVariant *
+nm_g_variant_singleton_aaLsvI(void)
+{
+    return _variant_singleton_get_array("a{sv}");
+}
+
+/*****************************************************************************/
+
+GHashTable *
+nm_utils_strdict_clone(GHashTable *src)
+{
+    GHashTable *   dst;
+    GHashTableIter iter;
+    const char *   key;
+    const char *   val;
+
+    if (!src)
+        return NULL;
+
+    dst = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free);
+    g_hash_table_iter_init(&iter, src);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &val))
+        g_hash_table_insert(dst, g_strdup(key), g_strdup(val));
+    return dst;
+}
+
+/* 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)
+{
+    gs_free NMUtilsNamedValue *values_free = NULL;
+    NMUtilsNamedValue          values_prepared[20];
+    const NMUtilsNamedValue *  values;
+    GVariantBuilder            builder;
+    guint                      i;
+    guint                      n;
+
+    values = nm_utils_named_values_from_strdict(strdict, &n, values_prepared, &values_free);
+
+    g_variant_builder_init(&builder, G_VARIANT_TYPE("a{ss}"));
+    for (i = 0; i < n; i++) {
+        g_variant_builder_add(&builder, "{ss}", values[i].name, values[i].value_str);
+    }
+    return g_variant_builder_end(&builder);
+}
+
+/*****************************************************************************/
+
+GVariant *
+nm_utils_strdict_to_variant_asv(GHashTable *strdict)
+{
+    gs_free NMUtilsNamedValue *values_free = NULL;
+    NMUtilsNamedValue          values_prepared[20];
+    const NMUtilsNamedValue *  values;
+    GVariantBuilder            builder;
+    guint                      i;
+    guint                      n;
+
+    values = nm_utils_named_values_from_strdict(strdict, &n, values_prepared, &values_free);
+
+    g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+    for (i = 0; i < n; i++) {
+        g_variant_builder_add(&builder,
+                              "{sv}",
+                              values[i].name,
+                              g_variant_new_string(values[i].value_str));
+    }
+    return g_variant_builder_end(&builder);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_strquote:
+ * @buf: the output buffer of where to write the quoted @str argument.
+ * @buf_len: the size of @buf.
+ * @str: (allow-none): the string to quote.
+ *
+ * Writes @str to @buf with quoting. The resulting buffer
+ * is always NUL terminated, unless @buf_len is zero.
+ * If @str is %NULL, it writes "(null)".
+ *
+ * If @str needs to be truncated, the closing quote is '^' instead
+ * of '"'.
+ *
+ * This is similar to nm_strquote_a(), which however uses alloca()
+ * to allocate a new buffer. Also, here @buf_len is the size of @buf,
+ * while nm_strquote_a() has the number of characters to print. The latter
+ * doesn't include the quoting.
+ *
+ * Returns: the input buffer with the quoted string.
+ */
+const char *
+nm_strquote(char *buf, gsize buf_len, const char *str)
+{
+    const char *const buf0 = buf;
+
+    if (!str) {
+        nm_utils_strbuf_append_str(&buf, &buf_len, "(null)");
+        goto out;
+    }
+
+    if (G_UNLIKELY(buf_len <= 2)) {
+        switch (buf_len) {
+        case 2:
+            *(buf++) = '^';
+            /* fall-through */
+        case 1:
+            *(buf++) = '\0';
+            break;
+        }
+        goto out;
+    }
+
+    *(buf++) = '"';
+    buf_len--;
+
+    nm_utils_strbuf_append_str(&buf, &buf_len, str);
+
+    /* if the string was too long we indicate truncation with a
+     * '^' instead of a closing quote. */
+    if (G_UNLIKELY(buf_len <= 1)) {
+        switch (buf_len) {
+        case 1:
+            buf[-1] = '^';
+            break;
+        case 0:
+            buf[-2] = '^';
+            break;
+        default:
+            nm_assert_not_reached();
+            break;
+        }
+    } else {
+        nm_assert(buf_len >= 2);
+        *(buf++) = '"';
+        *(buf++) = '\0';
+    }
+
+out:
+    return buf0;
+}
+
+/*****************************************************************************/
+
+char _nm_utils_to_string_buffer[];
+
+void
+nm_utils_to_string_buffer_init(char **buf, gsize *len)
+{
+    if (!*buf) {
+        *buf = _nm_utils_to_string_buffer;
+        *len = sizeof(_nm_utils_to_string_buffer);
+    }
+}
+
+gboolean
+nm_utils_to_string_buffer_init_null(gconstpointer obj, char **buf, gsize *len)
+{
+    nm_utils_to_string_buffer_init(buf, len);
+    if (!obj) {
+        g_strlcpy(*buf, "(null)", *len);
+        return FALSE;
+    }
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+const char *
+nm_utils_flags2str(const NMUtilsFlags2StrDesc *descs,
+                   gsize                       n_descs,
+                   unsigned                    flags,
+                   char *                      buf,
+                   gsize                       len)
+{
+    gsize i;
+    char *p;
+
+#if NM_MORE_ASSERTS > 10
+    nm_assert(descs);
+    nm_assert(n_descs > 0);
+    for (i = 0; i < n_descs; i++) {
+        gsize j;
+
+        nm_assert(descs[i].name && descs[i].name[0]);
+        for (j = 0; j < i; j++)
+            nm_assert(descs[j].flag != descs[i].flag);
+    }
+#endif
+
+    nm_utils_to_string_buffer_init(&buf, &len);
+
+    if (!len)
+        return buf;
+
+    buf[0] = '\0';
+    p      = buf;
+    if (!flags) {
+        for (i = 0; i < n_descs; i++) {
+            if (!descs[i].flag) {
+                nm_utils_strbuf_append_str(&p, &len, descs[i].name);
+                break;
+            }
+        }
+        return buf;
+    }
+
+    for (i = 0; flags && i < n_descs; i++) {
+        if (descs[i].flag && NM_FLAGS_ALL(flags, descs[i].flag)) {
+            flags &= ~descs[i].flag;
+
+            if (buf[0] != '\0')
+                nm_utils_strbuf_append_c(&p, &len, ',');
+            nm_utils_strbuf_append_str(&p, &len, descs[i].name);
+        }
+    }
+    if (flags) {
+        if (buf[0] != '\0')
+            nm_utils_strbuf_append_c(&p, &len, ',');
+        nm_utils_strbuf_append(&p, &len, "0x%x", flags);
+    }
+    return buf;
+};
+
+/*****************************************************************************/
+
+/**
+ * _nm_utils_ip4_prefix_to_netmask:
+ * @prefix: a CIDR prefix
+ *
+ * Returns: the netmask represented by the prefix, in network byte order
+ **/
+guint32
+_nm_utils_ip4_prefix_to_netmask(guint32 prefix)
+{
+    return prefix < 32 ? ~htonl(0xFFFFFFFFu >> prefix) : 0xFFFFFFFFu;
+}
+
+/*****************************************************************************/
+
+guint32
+_nm_utils_ip4_get_default_prefix0(in_addr_t ip)
+{
+    /* The function is originally from ipcalc.c of Red Hat's initscripts. */
+    switch (ntohl(ip) >> 24) {
+    case 0 ... 127:
+        return 8; /* Class A */
+    case 128 ... 191:
+        return 16; /* Class B */
+    case 192 ... 223:
+        return 24; /* Class C */
+    }
+    return 0;
+}
+
+guint32
+_nm_utils_ip4_get_default_prefix(in_addr_t ip)
+{
+    return _nm_utils_ip4_get_default_prefix0(ip) ?: 24;
+}
+
+gboolean
+nm_utils_ip_is_site_local(int addr_family, const void *address)
+{
+    in_addr_t addr4;
+
+    switch (addr_family) {
+    case AF_INET:
+        /* RFC1918 private addresses
+         * 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 */
+        addr4 = ntohl(*((const in_addr_t *) address));
+        return (addr4 & 0xff000000) == 0x0a000000 || (addr4 & 0xfff00000) == 0xac100000
+               || (addr4 & 0xffff0000) == 0xc0a80000;
+    case AF_INET6:
+        return IN6_IS_ADDR_SITELOCAL(address);
+    default:
+        g_return_val_if_reached(FALSE);
+    }
+}
+
+/*****************************************************************************/
+
+static gboolean
+_parse_legacy_addr4(const char *text, in_addr_t *out_addr, GError **error)
+{
+    gs_free char * s_free = NULL;
+    struct in_addr a1;
+    guint8         bin[sizeof(a1)];
+    char *         s;
+    int            i;
+
+    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.
+     * That's what "legacy" means here. inet_pton() doesn't accept those.
+     *
+     * But inet_aton() also ignores trailing garbage and formats with fewer than
+     * 4 digits. That is just too crazy and we don't do that. Perform additional checks
+     * and reject some forms that inet_aton() accepted.
+     *
+     * Note that we still should (of course) accept everything that inet_pton()
+     * accepts. However this code never gets called if inet_pton() succeeds
+     * (see below, aside the assertion code). */
+
+    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;
+    }
+
+    s = nm_memdup_maybe_a(300, text, strlen(text) + 1, &s_free);
+
+    for (i = 0; i < G_N_ELEMENTS(bin); i++) {
+        char * current_token = s;
+        gint32 v;
+
+        s = strchr(s, '.');
+        if (s) {
+            s[0] = '\0';
+            s++;
+        }
+
+        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;
+        }
+
+        bin[i] = v;
+    }
+
+    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;
+    }
+
+    *out_addr = a1.s_addr;
+    return TRUE;
+}
+
+gboolean
+nm_utils_parse_inaddr_bin_full(int         addr_family,
+                               gboolean    accept_legacy,
+                               const char *text,
+                               int *       out_addr_family,
+                               gpointer    out_addr)
+{
+    NMIPAddr addrbin;
+
+    g_return_val_if_fail(text, FALSE);
+
+    if (addr_family == AF_UNSPEC) {
+        g_return_val_if_fail(!out_addr || out_addr_family, FALSE);
+        addr_family = strchr(text, ':') ? AF_INET6 : AF_INET;
+    } else
+        g_return_val_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6), FALSE);
+
+    if (inet_pton(addr_family, text, &addrbin) != 1) {
+        if (accept_legacy && addr_family == AF_INET
+            && _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
+            return FALSE;
+    }
+
+#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. */
+        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
+
+    NM_SET_OUT(out_addr_family, addr_family);
+    if (out_addr)
+        nm_ip_addr_set(addr_family, out_addr, &addrbin);
+    return TRUE;
+}
+
+gboolean
+nm_utils_parse_inaddr(int addr_family, const char *text, char **out_addr)
+{
+    NMIPAddr addrbin;
+    char     addrstr_buf[MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN)];
+
+    g_return_val_if_fail(text, FALSE);
+
+    if (addr_family == AF_UNSPEC)
+        addr_family = strchr(text, ':') ? AF_INET6 : AF_INET;
+    else
+        g_return_val_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6), FALSE);
+
+    if (inet_pton(addr_family, text, &addrbin) != 1)
+        return FALSE;
+
+    NM_SET_OUT(out_addr,
+               g_strdup(inet_ntop(addr_family, &addrbin, addrstr_buf, sizeof(addrstr_buf))));
+    return TRUE;
+}
+
+gboolean
+nm_utils_parse_inaddr_prefix_bin(int         addr_family,
+                                 const char *text,
+                                 int *       out_addr_family,
+                                 gpointer    out_addr,
+                                 int *       out_prefix)
+{
+    gs_free char *addrstr_free = NULL;
+    int           prefix       = -1;
+    const char *  slash;
+    const char *  addrstr;
+    NMIPAddr      addrbin;
+
+    g_return_val_if_fail(text, FALSE);
+
+    if (addr_family == AF_UNSPEC) {
+        g_return_val_if_fail(!out_addr || out_addr_family, FALSE);
+        addr_family = strchr(text, ':') ? AF_INET6 : AF_INET;
+    } else
+        g_return_val_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6), FALSE);
+
+    slash = strchr(text, '/');
+    if (slash)
+        addrstr = nm_strndup_a(300, text, slash - text, &addrstr_free);
+    else
+        addrstr = text;
+
+    if (inet_pton(addr_family, addrstr, &addrbin) != 1)
+        return FALSE;
+
+    if (slash) {
+        /* For IPv4, `ip addr add` supports the prefix-length as a netmask. We don't
+         * do that. */
+        prefix =
+            _nm_utils_ascii_str_to_int64(&slash[1], 10, 0, addr_family == AF_INET ? 32 : 128, -1);
+        if (prefix == -1)
+            return FALSE;
+    }
+
+    NM_SET_OUT(out_addr_family, addr_family);
+    if (out_addr)
+        nm_ip_addr_set(addr_family, out_addr, &addrbin);
+    NM_SET_OUT(out_prefix, prefix);
+    return TRUE;
+}
+
+gboolean
+nm_utils_parse_inaddr_prefix(int addr_family, const char *text, char **out_addr, int *out_prefix)
+{
+    NMIPAddr addrbin;
+    char     addrstr_buf[MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN)];
+
+    if (!nm_utils_parse_inaddr_prefix_bin(addr_family, text, &addr_family, &addrbin, out_prefix))
+        return FALSE;
+    NM_SET_OUT(out_addr,
+               g_strdup(inet_ntop(addr_family, &addrbin, addrstr_buf, sizeof(addrstr_buf))));
+    return TRUE;
+}
+
+gboolean
+nm_utils_parse_next_line(const char **inout_ptr,
+                         gsize *      inout_len,
+                         const char **out_line,
+                         gsize *      out_line_len)
+{
+    gboolean    eol_is_carriage_return;
+    const char *line_start;
+    gsize       line_len;
+
+    nm_assert(inout_ptr);
+    nm_assert(inout_len);
+    nm_assert(*inout_len == 0 || *inout_ptr);
+    nm_assert(out_line);
+    nm_assert(out_line_len);
+
+    if (G_UNLIKELY(*inout_len == 0))
+        return FALSE;
+
+    line_start = *inout_ptr;
+
+    eol_is_carriage_return = FALSE;
+    for (line_len = 0;; line_len++) {
+        if (line_len >= *inout_len) {
+            /* if we consumed the entire line, we place the pointer at
+             * one character after the end. */
+            *inout_ptr = &line_start[line_len];
+            *inout_len = 0;
+            goto done;
+        }
+        switch (line_start[line_len]) {
+        case '\r':
+            eol_is_carriage_return = TRUE;
+            /* fall-through*/
+        case '\0':
+        case '\n':
+            *inout_ptr = &line_start[line_len + 1];
+            *inout_len = *inout_len - line_len - 1u;
+            if (eol_is_carriage_return && *inout_len > 0 && (*inout_ptr)[0] == '\n') {
+                /* also consume "\r\n" as one. */
+                (*inout_len)--;
+                (*inout_ptr)++;
+            }
+            goto done;
+        }
+    }
+
+done:
+    *out_line     = line_start;
+    *out_line_len = line_len;
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+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_UTILS_ENUM2STR_DEFINE(nm_icmpv6_router_pref_to_string,
+                         NMIcmpv6RouterPref,
+                         NM_UTILS_ENUM2STR(NM_ICMPV6_ROUTER_PREF_LOW, "low"),
+                         NM_UTILS_ENUM2STR(NM_ICMPV6_ROUTER_PREF_MEDIUM, "medium"),
+                         NM_UTILS_ENUM2STR(NM_ICMPV6_ROUTER_PREF_HIGH, "high"),
+                         NM_UTILS_ENUM2STR(NM_ICMPV6_ROUTER_PREF_INVALID, "invalid"), );
+
+/*****************************************************************************/
+
+/**
+ * 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
+ * can be successfully converted to a number and is within a given
+ * range. On any error, @fallback will be returned and %errno will be set
+ * to a non-zero value. On success, %errno will be set to zero, check %errno
+ * for errors. Any trailing or leading (ascii) white space is ignored and the
+ * functions is locale independent.
+ *
+ * The function is guaranteed to return a value between @min and @max
+ * (inclusive) or @fallback. Also, the parsing is rather strict, it does
+ * not allow for any unrecognized characters, except leading and trailing
+ * white space.
+ **/
+gint64
+_nm_utils_ascii_str_to_int64(const char *str, guint base, gint64 min, gint64 max, gint64 fallback)
+{
+    gint64      v;
+    const char *s = NULL;
+
+    str = nm_str_skip_leading_spaces(str);
+    if (!str || !str[0]) {
+        errno = EINVAL;
+        return fallback;
+    }
+
+    errno = 0;
+    v     = nm_g_ascii_strtoll(str, (char **) &s, base);
+
+    if (errno != 0)
+        return fallback;
+
+    if (s[0] != '\0') {
+        s = nm_str_skip_leading_spaces(s);
+        if (s[0] != '\0') {
+            errno = EINVAL;
+            return fallback;
+        }
+    }
+    if (v > max || v < min) {
+        errno = ERANGE;
+        return fallback;
+    }
+
+    return v;
+}
+
+guint64
+_nm_utils_ascii_str_to_uint64(const char *str,
+                              guint       base,
+                              guint64     min,
+                              guint64     max,
+                              guint64     fallback)
+{
+    guint64     v;
+    const char *s = NULL;
+
+    if (str) {
+        while (g_ascii_isspace(str[0]))
+            str++;
+    }
+    if (!str || !str[0]) {
+        errno = EINVAL;
+        return fallback;
+    }
+
+    errno = 0;
+    v     = nm_g_ascii_strtoull(str, (char **) &s, base);
+
+    if (errno != 0)
+        return fallback;
+    if (s[0] != '\0') {
+        while (g_ascii_isspace(s[0]))
+            s++;
+        if (s[0] != '\0') {
+            errno = EINVAL;
+            return fallback;
+        }
+    }
+    if (v > max || v < min) {
+        errno = ERANGE;
+        return fallback;
+    }
+
+    if (v != 0 && str[0] == '-') {
+        /* As documented, g_ascii_strtoull() accepts negative values, and returns their
+         * absolute value. We don't. */
+        errno = ERANGE;
+        return fallback;
+    }
+
+    return v;
+}
+
+/*****************************************************************************/
+
+gint64
+_nm_utils_ascii_str_to_int64_bin(const char *str,
+                                 gssize      len,
+                                 guint       base,
+                                 gint64      min,
+                                 gint64      max,
+                                 gint64      fallback)
+{
+    gs_free char *str_clone = NULL;
+
+    /* This is like _nm_utils_ascii_str_to_int64(), but the user may provide
+     * an optional string length, in which case str is not assumed to be NUL
+     * terminated. In that case, any NUL characters inside the first len characters
+     * lead to a failure, except one last NUL character is allowed. */
+
+    if (len >= 0) {
+        gsize l = len;
+
+        nm_assert(l == 0 || str);
+
+        if (l > 0 && str[l - 1u] == '\0') {
+            /* we accept one '\0' at the end of the string. */
+            l--;
+        }
+
+        if (l > 0 && memchr(str, '\0', l)) {
+            /* but we don't accept other NUL characters in the middle. */
+            errno = EINVAL;
+            return fallback;
+        }
+
+        str = nm_strndup_a(300, str, len, &str_clone);
+    }
+
+    return _nm_utils_ascii_str_to_int64(str, base, min, max, fallback);
+}
+
+/*****************************************************************************/
+
+int
+nm_strcmp_with_data(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+    const char *s1 = a;
+    const char *s2 = b;
+
+    return strcmp(s1, s2);
+}
+
+/* like nm_strcmp_p(), suitable for g_ptr_array_sort_with_data().
+ * g_ptr_array_sort() just casts nm_strcmp_p() to a function of different
+ * signature. I guess, in glib there are knowledgeable people that ensure
+ * that this additional argument doesn't cause problems due to different ABI
+ * for every architecture that glib supports.
+ * For NetworkManager, we'd rather avoid such stunts.
+ **/
+int
+nm_strcmp_p_with_data(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+    const char *s1 = *((const char **) a);
+    const char *s2 = *((const char **) b);
+
+    return strcmp(s1, s2);
+}
+
+int
+nm_strcmp0_p_with_data(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+    const char *s1 = *((const char **) a);
+    const char *s2 = *((const char **) b);
+
+    return nm_strcmp0(s1, s2);
+}
+
+int
+nm_strcmp_ascii_case_with_data(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+    const char *s1 = a;
+    const char *s2 = b;
+
+    return g_ascii_strcasecmp(s1, s2);
+}
+
+int
+nm_cmp_uint32_p_with_data(gconstpointer p_a, gconstpointer p_b, gpointer user_data)
+{
+    const guint32 a = *((const guint32 *) p_a);
+    const guint32 b = *((const guint32 *) p_b);
+
+    if (a < b)
+        return -1;
+    if (a > b)
+        return 1;
+    return 0;
+}
+
+int
+nm_cmp_int2ptr_p_with_data(gconstpointer p_a, gconstpointer p_b, gpointer user_data)
+{
+    /* p_a and p_b are two pointers to a pointer, where the pointer is
+     * interpreted as a integer using GPOINTER_TO_INT().
+     *
+     * That is the case of a hash-table that uses GINT_TO_POINTER() to
+     * convert integers as pointers, and the resulting keys-as-array
+     * array. */
+    const int a = GPOINTER_TO_INT(*((gconstpointer *) p_a));
+    const int b = GPOINTER_TO_INT(*((gconstpointer *) p_b));
+
+    if (a < b)
+        return -1;
+    if (a > b)
+        return 1;
+    return 0;
+}
+
+/*****************************************************************************/
+
+const char *
+nm_utils_dbus_path_get_last_component(const char *dbus_path)
+{
+    if (dbus_path) {
+        dbus_path = strrchr(dbus_path, '/');
+        if (dbus_path)
+            return dbus_path + 1;
+    }
+    return NULL;
+}
+
+static gint64
+_dbus_path_component_as_num(const char *p)
+{
+    gint64 n;
+
+    /* no odd stuff. No leading zeros, only a non-negative, decimal integer.
+     *
+     * Otherwise, there would be multiple ways to encode the same number "10"
+     * and "010". That is just confusing. A number has no leading zeros,
+     * if it has, it's not a number (as far as we are concerned here). */
+    if (p[0] == '0') {
+        if (p[1] != '\0')
+            return -1;
+        else
+            return 0;
+    }
+    if (!(p[0] >= '1' && p[0] <= '9'))
+        return -1;
+    if (!NM_STRCHAR_ALL(&p[1], ch, (ch >= '0' && ch <= '9')))
+        return -1;
+    n = _nm_utils_ascii_str_to_int64(p, 10, 0, G_MAXINT64, -1);
+    nm_assert(n == -1 || nm_streq0(p, nm_sprintf_bufa(100, "%" G_GINT64_FORMAT, n)));
+    return n;
+}
+
+int
+nm_utils_dbus_path_cmp(const char *dbus_path_a, const char *dbus_path_b)
+{
+    const char *l_a, *l_b;
+    gsize       plen;
+    gint64      n_a, n_b;
+
+    /* compare function for two D-Bus paths. It behaves like
+     * strcmp(), except, if both paths have the same prefix,
+     * and both end in a (positive) number, then the paths
+     * will be sorted by number. */
+
+    NM_CMP_SELF(dbus_path_a, dbus_path_b);
+
+    /* if one or both paths have no slash (and no last component)
+     * compare the full paths directly. */
+    if (!(l_a = nm_utils_dbus_path_get_last_component(dbus_path_a))
+        || !(l_b = nm_utils_dbus_path_get_last_component(dbus_path_b)))
+        goto comp_full;
+
+    /* check if both paths have the same prefix (up to the last-component). */
+    plen = l_a - dbus_path_a;
+    if (plen != (l_b - dbus_path_b))
+        goto comp_full;
+    NM_CMP_RETURN(strncmp(dbus_path_a, dbus_path_b, plen));
+
+    n_a = _dbus_path_component_as_num(l_a);
+    n_b = _dbus_path_component_as_num(l_b);
+    if (n_a == -1 && n_b == -1)
+        goto comp_l;
+
+    /* both components must be convertible to a number. If they are not,
+     * (and only one of them is), then we must always strictly sort numeric parts
+     * after non-numeric components. If we wouldn't, we wouldn't have
+     * a total order.
+     *
+     * An example of a not total ordering would be:
+     *   "8"   < "010"  (numeric)
+     *   "0x"  < "8"    (lexical)
+     *   "0x"  > "010"  (lexical)
+     * We avoid this, by forcing that a non-numeric entry "0x" always sorts
+     * before numeric entries.
+     *
+     * Additionally, _dbus_path_component_as_num() would also reject "010" as
+     * not a valid number.
+     */
+    if (n_a == -1)
+        return -1;
+    if (n_b == -1)
+        return 1;
+
+    NM_CMP_DIRECT(n_a, n_b);
+    nm_assert(nm_streq(dbus_path_a, dbus_path_b));
+    return 0;
+
+comp_full:
+    NM_CMP_DIRECT_STRCMP0(dbus_path_a, dbus_path_b);
+    return 0;
+comp_l:
+    NM_CMP_DIRECT_STRCMP0(l_a, l_b);
+    nm_assert(nm_streq(dbus_path_a, dbus_path_b));
+    return 0;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    union {
+        guint8  table[256];
+        guint64 _dummy_for_alignment;
+    };
+} CharLookupTable;
+
+static void
+_char_lookup_table_set_one(CharLookupTable *lookup, char ch)
+{
+    lookup->table[(guint8) ch] = 1;
+}
+
+static void
+_char_lookup_table_set_all(CharLookupTable *lookup, const char *candidates)
+{
+    while (candidates[0] != '\0')
+        _char_lookup_table_set_one(lookup, (candidates++)[0]);
+}
+
+static void
+_char_lookup_table_init(CharLookupTable *lookup, const char *candidates)
+{
+    *lookup = (CharLookupTable){
+        .table = {0},
+    };
+    if (candidates)
+        _char_lookup_table_set_all(lookup, candidates);
+}
+
+static gboolean
+_char_lookup_has(const CharLookupTable *lookup, char ch)
+{
+    /* with some optimization levels, the compiler thinks this code
+     * might access uninitialized @lookup. It is not -- when you look at the
+     * callers of this function. */
+    NM_PRAGMA_WARNING_DISABLE("-Wmaybe-uninitialized")
+    nm_assert(lookup->table[(guint8) '\0'] == 0);
+    return lookup->table[(guint8) ch] != 0;
+    NM_PRAGMA_WARNING_REENABLE
+}
+
+static gboolean
+_char_lookup_has_all(const CharLookupTable *lookup, 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.
+ * @delimiters: the set of delimiters.
+ * @flags: additional flags for controlling the operation.
+ *
+ * This is a replacement for g_strsplit_set() which avoids copying
+ * each word once (the entire strv array), but instead copies it once
+ * and all words point into that internal copy.
+ *
+ * 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 "".
+ * 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
+ *   is not set, it also returns %NULL.
+ *   Otherwise, a %NULL terminated strv array containing the split words.
+ *   (delimiter characters are removed).
+ *   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 individual strings in-place,
+ *   like "g_strstrip((char *) iter[0])".
+ */
+const char **
+nm_utils_strsplit_set_full(const char *str, const char *delimiters, NMUtilsStrsplitSetFlags flags)
+{
+    const char **   ptr;
+    gsize           num_tokens;
+    gsize           i_token;
+    gsize           str_len_p1;
+    const char *    c_str;
+    char *          s;
+    CharLookupTable ch_lookup;
+    const gboolean  f_escaped = NM_FLAGS_HAS(flags, NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED);
+    const gboolean  f_allow_escaping =
+        f_escaped || NM_FLAGS_HAS(flags, NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING);
+    const gboolean f_preserve_empty =
+        NM_FLAGS_HAS(flags, NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY);
+    const gboolean f_strstrip = NM_FLAGS_HAS(flags, NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP);
+
+    if (!str)
+        return NULL;
+
+    if (!delimiters) {
+        nm_assert_not_reached();
+        delimiters = " \t\n";
+    }
+    _char_lookup_table_init(&ch_lookup, delimiters);
+
+    nm_assert(!f_allow_escaping || !_char_lookup_has(&ch_lookup, '\\'));
+
+    if (!f_preserve_empty) {
+        while (_char_lookup_has(&ch_lookup, str[0]))
+            str++;
+    }
+
+    if (!str[0]) {
+        /* We return %NULL here, also with NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY.
+         * That makes nm_utils_strsplit_set_full() with NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY
+         * different from g_strsplit_set(), which would in this case return an empty array.
+         * If you need to handle %NULL, and "" specially, then check the input string first. */
+        return NULL;
+    }
+
+#define _char_is_escaped(str_start, str_cur)              \
+    ({                                                    \
+        const char *const _str_start = (str_start);       \
+        const char *const _str_cur   = (str_cur);         \
+        const char *      _str_i     = (_str_cur);        \
+                                                          \
+        while (_str_i > _str_start && _str_i[-1] == '\\') \
+            _str_i--;                                     \
+        (((_str_cur - _str_i) % 2) != 0);                 \
+    })
+
+    num_tokens = 1;
+    c_str      = str;
+    while (TRUE) {
+        while (G_LIKELY(!_char_lookup_has(&ch_lookup, c_str[0]))) {
+            if (c_str[0] == '\0')
+                goto done1;
+            c_str++;
+        }
+
+        /* we assume escapings are not frequent. After we found
+         * this delimiter, check whether it was escaped by counting
+         * the backslashed before. */
+        if (f_allow_escaping && _char_is_escaped(str, c_str)) {
+            /* the delimiter is escaped. This was not an accepted delimiter. */
+            c_str++;
+            continue;
+        }
+
+        c_str++;
+
+        /* if we drop empty tokens, then we now skip over all consecutive delimiters. */
+        if (!f_preserve_empty) {
+            while (_char_lookup_has(&ch_lookup, c_str[0]))
+                c_str++;
+            if (c_str[0] == '\0')
+                break;
+        }
+
+        num_tokens++;
+    }
+
+done1:
+
+    nm_assert(c_str[0] == '\0');
+
+    str_len_p1 = (c_str - str) + 1;
+
+    nm_assert(str[str_len_p1 - 1] == '\0');
+
+    ptr = g_malloc((sizeof(const char *) * (num_tokens + 1)) + str_len_p1);
+    s   = (char *) &ptr[num_tokens + 1];
+    memcpy(s, str, str_len_p1);
+
+    i_token = 0;
+
+    while (TRUE) {
+        nm_assert(i_token < num_tokens);
+        ptr[i_token++] = s;
+
+        if (s[0] == '\0') {
+            nm_assert(f_preserve_empty);
+            goto done2;
+        }
+        nm_assert(f_preserve_empty || !_char_lookup_has(&ch_lookup, s[0]));
+
+        while (!_char_lookup_has(&ch_lookup, s[0])) {
+            if (G_UNLIKELY(s[0] == '\\' && f_allow_escaping)) {
+                s++;
+                if (s[0] == '\0')
+                    goto done2;
+                s++;
+            } else if (s[0] == '\0')
+                goto done2;
+            else
+                s++;
+        }
+
+        nm_assert(_char_lookup_has(&ch_lookup, s[0]));
+        s[0] = '\0';
+        s++;
+
+        if (!f_preserve_empty) {
+            while (_char_lookup_has(&ch_lookup, s[0]))
+                s++;
+            if (s[0] == '\0')
+                goto done2;
+        }
+    }
+
+done2:
+    nm_assert(i_token == num_tokens);
+    ptr[i_token] = NULL;
+
+    if (f_strstrip) {
+        gsize i;
+
+        i_token = 0;
+        for (i = 0; ptr[i]; i++) {
+            s = (char *) nm_str_skip_leading_spaces(ptr[i]);
+            if (s[0] != '\0') {
+                char *s_last;
+
+                s_last = &s[strlen(s) - 1];
+                while (s_last > s && g_ascii_isspace(s_last[0])
+                       && (!f_allow_escaping || !_char_is_escaped(s, s_last)))
+                    (s_last--)[0] = '\0';
+            }
+
+            if (!f_preserve_empty && s[0] == '\0')
+                continue;
+
+            ptr[i_token++] = s;
+        }
+
+        if (i_token == 0) {
+            g_free(ptr);
+            return NULL;
+        }
+        ptr[i_token] = NULL;
+    }
+
+    if (f_escaped) {
+        gsize i, j;
+
+        /* We no longer need ch_lookup for its original purpose. Modify it, so it
+         * can detect the delimiters, '\\', and (optionally) whitespaces. */
+        _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];
+            j = 0;
+            for (i = 0; s[i] != '\0';) {
+                if (s[i] == '\\' && _char_lookup_has(&ch_lookup, s[i + 1]))
+                    i++;
+                s[j++] = s[i++];
+            }
+            s[j] = '\0';
+        }
+    }
+
+    nm_assert(ptr && ptr[0]);
+    return ptr;
+}
+
+/*****************************************************************************/
+
+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)
+{
+    CharLookupTable ch_lookup;
+    CharLookupTable ch_lookup_as_needed;
+    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;
+
+    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);
+    }
+
+    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++;
+
+    if (n_escapes == 0u) {
+        *out_to_free = NULL;
+        return str;
+    }
+
+    alloc_len = str_len + n_escapes + 1u;
+    ret       = g_new(char, alloc_len);
+
+    j = 0;
+    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++] = '\\';
+        ret[j++] = str[i];
+    }
+    if (escape_trailing_space) {
+        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++;
+    }
+
+    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;
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_utils_strsplit_quoted:
+ * @str: the string to split (e.g. from /proc/cmdline).
+ *
+ * This basically does that systemd's extract_first_word() does
+ * with the flags "EXTRACT_UNQUOTE | EXTRACT_RELAX". This is what
+ * systemd uses to parse /proc/cmdline, and we do too.
+ *
+ * Splits the string. We have nm_utils_strsplit_set() which
+ * supports a variety of flags. However, extending that already
+ * complex code to also support quotation and escaping is hard.
+ * Instead, add a naive implementation.
+ *
+ * Returns: (transfer full): the split string.
+ */
+char **
+nm_utils_strsplit_quoted(const char *str)
+{
+    gs_unref_ptrarray GPtrArray *arr     = NULL;
+    gs_free char *               str_out = NULL;
+    CharLookupTable              ch_lookup;
+
+    nm_assert(str);
+
+    _char_lookup_table_init(&ch_lookup, NM_ASCII_WHITESPACES);
+
+    for (;;) {
+        char  quote;
+        gsize j;
+
+        while (_char_lookup_has(&ch_lookup, str[0]))
+            str++;
+
+        if (str[0] == '\0')
+            break;
+
+        if (!str_out)
+            str_out = g_new(char, strlen(str) + 1);
+
+        quote = '\0';
+        j     = 0;
+        for (;;) {
+            if (str[0] == '\\') {
+                str++;
+                if (str[0] == '\0')
+                    break;
+                str_out[j++] = str[0];
+                str++;
+                continue;
+            }
+            if (quote) {
+                if (str[0] == '\0')
+                    break;
+                if (str[0] == quote) {
+                    quote = '\0';
+                    str++;
+                    continue;
+                }
+                str_out[j++] = str[0];
+                str++;
+                continue;
+            }
+            if (str[0] == '\0')
+                break;
+            if (NM_IN_SET(str[0], '\'', '"')) {
+                quote = str[0];
+                str++;
+                continue;
+            }
+            if (_char_lookup_has(&ch_lookup, str[0])) {
+                str++;
+                break;
+            }
+            str_out[j++] = str[0];
+            str++;
+        }
+
+        if (!arr)
+            arr = g_ptr_array_new();
+        g_ptr_array_add(arr, g_strndup(str_out, j));
+    }
+
+    if (!arr)
+        return g_new0(char *, 1);
+
+    g_ptr_array_add(arr, NULL);
+
+    /* We want to return an optimally sized strv array, with no excess
+     * memory allocated. Hence, clone once more. */
+    return nm_memdup(arr->pdata, sizeof(char *) * arr->len);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_utils_strv_find_first:
+ * @list: the strv list to search
+ * @len: the length of the list, or a negative value if @list is %NULL terminated.
+ * @needle: the value to search for. The search is done using strcmp().
+ *
+ * Searches @list for @needle and returns the index of the first match (based
+ * on strcmp()).
+ *
+ * For convenience, @list has type 'char**' instead of 'const char **'.
+ *
+ * Returns: index of first occurrence or -1 if @needle is not found in @list.
+ */
+gssize
+nm_utils_strv_find_first(char **list, gssize len, const char *needle)
+{
+    gssize i;
+
+    if (len > 0) {
+        g_return_val_if_fail(list, -1);
+
+        if (!needle) {
+            /* if we search a list with known length, %NULL is a valid @needle. */
+            for (i = 0; i < len; i++) {
+                if (!list[i])
+                    return i;
+            }
+        } else {
+            for (i = 0; i < len; i++) {
+                if (list[i] && !strcmp(needle, list[i]))
+                    return i;
+            }
+        }
+    } else if (len < 0) {
+        g_return_val_if_fail(needle, -1);
+
+        if (list) {
+            for (i = 0; list[i]; i++) {
+                if (strcmp(needle, list[i]) == 0)
+                    return i;
+            }
+        }
+    }
+    return -1;
+}
+
+gboolean
+nm_strv_has_duplicate(const char *const *strv, gssize len, gboolean is_sorted)
+{
+    gsize l;
+    gsize i;
+    gsize j;
+
+    l = len < 0 ? NM_PTRARRAY_LEN(strv) : (gsize) len;
+
+    if (is_sorted) {
+#if NM_MORE_ASSERTS > 10
+        for (i = 1; i < l; i++)
+            nm_assert(nm_strcmp0(strv[i - 1], strv[i]) <= 0);
+#endif
+        for (i = 1; i < l; i++) {
+            if (nm_streq0(strv[i - 1], strv[i]))
+                return TRUE;
+        }
+    } else {
+        for (i = 1; i < l; i++) {
+            for (j = 0; j < i; j++) {
+                if (nm_streq0(strv[j], strv[i]))
+                    return TRUE;
+            }
+        }
+    }
+
+    return FALSE;
+}
+
+char **
+_nm_utils_strv_cleanup(char **  strv,
+                       gboolean strip_whitespace,
+                       gboolean skip_empty,
+                       gboolean skip_repeated)
+{
+    guint i, j;
+
+    if (!strv || !*strv)
+        return strv;
+
+    if (strip_whitespace) {
+        /* we only modify the strings pointed to by @strv if @strip_whitespace is
+         * requested. Otherwise, the strings themselves are untouched. */
+        for (i = 0; strv[i]; i++)
+            g_strstrip(strv[i]);
+    }
+    if (!skip_empty && !skip_repeated)
+        return strv;
+    j = 0;
+    for (i = 0; strv[i]; i++) {
+        if ((skip_empty && !*strv[i])
+            || (skip_repeated && nm_utils_strv_find_first(strv, j, strv[i]) >= 0))
+            g_free(strv[i]);
+        else
+            strv[j++] = strv[i];
+    }
+    strv[j] = NULL;
+    return strv;
+}
+
+/*****************************************************************************/
+
+GPtrArray *
+_nm_g_ptr_array_copy(GPtrArray *    array,
+                     GCopyFunc      func,
+                     gpointer       user_data,
+                     GDestroyNotify element_free_func)
+{
+    GPtrArray *new_array;
+    guint      i;
+
+    g_return_val_if_fail(array, NULL);
+
+    new_array = g_ptr_array_new_full(array->len, element_free_func);
+    for (i = 0; i < array->len; i++) {
+        g_ptr_array_add(new_array, func ? func(array->pdata[i], user_data) : array->pdata[i]);
+    }
+    return new_array;
+}
+
+/*****************************************************************************/
+
+int
+_nm_utils_ascii_str_to_bool(const char *str, int default_value)
+{
+    gs_free char *str_free = NULL;
+
+    if (!str)
+        return default_value;
+
+    str = nm_strstrip_avoid_copy_a(300, str, &str_free);
+    if (str[0] == '\0')
+        return default_value;
+
+    if (!g_ascii_strcasecmp(str, "true") || !g_ascii_strcasecmp(str, "yes")
+        || !g_ascii_strcasecmp(str, "on") || !g_ascii_strcasecmp(str, "1"))
+        return TRUE;
+
+    if (!g_ascii_strcasecmp(str, "false") || !g_ascii_strcasecmp(str, "no")
+        || !g_ascii_strcasecmp(str, "off") || !g_ascii_strcasecmp(str, "0"))
+        return FALSE;
+
+    return default_value;
+}
+
+/*****************************************************************************/
+
+NM_CACHED_QUARK_FCN("nm-manager-error-quark", nm_manager_error_quark);
+
+NM_CACHED_QUARK_FCN("nm-utils-error-quark", nm_utils_error_quark);
+
+void
+nm_utils_error_set_cancelled(GError **error, gboolean is_disposing, const char *instance_name)
+{
+    if (is_disposing) {
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_CANCELLED_DISPOSING,
+                    "Disposing %s instance",
+                    instance_name && *instance_name ? instance_name : "source");
+    } else {
+        g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Request cancelled");
+    }
+}
+
+gboolean
+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 (error->domain == NM_UTILS_ERROR)
+            return NM_IN_SET(error->code, NM_UTILS_ERROR_CANCELLED_DISPOSING);
+    }
+    return FALSE;
+}
+
+gboolean
+nm_utils_error_is_notfound(GError *error)
+{
+    if (error) {
+        if (error->domain == G_IO_ERROR)
+            return NM_IN_SET(error->code, G_IO_ERROR_NOT_FOUND);
+        if (error->domain == G_FILE_ERROR)
+            return NM_IN_SET(error->code, G_FILE_ERROR_NOENT);
+    }
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_g_object_set_property:
+ * @object: the target object
+ * @property_name: the property name
+ * @value: the #GValue to set
+ * @error: (allow-none): optional error argument
+ *
+ * A reimplementation of g_object_set_property(), but instead
+ * returning an error instead of logging a warning. All g_object_set*()
+ * versions in glib require you to not pass invalid types or they will
+ * log a g_warning() -- without reporting an error. We don't want that,
+ * so we need to hack error checking around it.
+ *
+ * Returns: whether the value was successfully set.
+ */
+gboolean
+nm_g_object_set_property(GObject *     object,
+                         const char *  property_name,
+                         const GValue *value,
+                         GError **     error)
+{
+    GParamSpec *                pspec;
+    nm_auto_unset_gvalue GValue tmp_value = G_VALUE_INIT;
+    GObjectClass *              klass;
+
+    g_return_val_if_fail(G_IS_OBJECT(object), FALSE);
+    g_return_val_if_fail(property_name != NULL, FALSE);
+    g_return_val_if_fail(G_IS_VALUE(value), FALSE);
+    g_return_val_if_fail(!error || !*error, FALSE);
+
+    /* g_object_class_find_property() does g_param_spec_get_redirect_target(),
+     * where we differ from a plain g_object_set_property(). */
+    pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(object), property_name);
+
+    if (!pspec) {
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    _("object class '%s' has no property named '%s'"),
+                    G_OBJECT_TYPE_NAME(object),
+                    property_name);
+        return FALSE;
+    }
+    if (!(pspec->flags & G_PARAM_WRITABLE)) {
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    _("property '%s' of object class '%s' is not writable"),
+                    pspec->name,
+                    G_OBJECT_TYPE_NAME(object));
+        return FALSE;
+    }
+    if ((pspec->flags & G_PARAM_CONSTRUCT_ONLY)) {
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    _("construct property \"%s\" for object '%s' can't be set after construction"),
+                    pspec->name,
+                    G_OBJECT_TYPE_NAME(object));
+        return FALSE;
+    }
+
+    klass = g_type_class_peek(pspec->owner_type);
+    if (klass == NULL) {
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    _("'%s::%s' is not a valid property name; '%s' is not a GObject subtype"),
+                    g_type_name(pspec->owner_type),
+                    pspec->name,
+                    g_type_name(pspec->owner_type));
+        return FALSE;
+    }
+
+    /* provide a copy to work from, convert (if necessary) and validate */
+    g_value_init(&tmp_value, pspec->value_type);
+    if (!g_value_transform(value, &tmp_value)) {
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    _("unable to set property '%s' of type '%s' from value of type '%s'"),
+                    pspec->name,
+                    g_type_name(pspec->value_type),
+                    G_VALUE_TYPE_NAME(value));
+        return FALSE;
+    }
+    if (g_param_value_validate(pspec, &tmp_value) && !(pspec->flags & G_PARAM_LAX_VALIDATION)) {
+        gs_free char *contents = g_strdup_value_contents(value);
+
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    _("value \"%s\" of type '%s' is invalid or out of range for property '%s' of "
+                      "type '%s'"),
+                    contents,
+                    G_VALUE_TYPE_NAME(value),
+                    pspec->name,
+                    g_type_name(pspec->value_type));
+        return FALSE;
+    }
+
+    g_object_set_property(object, property_name, &tmp_value);
+    return TRUE;
+}
+
+#define _set_property(object, property_name, gtype, gtype_set, value, error)          \
+    G_STMT_START                                                                      \
+    {                                                                                 \
+        nm_auto_unset_gvalue GValue gvalue = {0};                                     \
+                                                                                      \
+        g_value_init(&gvalue, gtype);                                                 \
+        gtype_set(&gvalue, (value));                                                  \
+        return nm_g_object_set_property((object), (property_name), &gvalue, (error)); \
+    }                                                                                 \
+    G_STMT_END
+
+gboolean
+nm_g_object_set_property_string(GObject *   object,
+                                const char *property_name,
+                                const char *value,
+                                GError **   error)
+{
+    _set_property(object, property_name, G_TYPE_STRING, g_value_set_string, value, error);
+}
+
+gboolean
+nm_g_object_set_property_string_static(GObject *   object,
+                                       const char *property_name,
+                                       const char *value,
+                                       GError **   error)
+{
+    _set_property(object, property_name, G_TYPE_STRING, g_value_set_static_string, value, error);
+}
+
+gboolean
+nm_g_object_set_property_string_take(GObject *   object,
+                                     const char *property_name,
+                                     char *      value,
+                                     GError **   error)
+{
+    _set_property(object, property_name, G_TYPE_STRING, g_value_take_string, value, error);
+}
+
+gboolean
+nm_g_object_set_property_boolean(GObject *   object,
+                                 const char *property_name,
+                                 gboolean    value,
+                                 GError **   error)
+{
+    _set_property(object, property_name, G_TYPE_BOOLEAN, g_value_set_boolean, !!value, error);
+}
+
+gboolean
+nm_g_object_set_property_char(GObject *   object,
+                              const char *property_name,
+                              gint8       value,
+                              GError **   error)
+{
+    /* glib says about G_TYPE_CHAR:
+     *
+     * The type designated by G_TYPE_CHAR is unconditionally an 8-bit signed integer.
+     *
+     * This is always a (signed!) char. */
+    _set_property(object, property_name, G_TYPE_CHAR, g_value_set_schar, value, error);
+}
+
+gboolean
+nm_g_object_set_property_uchar(GObject *   object,
+                               const char *property_name,
+                               guint8      value,
+                               GError **   error)
+{
+    _set_property(object, property_name, G_TYPE_UCHAR, g_value_set_uchar, value, error);
+}
+
+gboolean
+nm_g_object_set_property_int(GObject *object, const char *property_name, int value, GError **error)
+{
+    _set_property(object, property_name, G_TYPE_INT, g_value_set_int, value, error);
+}
+
+gboolean
+nm_g_object_set_property_int64(GObject *   object,
+                               const char *property_name,
+                               gint64      value,
+                               GError **   error)
+{
+    _set_property(object, property_name, G_TYPE_INT64, g_value_set_int64, value, error);
+}
+
+gboolean
+nm_g_object_set_property_uint(GObject *   object,
+                              const char *property_name,
+                              guint       value,
+                              GError **   error)
+{
+    _set_property(object, property_name, G_TYPE_UINT, g_value_set_uint, value, error);
+}
+
+gboolean
+nm_g_object_set_property_uint64(GObject *   object,
+                                const char *property_name,
+                                guint64     value,
+                                GError **   error)
+{
+    _set_property(object, property_name, G_TYPE_UINT64, g_value_set_uint64, value, error);
+}
+
+gboolean
+nm_g_object_set_property_flags(GObject *   object,
+                               const char *property_name,
+                               GType       gtype,
+                               guint       value,
+                               GError **   error)
+{
+    nm_assert(({
+        nm_auto_unref_gtypeclass GTypeClass *gtypeclass = g_type_class_ref(gtype);
+        G_IS_FLAGS_CLASS(gtypeclass);
+    }));
+    _set_property(object, property_name, gtype, g_value_set_flags, value, error);
+}
+
+gboolean
+nm_g_object_set_property_enum(GObject *   object,
+                              const char *property_name,
+                              GType       gtype,
+                              int         value,
+                              GError **   error)
+{
+    nm_assert(({
+        nm_auto_unref_gtypeclass GTypeClass *gtypeclass = g_type_class_ref(gtype);
+        G_IS_ENUM_CLASS(gtypeclass);
+    }));
+    _set_property(object, property_name, gtype, g_value_set_enum, value, error);
+}
+
+GParamSpec *
+nm_g_object_class_find_property_from_gtype(GType gtype, const char *property_name)
+{
+    nm_auto_unref_gtypeclass GObjectClass *gclass = NULL;
+
+    gclass = g_type_class_ref(gtype);
+    return g_object_class_find_property(gclass, property_name);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_g_type_find_implementing_class_for_property:
+ * @gtype: the GObject type which has a property @pname
+ * @pname: the name of the property to look up
+ *
+ * This is only a helper function for printf debugging. It's not
+ * used in actual code. Hence, the function just asserts that
+ * @pname and @gtype arguments are suitable. It cannot fail.
+ *
+ * Returns: the most ancestor type of @gtype, that
+ *   implements the property @pname. It means, it
+ *   searches the type hierarchy to find the type
+ *   that added @pname.
+ */
+GType
+nm_g_type_find_implementing_class_for_property(GType gtype, const char *pname)
+{
+    nm_auto_unref_gtypeclass GObjectClass *klass = NULL;
+    GParamSpec *                           pspec;
+
+    g_return_val_if_fail(pname, G_TYPE_INVALID);
+
+    klass = g_type_class_ref(gtype);
+    g_return_val_if_fail(G_IS_OBJECT_CLASS(klass), G_TYPE_INVALID);
+
+    pspec = g_object_class_find_property(klass, pname);
+    g_return_val_if_fail(pspec, G_TYPE_INVALID);
+
+    gtype = G_TYPE_FROM_CLASS(klass);
+
+    while (TRUE) {
+        nm_auto_unref_gtypeclass GObjectClass *k = NULL;
+
+        k = g_type_class_ref(g_type_parent(gtype));
+
+        g_return_val_if_fail(G_IS_OBJECT_CLASS(k), G_TYPE_INVALID);
+
+        if (g_object_class_find_property(k, pname) != pspec)
+            return gtype;
+
+        gtype = G_TYPE_FROM_CLASS(k);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+_str_buf_append_c_escape_octal(NMStrBuf *strbuf, char ch)
+{
+    nm_str_buf_append_c(strbuf,
+                        '\\',
+                        '0' + ((char) ((((guchar) ch) >> 6) & 07)),
+                        '0' + ((char) ((((guchar) ch) >> 3) & 07)),
+                        '0' + ((char) ((((guchar) ch)) & 07)));
+}
+
+/**
+ * nm_utils_buf_utf8safe_unescape:
+ * @str: (allow-none): the string to unescape. The string itself is a NUL terminated
+ *   ASCII string, that can have C-style backslash escape sequences (which
+ *   are to be unescaped). Non-ASCII characters (e.g. UTF-8) are taken verbatim, so
+ *   it doesn't care that this string is UTF-8. However, usually this is a UTF-8 encoded
+ *   string.
+ * @flags: flags for unescaping. The following flags are supported.
+ *   %NM_UTILS_STR_UTF8_SAFE_UNESCAPE_STRIP_SPACES performs a g_strstrip() on the input string,
+ *   but preserving escaped spaces. For example, "a\\t " gives "a\t" (that is, the escaped space does
+ *   not get stripped). Likewise, the invalid escape sequence "a\\  " results in "a " (stripping
+ *   the unescaped space, but preserving the escaped one).
+ * @out_len: (out): the length of the parsed string.
+ * @to_free: (out): if @str requires unescaping, the function will clone the string. In
+ *   that case, the allocated buffer will be returned here.
+ *
+ * See C-style escapes at https://en.wikipedia.org/wiki/Escape_sequences_in_C#Table_of_escape_sequences.
+ * Note that hex escapes ("\\xhh") and unicode escapes ("\\uhhhh", "\\Uhhhhhhhh") are not supported.
+ *
+ * Also, this function is very similar to g_strcompress() but without issuing g_warning()
+ * assertions and proper handling of "\\000" escape sequences.
+ *
+ * Invalid escape sequences (or non-UTF-8 input) are gracefully accepted. For example "\\ "
+ * is an invalid escape sequence, in this case the backslash is removed and " " gets returned.
+ *
+ * The function never leaks secrets in memory.
+ *
+ * Returns: the unescaped buffer of length @out_len. If @str is %NULL, this returns %NULL
+ *   and sets @out_len to 0. Otherwise, a non-%NULL binary buffer is returned with
+ *   @out_len bytes. Note that the binary buffer is guaranteed to be NUL terminated. That
+ *   is @result[@out_len] is NUL.
+ *   Note that the result is binary, and may have embedded NUL characters and non-UTF-8.
+ *   If the function can avoid cloning the input string, it will return a pointer inside
+ *   the input @str. For example, if there is no backslash, no cloning is necessary. In that
+ *   case, @to_free will be %NULL. Otherwise, @to_free is set to a newly allocated buffer
+ *   containing the unescaped string and returned.
+ */
+gconstpointer
+nm_utils_buf_utf8safe_unescape(const char *            str,
+                               NMUtilsStrUtf8SafeFlags flags,
+                               gsize *                 out_len,
+                               gpointer *              to_free)
+{
+    gboolean    strip_spaces = NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_UNESCAPE_STRIP_SPACES);
+    NMStrBuf    strbuf;
+    const char *s;
+    gsize       len;
+
+    g_return_val_if_fail(to_free, NULL);
+    g_return_val_if_fail(out_len, NULL);
+
+    if (!str) {
+        *out_len = 0;
+        *to_free = NULL;
+        return NULL;
+    }
+
+    if (strip_spaces)
+        str = nm_str_skip_leading_spaces(str);
+
+    len = strlen(str);
+
+    s = memchr(str, '\\', len);
+    if (!s) {
+        if (strip_spaces && len > 0 && g_ascii_isspace(str[len - 1])) {
+            len--;
+            while (len > 0 && g_ascii_isspace(str[len - 1]))
+                len--;
+            *out_len = len;
+            return (*to_free = g_strndup(str, len));
+        }
+        *out_len = len;
+        *to_free = NULL;
+        return str;
+    }
+
+    nm_str_buf_init(&strbuf, len + 1u, FALSE);
+
+    nm_str_buf_append_len(&strbuf, str, s - str);
+    str = s;
+
+    for (;;) {
+        char  ch;
+        guint v;
+
+        nm_assert(str[0] == '\\');
+
+        ch = (++str)[0];
+
+        if (ch == '\0') {
+            /* error. Trailing '\\' */
+            break;
+        }
+
+        if (ch >= '0' && ch <= '9') {
+            v  = ch - '0';
+            ch = (++str)[0];
+            if (ch >= '0' && ch <= '7') {
+                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;
+                }
+            }
+            ch = v;
+        } else {
+            switch (ch) {
+            case 'b':
+                ch = '\b';
+                break;
+            case 'f':
+                ch = '\f';
+                break;
+            case 'n':
+                ch = '\n';
+                break;
+            case 'r':
+                ch = '\r';
+                break;
+            case 't':
+                ch = '\t';
+                break;
+            case 'v':
+                ch = '\v';
+                break;
+            default:
+                /* Here we handle "\\\\", but all other unexpected escape sequences are really a bug.
+                 * Take them literally, after removing the escape character */
+                break;
+            }
+            str++;
+        }
+
+        nm_str_buf_append_c(&strbuf, ch);
+
+        s = strchr(str, '\\');
+        if (!s) {
+            gsize l = strlen(str);
+
+            if (strip_spaces) {
+                while (l > 0 && g_ascii_isspace(str[l - 1]))
+                    l--;
+            }
+            nm_str_buf_append_len(&strbuf, str, l);
+            break;
+        }
+
+        nm_str_buf_append_len(&strbuf, str, s - str);
+        str = s;
+    }
+
+    /* assert that no reallocation was necessary. For one, unescaping should
+     * never result in a longer string than the input. Also, when unescaping
+     * secrets, we want to ensure that we don't leak secrets in memory. */
+    nm_assert(strbuf.allocated == len + 1u);
+
+    return (*to_free = nm_str_buf_finalize(&strbuf, out_len));
+}
+
+/**
+ * nm_utils_buf_utf8safe_escape:
+ * @buf: byte array, possibly in utf-8 encoding, may have NUL characters.
+ * @buflen: the length of @buf in bytes, or -1 if @buf is a NUL terminated
+ *   string. Note that if @buflen is zero, then the function returns NULL.
+ *   If @buflen is negative, then the function returns NULL if @buf is NULL
+ *   and @buf if @buf is "".
+ * @flags: #NMUtilsStrUtf8SafeFlags flags
+ * @to_free: (out): return the pointer location of the string
+ *   if a copying was necessary.
+ *
+ * Based on the assumption, that @buf contains UTF-8 encoded bytes,
+ * this will return valid UTF-8 sequence, and invalid sequences
+ * will be escaped with backslash (C escaping, like g_strescape()).
+ * This is sanitize non UTF-8 characters. The result is valid
+ * UTF-8.
+ *
+ * The operation can be reverted with nm_utils_buf_utf8safe_unescape().
+ * Note that if, and only if @buf contains no NUL bytes, the operation
+ * can also be reverted with g_strcompress().
+ *
+ * Depending on @flags, valid UTF-8 characters are not escaped at all
+ * (except the escape character '\\'). This is the difference to g_strescape(),
+ * which escapes all non-ASCII characters. This allows to pass on
+ * valid UTF-8 characters as-is and can be directly shown to the user
+ * as UTF-8 -- with exception of the backslash escape character,
+ * invalid UTF-8 sequences, and other (depending on @flags).
+ *
+ * Returns: the escaped input buffer, as valid UTF-8. If no escaping
+ *   is necessary, it returns the input @buf. Otherwise, an allocated
+ *   string @to_free is returned which must be freed by the caller
+ *   with g_free. The escaping can be reverted by g_strcompress().
+ *   There are cases where this function can return NULL:
+ *   - if @buflen is 0
+ *   - if @buflen is negative and @buf is NULL.
+ **/
+const char *
+nm_utils_buf_utf8safe_escape(gconstpointer           buf,
+                             gssize                  buflen,
+                             NMUtilsStrUtf8SafeFlags flags,
+                             char **                 to_free)
+{
+    const char *const str = buf;
+    const char *      p   = NULL;
+    const char *      s;
+    gboolean          nul_terminated = FALSE;
+    NMStrBuf          strbuf;
+
+    g_return_val_if_fail(to_free, NULL);
+
+    *to_free = NULL;
+
+    if (buflen == 0)
+        return NULL;
+
+    if (buflen < 0) {
+        if (!str)
+            return NULL;
+        buflen = strlen(str);
+        if (buflen == 0)
+            return str;
+        nul_terminated = TRUE;
+    }
+
+    if (g_utf8_validate(str, buflen, &p) && nul_terminated) {
+        /* note that g_utf8_validate() does not allow NUL character inside @str. Good.
+         * We can treat @str like a NUL terminated string. */
+        if (!NM_STRCHAR_ANY(str,
+                            ch,
+                            (ch == '\\'
+                             || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL)
+                                 && nm_ascii_is_ctrl_or_del(ch))
+                             || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII)
+                                 && nm_ascii_is_non_ascii(ch)))))
+            return str;
+    }
+
+    nm_str_buf_init(&strbuf, buflen + 5, NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_SECRET));
+
+    s = str;
+    do {
+        buflen -= p - s;
+        nm_assert(buflen >= 0);
+
+        for (; s < p; s++) {
+            char ch = s[0];
+
+            nm_assert(ch);
+            if (ch == '\\')
+                nm_str_buf_append_c(&strbuf, '\\', '\\');
+            else if ((NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL)
+                      && nm_ascii_is_ctrl_or_del(ch))
+                     || (NM_FLAGS_HAS(flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII)
+                         && nm_ascii_is_non_ascii(ch)))
+                _str_buf_append_c_escape_octal(&strbuf, ch);
+            else
+                nm_str_buf_append_c(&strbuf, ch);
+        }
+
+        if (buflen <= 0)
+            break;
+
+        _str_buf_append_c_escape_octal(&strbuf, p[0]);
+
+        buflen--;
+        if (buflen == 0)
+            break;
+
+        s = &p[1];
+        (void) g_utf8_validate(s, buflen, &p);
+    } while (TRUE);
+
+    return (*to_free = nm_str_buf_finalize(&strbuf, NULL));
+}
+
+const char *
+nm_utils_buf_utf8safe_escape_bytes(GBytes *bytes, NMUtilsStrUtf8SafeFlags flags, char **to_free)
+{
+    gconstpointer p;
+    gsize         l;
+
+    if (bytes)
+        p = g_bytes_get_data(bytes, &l);
+    else {
+        p = NULL;
+        l = 0;
+    }
+
+    return nm_utils_buf_utf8safe_escape(p, l, flags, to_free);
+}
+
+char *
+nm_utils_buf_utf8safe_escape_cp(gconstpointer buf, gssize buflen, NMUtilsStrUtf8SafeFlags flags)
+{
+    const char *s_const;
+    char *      s;
+
+    s_const = nm_utils_buf_utf8safe_escape(buf, buflen, flags, &s);
+    nm_assert(!s || s == s_const);
+    return s ?: g_strdup(s_const);
+}
+
+/*****************************************************************************/
+
+const char *
+nm_utils_str_utf8safe_unescape(const char *str, NMUtilsStrUtf8SafeFlags flags, char **to_free)
+{
+    const char *res;
+    gsize       len;
+
+    g_return_val_if_fail(to_free, NULL);
+
+    res = nm_utils_buf_utf8safe_unescape(str, flags, &len, (gpointer *) to_free);
+
+    nm_assert((!res && len == 0) || (strlen(res) <= len));
+
+    return res;
+}
+
+/**
+ * nm_utils_str_utf8safe_escape:
+ * @str: NUL terminated input string, possibly in utf-8 encoding
+ * @flags: #NMUtilsStrUtf8SafeFlags flags
+ * @to_free: (out): return the pointer location of the string
+ *   if a copying was necessary.
+ *
+ * Returns the possible non-UTF-8 NUL terminated string @str
+ * and uses backslash escaping (C escaping, like g_strescape())
+ * to sanitize non UTF-8 characters. The result is valid
+ * UTF-8.
+ *
+ * The operation can be reverted with g_strcompress() or
+ * nm_utils_str_utf8safe_unescape().
+ *
+ * Depending on @flags, valid UTF-8 characters are not escaped at all
+ * (except the escape character '\\'). This is the difference to g_strescape(),
+ * which escapes all non-ASCII characters. This allows to pass on
+ * valid UTF-8 characters as-is and can be directly shown to the user
+ * as UTF-8 -- with exception of the backslash escape character,
+ * invalid UTF-8 sequences, and other (depending on @flags).
+ *
+ * Returns: the escaped input string, as valid UTF-8. If no escaping
+ *   is necessary, it returns the input @str. Otherwise, an allocated
+ *   string @to_free is returned which must be freed by the caller
+ *   with g_free. The escaping can be reverted by g_strcompress().
+ **/
+const char *
+nm_utils_str_utf8safe_escape(const char *str, NMUtilsStrUtf8SafeFlags flags, char **to_free)
+{
+    return nm_utils_buf_utf8safe_escape(str, -1, flags, to_free);
+}
+
+/**
+ * nm_utils_str_utf8safe_escape_cp:
+ * @str: NUL terminated input string, possibly in utf-8 encoding
+ * @flags: #NMUtilsStrUtf8SafeFlags flags
+ *
+ * Like nm_utils_str_utf8safe_escape(), except the returned value
+ * is always a copy of the input and must be freed by the caller.
+ *
+ * Returns: the escaped input string in UTF-8 encoding. The returned
+ *   value should be freed with g_free().
+ *   The escaping can be reverted by g_strcompress().
+ **/
+char *
+nm_utils_str_utf8safe_escape_cp(const char *str, NMUtilsStrUtf8SafeFlags flags)
+{
+    char *s;
+
+    nm_utils_str_utf8safe_escape(str, flags, &s);
+    return s ?: g_strdup(str);
+}
+
+char *
+nm_utils_str_utf8safe_unescape_cp(const char *str, NMUtilsStrUtf8SafeFlags flags)
+{
+    char *s;
+
+    str = nm_utils_str_utf8safe_unescape(str, flags, &s);
+    return s ?: g_strdup(str);
+}
+
+char *
+nm_utils_str_utf8safe_escape_take(char *str, NMUtilsStrUtf8SafeFlags flags)
+{
+    char *str_to_free;
+
+    nm_utils_str_utf8safe_escape(str, flags, &str_to_free);
+    if (str_to_free) {
+        g_free(str);
+        return str_to_free;
+    }
+    return str;
+}
+
+/*****************************************************************************/
+
+/* 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_nsec)
+{
+    struct pollfd pollfd = {
+        .fd     = fd,
+        .events = event,
+    };
+    struct timespec ts, *pts;
+    int             r;
+
+    if (timeout_nsec < 0)
+        pts = NULL;
+    else {
+        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;
+    }
+
+    r = ppoll(&pollfd, 1, pts, NULL);
+    if (r < 0)
+        return -NM_ERRNO_NATIVE(errno);
+    if (r == 0)
+        return 0;
+    return pollfd.revents;
+}
+
+/* taken from systemd's loop_read() */
+ssize_t
+nm_utils_fd_read_loop(int fd, void *buf, size_t nbytes, bool do_poll)
+{
+    uint8_t *p = buf;
+    ssize_t  n = 0;
+
+    g_return_val_if_fail(fd >= 0, -EINVAL);
+    g_return_val_if_fail(buf, -EINVAL);
+
+    /* If called with nbytes == 0, let's call read() at least
+     * once, to validate the operation */
+
+    if (nbytes > (size_t) SSIZE_MAX)
+        return -EINVAL;
+
+    do {
+        ssize_t k;
+
+        k = read(fd, p, nbytes);
+        if (k < 0) {
+            int errsv = errno;
+
+            if (errsv == EINTR)
+                continue;
+
+            if (errsv == EAGAIN && do_poll) {
+                /* We knowingly ignore any return value here,
+                 * and expect that any error/EOF is reported
+                 * via read() */
+
+                (void) nm_utils_fd_wait_for_event(fd, POLLIN, -1);
+                continue;
+            }
+
+            return n > 0 ? n : -NM_ERRNO_NATIVE(errsv);
+        }
+
+        if (k == 0)
+            return n;
+
+        g_assert((size_t) k <= nbytes);
+
+        p += k;
+        nbytes -= k;
+        n += k;
+    } while (nbytes > 0);
+
+    return n;
+}
+
+/* taken from systemd's loop_read_exact() */
+int
+nm_utils_fd_read_loop_exact(int fd, void *buf, size_t nbytes, bool do_poll)
+{
+    ssize_t n;
+
+    n = nm_utils_fd_read_loop(fd, buf, nbytes, do_poll);
+    if (n < 0)
+        return (int) n;
+    if ((size_t) n != nbytes)
+        return -EIO;
+
+    return 0;
+}
+
+/*****************************************************************************/
+
+void
+nm_utils_named_value_clear_with_g_free(NMUtilsNamedValue *val)
+{
+    if (val) {
+        nm_clear_g_free(&val->name_mutable);
+        nm_clear_g_free(&val->value_ptr);
+    }
+}
+
+G_STATIC_ASSERT(G_STRUCT_OFFSET(NMUtilsNamedValue, name) == 0);
+
+NMUtilsNamedValue *
+nm_utils_named_values_from_strdict_full(GHashTable *        hash,
+                                        guint *             out_len,
+                                        GCompareDataFunc    compare_func,
+                                        gpointer            user_data,
+                                        NMUtilsNamedValue * provided_buffer,
+                                        guint               provided_buffer_len,
+                                        NMUtilsNamedValue **out_allocated_buffer)
+{
+    GHashTableIter     iter;
+    NMUtilsNamedValue *values;
+    guint              i, len;
+
+    nm_assert(provided_buffer_len == 0 || provided_buffer);
+    nm_assert(!out_allocated_buffer || !*out_allocated_buffer);
+
+    if (!hash || !(len = g_hash_table_size(hash))) {
+        NM_SET_OUT(out_len, 0);
+        return NULL;
+    }
+
+    if (provided_buffer_len >= len + 1) {
+        /* the buffer provided by the caller is large enough. Use it. */
+        values = provided_buffer;
+    } else {
+        /* allocate a new buffer. */
+        values = g_new(NMUtilsNamedValue, len + 1);
+        NM_SET_OUT(out_allocated_buffer, values);
+    }
+
+    i = 0;
+    g_hash_table_iter_init(&iter, hash);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &values[i].name, &values[i].value_ptr))
+        i++;
+    nm_assert(i == len);
+    values[i].name      = NULL;
+    values[i].value_ptr = NULL;
+
+    if (compare_func)
+        nm_utils_named_value_list_sort(values, len, compare_func, user_data);
+
+    NM_SET_OUT(out_len, len);
+    return values;
+}
+
+gssize
+nm_utils_named_value_list_find(const NMUtilsNamedValue *arr,
+                               gsize                    len,
+                               const char *             name,
+                               gboolean                 sorted)
+{
+    gsize i;
+
+    nm_assert(name);
+
+#if NM_MORE_ASSERTS > 5
+    {
+        for (i = 0; i < len; i++) {
+            const NMUtilsNamedValue *v = &arr[i];
+
+            nm_assert(v->name);
+            if (sorted && i > 0)
+                nm_assert(strcmp(arr[i - 1].name, v->name) < 0);
+        }
+    }
+
+    nm_assert(!sorted || nm_utils_named_value_list_is_sorted(arr, len, FALSE, NULL, NULL));
+#endif
+
+    if (sorted) {
+        return nm_utils_array_find_binary_search(arr,
+                                                 sizeof(NMUtilsNamedValue),
+                                                 len,
+                                                 &name,
+                                                 nm_strcmp_p_with_data,
+                                                 NULL);
+    }
+    for (i = 0; i < len; i++) {
+        if (nm_streq(arr[i].name, name))
+            return i;
+    }
+    return ~((gssize) len);
+}
+
+gboolean
+nm_utils_named_value_list_is_sorted(const NMUtilsNamedValue *arr,
+                                    gsize                    len,
+                                    gboolean                 accept_duplicates,
+                                    GCompareDataFunc         compare_func,
+                                    gpointer                 user_data)
+{
+    gsize i;
+    int   c_limit;
+
+    if (len == 0)
+        return TRUE;
+
+    g_return_val_if_fail(arr, FALSE);
+
+    if (!compare_func)
+        compare_func = nm_strcmp_p_with_data;
+
+    c_limit = accept_duplicates ? 0 : -1;
+
+    for (i = 1; i < len; i++) {
+        int c;
+
+        c = compare_func(&arr[i - 1], &arr[i], user_data);
+        if (c > c_limit)
+            return FALSE;
+    }
+    return TRUE;
+}
+
+void
+nm_utils_named_value_list_sort(NMUtilsNamedValue *arr,
+                               gsize              len,
+                               GCompareDataFunc   compare_func,
+                               gpointer           user_data)
+{
+    if (len == 0)
+        return;
+
+    g_return_if_fail(arr);
+
+    if (len == 1)
+        return;
+
+    g_qsort_with_data(arr,
+                      len,
+                      sizeof(NMUtilsNamedValue),
+                      compare_func ?: nm_strcmp_p_with_data,
+                      user_data);
+}
+
+/*****************************************************************************/
+
+gpointer *
+nm_utils_hash_keys_to_array(GHashTable *     hash,
+                            GCompareDataFunc compare_func,
+                            gpointer         user_data,
+                            guint *          out_len)
+{
+    guint     len;
+    gpointer *keys;
+
+    /* by convention, we never return an empty array. In that
+     * case, always %NULL. */
+    if (!hash || g_hash_table_size(hash) == 0) {
+        NM_SET_OUT(out_len, 0);
+        return NULL;
+    }
+
+    keys = g_hash_table_get_keys_as_array(hash, &len);
+    if (len > 1 && compare_func) {
+        g_qsort_with_data(keys, len, sizeof(gpointer), compare_func, user_data);
+    }
+    NM_SET_OUT(out_len, len);
+    return keys;
+}
+
+gpointer *
+nm_utils_hash_values_to_array(GHashTable *     hash,
+                              GCompareDataFunc compare_func,
+                              gpointer         user_data,
+                              guint *          out_len)
+{
+    GHashTableIter iter;
+    gpointer       value;
+    gpointer *     arr;
+    guint          i, len;
+
+    if (!hash || (len = g_hash_table_size(hash)) == 0u) {
+        NM_SET_OUT(out_len, 0);
+        return NULL;
+    }
+
+    arr = g_new(gpointer, ((gsize) len) + 1);
+    i   = 0;
+    g_hash_table_iter_init(&iter, hash);
+    while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &value))
+        arr[i++] = value;
+
+    nm_assert(i == len);
+    arr[len] = NULL;
+
+    if (len > 1 && compare_func) {
+        g_qsort_with_data(arr, len, sizeof(gpointer), compare_func, user_data);
+    }
+
+    NM_SET_OUT(out_len, len);
+    return arr;
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_utils_hashtable_equal:
+ * @a: one #GHashTable
+ * @b: other #GHashTable
+ * @treat_null_as_empty: if %TRUE, when either @a or @b is %NULL, it is
+ *   treated like an empty hash. It means, a %NULL hash will compare equal
+ *   to an empty hash.
+ * @equal_func: the equality function, for comparing the values.
+ *   If %NULL, the values are not compared. In that case, the function
+ *   only checks, if both dictionaries have the same keys -- according
+ *   to @b's key equality function.
+ *   Note that the values of @a will be passed as first argument
+ *   to @equal_func.
+ *
+ * Compares two hash tables, whether they have equal content.
+ * This only makes sense, if @a and @b have the same key types and
+ * the same key compare-function.
+ *
+ * Returns: %TRUE, if both dictionaries have the same content.
+ */
+gboolean
+nm_utils_hashtable_equal(const GHashTable *a,
+                         const GHashTable *b,
+                         gboolean          treat_null_as_empty,
+                         GEqualFunc        equal_func)
+{
+    guint          n;
+    GHashTableIter iter;
+    gconstpointer  key, v_a, v_b;
+
+    if (a == b)
+        return TRUE;
+    if (!treat_null_as_empty) {
+        if (!a || !b)
+            return FALSE;
+    }
+
+    n = a ? g_hash_table_size((GHashTable *) a) : 0;
+    if (n != (b ? g_hash_table_size((GHashTable *) b) : 0))
+        return FALSE;
+
+    if (n > 0) {
+        g_hash_table_iter_init(&iter, (GHashTable *) a);
+        while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &v_a)) {
+            if (!g_hash_table_lookup_extended((GHashTable *) b, key, NULL, (gpointer *) &v_b))
+                return FALSE;
+            if (equal_func && !equal_func(v_a, v_b))
+                return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_utils_hashtable_equal(GHashTable *     hash_a,
+                       GHashTable *     hash_b,
+                       GCompareDataFunc cmp_values,
+                       gpointer         user_data)
+{
+    GHashTableIter h;
+    gpointer       a_key;
+    gpointer       a_val;
+    gpointer       b_val;
+
+    nm_assert(hash_a);
+    nm_assert(hash_b);
+    nm_assert(hash_a != hash_b);
+    nm_assert(g_hash_table_size(hash_a) == g_hash_table_size(hash_b));
+
+    /* We rely on both hashes to have the same hash/equal function. Otherwise, we would have to iterate
+     * both hashes and check whether all keys/values are present in the respective other hash (which
+     * would be O(n^2), since we couldn't use the plain lookup function. That is not a useful thing
+     * for this function. */
+
+    g_hash_table_iter_init(&h, hash_a);
+    while (g_hash_table_iter_next(&h, &a_key, &a_val)) {
+        if (!g_hash_table_lookup_extended(hash_b, a_key, NULL, &b_val))
+            return FALSE;
+
+        if (!cmp_values) {
+            /* we accept %NULL compare function to indicate that we don't care about the key. */
+            continue;
+        }
+
+        if (cmp_values(a_val, b_val, user_data) != 0)
+            return FALSE;
+    }
+
+    return TRUE;
+}
+
+/**
+ * nm_utils_hashtable_cmp_equal:
+ * @a: (allow-none): the hash table or %NULL
+ * @b: (allow-none): the other hash table or %NULL
+ * @cmp_values: (allow-none): if %NULL, only the keys
+ *   will be compared. Otherwise, this function is used to
+ *   check whether all keys are equal.
+ * @user_data: the argument for @cmp_values.
+ *
+ * It is required that both @a and @b have the same hash and equals
+ * function.
+ *
+ * Returns: %TRUE, if both keys have the same keys and (if
+ *   @cmp_values is given) all values are the same.
+ */
+gboolean
+nm_utils_hashtable_cmp_equal(const GHashTable *a,
+                             const GHashTable *b,
+                             GCompareDataFunc  cmp_values,
+                             gpointer          user_data)
+{
+    GHashTable *hash_a = (GHashTable *) a;
+    GHashTable *hash_b = (GHashTable *) b;
+    gboolean    same;
+    guint       size;
+
+    if (hash_a == hash_b)
+        return TRUE;
+
+    if (!hash_a || !hash_b)
+        return FALSE;
+
+    size = g_hash_table_size(hash_a);
+    if (size != g_hash_table_size(hash_b))
+        return FALSE;
+
+    if (size == 0)
+        return TRUE;
+
+    same = _utils_hashtable_equal(hash_a, hash_b, cmp_values, user_data);
+
+#if NM_MORE_ASSERTS > 5
+    nm_assert(same == _utils_hashtable_equal(hash_b, hash_a, cmp_values, user_data));
+#endif
+
+    return same;
+}
+
+typedef struct {
+    gpointer key;
+    gpointer val;
+} HashTableCmpData;
+
+typedef struct {
+    GCompareDataFunc cmp_keys;
+    gpointer         user_data;
+} HashTableUserData;
+
+static int
+_hashtable_cmp_func(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+    const HashTableUserData *d   = user_data;
+    const HashTableCmpData * d_a = *((const HashTableCmpData *const *) a);
+    const HashTableCmpData * d_b = *((const HashTableCmpData *const *) b);
+
+    NM_CMP_RETURN(d->cmp_keys(d_a, d_b, d->user_data));
+    return 0;
+}
+
+/**
+ * nm_utils_hashtable_cmp:
+ * @a: (allow-none): the hash to compare. May be %NULL.
+ * @b: (allow-none): the other hash to compare. May be %NULL.
+ * @do_fast_precheck: if %TRUE, assume that the hashes are equal
+ *   and that it is worth calling nm_utils_hashtable_cmp_equal() first.
+ *   That requires, that both hashes have the same equals function
+ *   which is compatible with the @cmp_keys function.
+ * @cmp_keys: the compare function for keys. Usually, the hash/equal function
+ *   of both hashes corresponds to this function. If you set @do_fast_precheck
+ *   to false, then this is not a requirement.
+ * @cmp_values: (allow-none): if %NULL, only the keys are compared.
+ *   Otherwise, the values must are also compared with this function.
+ *
+ * Both hashes must have keys/values of the same domain, so that
+ * they can be effectively compared with @cmp_keys and @cmp_values.
+ *
+ * %NULL hashes compare equal to %NULL, but not to empty hashes.
+ *
+ * Returns: 0 if both hashes are equal, or -1 or 1 if one of the hashes
+ *   sorts before/after.
+ */
+int
+nm_utils_hashtable_cmp(const GHashTable *a,
+                       const GHashTable *b,
+                       gboolean          do_fast_precheck,
+                       GCompareDataFunc  cmp_keys,
+                       GCompareDataFunc  cmp_values,
+                       gpointer          user_data)
+{
+    GHashTable *hash_a                       = (GHashTable *) a;
+    GHashTable *hash_b                       = (GHashTable *) b;
+    gs_free HashTableCmpData *cmp_array_free = NULL;
+    HashTableCmpData *        cmp_array_a;
+    HashTableCmpData *        cmp_array_b;
+    GHashTableIter            h;
+    gpointer                  i_key;
+    gpointer                  i_val;
+    gsize                     size2;
+    guint                     size;
+    guint                     i;
+
+    nm_assert(cmp_keys);
+
+    NM_CMP_SELF(hash_a, hash_b);
+
+    size = g_hash_table_size(hash_a);
+
+    NM_CMP_DIRECT(size, g_hash_table_size(hash_b));
+
+    if (size == 0)
+        return 0;
+
+    if (do_fast_precheck) {
+        gboolean same;
+
+        /* we expect that the hashes are equal and the caller ensures us that they
+         * use the same hash/equal functions. Do a fast path check first...
+         *
+         * It's unclear whether this is worth it. The full comparison is O(n*ln(n)),
+         * while the fast check (using the hash lookup) is O(n). But then, the pre-check
+         * makes additional requirements on the hash's hash/equal functions -- the
+         * full comparison does not make such requirements. */
+        same = _utils_hashtable_equal(hash_a, hash_b, cmp_values, user_data);
+#if NM_MORE_ASSERTS > 5
+        nm_assert(same == _utils_hashtable_equal(hash_b, hash_a, cmp_values, user_data));
+#endif
+        if (same)
+            return 0;
+    }
+
+    size2 = ((gsize) size) * 2u;
+    if (size2 > 600u / sizeof(HashTableCmpData)) {
+        cmp_array_free = g_new(HashTableCmpData, size2);
+        cmp_array_a    = cmp_array_free;
+    } else
+        cmp_array_a = g_newa(HashTableCmpData, size2);
+    cmp_array_b = &cmp_array_a[size];
+
+    i = 0;
+    g_hash_table_iter_init(&h, hash_a);
+    while (g_hash_table_iter_next(&h, &i_key, &i_val)) {
+        nm_assert(i < size);
+        cmp_array_a[i++] = (HashTableCmpData){
+            .key = i_key,
+            .val = i_val,
+        };
+    }
+    nm_assert(i == size);
+
+    i = 0;
+    g_hash_table_iter_init(&h, hash_b);
+    while (g_hash_table_iter_next(&h, &i_key, &i_val)) {
+        nm_assert(i < size);
+        cmp_array_b[i++] = (HashTableCmpData){
+            .key = i_key,
+            .val = i_val,
+        };
+    }
+    nm_assert(i == size);
+
+    g_qsort_with_data(cmp_array_a,
+                      size,
+                      sizeof(HashTableCmpData),
+                      _hashtable_cmp_func,
+                      &((HashTableUserData){
+                          .cmp_keys  = cmp_keys,
+                          .user_data = user_data,
+                      }));
+
+    g_qsort_with_data(cmp_array_b,
+                      size,
+                      sizeof(HashTableCmpData),
+                      _hashtable_cmp_func,
+                      &((HashTableUserData){
+                          .cmp_keys  = cmp_keys,
+                          .user_data = user_data,
+                      }));
+
+    for (i = 0; i < size; i++) {
+        NM_CMP_RETURN(cmp_keys(cmp_array_a[i].key, cmp_array_b[i].key, user_data));
+    }
+
+    if (cmp_values) {
+        for (i = 0; i < size; i++) {
+            NM_CMP_RETURN(cmp_values(cmp_array_a[i].val, cmp_array_b[i].val, user_data));
+        }
+    }
+
+    /* the fast-precheck should have already told that the arrays are equal. */
+    nm_assert(!do_fast_precheck);
+
+    return 0;
+}
+
+char **
+nm_utils_strv_make_deep_copied(const char **strv)
+{
+    gsize i;
+
+    /* it takes a strv list, and copies each
+     * strings. Note that this updates @strv *in-place*
+     * and returns it. */
+
+    if (!strv)
+        return NULL;
+    for (i = 0; strv[i]; i++)
+        strv[i] = g_strdup(strv[i]);
+
+    return (char **) strv;
+}
+
+char **
+nm_utils_strv_make_deep_copied_n(const char **strv, gsize len)
+{
+    gsize i;
+
+    /* it takes a strv array with len elements, and copies each
+     * strings. Note that this updates @strv *in-place*
+     * and returns it. */
+
+    if (!strv)
+        return NULL;
+    for (i = 0; i < len; i++)
+        strv[i] = g_strdup(strv[i]);
+
+    return (char **) strv;
+}
+
+/**
+ * @strv: the strv array to copy. It may be %NULL if @len
+ *   is negative or zero (in which case %NULL will be returned).
+ * @len: the length of strings in @str. If negative, strv is assumed
+ *   to be a NULL terminated array.
+ * @deep_copied: if %TRUE, clones the individual strings. In that case,
+ *   the returned array must be freed with g_strfreev(). Otherwise, the
+ *   strings themself are not copied. You must take care of who owns the
+ *   strings yourself.
+ *
+ * Like g_strdupv(), with two differences:
+ *
+ * - accepts a @len parameter for non-null terminated strv array.
+ *
+ * - this never returns an empty strv array, but always %NULL if
+ *   there are no strings.
+ *
+ * Note that if @len is non-negative, then it still must not
+ * contain any %NULL pointers within the first @len elements.
+ * Otherwise, you would leak elements if you try to free the
+ * array with g_strfreev(). Allowing that would be error prone.
+ *
+ * Returns: (transfer full): a clone of the strv array. Always
+ *   %NULL terminated. Depending on @deep_copied, the strings are
+ *   cloned or not.
+ */
+char **
+_nm_utils_strv_dup(const char *const *strv, gssize len, gboolean deep_copied)
+{
+    gsize  i, l;
+    char **v;
+
+    if (len < 0)
+        l = NM_PTRARRAY_LEN(strv);
+    else
+        l = len;
+    if (l == 0) {
+        /* this function never returns an empty strv array. If you
+         * need that, handle it yourself. */
+        return NULL;
+    }
+
+    v = g_new(char *, l + 1);
+    for (i = 0; i < l; i++) {
+        if (G_UNLIKELY(!strv[i])) {
+            /* NULL strings are not allowed. Clear the remainder of the array
+             * and return it (with assertion failure). */
+            l++;
+            for (; i < l; i++)
+                v[i] = NULL;
+            g_return_val_if_reached(v);
+        }
+
+        if (deep_copied)
+            v[i] = g_strdup(strv[i]);
+        else
+            v[i] = (char *) strv[i];
+    }
+    v[l] = NULL;
+    return v;
+}
+
+const char **
+_nm_utils_strv_dup_packed(const char *const *strv, gssize len)
+
+{
+    gs_free gsize *str_len_free = NULL;
+    gsize *        str_len;
+    const char **  result;
+    gsize          mem_len;
+    gsize          pre_len;
+    gsize          len2;
+    char *         sbuf;
+    gsize          i;
+
+    nm_assert(len >= -1);
+
+    if (G_LIKELY(len < 0)) {
+        if (!strv || !strv[0]) {
+            /* This function never returns an empty strv array. If you need that, handle it
+             * yourself. */
+            return NULL;
+        }
+        len2 = NM_PTRARRAY_LEN(strv);
+    } else {
+        if (len == 0)
+            return NULL;
+        len2 = len;
+    }
+
+    if (len2 > 300u / sizeof(gsize)) {
+        str_len_free = g_new(gsize, len2);
+        str_len      = str_len_free;
+    } else
+        str_len = g_newa(gsize, len2);
+
+    mem_len = 0;
+    for (i = 0; i < len2; i++) {
+        gsize l;
+
+        if (G_LIKELY(strv[i]))
+            l = strlen(strv[i]) + 1u;
+        else
+            l = 0;
+        str_len[i] = l;
+        mem_len += l;
+    }
+
+    pre_len = sizeof(const char *) * (len2 + 1u);
+
+    result = g_malloc(pre_len + mem_len);
+    sbuf   = &(((char *) result)[pre_len]);
+    for (i = 0; i < len2; i++) {
+        gsize l;
+
+        if (G_UNLIKELY(!strv[i])) {
+            /* Technically there is no problem with accepting NULL strings. But that
+             * does not really result in a strv array, and likely this only happens due
+             * to a bug. We want to catch such bugs by asserting.
+             *
+             * We clear the remainder of the buffer and fail with an assertion. */
+            len2++;
+            for (; i < len2; i++)
+                result[i] = NULL;
+            g_return_val_if_reached(result);
+        }
+
+        result[i] = sbuf;
+
+        l = str_len[i];
+        memcpy(sbuf, strv[i], l);
+        sbuf += l;
+    }
+    result[i] = NULL;
+    nm_assert(i == len2);
+    nm_assert(sbuf == (&((const char *) result)[pre_len]) + mem_len);
+
+    return result;
+}
+
+/*****************************************************************************/
+
+gssize
+nm_utils_ptrarray_find_first(gconstpointer *list, gssize len, gconstpointer needle)
+{
+    gssize i;
+
+    if (len == 0)
+        return -1;
+
+    if (len > 0) {
+        g_return_val_if_fail(list, -1);
+        for (i = 0; i < len; i++) {
+            if (list[i] == needle)
+                return i;
+        }
+    } else {
+        g_return_val_if_fail(needle, -1);
+        for (i = 0; list && list[i]; i++) {
+            if (list[i] == needle)
+                return i;
+        }
+    }
+    return -1;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_utils_ptrarray_is_sorted(gconstpointer *  list,
+                            gsize            len,
+                            gboolean         require_strict,
+                            GCompareDataFunc cmpfcn,
+                            gpointer         user_data)
+{
+    gsize i;
+
+    for (i = 1; i < len; i++) {
+        int c;
+
+        c = cmpfcn(list[i - 1], list[i], user_data);
+        if (G_LIKELY(c < 0))
+            continue;
+
+        if (c > 0 || require_strict)
+            return FALSE;
+    }
+    return TRUE;
+}
+
+gssize
+nm_utils_ptrarray_find_binary_search(gconstpointer *  list,
+                                     gsize            len,
+                                     gconstpointer    needle,
+                                     GCompareDataFunc cmpfcn,
+                                     gpointer         user_data)
+{
+    gssize imin, imax, imid;
+    int    cmp;
+
+    g_return_val_if_fail(list || !len, ~((gssize) 0));
+    g_return_val_if_fail(cmpfcn, ~((gssize) 0));
+
+    imin = 0;
+    if (len > 0) {
+        imax = len - 1;
+
+        while (imin <= imax) {
+            imid = imin + (imax - imin) / 2;
+
+            cmp = cmpfcn(list[imid], needle, user_data);
+            if (cmp == 0)
+                return imid;
+
+            if (cmp < 0)
+                imin = imid + 1;
+            else
+                imax = imid - 1;
+        }
+    }
+
+    /* return the inverse of @imin. This is a negative number, but
+     * also is ~imin the position where the value should be inserted. */
+    imin = ~imin;
+    return imin;
+}
+
+gssize
+nm_utils_ptrarray_find_binary_search_range(gconstpointer *  list,
+                                           gsize            len,
+                                           gconstpointer    needle,
+                                           GCompareDataFunc cmpfcn,
+                                           gpointer         user_data,
+                                           gssize *         out_idx_first,
+                                           gssize *         out_idx_last)
+{
+    gssize imin, imax, imid, i2min, i2max, i2mid;
+    int    cmp;
+
+    g_return_val_if_fail(list || !len, ~((gssize) 0));
+    g_return_val_if_fail(cmpfcn, ~((gssize) 0));
+
+    imin = 0;
+    if (len > 0) {
+        imax = len - 1;
+
+        while (imin <= imax) {
+            imid = imin + (imax - imin) / 2;
+
+            cmp = cmpfcn(list[imid], needle, user_data);
+            if (cmp == 0) {
+                /* we found a matching entry at index imid.
+                 *
+                 * Does the caller request the first/last index as well (in case that
+                 * there are multiple entries which compare equal). */
+
+                if (out_idx_first) {
+                    i2min = imin;
+                    i2max = imid + 1;
+                    while (i2min <= i2max) {
+                        i2mid = i2min + (i2max - i2min) / 2;
+
+                        cmp = cmpfcn(list[i2mid], needle, user_data);
+                        if (cmp == 0)
+                            i2max = i2mid - 1;
+                        else {
+                            nm_assert(cmp < 0);
+                            i2min = i2mid + 1;
+                        }
+                    }
+                    *out_idx_first = i2min;
+                }
+                if (out_idx_last) {
+                    i2min = imid + 1;
+                    i2max = imax;
+                    while (i2min <= i2max) {
+                        i2mid = i2min + (i2max - i2min) / 2;
+
+                        cmp = cmpfcn(list[i2mid], needle, user_data);
+                        if (cmp == 0)
+                            i2min = i2mid + 1;
+                        else {
+                            nm_assert(cmp > 0);
+                            i2max = i2mid - 1;
+                        }
+                    }
+                    *out_idx_last = i2min - 1;
+                }
+                return imid;
+            }
+
+            if (cmp < 0)
+                imin = imid + 1;
+            else
+                imax = imid - 1;
+        }
+    }
+
+    /* return the inverse of @imin. This is a negative number, but
+     * also is ~imin the position where the value should be inserted. */
+    imin = ~imin;
+    NM_SET_OUT(out_idx_first, imin);
+    NM_SET_OUT(out_idx_last, imin);
+    return imin;
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_utils_array_find_binary_search:
+ * @list: the list to search. It must be sorted according to @cmpfcn ordering.
+ * @elem_size: the size in bytes of each element in the list
+ * @len: the number of elements in @list
+ * @needle: the value that is searched
+ * @cmpfcn: the compare function. The elements @list are passed as first
+ *   argument to @cmpfcn, while @needle is passed as second. Usually, the
+ *   needle is the same data type as inside the list, however, that is
+ *   not necessary, as long as @cmpfcn takes care to cast the two arguments
+ *   accordingly.
+ * @user_data: optional argument passed to @cmpfcn
+ *
+ * Performs binary search for @needle in @list. On success, returns the
+ * (non-negative) index where the compare function found the searched element.
+ * On success, it returns a negative value. Note that the return negative value
+ * is the bitwise inverse of the position where the element should be inserted.
+ *
+ * If the list contains multiple matching elements, an arbitrary index is
+ * returned.
+ *
+ * Returns: the index to the element in the list, or the (negative, bitwise inverted)
+ *   position where it should be.
+ */
+gssize
+nm_utils_array_find_binary_search(gconstpointer    list,
+                                  gsize            elem_size,
+                                  gsize            len,
+                                  gconstpointer    needle,
+                                  GCompareDataFunc cmpfcn,
+                                  gpointer         user_data)
+{
+    gssize imin, imax, imid;
+    int    cmp;
+
+    g_return_val_if_fail(list || !len, ~((gssize) 0));
+    g_return_val_if_fail(cmpfcn, ~((gssize) 0));
+    g_return_val_if_fail(elem_size > 0, ~((gssize) 0));
+
+    imin = 0;
+    if (len == 0)
+        return ~imin;
+
+    imax = len - 1;
+
+    while (imin <= imax) {
+        imid = imin + (imax - imin) / 2;
+
+        cmp = cmpfcn(&((const char *) list)[elem_size * imid], needle, user_data);
+        if (cmp == 0)
+            return imid;
+
+        if (cmp < 0)
+            imin = imid + 1;
+        else
+            imax = imid - 1;
+    }
+
+    /* return the inverse of @imin. This is a negative number, but
+     * also is ~imin the position where the value should be inserted. */
+    return ~imin;
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_utils_get_start_time_for_pid:
+ * @pid: the process identifier
+ * @out_state: return the state character, like R, S, Z. See `man 5 proc`.
+ * @out_ppid: parent process id
+ *
+ * Originally copied from polkit source (src/polkit/polkitunixprocess.c)
+ * and adjusted.
+ *
+ * Returns: the timestamp when the process started (by parsing /proc/$PID/stat).
+ * If an error occurs (e.g. the process does not exist), 0 is returned.
+ *
+ * The returned start time counts since boot, in the unit HZ (with HZ usually being (1/100) seconds)
+ **/
+guint64
+nm_utils_get_start_time_for_pid(pid_t pid, char *out_state, pid_t *out_ppid)
+{
+    guint64              start_time;
+    char                 filename[256];
+    gs_free char *       contents = NULL;
+    size_t               length;
+    gs_free const char **tokens = NULL;
+    char *               p;
+    char                 state = ' ';
+    gint64               ppid  = 0;
+
+    start_time = 0;
+    contents   = NULL;
+
+    g_return_val_if_fail(pid > 0, 0);
+
+    G_STATIC_ASSERT_EXPR(sizeof(GPid) >= sizeof(pid_t));
+
+    nm_sprintf_buf(filename, "/proc/%" G_PID_FORMAT "/stat", (GPid) pid);
+
+    if (!g_file_get_contents(filename, &contents, &length, NULL))
+        goto fail;
+
+    /* start time is the token at index 19 after the '(process name)' entry - since only this
+     * field can contain the ')' character, search backwards for this to avoid malicious
+     * processes trying to fool us
+     */
+    p = strrchr(contents, ')');
+    if (!p)
+        goto fail;
+    p += 2; /* skip ') ' */
+    if (p - contents >= (int) length)
+        goto fail;
+
+    state = p[0];
+
+    tokens = nm_utils_strsplit_set(p, " ");
+
+    if (NM_PTRARRAY_LEN(tokens) < 20)
+        goto fail;
+
+    if (out_ppid) {
+        ppid = _nm_utils_ascii_str_to_int64(tokens[1], 10, 1, G_MAXINT, 0);
+        if (ppid == 0)
+            goto fail;
+    }
+
+    start_time = _nm_utils_ascii_str_to_int64(tokens[19], 10, 1, G_MAXINT64, 0);
+    if (start_time == 0)
+        goto fail;
+
+    NM_SET_OUT(out_state, state);
+    NM_SET_OUT(out_ppid, ppid);
+    return start_time;
+
+fail:
+    NM_SET_OUT(out_state, ' ');
+    NM_SET_OUT(out_ppid, 0);
+    return 0;
+}
+
+/*****************************************************************************/
+
+/**
+ * _nm_utils_strv_sort:
+ * @strv: pointer containing strings that will be sorted
+ *   in-place, %NULL is allowed, unless @len indicates
+ *   that there are more elements.
+ * @len: the number of elements in strv. If negative,
+ *   strv must be a NULL terminated array and the length
+ *   will be calculated first. If @len is a positive
+ *   number, @strv is allowed to contain %NULL strings
+ *   too.
+ *
+ * Ascending sort of the array @strv inplace, using plain strcmp() string
+ * comparison.
+ */
+void
+_nm_utils_strv_sort(const char **strv, gssize len)
+{
+    GCompareDataFunc cmp;
+    gsize            l;
+
+    if (len < 0) {
+        l   = NM_PTRARRAY_LEN(strv);
+        cmp = nm_strcmp_p_with_data;
+    } else {
+        l   = len;
+        cmp = nm_strcmp0_p_with_data;
+    }
+
+    if (l <= 1)
+        return;
+
+    nm_assert(l <= (gsize) G_MAXINT);
+
+    g_qsort_with_data(strv, l, sizeof(const char *), cmp, NULL);
+}
+
+/**
+ * _nm_utils_strv_cmp_n:
+ * @strv1: a string array
+ * @len1: the length of @strv1, or -1 for NULL terminated array.
+ * @strv2: a string array
+ * @len2: the length of @strv2, or -1 for NULL terminated array.
+ *
+ * Note that
+ *   - len == -1 && strv == NULL
+ * is treated like a %NULL argument and compares differently from
+ * other arrays.
+ *
+ * Note that an empty array can be represented as
+ *   - len == -1 &&  strv && !strv[0]
+ *   - len ==  0 && !strv
+ *   - len ==  0 &&  strv
+ * These 3 forms all compare equal.
+ * It also means, if length is 0, then it is permissible for strv to be %NULL.
+ *
+ * The strv arrays may contain %NULL strings (if len is positive).
+ *
+ * Returns: 0 if the arrays are equal (using strcmp).
+ **/
+int
+_nm_utils_strv_cmp_n(const char *const *strv1, gssize len1, const char *const *strv2, gssize len2)
+{
+    gsize n, n2;
+
+    if (len1 < 0) {
+        if (!strv1)
+            return (len2 < 0 && !strv2) ? 0 : -1;
+        n = NM_PTRARRAY_LEN(strv1);
+    } else
+        n = len1;
+
+    if (len2 < 0) {
+        if (!strv2)
+            return 1;
+        n2 = NM_PTRARRAY_LEN(strv2);
+    } else
+        n2 = len2;
+
+    NM_CMP_DIRECT(n, n2);
+    for (; n > 0; n--, strv1++, strv2++)
+        NM_CMP_DIRECT_STRCMP0(*strv1, *strv2);
+    return 0;
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_utils_g_slist_find_str:
+ * @list: the #GSList with NUL terminated strings to search
+ * @needle: the needle string to look for.
+ *
+ * Search the list for @needle and return the first found match
+ * (or %NULL if not found). Uses strcmp() for finding the first matching
+ * element.
+ *
+ * Returns: the #GSList element with @needle as string value or
+ *   %NULL if not found.
+ */
+GSList *
+nm_utils_g_slist_find_str(const GSList *list, const char *needle)
+{
+    nm_assert(needle);
+
+    for (; list; list = list->next) {
+        nm_assert(list->data);
+        if (nm_streq(list->data, needle))
+            return (GSList *) list;
+    }
+    return NULL;
+}
+
+/**
+ * nm_utils_g_slist_strlist_cmp:
+ * @a: the left #GSList of strings
+ * @b: the right #GSList of strings to compare.
+ *
+ * Compares two string lists. The data elements are compared with
+ * strcmp(), allowing %NULL elements.
+ *
+ * Returns: 0, 1, or -1, depending on how the lists compare.
+ */
+int
+nm_utils_g_slist_strlist_cmp(const GSList *a, const GSList *b)
+{
+    while (TRUE) {
+        if (!a)
+            return !b ? 0 : -1;
+        if (!b)
+            return 1;
+        NM_CMP_DIRECT_STRCMP0(a->data, b->data);
+        a = a->next;
+        b = b->next;
+    }
+}
+
+char *
+nm_utils_g_slist_strlist_join(const GSList *a, const char *separator)
+{
+    GString *str = NULL;
+
+    if (!a)
+        return NULL;
+
+    for (; a; a = a->next) {
+        if (!str)
+            str = g_string_new(NULL);
+        else
+            g_string_append(str, separator);
+        g_string_append(str, a->data);
+    }
+    return g_string_free(str, FALSE);
+}
+
+/*****************************************************************************/
+
+NMUtilsUserData *
+_nm_utils_user_data_pack(int nargs, gconstpointer *args)
+{
+    int       i;
+    gpointer *data;
+
+    nm_assert(nargs > 0);
+    nm_assert(args);
+
+    data = g_slice_alloc(((gsize) nargs) * sizeof(gconstpointer));
+    for (i = 0; i < nargs; i++)
+        data[i] = (gpointer) args[i];
+    return (NMUtilsUserData *) data;
+}
+
+void
+_nm_utils_user_data_unpack(NMUtilsUserData *user_data, int nargs, ...)
+{
+    gpointer *data = (gpointer *) user_data;
+    va_list   ap;
+    int       i;
+
+    nm_assert(data);
+    nm_assert(nargs > 0);
+
+    va_start(ap, nargs);
+    for (i = 0; i < nargs; i++) {
+        gpointer *dst;
+
+        dst = va_arg(ap, gpointer *);
+        nm_assert(dst);
+
+        *dst = data[i];
+    }
+    va_end(ap);
+
+    g_slice_free1(((gsize) nargs) * sizeof(gconstpointer), data);
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    gpointer                    callback_user_data;
+    GCancellable *              cancellable;
+    GSource *                   source;
+    NMUtilsInvokeOnIdleCallback callback;
+    gulong                      cancelled_id;
+} InvokeOnIdleData;
+
+static void
+_nm_utils_invoke_on_idle_complete(InvokeOnIdleData *data)
+{
+    nm_clear_g_signal_handler(data->cancellable, &data->cancelled_id);
+
+    data->callback(data->callback_user_data, data->cancellable);
+
+    nm_g_object_unref(data->cancellable);
+    g_source_destroy(data->source);
+    nm_g_slice_free(data);
+}
+
+static gboolean
+_nm_utils_invoke_on_idle_cb_idle(gpointer user_data)
+{
+    _nm_utils_invoke_on_idle_complete(user_data);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+_nm_utils_invoke_on_idle_cb_cancelled(GCancellable *cancellable, InvokeOnIdleData *data)
+{
+    if (data->cancelled_id == 0) {
+        /* this can only happen during _nm_utils_invoke_on_idle_start(). Don't do anything,
+         * we still schedule an idle action. */
+        return;
+    }
+
+    /* On cancellation, we invoke the callback synchronously.
+     *
+     * Note that this is not thread-safe, meaning: you can only cancel the cancellable
+     * while not iterating the GMainContext (that has the idle/timeout source attached).
+     * Making this thread safe would be complicated, and it's simply not used by our
+     * callers. */
+    _nm_utils_invoke_on_idle_complete(data);
+}
+
+static void
+_nm_utils_invoke_on_idle_start(gboolean                    use_timeout,
+                               guint                       timeout_msec,
+                               GCancellable *              cancellable,
+                               NMUtilsInvokeOnIdleCallback callback,
+                               gpointer                    callback_user_data)
+{
+    InvokeOnIdleData *data;
+    GSource *         source;
+
+    g_return_if_fail(callback);
+
+    data  = g_slice_new(InvokeOnIdleData);
+    *data = (InvokeOnIdleData){
+        .callback           = callback,
+        .callback_user_data = callback_user_data,
+        .cancellable        = nm_g_object_ref(cancellable),
+        .cancelled_id       = 0,
+    };
+
+    if (cancellable) {
+        gulong cancelled_id;
+
+        cancelled_id = g_cancellable_connect(cancellable,
+                                             G_CALLBACK(_nm_utils_invoke_on_idle_cb_cancelled),
+                                             data,
+                                             NULL);
+        if (cancelled_id == 0) {
+            /* the cancellable is already cancelled. We still schedule an idle action. */
+            use_timeout = FALSE;
+        } else
+            data->cancelled_id = cancelled_id;
+    }
+
+    if (use_timeout) {
+        source = nm_g_timeout_source_new(timeout_msec,
+                                         G_PRIORITY_DEFAULT,
+                                         _nm_utils_invoke_on_idle_cb_idle,
+                                         data,
+                                         NULL);
+    } else {
+        source =
+            nm_g_idle_source_new(G_PRIORITY_DEFAULT, _nm_utils_invoke_on_idle_cb_idle, data, NULL);
+    }
+
+    /* use the current thread default context. */
+    g_source_attach(source, g_main_context_get_thread_default());
+
+    data->source = source;
+}
+
+void
+nm_utils_invoke_on_idle(GCancellable *              cancellable,
+                        NMUtilsInvokeOnIdleCallback callback,
+                        gpointer                    callback_user_data)
+{
+    _nm_utils_invoke_on_idle_start(FALSE, 0, cancellable, callback, callback_user_data);
+}
+
+void
+nm_utils_invoke_on_timeout(guint                       timeout_msec,
+                           GCancellable *              cancellable,
+                           NMUtilsInvokeOnIdleCallback callback,
+                           gpointer                    callback_user_data)
+{
+    _nm_utils_invoke_on_idle_start(TRUE, timeout_msec, cancellable, callback, callback_user_data);
+}
+
+/*****************************************************************************/
+
+int
+nm_utils_getpagesize(void)
+{
+    static volatile int val = 0;
+    long                l;
+    int                 v;
+
+    v = g_atomic_int_get(&val);
+
+    if (G_UNLIKELY(v == 0)) {
+        l = sysconf(_SC_PAGESIZE);
+
+        g_return_val_if_fail(l > 0 && l < G_MAXINT, 4 * 1024);
+
+        v = (int) l;
+        if (!g_atomic_int_compare_and_exchange(&val, 0, v)) {
+            v = g_atomic_int_get(&val);
+            g_return_val_if_fail(v > 0, 4 * 1024);
+        }
+    }
+
+    nm_assert(v > 0);
+#if NM_MORE_ASSERTS > 5
+    nm_assert(v == getpagesize());
+    nm_assert(v == sysconf(_SC_PAGESIZE));
+#endif
+
+    return v;
+}
+
+gboolean
+nm_utils_memeqzero(gconstpointer data, gsize length)
+{
+    const unsigned char *p = data;
+    int                  len;
+
+    /* Taken from https://github.com/rustyrussell/ccan/blob/9d2d2c49f053018724bcc6e37029da10b7c3d60d/ccan/mem/mem.c#L92,
+     * CC-0 licensed. */
+
+    /* Check first 16 bytes manually */
+    for (len = 0; len < 16; len++) {
+        if (!length)
+            return TRUE;
+        if (*p)
+            return FALSE;
+        p++;
+        length--;
+    }
+
+    /* Now we know that's zero, memcmp with self. */
+    return memcmp(data, p, length) == 0;
+}
+
+/**
+ * nm_utils_bin2hexstr_full:
+ * @addr: pointer of @length bytes. If @length is zero, this may
+ *   also be %NULL.
+ * @length: number of bytes in @addr. May also be zero, in which
+ *   case this will return an empty string.
+ * @delimiter: either '\0', otherwise the output string will have the
+ *   given delimiter character between each two hex numbers.
+ * @upper_case: if TRUE, use upper case ASCII characters for hex.
+ * @out: if %NULL, the function will allocate a new buffer of
+ *   either (@length*2+1) or (@length*3) bytes, depending on whether
+ *   a @delimiter is specified. In that case, the allocated buffer will
+ *   be returned and must be freed by the caller.
+ *   If not %NULL, the buffer must already be preallocated and contain
+ *   at least (@length*2+1) or (@length*3) bytes, depending on the delimiter.
+ *   If @length is zero, then of course at least one byte will be allocated
+ *   or @out (if given) must contain at least room for the trailing NUL byte.
+ *
+ * Returns: the binary value converted to a hex string. If @out is given,
+ *   this always returns @out. If @out is %NULL, a newly allocated string
+ *   is returned. This never returns %NULL, for buffers of length zero
+ *   an empty string is returned.
+ */
+char *
+nm_utils_bin2hexstr_full(gconstpointer addr,
+                         gsize         length,
+                         char          delimiter,
+                         gboolean      upper_case,
+                         char *        out)
+{
+    const guint8 *in     = addr;
+    const char *  LOOKUP = upper_case ? "0123456789ABCDEF" : "0123456789abcdef";
+    char *        out0;
+
+    if (out)
+        out0 = out;
+    else {
+        out0 = out =
+            g_new(char, length == 0 ? 1u : (delimiter == '\0' ? length * 2u + 1u : length * 3u));
+    }
+
+    /* @out must contain at least @length*3 bytes if @delimiter is set,
+     * otherwise, @length*2+1. */
+
+    if (length > 0) {
+        nm_assert(in);
+        for (;;) {
+            const guint8 v = *in++;
+
+            *out++ = LOOKUP[v >> 4];
+            *out++ = LOOKUP[v & 0x0F];
+            length--;
+            if (!length)
+                break;
+            if (delimiter)
+                *out++ = delimiter;
+        }
+    }
+
+    *out = '\0';
+    return out0;
+}
+
+guint8 *
+nm_utils_hexstr2bin_full(const char *hexstr,
+                         gboolean    allow_0x_prefix,
+                         gboolean    delimiter_required,
+                         gboolean    hexdigit_pairs_required,
+                         const char *delimiter_candidates,
+                         gsize       required_len,
+                         guint8 *    buffer,
+                         gsize       buffer_len,
+                         gsize *     out_len)
+{
+    const char *in            = hexstr;
+    guint8 *    out           = buffer;
+    gboolean    delimiter_has = TRUE;
+    guint8      delimiter     = '\0';
+    gsize       len;
+
+    nm_assert(hexstr);
+    nm_assert(buffer);
+    nm_assert(required_len > 0 || out_len);
+
+    if (allow_0x_prefix && in[0] == '0' && in[1] == 'x')
+        in += 2;
+
+    while (TRUE) {
+        const guint8 d1 = in[0];
+        guint8       d2;
+        int          i1, i2;
+
+        i1 = nm_utils_hexchar_to_int(d1);
+        if (i1 < 0)
+            goto fail;
+
+        /* If there's no leading zero (ie "aa:b:cc") then fake it */
+        d2 = in[1];
+        if (d2 && (i2 = nm_utils_hexchar_to_int(d2)) >= 0) {
+            *out++ = (i1 << 4) + i2;
+            d2     = in[2];
+            if (!d2)
+                break;
+            in += 2;
+        } else {
+            /* Fake leading zero */
+            *out++ = i1;
+            if (!d2) {
+                if (!delimiter_has || hexdigit_pairs_required) {
+                    /* when using no delimiter, there must be pairs of hex chars */
+                    goto fail;
+                }
+                break;
+            } else if (hexdigit_pairs_required)
+                goto fail;
+            in += 1;
+        }
+
+        if (--buffer_len == 0)
+            goto fail;
+
+        if (delimiter_has) {
+            if (d2 != delimiter) {
+                if (delimiter)
+                    goto fail;
+                if (delimiter_candidates) {
+                    while (delimiter_candidates[0]) {
+                        if (delimiter_candidates++[0] == d2)
+                            delimiter = d2;
+                    }
+                }
+                if (!delimiter) {
+                    if (delimiter_required)
+                        goto fail;
+                    delimiter_has = FALSE;
+                    continue;
+                }
+            }
+            in++;
+        }
+    }
+
+    len = out - buffer;
+    if (required_len == 0 || len == required_len) {
+        NM_SET_OUT(out_len, len);
+        return buffer;
+    }
+
+fail:
+    NM_SET_OUT(out_len, 0);
+    return NULL;
+}
+
+guint8 *
+nm_utils_hexstr2bin_alloc(const char *hexstr,
+                          gboolean    allow_0x_prefix,
+                          gboolean    delimiter_required,
+                          const char *delimiter_candidates,
+                          gsize       required_len,
+                          gsize *     out_len)
+{
+    guint8 *buffer;
+    gsize   buffer_len, len;
+
+    if (G_UNLIKELY(!hexstr)) {
+        NM_SET_OUT(out_len, 0);
+        g_return_val_if_fail(hexstr, NULL);
+    }
+
+    nm_assert(required_len > 0 || out_len);
+
+    if (allow_0x_prefix && hexstr[0] == '0' && hexstr[1] == 'x')
+        hexstr += 2;
+
+    if (!hexstr[0])
+        goto fail;
+
+    if (required_len > 0)
+        buffer_len = required_len;
+    else
+        buffer_len = strlen(hexstr) / 2 + 3;
+
+    buffer = g_malloc(buffer_len);
+
+    if (nm_utils_hexstr2bin_full(hexstr,
+                                 FALSE,
+                                 delimiter_required,
+                                 FALSE,
+                                 delimiter_candidates,
+                                 required_len,
+                                 buffer,
+                                 buffer_len,
+                                 &len)) {
+        NM_SET_OUT(out_len, len);
+        return buffer;
+    }
+
+    g_free(buffer);
+
+fail:
+    NM_SET_OUT(out_len, 0);
+    return NULL;
+}
+
+/*****************************************************************************/
+
+GVariant *
+nm_utils_gvariant_vardict_filter(GVariant *src,
+                                 gboolean (*filter_fcn)(const char *key,
+                                                        GVariant *  val,
+                                                        char **     out_key,
+                                                        GVariant ** out_val,
+                                                        gpointer    user_data),
+                                 gpointer user_data)
+{
+    GVariantIter    iter;
+    GVariantBuilder builder;
+    const char *    key;
+    GVariant *      val;
+
+    g_return_val_if_fail(src && g_variant_is_of_type(src, G_VARIANT_TYPE_VARDICT), NULL);
+    g_return_val_if_fail(filter_fcn, NULL);
+
+    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
+
+    g_variant_iter_init(&iter, src);
+    while (g_variant_iter_next(&iter, "{&sv}", &key, &val)) {
+        _nm_unused gs_unref_variant GVariant *val_free = val;
+        gs_free char *                        key2     = NULL;
+        gs_unref_variant GVariant *val2                = NULL;
+
+        if (filter_fcn(key, val, &key2, &val2, user_data)) {
+            g_variant_builder_add(&builder, "{sv}", key2 ?: key, val2 ?: val);
+        }
+    }
+
+    return g_variant_builder_end(&builder);
+}
+
+static gboolean
+_gvariant_vardict_filter_drop_one(const char *key,
+                                  GVariant *  val,
+                                  char **     out_key,
+                                  GVariant ** out_val,
+                                  gpointer    user_data)
+{
+    return !nm_streq(key, user_data);
+}
+
+GVariant *
+nm_utils_gvariant_vardict_filter_drop_one(GVariant *src, const char *key)
+{
+    return nm_utils_gvariant_vardict_filter(src, _gvariant_vardict_filter_drop_one, (gpointer) key);
+}
+
+/*****************************************************************************/
+
+static gboolean
+debug_key_matches(const char *key, const char *token, guint length)
+{
+    /* may not call GLib functions: see note in g_parse_debug_string() */
+    for (; length; length--, key++, token++) {
+        char k = (*key == '_') ? '-' : g_ascii_tolower(*key);
+        char t = (*token == '_') ? '-' : g_ascii_tolower(*token);
+
+        if (k != t)
+            return FALSE;
+    }
+
+    return *key == '\0';
+}
+
+/**
+ * nm_utils_parse_debug_string:
+ * @string: the string to parse
+ * @keys: the debug keys
+ * @nkeys: number of entries in @keys
+ *
+ * Similar to g_parse_debug_string(), but does not special
+ * case "help" or "all".
+ *
+ * Returns: the flags
+ */
+guint
+nm_utils_parse_debug_string(const char *string, const GDebugKey *keys, guint nkeys)
+{
+    guint       i;
+    guint       result = 0;
+    const char *q;
+
+    if (string == NULL)
+        return 0;
+
+    while (*string) {
+        q = strpbrk(string, ":;, \t");
+        if (!q)
+            q = string + strlen(string);
+
+        for (i = 0; i < nkeys; i++) {
+            if (debug_key_matches(keys[i].key, string, q - string))
+                result |= keys[i].value;
+        }
+
+        string = q;
+        if (*string)
+            string++;
+    }
+
+    return result;
+}
+
+/*****************************************************************************/
+
+GSource *_nm_g_source_sentinel[] = {
+    NULL,
+};
+
+GSource *
+_nm_g_source_sentinel_get_init(GSource **p_source)
+{
+    static const GSourceFuncs source_funcs = {
+        NULL,
+    };
+    GSource *source;
+
+    source = g_source_new((GSourceFuncs *) &source_funcs, sizeof(GSource));
+    g_source_set_priority(source, G_PRIORITY_DEFAULT_IDLE);
+    g_source_set_name(source, "nm_g_source_sentinel");
+
+    if (!g_atomic_pointer_compare_and_exchange(p_source, NULL, source)) {
+        g_source_unref(source);
+        source = g_atomic_pointer_get(p_source);
+        nm_assert(source);
+    }
+
+    return source;
+}
+
+/*****************************************************************************/
+
+GSource *
+nm_g_idle_source_new(int            priority,
+                     GSourceFunc    func,
+                     gpointer       user_data,
+                     GDestroyNotify destroy_notify)
+{
+    GSource *source;
+
+    source = g_idle_source_new();
+    if (priority != G_PRIORITY_DEFAULT)
+        g_source_set_priority(source, priority);
+    g_source_set_callback(source, func, user_data, destroy_notify);
+    return source;
+}
+
+GSource *
+nm_g_timeout_source_new(guint          timeout_msec,
+                        int            priority,
+                        GSourceFunc    func,
+                        gpointer       user_data,
+                        GDestroyNotify destroy_notify)
+{
+    GSource *source;
+
+    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);
+    return source;
+}
+
+GSource *
+nm_g_timeout_source_new_seconds(guint          timeout_sec,
+                                int            priority,
+                                GSourceFunc    func,
+                                gpointer       user_data,
+                                GDestroyNotify destroy_notify)
+{
+    GSource *source;
+
+    source = g_timeout_source_new_seconds(timeout_sec);
+    if (priority != G_PRIORITY_DEFAULT)
+        g_source_set_priority(source, priority);
+    g_source_set_callback(source, func, user_data, destroy_notify);
+    return source;
+}
+
+GSource *
+nm_g_unix_signal_source_new(int            signum,
+                            int            priority,
+                            GSourceFunc    handler,
+                            gpointer       user_data,
+                            GDestroyNotify notify)
+{
+    GSource *source;
+
+    source = g_unix_signal_source_new(signum);
+
+    if (priority != G_PRIORITY_DEFAULT)
+        g_source_set_priority(source, priority);
+    g_source_set_callback(source, handler, user_data, notify);
+    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, ...)                                                                       \
+    G_STMT_START                                                                                 \
+    {                                                                                            \
+        if (FALSE) {                                                                             \
+            gint64 _ts = g_get_monotonic_time() / 100;                                           \
+                                                                                                 \
+            g_printerr(">>>> [%" G_GINT64_FORMAT ".%05" G_GINT64_FORMAT "] [src:%p]: " fmt "\n", \
+                       _ts / 10000,                                                              \
+                       _ts % 10000,                                                              \
+                       (ctx_src),                                                                \
+                       ##__VA_ARGS__);                                                           \
+        }                                                                                        \
+    }                                                                                            \
+    G_STMT_END
+
+typedef struct {
+    int   fd;
+    guint events;
+    guint registered_events;
+    union {
+        int  one;
+        int *many;
+    } idx;
+    gpointer tag;
+    bool     stale : 1;
+    bool     has_many_idx : 1;
+} PollData;
+
+typedef struct {
+    GSource       source;
+    GMainContext *context;
+    GHashTable *  fds;
+    GPollFD *     fds_arr;
+    guint         fds_len;
+    int           max_priority;
+    bool          acquired : 1;
+} CtxIntegSource;
+
+static void
+_poll_data_free(gpointer user_data)
+{
+    PollData *poll_data = user_data;
+
+    if (poll_data->has_many_idx)
+        g_free(poll_data->idx.many);
+    nm_g_slice_free(poll_data);
+}
+
+static void
+_ctx_integ_source_reacquire(CtxIntegSource *ctx_src)
+{
+    if (G_LIKELY(ctx_src->acquired && g_main_context_is_owner(ctx_src->context)))
+        return;
+
+    /* the parent context now iterates on a different thread.
+     * We need to release and reacquire the inner context. */
+
+    if (ctx_src->acquired)
+        g_main_context_release(ctx_src->context);
+
+    if (G_UNLIKELY(!g_main_context_acquire(ctx_src->context))) {
+        /* Nobody is supposed to reacquire the context while we use it. This is a bug
+         * of the user. */
+        ctx_src->acquired = FALSE;
+        g_return_if_reached();
+    }
+    ctx_src->acquired = TRUE;
+}
+
+static gboolean
+_ctx_integ_source_prepare(GSource *source, int *out_timeout)
+{
+    CtxIntegSource *ctx_src = ((CtxIntegSource *) source);
+    int             max_priority;
+    int             timeout = -1;
+    gboolean        any_ready;
+    GHashTableIter  h_iter;
+    PollData *      poll_data;
+    gboolean        fds_changed;
+    GPollFD         new_fds_stack[300u / sizeof(GPollFD)];
+    gs_free GPollFD *new_fds_heap = NULL;
+    GPollFD *        new_fds;
+    guint            new_fds_len;
+    guint            new_fds_alloc;
+    guint            i;
+
+    _CTX_LOG("prepare...");
+
+    _ctx_integ_source_reacquire(ctx_src);
+
+    any_ready = g_main_context_prepare(ctx_src->context, &max_priority);
+
+    new_fds_alloc = NM_MAX(G_N_ELEMENTS(new_fds_stack), ctx_src->fds_len);
+
+    if (new_fds_alloc > G_N_ELEMENTS(new_fds_stack)) {
+        new_fds_heap = g_new(GPollFD, new_fds_alloc);
+        new_fds      = new_fds_heap;
+    } else
+        new_fds = new_fds_stack;
+
+    for (;;) {
+        int l;
+
+        nm_assert(new_fds_alloc <= (guint) G_MAXINT);
+
+        l = g_main_context_query(ctx_src->context,
+                                 max_priority,
+                                 &timeout,
+                                 new_fds,
+                                 (int) new_fds_alloc);
+        nm_assert(l >= 0);
+
+        new_fds_len = (guint) l;
+
+        if (G_LIKELY(new_fds_len <= new_fds_alloc))
+            break;
+
+        new_fds_alloc = new_fds_len;
+        g_free(new_fds_heap);
+        new_fds_heap = g_new(GPollFD, new_fds_alloc);
+        new_fds      = new_fds_heap;
+    }
+
+    fds_changed = FALSE;
+    if (new_fds_len != ctx_src->fds_len)
+        fds_changed = TRUE;
+    else {
+        for (i = 0; i < new_fds_len; i++) {
+            if (new_fds[i].fd != ctx_src->fds_arr[i].fd
+                || new_fds[i].events != ctx_src->fds_arr[i].events) {
+                fds_changed = TRUE;
+                break;
+            }
+        }
+    }
+
+    if (G_UNLIKELY(fds_changed)) {
+        g_free(ctx_src->fds_arr);
+        ctx_src->fds_len = new_fds_len;
+        if (G_LIKELY(new_fds == new_fds_stack) || new_fds_alloc != new_fds_len)
+            ctx_src->fds_arr = nm_memdup(new_fds, sizeof(*new_fds) * new_fds_len);
+        else
+            ctx_src->fds_arr = g_steal_pointer(&new_fds_heap);
+
+        g_hash_table_iter_init(&h_iter, ctx_src->fds);
+        while (g_hash_table_iter_next(&h_iter, (gpointer *) &poll_data, NULL))
+            poll_data->stale = TRUE;
+
+        for (i = 0; i < ctx_src->fds_len; i++) {
+            const GPollFD *fd = &ctx_src->fds_arr[i];
+
+            poll_data = g_hash_table_lookup(ctx_src->fds, &fd->fd);
+
+            if (G_UNLIKELY(!poll_data)) {
+                poll_data  = g_slice_new(PollData);
+                *poll_data = (PollData){
+                    .fd                = fd->fd,
+                    .idx.one           = i,
+                    .has_many_idx      = FALSE,
+                    .events            = fd->events,
+                    .registered_events = 0,
+                    .tag               = NULL,
+                    .stale             = FALSE,
+                };
+                g_hash_table_add(ctx_src->fds, poll_data);
+                nm_assert(poll_data == g_hash_table_lookup(ctx_src->fds, &fd->fd));
+                continue;
+            }
+
+            if (G_LIKELY(poll_data->stale)) {
+                if (poll_data->has_many_idx) {
+                    g_free(poll_data->idx.many);
+                    poll_data->has_many_idx = FALSE;
+                }
+                poll_data->events  = fd->events;
+                poll_data->idx.one = i;
+                poll_data->stale   = FALSE;
+                continue;
+            }
+
+            /* How odd. We have duplicate FDs. In fact, currently g_main_context_query() always
+             * coalesces the FDs and this cannot happen. However, that is not documented behavior,
+             * so we should not rely on that. So we need to keep a list of indexes... */
+            poll_data->events |= fd->events;
+            if (!poll_data->has_many_idx) {
+                int idx0;
+
+                idx0                    = poll_data->idx.one;
+                poll_data->has_many_idx = TRUE;
+                poll_data->idx.many     = g_new(int, 4);
+                poll_data->idx.many[0]  = 2; /* number allocated */
+                poll_data->idx.many[1]  = 2; /* number used */
+                poll_data->idx.many[2]  = idx0;
+                poll_data->idx.many[3]  = i;
+            } else {
+                if (poll_data->idx.many[0] == poll_data->idx.many[1]) {
+                    poll_data->idx.many[0] *= 2;
+                    poll_data->idx.many =
+                        g_realloc(poll_data->idx.many, sizeof(int) * (2 + poll_data->idx.many[0]));
+                }
+                poll_data->idx.many[2 + poll_data->idx.many[1]] = i;
+                poll_data->idx.many[1]++;
+            }
+        }
+
+        g_hash_table_iter_init(&h_iter, ctx_src->fds);
+        while (g_hash_table_iter_next(&h_iter, (gpointer *) &poll_data, NULL)) {
+            if (poll_data->stale) {
+                nm_assert(poll_data->tag);
+                nm_assert(poll_data->events == poll_data->registered_events);
+                _CTX_LOG("prepare: remove poll fd=%d, events=0x%x",
+                         poll_data->fd,
+                         poll_data->events);
+                g_source_remove_unix_fd(&ctx_src->source, poll_data->tag);
+                g_hash_table_iter_remove(&h_iter);
+                continue;
+            }
+            if (!poll_data->tag) {
+                _CTX_LOG("prepare: add poll fd=%d, events=0x%x", poll_data->fd, poll_data->events);
+                poll_data->registered_events = poll_data->events;
+                poll_data->tag               = g_source_add_unix_fd(&ctx_src->source,
+                                                      poll_data->fd,
+                                                      poll_data->registered_events);
+                continue;
+            }
+            if (poll_data->registered_events != poll_data->events) {
+                _CTX_LOG("prepare: update poll fd=%d, events=0x%x",
+                         poll_data->fd,
+                         poll_data->events);
+                poll_data->registered_events = poll_data->events;
+                g_source_modify_unix_fd(&ctx_src->source,
+                                        poll_data->tag,
+                                        poll_data->registered_events);
+            }
+        }
+    }
+
+    NM_SET_OUT(out_timeout, timeout);
+    ctx_src->max_priority = max_priority;
+
+    _CTX_LOG("prepare: done, any-ready=%d, timeout=%d, max-priority=%d",
+             any_ready,
+             timeout,
+             max_priority);
+
+    /* we always need to poll, because we have some file descriptors. */
+    return FALSE;
+}
+
+static gboolean
+_ctx_integ_source_check(GSource *source)
+{
+    CtxIntegSource *ctx_src = ((CtxIntegSource *) source);
+    GHashTableIter  h_iter;
+    gboolean        some_ready;
+    PollData *      poll_data;
+
+    nm_assert(ctx_src->context);
+
+    _CTX_LOG("check");
+
+    _ctx_integ_source_reacquire(ctx_src);
+
+    g_hash_table_iter_init(&h_iter, ctx_src->fds);
+    while (g_hash_table_iter_next(&h_iter, (gpointer *) &poll_data, NULL)) {
+        guint revents;
+
+        revents = g_source_query_unix_fd(&ctx_src->source, poll_data->tag);
+        if (G_UNLIKELY(poll_data->has_many_idx)) {
+            int  num   = poll_data->idx.many[1];
+            int *p_idx = &poll_data->idx.many[2];
+
+            for (; num > 0; num--, p_idx++)
+                ctx_src->fds_arr[*p_idx].revents = revents;
+        } else
+            ctx_src->fds_arr[poll_data->idx.one].revents = revents;
+    }
+
+    nm_assert(ctx_src->fds_len <= (guint) G_MAXINT);
+
+    some_ready = g_main_context_check(ctx_src->context,
+                                      ctx_src->max_priority,
+                                      ctx_src->fds_arr,
+                                      (int) ctx_src->fds_len);
+
+    _CTX_LOG("check (some-ready=%d)...", some_ready);
+
+    return some_ready;
+}
+
+static gboolean
+_ctx_integ_source_dispatch(GSource *source, GSourceFunc callback, gpointer user_data)
+{
+    CtxIntegSource *ctx_src = ((CtxIntegSource *) source);
+
+    nm_assert(ctx_src->context);
+
+    _ctx_integ_source_reacquire(ctx_src);
+
+    _CTX_LOG("dispatch");
+
+    g_main_context_dispatch(ctx_src->context);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+_ctx_integ_source_finalize(GSource *source)
+{
+    CtxIntegSource *ctx_src = ((CtxIntegSource *) source);
+    GHashTableIter  h_iter;
+    PollData *      poll_data;
+
+    g_return_if_fail(ctx_src->context);
+
+    _CTX_LOG("finalize...");
+
+    g_hash_table_iter_init(&h_iter, ctx_src->fds);
+    while (g_hash_table_iter_next(&h_iter, (gpointer *) &poll_data, NULL)) {
+        nm_assert(poll_data->tag);
+        _CTX_LOG("prepare: remove poll fd=%d, events=0x%x", poll_data->fd, poll_data->events);
+        g_source_remove_unix_fd(&ctx_src->source, poll_data->tag);
+        g_hash_table_iter_remove(&h_iter);
+    }
+
+    nm_clear_pointer(&ctx_src->fds, g_hash_table_unref);
+    nm_clear_g_free(&ctx_src->fds_arr);
+    ctx_src->fds_len = 0;
+
+    if (ctx_src->acquired) {
+        ctx_src->acquired = FALSE;
+        g_main_context_release(ctx_src->context);
+    }
+
+    nm_clear_pointer(&ctx_src->context, g_main_context_unref);
+}
+
+static GSourceFuncs ctx_integ_source_funcs = {
+    .prepare  = _ctx_integ_source_prepare,
+    .check    = _ctx_integ_source_check,
+    .dispatch = _ctx_integ_source_dispatch,
+    .finalize = _ctx_integ_source_finalize,
+};
+
+/**
+ * nm_utils_g_main_context_create_integrate_source:
+ * @inner_context: the inner context that will be integrated to an
+ *   outer #GMainContext.
+ *
+ * By integrating the inner context with an outer context, when iterating the outer
+ * context sources on the inner context will be dispatched. Note that while the
+ * created source exists, the @inner_context will be acquired. The user gets restricted
+ * what to do with the inner context. In particular while the inner context is integrated,
+ * the user should not acquire the inner context again or explicitly iterate it. What
+ * the user of course still can (and wants to) do is attaching new sources to the inner
+ * context.
+ *
+ * Note that GSource has a priority. While each context dispatches events based on
+ * their source's priorities, the outer context dispatches to the inner context
+ * only with one priority (the priority of the created source). That is, the sources
+ * from the two contexts are kept separate and are not sorted by their priorities.
+ *
+ * Returns: a newly created GSource that should be attached to the
+ *   outer context.
+ */
+GSource *
+nm_utils_g_main_context_create_integrate_source(GMainContext *inner_context)
+{
+    CtxIntegSource *ctx_src;
+
+    g_return_val_if_fail(inner_context, NULL);
+
+    if (!g_main_context_acquire(inner_context)) {
+        /* We require to acquire the context while it's integrated. We need to keep it acquired
+         * for the entire duration.
+         *
+         * This is also necessary because g_source_attach() only wakes up the context, if
+         * the context is currently acquired. */
+        g_return_val_if_reached(NULL);
+    }
+
+    ctx_src = (CtxIntegSource *) g_source_new(&ctx_integ_source_funcs, sizeof(CtxIntegSource));
+
+    g_source_set_name(&ctx_src->source, "ContextIntegrateSource");
+
+    ctx_src->context  = g_main_context_ref(inner_context);
+    ctx_src->fds      = g_hash_table_new_full(nm_pint_hash, nm_pint_equal, _poll_data_free, NULL);
+    ctx_src->fds_len  = 0;
+    ctx_src->fds_arr  = NULL;
+    ctx_src->acquired = TRUE;
+    ctx_src->max_priority = G_MAXINT;
+
+    _CTX_LOG("create new integ-source for %p", inner_context);
+
+    return &ctx_src->source;
+}
+
+/*****************************************************************************/
+
+void
+nm_utils_ifname_cpy(char *dst, const char *name)
+{
+    int i;
+
+    g_return_if_fail(dst);
+    g_return_if_fail(name && name[0]);
+
+    nm_assert(nm_utils_ifname_valid_kernel(name, NULL));
+
+    /* ensures NUL padding of the entire IFNAMSIZ buffer. */
+
+    for (i = 0; i < (int) IFNAMSIZ && name[i] != '\0'; i++)
+        dst[i] = name[i];
+
+    nm_assert(name[i] == '\0');
+
+    for (; i < (int) IFNAMSIZ; i++)
+        dst[i] = '\0';
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_utils_ifname_valid_kernel(const char *name, GError **error)
+{
+    int i;
+
+    /* This function follows kernel's interface validation
+     * function dev_valid_name() in net/core/dev.c.
+     */
+
+    if (!name) {
+        g_set_error_literal(error,
+                            NM_UTILS_ERROR,
+                            NM_UTILS_ERROR_UNKNOWN,
+                            _("interface name is missing"));
+        return FALSE;
+    }
+
+    if (name[0] == '\0') {
+        g_set_error_literal(error,
+                            NM_UTILS_ERROR,
+                            NM_UTILS_ERROR_UNKNOWN,
+                            _("interface name is too short"));
+        return FALSE;
+    }
+
+    if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) {
+        g_set_error_literal(error,
+                            NM_UTILS_ERROR,
+                            NM_UTILS_ERROR_UNKNOWN,
+                            _("interface name is reserved"));
+        return FALSE;
+    }
+
+    for (i = 0; i < IFNAMSIZ; i++) {
+        char ch = name[i];
+
+        if (ch == '\0')
+            return TRUE;
+        if (NM_IN_SET(ch, '/', ':') || g_ascii_isspace(ch)) {
+            g_set_error_literal(error,
+                                NM_UTILS_ERROR,
+                                NM_UTILS_ERROR_UNKNOWN,
+                                _("interface name contains an invalid character"));
+            return FALSE;
+        }
+    }
+
+    g_set_error_literal(error,
+                        NM_UTILS_ERROR,
+                        NM_UTILS_ERROR_UNKNOWN,
+                        _("interface name is longer than 15 characters"));
+    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)
+{
+    const char *ch;
+
+    /* OVS actually accepts a wider range of chars (all printable UTF-8 chars),
+     * NetworkManager restricts this to ASCII char as it's a safer option for
+     * now since OVS is not well documented on this matter.
+     **/
+    for (ch = name; *ch; ++ch) {
+        if (*ch == '\\' || *ch == '/' || !g_ascii_isgraph(*ch)) {
+            g_set_error_literal(error,
+                                NM_UTILS_ERROR,
+                                NM_UTILS_ERROR_UNKNOWN,
+                                _("interface name must be alphanumerical with "
+                                  "no forward or backward slashes"));
+            return FALSE;
+        }
+    };
+    return TRUE;
+}
+
+gboolean
+nm_utils_ifname_valid(const char *name, NMUtilsIfaceType type, GError **error)
+{
+    g_return_val_if_fail(!error || !(*error), FALSE);
+
+    if (!name || !(name[0])) {
+        g_set_error_literal(error,
+                            NM_UTILS_ERROR,
+                            NM_UTILS_ERROR_UNKNOWN,
+                            _("interface name must not be empty"));
+        return FALSE;
+    }
+
+    if (!g_utf8_validate(name, -1, NULL)) {
+        g_set_error_literal(error,
+                            NM_UTILS_ERROR,
+                            NM_UTILS_ERROR_UNKNOWN,
+                            _("interface name must be UTF-8 encoded"));
+        return FALSE;
+    }
+
+    switch (type) {
+    case NMU_IFACE_KERNEL:
+        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;
+
+    nm_assert(available < G_MAXULONG);
+
+    va_start(args, format);
+    l = g_vsnprintf(strbuf->_priv_allocated > 0 ? &strbuf->_priv_str[strbuf->_priv_len] : NULL,
+                    available,
+                    format,
+                    args);
+    va_end(args);
+
+    nm_assert(l >= 0);
+    nm_assert(l < G_MAXINT);
+
+    if ((gsize) l >= available) {
+        gsize l2;
+
+        if (l == 0)
+            return;
+
+        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;
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_indirect_g_free:
+ * @arg: a pointer to a pointer that is to be freed.
+ *
+ * This does the same as nm_clear_g_free(arg) (g_clear_pointer (arg, g_free)).
+ * This is for example useful when you have a GArray with pointers and a
+ * clear function to free them. g_array_set_clear_func()'s destroy notify
+ * function gets a pointer to the array location, so we have to follow
+ * the first pointer.
+ */
+void
+nm_indirect_g_free(gpointer arg)
+{
+    gpointer *p = arg;
+
+    nm_clear_g_free(p);
+}
+
+/*****************************************************************************/
+
+static char *
+attribute_escape(const char *src, char c1, char c2)
+{
+    char *ret, *dest;
+
+    dest = ret = g_malloc(strlen(src) * 2 + 1);
+
+    while (*src) {
+        if (*src == c1 || *src == c2 || *src == '\\')
+            *dest++ = '\\';
+        *dest++ = *src++;
+    }
+    *dest++ = '\0';
+
+    return ret;
+}
+
+void
+_nm_utils_format_variant_attributes_full(GString *                            str,
+                                         const NMUtilsNamedValue *            values,
+                                         guint                                num_values,
+                                         const NMVariantAttributeSpec *const *spec,
+                                         char                                 attr_separator,
+                                         char                                 key_value_separator)
+{
+    const NMVariantAttributeSpec *const *s;
+    const char *                         name, *value;
+    GVariant *                           variant;
+    char *                               escaped;
+    char                                 buf[64];
+    char                                 sep = 0;
+    guint                                i;
+
+    for (i = 0; i < num_values; i++) {
+        name    = values[i].name;
+        variant = values[i].value_ptr;
+        value   = NULL;
+        s       = NULL;
+
+        if (spec) {
+            for (s = spec; *s; s++) {
+                if (nm_streq0((*s)->name, name))
+                    break;
+            }
+
+            if (!*s)
+                continue;
+        }
+
+        if (g_variant_is_of_type(variant, G_VARIANT_TYPE_UINT32))
+            value = nm_sprintf_buf(buf, "%u", g_variant_get_uint32(variant));
+        else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_INT32))
+            value = nm_sprintf_buf(buf, "%d", (int) g_variant_get_int32(variant));
+        else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_UINT64))
+            value = nm_sprintf_buf(buf, "%" G_GUINT64_FORMAT, g_variant_get_uint64(variant));
+        else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_BYTE))
+            value = nm_sprintf_buf(buf, "%hhu", g_variant_get_byte(variant));
+        else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_BOOLEAN))
+            value = g_variant_get_boolean(variant) ? "true" : "false";
+        else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_STRING))
+            value = g_variant_get_string(variant, NULL);
+        else if (g_variant_is_of_type(variant, G_VARIANT_TYPE_BYTESTRING)) {
+            /* FIXME: there is no guarantee that the byte array
+             * is valid UTF-8.*/
+            value = g_variant_get_bytestring(variant);
+        } else
+            continue;
+
+        if (sep)
+            g_string_append_c(str, sep);
+
+        escaped = attribute_escape(name, attr_separator, key_value_separator);
+        g_string_append(str, escaped);
+        g_free(escaped);
+
+        if (!s || !*s || !(*s)->no_value) {
+            g_string_append_c(str, key_value_separator);
+
+            escaped = attribute_escape(value, attr_separator, key_value_separator);
+            g_string_append(str, escaped);
+            g_free(escaped);
+        }
+
+        sep = attr_separator;
+    }
+}
+
+char *
+_nm_utils_format_variant_attributes(GHashTable *                         attributes,
+                                    const NMVariantAttributeSpec *const *spec,
+                                    char                                 attr_separator,
+                                    char                                 key_value_separator)
+{
+    gs_free NMUtilsNamedValue *values_free = NULL;
+    NMUtilsNamedValue          values_prepared[20];
+    const NMUtilsNamedValue *  values;
+    GString *                  str = NULL;
+    guint                      len;
+
+    g_return_val_if_fail(attr_separator, NULL);
+    g_return_val_if_fail(key_value_separator, NULL);
+
+    if (!attributes)
+        return NULL;
+
+    values = nm_utils_named_values_from_strdict(attributes, &len, values_prepared, &values_free);
+    if (len == 0)
+        return NULL;
+
+    str = g_string_new("");
+    _nm_utils_format_variant_attributes_full(str,
+                                             values,
+                                             len,
+                                             spec,
+                                             attr_separator,
+                                             key_value_separator);
+    return g_string_free(str, FALSE);
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_utils_is_localhost(const char *name)
+{
+    static const char *const NAMES[] = {
+        "localhost",
+        "localhost4",
+        "localhost6",
+        "localhost.localdomain",
+        "localhost4.localdomain4",
+        "localhost6.localdomain6",
+    };
+    gsize name_len;
+    int   i;
+
+    if (!name)
+        return FALSE;
+
+    /* This tries to identify local host and domain names
+     * described in RFC6761 plus the redhatism of localdomain.
+     *
+     * Similar to systemd's is_localhost(). */
+
+    name_len = strlen(name);
+
+    if (name_len == 0)
+        return FALSE;
+
+    if (name[name_len - 1] == '.') {
+        /* one trailing dot is fine. Hide it. */
+        name_len--;
+    }
+
+    for (i = 0; i < (int) G_N_ELEMENTS(NAMES); i++) {
+        const char *n = NAMES[i];
+        gsize       l = strlen(n);
+        gsize       s;
+
+        if (name_len < l)
+            continue;
+
+        s = name_len - l;
+
+        if (g_ascii_strncasecmp(&name[s], n, l) != 0)
+            continue;
+
+        /* we accept the name if it is equal to one of the well-known names,
+         * or if it is some prefix, a '.' and the well-known name. */
+        if (s == 0)
+            return TRUE;
+        if (name[s - 1] == '.')
+            return TRUE;
+    }
+
+    return FALSE;
+}
+
+gboolean
+nm_utils_is_specific_hostname(const char *name)
+{
+    if (nm_str_is_empty(name))
+        return FALSE;
+
+    if (nm_streq(name, "(none)")) {
+        /* This is not a special hostname. Probably an artefact by somebody wrongly
+         * printing NULL. */
+        return FALSE;
+    }
+
+    if (nm_utils_is_localhost(name))
+        return FALSE;
+
+    /* FIXME: properly validate the hostname, like systemd's hostname_is_valid() */
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+/* taken from systemd's uid_to_name(). */
+char *
+nm_utils_uid_to_name(uid_t uid)
+{
+    gs_free char *buf_heap = NULL;
+    char          buf_stack[4096];
+    gsize         bufsize;
+    char *        buf;
+
+    bufsize = sizeof(buf_stack);
+    buf     = buf_stack;
+
+    for (;;) {
+        struct passwd  pwbuf;
+        struct passwd *pw = NULL;
+        int            r;
+
+        r = getpwuid_r(uid, &pwbuf, buf, bufsize, &pw);
+        if (r == 0 && pw)
+            return nm_strdup_not_empty(pw->pw_name);
+
+        if (r != ERANGE)
+            return NULL;
+
+        if (bufsize > G_MAXSIZE / 2u)
+            return NULL;
+
+        bufsize *= 2u;
+        g_free(buf_heap);
+        buf_heap = g_malloc(bufsize);
+        buf      = buf_heap;
+    }
+}
+
+/* taken from systemd's nss_user_record_by_name() */
+gboolean
+nm_utils_name_to_uid(const char *name, uid_t *out_uid)
+{
+    gs_free char *buf_heap = NULL;
+    char          buf_stack[4096];
+    gsize         bufsize;
+    char *        buf;
+
+    if (!name)
+        return nm_assert_unreachable_val(FALSE);
+
+    bufsize = sizeof(buf_stack);
+    buf     = buf_stack;
+
+    for (;;) {
+        struct passwd *result;
+        struct passwd  pwd;
+        int            r;
+
+        r = getpwnam_r(name, &pwd, buf, bufsize, &result);
+        if (r == 0) {
+            if (!result)
+                return FALSE;
+            NM_SET_OUT(out_uid, pwd.pw_uid);
+            return TRUE;
+        }
+
+        if (r != ERANGE)
+            return FALSE;
+
+        if (bufsize > G_MAXSIZE / 2u)
+            return FALSE;
+
+        bufsize *= 2u;
+        g_free(buf_heap);
+        buf_heap = g_malloc(bufsize);
+        buf      = buf_heap;
+    }
+}
+
+/*****************************************************************************/
+
+static double
+_exp10(guint16 ex)
+{
+    double v;
+
+    if (ex == 0)
+        return 1.0;
+
+    v = _exp10(ex / 2);
+    v = v * v;
+    if (ex % 2)
+        v *= 10;
+    return v;
+}
+
+/*
+ * nm_utils_exp10:
+ * @ex: the exponent
+ *
+ * Returns: 10^ex, or pow(10, ex), or exp10(ex).
+ */
+double
+nm_utils_exp10(gint16 ex)
+{
+    if (ex >= 0)
+        return _exp10(ex);
+    return 1.0 / _exp10(-((gint32) ex));
+}
+
+/*****************************************************************************/
+
+gboolean
+_nm_utils_is_empty_ssid_arr(const guint8 *ssid, gsize len)
+{
+    /* Single white space is for Linksys APs */
+    if (len == 1 && ssid[0] == ' ')
+        return TRUE;
+
+    /* Otherwise, if the entire ssid is 0, we assume it is hidden */
+    while (len--) {
+        if (ssid[len] != '\0')
+            return FALSE;
+    }
+    return TRUE;
+}
+
+gboolean
+_nm_utils_is_empty_ssid_gbytes(GBytes *ssid)
+{
+    const guint8 *p;
+    gsize         l;
+
+    g_return_val_if_fail(ssid, FALSE);
+
+    p = g_bytes_get_data(ssid, &l);
+    return _nm_utils_is_empty_ssid_arr(p, l);
+}
+
+char *
+_nm_utils_ssid_to_string_arr(const guint8 *ssid, gsize len)
+{
+    gs_free char *s_copy = NULL;
+    const char *  s_cnst;
+
+    if (len == 0)
+        return g_strdup("(empty)");
+
+    s_cnst =
+        nm_utils_buf_utf8safe_escape(ssid, len, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL, &s_copy);
+    nm_assert(s_cnst);
+
+    if (_nm_utils_is_empty_ssid_arr(ssid, len))
+        return g_strdup_printf("\"%s\" (hidden)", s_cnst);
+
+    return g_strdup_printf("\"%s\"", s_cnst);
+}
+
+char *
+_nm_utils_ssid_to_string_gbytes(GBytes *ssid)
+{
+    gconstpointer p;
+    gsize         l;
+
+    if (!ssid)
+        return g_strdup("(none)");
+
+    p = g_bytes_get_data(ssid, &l);
+    return _nm_utils_ssid_to_string_arr(p, l);
+}
+
+/*****************************************************************************/
+
+gconstpointer
+nm_utils_ipx_address_clear_host_address(int family, gpointer dst, gconstpointer src, guint8 plen)
+{
+    g_return_val_if_fail(dst, NULL);
+
+    switch (family) {
+    case AF_INET:
+        g_return_val_if_fail(plen <= 32, NULL);
+
+        if (!src) {
+            /* allow "self-assignment", by specifying %NULL as source. */
+            src = dst;
+        }
+
+        *((guint32 *) dst) = nm_utils_ip4_address_clear_host_address(*((guint32 *) src), plen);
+        break;
+    case AF_INET6:
+        nm_utils_ip6_address_clear_host_address(dst, src, plen);
+        break;
+    default:
+        g_return_val_if_reached(NULL);
+    }
+    return dst;
+}
+
+/* nm_utils_ip4_address_clear_host_address:
+ * @addr: source ip6 address
+ * @plen: prefix length of network
+ *
+ * returns: the input address, with the host address set to 0.
+ */
+in_addr_t
+nm_utils_ip4_address_clear_host_address(in_addr_t addr, guint8 plen)
+{
+    return addr & _nm_utils_ip4_prefix_to_netmask(plen);
+}
+
+/* nm_utils_ip6_address_clear_host_address:
+ * @dst: destination output buffer, will contain the network part of the @src address
+ * @src: source ip6 address
+ * @plen: prefix length of network
+ *
+ * Note: this function is self assignment safe, to update @src inplace, set both
+ * @dst and @src to the same destination or set @src NULL.
+ */
+const struct in6_addr *
+nm_utils_ip6_address_clear_host_address(struct in6_addr *      dst,
+                                        const struct in6_addr *src,
+                                        guint8                 plen)
+{
+    g_return_val_if_fail(plen <= 128, NULL);
+    g_return_val_if_fail(dst, NULL);
+
+    if (!src)
+        src = dst;
+
+    if (plen < 128) {
+        guint nbytes = plen / 8;
+        guint nbits  = plen % 8;
+
+        if (nbytes && dst != src)
+            memcpy(dst, src, nbytes);
+        if (nbits) {
+            dst->s6_addr[nbytes] = (src->s6_addr[nbytes] & (0xFF << (8 - nbits)));
+            nbytes++;
+        }
+        if (nbytes <= 15)
+            memset(&dst->s6_addr[nbytes], 0, 16 - nbytes);
+    } else if (src != dst)
+        *dst = *src;
+
+    return dst;
+}
+
+int
+nm_utils_ip6_address_same_prefix_cmp(const struct in6_addr *addr_a,
+                                     const struct in6_addr *addr_b,
+                                     guint8                 plen)
+{
+    int    nbytes;
+    guint8 va, vb, m;
+
+    if (plen >= 128)
+        NM_CMP_DIRECT_MEMCMP(addr_a, addr_b, sizeof(struct in6_addr));
+    else {
+        nbytes = plen / 8;
+        if (nbytes)
+            NM_CMP_DIRECT_MEMCMP(addr_a, addr_b, nbytes);
+
+        plen = plen % 8;
+        if (plen != 0) {
+            m  = ~((1 << (8 - plen)) - 1);
+            va = ((((const guint8 *) addr_a))[nbytes]) & m;
+            vb = ((((const guint8 *) addr_b))[nbytes]) & m;
+            NM_CMP_DIRECT(va, vb);
+        }
+    }
+    return 0;
+}
+
+/*****************************************************************************/
+
+#define IPV6_PROPERTY_DIR "/proc/sys/net/ipv6/conf/"
+#define IPV4_PROPERTY_DIR "/proc/sys/net/ipv4/conf/"
+
+G_STATIC_ASSERT(sizeof(IPV4_PROPERTY_DIR) == sizeof(IPV6_PROPERTY_DIR));
+G_STATIC_ASSERT(NM_STRLEN(IPV6_PROPERTY_DIR) + IFNAMSIZ + 60
+                == NM_UTILS_SYSCTL_IP_CONF_PATH_BUFSIZE);
+
+/**
+ * nm_utils_sysctl_ip_conf_path:
+ * @addr_family: either AF_INET or AF_INET6.
+ * @buf: the output buffer where to write the path. It
+ *   must be at least NM_UTILS_SYSCTL_IP_CONF_PATH_BUFSIZE bytes
+ *   long.
+ * @ifname: an interface name
+ * @property: a property name
+ *
+ * Returns: the path to IPv6 property @property on @ifname. Note that
+ * this returns the input argument @buf.
+ */
+const char *
+nm_utils_sysctl_ip_conf_path(int addr_family, char *buf, const char *ifname, const char *property)
+{
+    int len;
+
+    nm_assert(buf);
+    nm_assert_addr_family(addr_family);
+
+    g_assert(nm_utils_ifname_valid_kernel(ifname, NULL));
+    property = NM_ASSERT_VALID_PATH_COMPONENT(property);
+
+    len = g_snprintf(buf,
+                     NM_UTILS_SYSCTL_IP_CONF_PATH_BUFSIZE,
+                     "%s%s/%s",
+                     addr_family == AF_INET6 ? IPV6_PROPERTY_DIR : IPV4_PROPERTY_DIR,
+                     ifname,
+                     property);
+    g_assert(len < NM_UTILS_SYSCTL_IP_CONF_PATH_BUFSIZE - 1);
+    return buf;
+}
+
+gboolean
+nm_utils_sysctl_ip_conf_is_path(int         addr_family,
+                                const char *path,
+                                const char *ifname,
+                                const char *property)
+{
+    g_return_val_if_fail(path, FALSE);
+    NM_ASSERT_VALID_PATH_COMPONENT(property);
+    g_assert(!ifname || nm_utils_ifname_valid_kernel(ifname, NULL));
+
+    if (addr_family == AF_INET) {
+        if (!g_str_has_prefix(path, IPV4_PROPERTY_DIR))
+            return FALSE;
+        path += NM_STRLEN(IPV4_PROPERTY_DIR);
+    } else if (addr_family == AF_INET6) {
+        if (!g_str_has_prefix(path, IPV6_PROPERTY_DIR))
+            return FALSE;
+        path += NM_STRLEN(IPV6_PROPERTY_DIR);
+    } else
+        g_return_val_if_reached(FALSE);
+
+    if (ifname) {
+        if (!g_str_has_prefix(path, ifname))
+            return FALSE;
+        path += strlen(ifname);
+        if (path[0] != '/')
+            return FALSE;
+        path++;
+    } else {
+        const char *slash;
+        char        buf[IFNAMSIZ];
+        gsize       l;
+
+        slash = strchr(path, '/');
+        if (!slash)
+            return FALSE;
+        l = slash - path;
+        if (l >= IFNAMSIZ)
+            return FALSE;
+        memcpy(buf, path, l);
+        buf[l] = '\0';
+        if (!nm_utils_ifname_valid_kernel(buf, NULL))
+            return FALSE;
+        path = slash + 1;
+    }
+
+    if (!nm_streq(path, property))
+        return FALSE;
+
+    return TRUE;
+}
+
+gboolean
+nm_utils_is_valid_path_component(const char *name)
+{
+    const char *n;
+
+    if (name == NULL || name[0] == '\0')
+        return FALSE;
+
+    if (name[0] == '.') {
+        if (name[1] == '\0')
+            return FALSE;
+        if (name[1] == '.' && name[2] == '\0')
+            return FALSE;
+    }
+    n = name;
+    do {
+        if (*n == '/')
+            return FALSE;
+    } while (*(++n) != '\0');
+
+    return TRUE;
+}
+
+const char *
+NM_ASSERT_VALID_PATH_COMPONENT(const char *name)
+{
+    if (G_LIKELY(nm_utils_is_valid_path_component(name)))
+        return name;
+
+    g_error("FATAL: Failed asserting path component: %s%s%s",
+            NM_PRINT_FMT_QUOTED(name, "\"", name, "\"", "(null)"));
+    g_assert_not_reached();
+}
+
+/*****************************************************************************/
+
+void
+nm_crypto_md5_hash(const guint8 *salt,
+                   gsize         salt_len,
+                   const guint8 *password,
+                   gsize         password_len,
+                   guint8 *      buffer,
+                   gsize         buflen)
+{
+    nm_auto_free_checksum GChecksum *                 ctx = NULL;
+    nm_auto_clear_static_secret_ptr const NMSecretPtr digest =
+        NM_SECRET_PTR_STATIC(NM_UTILS_CHECKSUM_LENGTH_MD5);
+    gsize bufidx = 0;
+    int   i;
+
+    g_return_if_fail(password_len == 0 || password);
+    g_return_if_fail(buffer);
+    g_return_if_fail(buflen > 0);
+    g_return_if_fail(salt_len == 0 || salt);
+
+    ctx = g_checksum_new(G_CHECKSUM_MD5);
+
+    for (;;) {
+        if (password_len > 0)
+            g_checksum_update(ctx, (const guchar *) password, password_len);
+        if (salt_len > 0)
+            g_checksum_update(ctx, (const guchar *) salt, salt_len);
+
+        nm_utils_checksum_get_digest_len(ctx, digest.bin, NM_UTILS_CHECKSUM_LENGTH_MD5);
+
+        for (i = 0; i < NM_UTILS_CHECKSUM_LENGTH_MD5; i++) {
+            if (bufidx >= buflen)
+                return;
+            buffer[bufidx++] = digest.bin[i];
+        }
+
+        g_checksum_reset(ctx);
+        g_checksum_update(ctx, digest.ptr, NM_UTILS_CHECKSUM_LENGTH_MD5);
+    }
+}
+
+/*****************************************************************************/
+
+char *
+nm_utils_get_process_exit_status_desc(int status)
+{
+    if (WIFEXITED(status))
+        return g_strdup_printf("exited with status %d", WEXITSTATUS(status));
+    else if (WIFSIGNALED(status))
+        return g_strdup_printf("killed by signal %d", WTERMSIG(status));
+    else if (WIFSTOPPED(status))
+        return g_strdup_printf("stopped by signal %d", WSTOPSIG(status));
+    else if (WIFCONTINUED(status))
+        return g_strdup("resumed by SIGCONT)");
+    else
+        return g_strdup_printf("exited with unknown status 0x%x", status);
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    CList          lst;
+    gpointer       tls_data;
+    GDestroyNotify destroy_notify;
+} TlsRegData;
+
+static pthread_key_t _tls_reg_key;
+
+static void
+_tls_reg_destroy(gpointer data)
+{
+    CList *     lst_head = data;
+    TlsRegData *entry;
+
+    if (!lst_head)
+        return;
+
+    /* For no strong reason are we destroying the elements in reverse
+     * order than they were added. It seems a bit more sensible (but shouldn't
+     * matter nor should you rely on that). */
+    while ((entry = c_list_last_entry(lst_head, TlsRegData, lst))) {
+        c_list_unlink_stale(&entry->lst);
+        entry->destroy_notify(entry->tls_data);
+        nm_g_slice_free(entry);
+    }
+
+    nm_g_slice_free(lst_head);
+}
+
+static void
+_tls_reg_make_key(void)
+{
+    if (pthread_key_create(&_tls_reg_key, _tls_reg_destroy) != 0)
+        g_return_if_reached();
+}
+
+/**
+ * nm_utils_thread_local_register_destroy:
+ * @tls_data: the thread local storage data that should be destroyed when the thread
+ *   exits. This pointer will be "owned" by the current thread. There is no way
+ *   to un-register the destruction.
+ * @destroy_notify: the free function that will be called when the thread exits.
+ *
+ * If _nm_tread_local storage is heap allocated it requires freeing the pointer
+ * when the thread exits. Use this function to register the pointer to be
+ * released.
+ *
+ * This function does not change errno.
+ */
+void
+nm_utils_thread_local_register_destroy(gpointer tls_data, GDestroyNotify destroy_notify)
+{
+    NM_AUTO_PROTECT_ERRNO(errsv);
+    static pthread_once_t key_once = PTHREAD_ONCE_INIT;
+    CList *               lst_head;
+    TlsRegData *          entry;
+
+    nm_assert(destroy_notify);
+
+    if (pthread_once(&key_once, _tls_reg_make_key) != 0)
+        g_return_if_reached();
+
+    if ((lst_head = pthread_getspecific(_tls_reg_key)) == NULL) {
+        lst_head = g_slice_new(CList);
+        c_list_init(lst_head);
+        if (pthread_setspecific(_tls_reg_key, lst_head) != 0)
+            g_return_if_reached();
+    }
+
+    entry                 = g_slice_new(TlsRegData);
+    entry->tls_data       = tls_data;
+    entry->destroy_notify = destroy_notify;
+    c_list_link_tail(lst_head, &entry->lst);
+}
diff --git a/src/libnm-glib-aux/nm-shared-utils.h b/src/libnm-glib-aux/nm-shared-utils.h
new file mode 100644
index 00000000..dcf37cd3
--- /dev/null
+++ b/src/libnm-glib-aux/nm-shared-utils.h
@@ -0,0 +1,2995 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2016 Red Hat, Inc.
+ */
+
+#ifndef __NM_SHARED_UTILS_H__
+#define __NM_SHARED_UTILS_H__
+
+#include <netinet/in.h>
+
+/*****************************************************************************/
+
+/* An optional boolean (like NMTernary, with identical numerical
+ * enum values). Note that this enum type is _nm_packed! */
+typedef enum _nm_packed {
+    NM_OPTION_BOOL_DEFAULT = -1,
+    NM_OPTION_BOOL_FALSE   = 0,
+    NM_OPTION_BOOL_TRUE    = 1,
+} NMOptionBool;
+
+#define nm_assert_is_ternary(value) nm_assert(NM_IN_SET((value), -1, 0, 1))
+
+/*****************************************************************************/
+
+static inline gboolean
+nm_is_ascii(char ch)
+{
+    return ((uint8_t) ch) < 128;
+}
+
+/*****************************************************************************/
+
+pid_t nm_utils_gettid(void);
+
+gboolean _nm_assert_on_main_thread(void);
+
+#if NM_MORE_ASSERTS > 5
+    #define NM_ASSERT_ON_MAIN_THREAD()              \
+        G_STMT_START                                \
+        {                                           \
+            nm_assert(_nm_assert_on_main_thread()); \
+        }                                           \
+        G_STMT_END
+#else
+    #define NM_ASSERT_ON_MAIN_THREAD() \
+        G_STMT_START                   \
+        {                              \
+            ;                          \
+        }                              \
+        G_STMT_END
+#endif
+
+/*****************************************************************************/
+
+static inline gboolean
+_NM_INT_NOT_NEGATIVE(gssize val)
+{
+    /* whether an enum (without negative values) is a signed int, depends on compiler options
+     * and compiler implementation.
+     *
+     * When using such an enum for accessing an array, one naturally wants to check
+     * that the enum is not negative. However, the compiler doesn't like a plain
+     * comparison "enum_val >= 0", because (if the enum is unsigned), it will warn
+     * that the expression is always true *duh*. Not even a cast to a signed
+     * type helps to avoid the compiler warning in any case.
+     *
+     * The sole purpose of this function is to avoid a compiler warning, when checking
+     * that an enum is not negative. */
+    return val >= 0;
+}
+
+/* check whether the integer value is smaller than G_MAXINT32. This macro exists
+ * for the sole purpose, that a plain "((int) value <= G_MAXINT32)" comparison
+ * may cause the compiler or coverity that this check is always TRUE. But the
+ * check depends on compile time and the size of C type "int".  Of course, most
+ * of the time in is gint32 and an int value is always <= G_MAXINT32.  The check
+ * exists to catch cases where that is not true.
+ *
+ * Together with the G_STATIC_ASSERT(), we make sure that this is always satisfied. */
+G_STATIC_ASSERT(sizeof(int) == sizeof(gint32));
+#if _NM_CC_SUPPORT_GENERIC
+    #define _NM_INT_LE_MAXINT32(value)                 \
+        ({                                             \
+            _nm_unused typeof(value) _value = (value); \
+                                                       \
+            _Generic((value), int : TRUE);             \
+        })
+#else
+    #define _NM_INT_LE_MAXINT32(value)                   \
+        ({                                               \
+            _nm_unused typeof(value) _value   = (value); \
+            _nm_unused const int *   _p_value = &_value; \
+                                                         \
+            TRUE;                                        \
+        })
+#endif
+
+/*****************************************************************************/
+
+typedef enum {
+
+    /* No type, used as error value */
+    NM_LINK_TYPE_NONE,
+
+    NM_LINK_TYPE_UNKNOWN,
+
+    NM_LINK_TYPE_ANY,
+
+#define _NM_LINK_TYPE_REAL_FIRST NM_LINK_TYPE_ETHERNET
+
+/* Hardware types */
+#define _NM_LINK_TYPE_HW_FIRST NM_LINK_TYPE_ETHERNET
+    NM_LINK_TYPE_ETHERNET,
+    NM_LINK_TYPE_INFINIBAND,
+    NM_LINK_TYPE_OLPC_MESH,
+    NM_LINK_TYPE_WIFI,
+    NM_LINK_TYPE_WWAN_NET, /* WWAN kernel netdevice */
+    NM_LINK_TYPE_WIMAX,
+    NM_LINK_TYPE_WPAN,
+    NM_LINK_TYPE_6LOWPAN,
+    NM_LINK_TYPE_WIFI_P2P,
+#define _NM_LINK_TYPE_HW_LAST NM_LINK_TYPE_WIFI_P2P
+
+/* Software types */
+#define _NM_LINK_TYPE_SW_FIRST NM_LINK_TYPE_BNEP
+    NM_LINK_TYPE_BNEP, /* Bluetooth Ethernet emulation */
+    NM_LINK_TYPE_DUMMY,
+    NM_LINK_TYPE_GRE,
+    NM_LINK_TYPE_GRETAP,
+    NM_LINK_TYPE_IFB,
+    NM_LINK_TYPE_IP6TNL,
+    NM_LINK_TYPE_IP6GRE,
+    NM_LINK_TYPE_IP6GRETAP,
+    NM_LINK_TYPE_IPIP,
+    NM_LINK_TYPE_LOOPBACK,
+    NM_LINK_TYPE_MACSEC,
+    NM_LINK_TYPE_MACVLAN,
+    NM_LINK_TYPE_MACVTAP,
+    NM_LINK_TYPE_OPENVSWITCH,
+    NM_LINK_TYPE_PPP,
+    NM_LINK_TYPE_SIT,
+    NM_LINK_TYPE_TUN,
+    NM_LINK_TYPE_VETH,
+    NM_LINK_TYPE_VLAN,
+    NM_LINK_TYPE_VRF,
+    NM_LINK_TYPE_VXLAN,
+    NM_LINK_TYPE_WIREGUARD,
+#define _NM_LINK_TYPE_SW_LAST NM_LINK_TYPE_WIREGUARD
+
+/* Software types with slaves */
+#define _NM_LINK_TYPE_SW_MASTER_FIRST NM_LINK_TYPE_BRIDGE
+    NM_LINK_TYPE_BRIDGE,
+    NM_LINK_TYPE_BOND,
+    NM_LINK_TYPE_TEAM,
+#define _NM_LINK_TYPE_SW_MASTER_LAST NM_LINK_TYPE_TEAM
+
+#define _NM_LINK_TYPE_REAL_LAST NM_LINK_TYPE_TEAM
+
+#define _NM_LINK_TYPE_REAL_NUM ((int) (_NM_LINK_TYPE_REAL_LAST - _NM_LINK_TYPE_REAL_FIRST + 1))
+
+} NMLinkType;
+
+static inline gboolean
+nm_link_type_is_software(NMLinkType link_type)
+{
+    G_STATIC_ASSERT(_NM_LINK_TYPE_SW_LAST + 1 == _NM_LINK_TYPE_SW_MASTER_FIRST);
+
+    return link_type >= _NM_LINK_TYPE_SW_FIRST && link_type <= _NM_LINK_TYPE_SW_MASTER_LAST;
+}
+
+static inline gboolean
+nm_link_type_supports_slaves(NMLinkType link_type)
+{
+    return link_type >= _NM_LINK_TYPE_SW_MASTER_FIRST && link_type <= _NM_LINK_TYPE_SW_MASTER_LAST;
+}
+
+/*****************************************************************************/
+
+gboolean _nm_utils_inet6_is_token(const struct in6_addr *in6addr);
+
+typedef struct {
+    guint8 ether_addr_octet[6 /*ETH_ALEN*/];
+} NMEtherAddr;
+
+#define NM_ETHER_ADDR_FORMAT_STR "%02X:%02X:%02X:%02X:%02X:%02X"
+
+#define NM_ETHER_ADDR_FORMAT_VAL(x)                                               \
+    (x)->ether_addr_octet[0], (x)->ether_addr_octet[1], (x)->ether_addr_octet[2], \
+        (x)->ether_addr_octet[3], (x)->ether_addr_octet[4], (x)->ether_addr_octet[5]
+
+#define _NM_ETHER_ADDR_INIT(a0, a1, a2, a3, a4, a5) \
+    {                                               \
+        .ether_addr_octet = {                       \
+            (a0),                                   \
+            (a1),                                   \
+            (a2),                                   \
+            (a3),                                   \
+            (a4),                                   \
+            (a5),                                   \
+        },                                          \
+    }
+
+#define NM_ETHER_ADDR_INIT(...) ((NMEtherAddr) _NM_ETHER_ADDR_INIT(__VA_ARGS__))
+
+static inline int
+nm_ether_addr_cmp(const NMEtherAddr *a, const NMEtherAddr *b)
+{
+    NM_CMP_SELF(a, b);
+    NM_CMP_DIRECT_MEMCMP(a, b, sizeof(NMEtherAddr));
+    return 0;
+}
+
+static inline gboolean
+nm_ether_addr_equal(const NMEtherAddr *a, const NMEtherAddr *b)
+{
+    return nm_ether_addr_cmp(a, b) == 0;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    union {
+        guint8          addr_ptr[1];
+        in_addr_t       addr4;
+        struct in_addr  addr4_struct;
+        struct in6_addr addr6;
+
+        /* NMIPAddr is really a union for IP addresses.
+         * However, as ethernet addresses fit in here nicely, use
+         * it also for an ethernet MAC address. */
+        guint8      ether_addr_octet[6 /*ETH_ALEN*/];
+        NMEtherAddr ether_addr;
+
+        guint8 array[sizeof(struct in6_addr)];
+    };
+} NMIPAddr;
+
+#define NM_IP_ADDR_INIT \
+    {                   \
+        .array = { 0 }  \
+    }
+
+extern const NMIPAddr nm_ip_addr_zero;
+
+#define nm_ether_addr_zero (nm_ip_addr_zero.ether_addr)
+
+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)
+{
+    nm_assert(addr);
+    if (addr_family == AF_INET6)
+        return IN6_IS_ADDR_UNSPECIFIED((const struct in6_addr *) addr);
+    nm_assert(addr_family == AF_INET);
+    return ((const struct in_addr *) addr)->s_addr == 0;
+}
+
+static inline void
+nm_ip_addr_set(int addr_family, gpointer dst, gconstpointer src)
+{
+    nm_assert_addr_family(addr_family);
+    nm_assert(dst);
+    nm_assert(src);
+
+    memcpy(dst, src, NM_IS_IPv4(addr_family) ? sizeof(in_addr_t) : sizeof(struct in6_addr));
+}
+
+static inline NMIPAddr
+nm_ip_addr_init(int addr_family, gconstpointer src)
+{
+    NMIPAddr a;
+
+    nm_assert_addr_family(addr_family);
+    nm_assert(src);
+
+    G_STATIC_ASSERT_EXPR(sizeof(NMIPAddr) == sizeof(struct in6_addr));
+
+    if (NM_IS_IPv4(addr_family)) {
+        memcpy(&a, src, sizeof(in_addr_t));
+
+        /* ensure all bytes of the union are initialized. If only to make
+         * valgrind happy. */
+        memset(&a.array[sizeof(in_addr_t)], 0, sizeof(a) - sizeof(in_addr_t));
+    } else
+        memcpy(&a, src, sizeof(struct in6_addr));
+
+    return a;
+}
+
+gboolean nm_ip_addr_set_from_untrusted(int           addr_family,
+                                       gpointer      dst,
+                                       gconstpointer src,
+                                       gsize         src_len,
+                                       int *         out_addr_family);
+
+static inline gboolean
+nm_ip4_addr_is_localhost(in_addr_t addr4)
+{
+    return (addr4 & htonl(0xFF000000u)) == htonl(0x7F000000u);
+}
+
+static inline gconstpointer
+nm_ip_addr_from_packed_array(int addr_family, gconstpointer ipaddr_arr, gsize idx)
+{
+    return NM_IS_IPv4(addr_family)
+               ? ((gconstpointer) & (((const struct in_addr *) ipaddr_arr)[idx]))
+               : ((gconstpointer) & (((const struct in6_addr *) ipaddr_arr)[idx]));
+}
+
+/*****************************************************************************/
+
+struct ether_addr;
+
+static inline int
+nm_utils_ether_addr_cmp(const struct ether_addr *a1, const struct ether_addr *a2)
+{
+    nm_assert(a1);
+    nm_assert(a2);
+    return memcmp(a1, a2, 6 /*ETH_ALEN*/);
+}
+
+static inline gboolean
+nm_utils_ether_addr_equal(const struct ether_addr *a1, const struct ether_addr *a2)
+{
+    return nm_utils_ether_addr_cmp(a1, a2) == 0;
+}
+
+/*****************************************************************************/
+
+/**
+ * NMUtilsIPv6IfaceId:
+ * @id: convenience member for validity checking; never use directly
+ * @id_u8: the 64-bit Interface Identifier
+ *
+ * Holds a 64-bit IPv6 Interface Identifier.  The IID is a sequence of bytes
+ * and should not normally be treated as a %guint64, but this is done for
+ * convenience of validity checking and initialization.
+ */
+typedef struct _NMUtilsIPv6IfaceId {
+    union {
+        guint64 id;
+        guint8  id_u8[8];
+    };
+} NMUtilsIPv6IfaceId;
+
+#define NM_UTILS_IPV6_IFACE_ID_INIT \
+    {                               \
+        {                           \
+            .id = 0                 \
+        }                           \
+    }
+
+void nm_utils_ipv6_addr_set_interface_identifier(struct in6_addr *        addr,
+                                                 const NMUtilsIPv6IfaceId iid);
+
+void nm_utils_ipv6_interface_identifier_get_from_addr(NMUtilsIPv6IfaceId *   iid,
+                                                      const struct in6_addr *addr);
+
+gboolean nm_utils_ipv6_interface_identifier_get_from_token(NMUtilsIPv6IfaceId *iid,
+                                                           const char *        token);
+
+const char *nm_utils_inet6_interface_identifier_to_token(NMUtilsIPv6IfaceId iid,
+                                                         char buf[static INET6_ADDRSTRLEN]);
+
+gboolean nm_utils_get_ipv6_interface_identifier(NMLinkType          link_type,
+                                                const guint8 *      hwaddr,
+                                                guint               len,
+                                                guint               dev_id,
+                                                NMUtilsIPv6IfaceId *out_iid);
+
+/*****************************************************************************/
+
+gconstpointer
+nm_utils_ipx_address_clear_host_address(int family, gpointer dst, gconstpointer src, guint8 plen);
+in_addr_t              nm_utils_ip4_address_clear_host_address(in_addr_t addr, guint8 plen);
+const struct in6_addr *nm_utils_ip6_address_clear_host_address(struct in6_addr *      dst,
+                                                               const struct in6_addr *src,
+                                                               guint8                 plen);
+
+static inline int
+nm_utils_ip4_address_same_prefix_cmp(in_addr_t addr_a, in_addr_t addr_b, guint8 plen)
+{
+    NM_CMP_DIRECT(htonl(nm_utils_ip4_address_clear_host_address(addr_a, plen)),
+                  htonl(nm_utils_ip4_address_clear_host_address(addr_b, plen)));
+    return 0;
+}
+
+int nm_utils_ip6_address_same_prefix_cmp(const struct in6_addr *addr_a,
+                                         const struct in6_addr *addr_b,
+                                         guint8                 plen);
+
+static inline int
+nm_utils_ip_address_same_prefix_cmp(int           addr_family,
+                                    gconstpointer addr_a,
+                                    gconstpointer addr_b,
+                                    guint8        plen)
+{
+    nm_assert_addr_family(addr_family);
+
+    NM_CMP_SELF(addr_a, addr_b);
+
+    if (NM_IS_IPv4(addr_family)) {
+        return nm_utils_ip4_address_same_prefix_cmp(*((const in_addr_t *) addr_a),
+                                                    *((const in_addr_t *) addr_b),
+                                                    plen);
+    }
+
+    return nm_utils_ip6_address_same_prefix_cmp(addr_a, addr_b, plen);
+}
+
+static inline gboolean
+nm_utils_ip4_address_same_prefix(in_addr_t addr_a, in_addr_t addr_b, guint8 plen)
+{
+    return nm_utils_ip4_address_same_prefix_cmp(addr_a, addr_b, plen) == 0;
+}
+
+static inline gboolean
+nm_utils_ip6_address_same_prefix(const struct in6_addr *addr_a,
+                                 const struct in6_addr *addr_b,
+                                 guint8                 plen)
+{
+    return nm_utils_ip6_address_same_prefix_cmp(addr_a, addr_b, plen) == 0;
+}
+
+static inline gboolean
+nm_utils_ip_address_same_prefix(int           addr_family,
+                                gconstpointer addr_a,
+                                gconstpointer addr_b,
+                                guint8        plen)
+{
+    return nm_utils_ip_address_same_prefix_cmp(addr_family, addr_a, addr_b, plen) == 0;
+}
+
+#define NM_CMP_DIRECT_IN4ADDR_SAME_PREFIX(a, b, plen) \
+    NM_CMP_RETURN(nm_utils_ip4_address_same_prefix_cmp((a), (b), (plen)))
+
+#define NM_CMP_DIRECT_IN6ADDR_SAME_PREFIX(a, b, plen) \
+    NM_CMP_RETURN(nm_utils_ip6_address_same_prefix_cmp((a), (b), (plen)))
+
+/*****************************************************************************/
+
+#define NM_IPV4LL_NETWORK ((in_addr_t) (htonl(0xA9FE0000lu)))
+#define NM_IPV4LL_NETMASK ((in_addr_t) (htonl(0xFFFF0000lu)))
+
+static inline gboolean
+nm_utils_ip4_address_is_link_local(in_addr_t addr)
+{
+    return (addr & NM_IPV4LL_NETMASK) == NM_IPV4LL_NETWORK;
+}
+
+static inline gboolean
+nm_utils_ip4_address_is_zeronet(in_addr_t network)
+{
+    /* Same as ipv4_is_zeronet() from kernel's include/linux/in.h. */
+    return (network & htonl(0xFF000000u)) == htonl(0x00000000u);
+}
+
+/*****************************************************************************/
+
+#define NM_UTILS_INET_ADDRSTRLEN INET6_ADDRSTRLEN
+
+/* Forward declare function so we don't have to drag in <arpa/inet.h>. */
+const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
+
+static inline const char *
+nm_utils_inet_ntop(int addr_family, gconstpointer addr, char *dst)
+{
+    const char *s;
+
+    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);
+
+/*****************************************************************************/
+
+/* this enum is compatible with ICMPV6_ROUTER_PREF_* (from <linux/icmpv6.h>,
+ * the values for netlink attribute RTA_PREF) and "enum ndp_route_preference"
+ * from <ndp.h>. */
+typedef enum _nm_packed {
+    NM_ICMPV6_ROUTER_PREF_MEDIUM  = 0x0, /* ICMPV6_ROUTER_PREF_MEDIUM */
+    NM_ICMPV6_ROUTER_PREF_LOW     = 0x3, /* ICMPV6_ROUTER_PREF_LOW */
+    NM_ICMPV6_ROUTER_PREF_HIGH    = 0x1, /* ICMPV6_ROUTER_PREF_HIGH */
+    NM_ICMPV6_ROUTER_PREF_INVALID = 0x2, /* ICMPV6_ROUTER_PREF_INVALID */
+} NMIcmpv6RouterPref;
+
+const char *nm_icmpv6_router_pref_to_string(NMIcmpv6RouterPref pref, char *buf, gsize len);
+
+/*****************************************************************************/
+
+gboolean nm_utils_memeqzero(gconstpointer data, gsize length);
+
+/*****************************************************************************/
+
+extern const void *const _NM_PTRARRAY_EMPTY[1];
+
+#define NM_PTRARRAY_EMPTY(type) ((type const *) _NM_PTRARRAY_EMPTY)
+#define NM_STRV_EMPTY()         ((char **) _NM_PTRARRAY_EMPTY)
+#define NM_STRV_EMPTY_CC()      NM_PTRARRAY_EMPTY(const char *)
+
+static inline void
+_nm_utils_strbuf_init(char *buf, gsize len, char **p_buf_ptr, gsize *p_buf_len)
+{
+    NM_SET_OUT(p_buf_len, len);
+    NM_SET_OUT(p_buf_ptr, buf);
+    buf[0] = '\0';
+}
+
+#define nm_utils_strbuf_init(buf, p_buf_ptr, p_buf_len)                                    \
+    G_STMT_START                                                                           \
+    {                                                                                      \
+        G_STATIC_ASSERT(G_N_ELEMENTS(buf) == sizeof(buf) && sizeof(buf) > sizeof(char *)); \
+        _nm_utils_strbuf_init((buf), sizeof(buf), (p_buf_ptr), (p_buf_len));               \
+    }                                                                                      \
+    G_STMT_END
+void nm_utils_strbuf_append(char **buf, gsize *len, const char *format, ...) _nm_printf(3, 4);
+void nm_utils_strbuf_append_c(char **buf, gsize *len, char c);
+void nm_utils_strbuf_append_str(char **buf, gsize *len, const char *str);
+void nm_utils_strbuf_append_bin(char **buf, gsize *len, gconstpointer str, gsize str_len);
+void nm_utils_strbuf_seek_end(char **buf, gsize *len);
+
+const char *nm_strquote(char *buf, gsize buf_len, const char *str);
+
+static inline gboolean
+nm_utils_is_separator(const char c)
+{
+    return NM_IN_SET(c, ' ', '\t');
+}
+
+/*****************************************************************************/
+
+GBytes *nm_gbytes_get_empty(void);
+
+GBytes *nm_g_bytes_new_from_str(const char *str);
+
+static inline gboolean
+nm_gbytes_equal0(GBytes *a, GBytes *b)
+{
+    return a == b || (a && b && g_bytes_equal(a, b));
+}
+
+gboolean nm_utils_gbytes_equal_mem(GBytes *bytes, gconstpointer mem_data, gsize mem_len);
+
+GVariant *nm_utils_gbytes_to_variant_ay(GBytes *bytes);
+
+GHashTable *nm_utils_strdict_clone(GHashTable *src);
+
+GVariant *nm_utils_strdict_to_variant_ass(GHashTable *strdict);
+GVariant *nm_utils_strdict_to_variant_asv(GHashTable *strdict);
+
+/*****************************************************************************/
+
+GVariant *nm_utils_gvariant_vardict_filter(GVariant *src,
+                                           gboolean (*filter_fcn)(const char *key,
+                                                                  GVariant *  val,
+                                                                  char **     out_key,
+                                                                  GVariant ** out_val,
+                                                                  gpointer    user_data),
+                                           gpointer user_data);
+
+GVariant *nm_utils_gvariant_vardict_filter_drop_one(GVariant *src, const char *key);
+
+/*****************************************************************************/
+
+static inline int
+nm_utils_hexchar_to_int(char ch)
+{
+    G_STATIC_ASSERT_EXPR('0' < 'A');
+    G_STATIC_ASSERT_EXPR('A' < 'a');
+
+    if (ch >= '0') {
+        if (ch <= '9')
+            return ch - '0';
+        if (ch >= 'A') {
+            if (ch <= 'F')
+                return ((int) ch) + (10 - (int) 'A');
+            if (ch >= 'a' && ch <= 'f')
+                return ((int) ch) + (10 - (int) 'a');
+        }
+    }
+    return -1;
+}
+
+/*****************************************************************************/
+
+const char *nm_utils_dbus_path_get_last_component(const char *dbus_path);
+
+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.
+     * This will remove leading and trailing ascii whitespaces (g_ascii_isspace()
+     * and NM_ASCII_SPACES).
+     *
+     * - when combined with !%NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY,
+     *   empty tokens will be removed (and %NULL will be returned if that
+     *   results in an empty string array).
+     * - when combined with %NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING,
+     *   trailing whitespace escaped by backslash are not stripped. */
+    NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP = (1u << 2),
+
+    /* This implies %NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING.
+     *
+     * This will do a final run over all tokens and remove all backslash
+     * escape characters that
+     *   - precede a delimiter.
+     *   - precede a backslash.
+     *   - precede a whitespace (only with %NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP).
+     *
+     *  Note that with %NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP, it is only
+     *  necessary to escape the very last whitespace (if the delimiters
+     *  are not whitespace themself). So, technically, it would be sufficient
+     *  to only unescape a backslash before the last whitespace and the user
+     *  still could express everything. However, such a rule would be complicated
+     *  to understand, so when using backslash escaping with nm_utils_strsplit_set_full(),
+     *  then all characters (including backslash) are treated verbatim, except:
+     *
+     *    - "\\$DELIMITER" (escaped delimiter)
+     *    - "\\\\" (escaped backslash)
+     *    - "\\$SPACE" (escaped space) (only with %NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP).
+     *
+     * Note that all other escapes like "\\n" or "\\001" are left alone.
+     * That makes the escaping/unescaping rules simple. Also, for the most part
+     * a text is just taken as-is, with little additional rules. Only backslashes
+     * 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, const char *delimiter, NMUtilsStrsplitSetFlags flags);
+
+static inline const char **
+nm_utils_strsplit_set_with_empty(const char *str, const char *delimiters)
+{
+    /* this returns the same result as g_strsplit_set(str, delimiters, -1), except
+     * it does not deep-clone the strv array.
+     * Also, for @str == "", this returns %NULL while g_strsplit_set() would return
+     * an empty strv array. */
+    return nm_utils_strsplit_set_full(str, delimiters, NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY);
+}
+
+static inline const char **
+nm_utils_strsplit_set(const char *str, const char *delimiters)
+{
+    return nm_utils_strsplit_set_full(str, delimiters, NM_UTILS_STRSPLIT_SET_FLAGS_NONE);
+}
+
+gssize nm_utils_strv_find_first(char **list, gssize len, const char *needle);
+
+gboolean nm_strv_has_duplicate(const char *const *list, gssize len, gboolean is_sorted);
+
+char **_nm_utils_strv_cleanup(char **  strv,
+                              gboolean strip_whitespace,
+                              gboolean skip_empty,
+                              gboolean skip_repeated);
+
+/*****************************************************************************/
+
+static inline gpointer
+nm_copy_func_g_strdup(gconstpointer arg, gpointer user_data)
+{
+    return g_strdup(arg);
+}
+
+/*****************************************************************************/
+
+static inline const char **
+nm_utils_escaped_tokens_split(const char *str, const char *delimiters)
+{
+    return nm_utils_strsplit_set_full(str,
+                                      delimiters,
+                                      NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED
+                                          | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP);
+}
+
+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);
+}
+
+/**
+ * nm_utils_escaped_tokens_escape_unnecessary:
+ * @str: the string to check for "escape"
+ * @delimiters: the delimiters
+ *
+ * This asserts that calling nm_utils_escaped_tokens_escape()
+ * on @str has no effect and returns @str directly. This is only
+ * for asserting that @str is safe to not require any escaping.
+ *
+ * Returns: @str
+ */
+static inline const char *
+nm_utils_escaped_tokens_escape_unnecessary(const char *str, const char *delimiters)
+{
+#if NM_MORE_ASSERTS > 0
+
+    nm_assert(str);
+    nm_assert(delimiters);
+
+    {
+        gs_free char *str_to_free = NULL;
+        const char *  str0;
+
+        str0 = nm_utils_escaped_tokens_escape(str, delimiters, &str_to_free);
+        nm_assert(str0 == str);
+        nm_assert(!str_to_free);
+    }
+#endif
+
+    return str;
+}
+
+static inline void
+nm_utils_escaped_tokens_escape_gstr_assert(const char *str,
+                                           const char *delimiters,
+                                           GString *   gstring)
+{
+    g_string_append(gstring, nm_utils_escaped_tokens_escape_unnecessary(str, delimiters));
+}
+
+static inline GString *
+nm_utils_escaped_tokens_escape_gstr(const char *str, const char *delimiters, GString *gstring)
+{
+    gs_free char *str_to_free = NULL;
+
+    nm_assert(str);
+    nm_assert(gstring);
+
+    g_string_append(gstring, nm_utils_escaped_tokens_escape(str, delimiters, &str_to_free));
+    return gstring;
+}
+
+/*****************************************************************************/
+
+char **nm_utils_strsplit_quoted(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
+
+#define nm_utils_checksum_get_digest(sum, arr)                                   \
+    G_STMT_START                                                                 \
+    {                                                                            \
+        GChecksum *const _sum = (sum);                                           \
+        gsize            _len;                                                   \
+                                                                                 \
+        G_STATIC_ASSERT_EXPR(sizeof(arr) == NM_UTILS_CHECKSUM_LENGTH_MD5         \
+                             || sizeof(arr) == NM_UTILS_CHECKSUM_LENGTH_SHA1     \
+                             || sizeof(arr) == NM_UTILS_CHECKSUM_LENGTH_SHA256); \
+        G_STATIC_ASSERT_EXPR(sizeof(arr) == G_N_ELEMENTS(arr));                  \
+                                                                                 \
+        nm_assert(_sum);                                                         \
+                                                                                 \
+        _len = G_N_ELEMENTS(arr);                                                \
+                                                                                 \
+        g_checksum_get_digest(_sum, (arr), &_len);                               \
+        nm_assert(_len == G_N_ELEMENTS(arr));                                    \
+    }                                                                            \
+    G_STMT_END
+
+#define nm_utils_checksum_get_digest_len(sum, buf, len)        \
+    G_STMT_START                                               \
+    {                                                          \
+        GChecksum *const _sum  = (sum);                        \
+        const gsize      _len0 = (len);                        \
+        gsize            _len;                                 \
+                                                               \
+        nm_assert(NM_IN_SET(_len0,                             \
+                            NM_UTILS_CHECKSUM_LENGTH_MD5,      \
+                            NM_UTILS_CHECKSUM_LENGTH_SHA1,     \
+                            NM_UTILS_CHECKSUM_LENGTH_SHA256)); \
+        nm_assert(_sum);                                       \
+                                                               \
+        _len = _len0;                                          \
+        g_checksum_get_digest(_sum, (buf), &_len);             \
+        nm_assert(_len == _len0);                              \
+    }                                                          \
+    G_STMT_END
+
+/*****************************************************************************/
+
+guint32 _nm_utils_ip4_prefix_to_netmask(guint32 prefix);
+guint32 _nm_utils_ip4_get_default_prefix0(in_addr_t ip);
+guint32 _nm_utils_ip4_get_default_prefix(in_addr_t ip);
+
+gconstpointer
+nm_utils_ipx_address_clear_host_address(int family, gpointer dst, gconstpointer src, guint8 plen);
+in_addr_t              nm_utils_ip4_address_clear_host_address(in_addr_t addr, guint8 plen);
+const struct in6_addr *nm_utils_ip6_address_clear_host_address(struct in6_addr *      dst,
+                                                               const struct in6_addr *src,
+                                                               guint8                 plen);
+int                    nm_utils_ip6_address_same_prefix_cmp(const struct in6_addr *addr_a,
+                                                            const struct in6_addr *addr_b,
+                                                            guint8                 plen);
+
+gboolean nm_utils_ip_is_site_local(int addr_family, const void *address);
+
+/*****************************************************************************/
+
+gboolean nm_utils_parse_inaddr_bin_full(int         addr_family,
+                                        gboolean    accept_legacy,
+                                        const char *text,
+                                        int *       out_addr_family,
+                                        gpointer    out_addr);
+static inline gboolean
+nm_utils_parse_inaddr_bin(int         addr_family,
+                          const char *text,
+                          int *       out_addr_family,
+                          gpointer    out_addr)
+{
+    return nm_utils_parse_inaddr_bin_full(addr_family, FALSE, text, out_addr_family, out_addr);
+}
+
+gboolean nm_utils_parse_inaddr(int addr_family, const char *text, char **out_addr);
+
+gboolean nm_utils_parse_inaddr_prefix_bin(int         addr_family,
+                                          const char *text,
+                                          int *       out_addr_family,
+                                          gpointer    out_addr,
+                                          int *       out_prefix);
+
+gboolean
+nm_utils_parse_inaddr_prefix(int addr_family, const char *text, char **out_addr, int *out_prefix);
+
+gboolean nm_utils_parse_next_line(const char **inout_ptr,
+                                  gsize *      inout_len,
+                                  const char **out_line,
+                                  gsize *      out_line_len);
+
+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);
+
+gint64 _nm_utils_ascii_str_to_int64_bin(const char *str,
+                                        gssize      len,
+                                        guint       base,
+                                        gint64      min,
+                                        gint64      max,
+                                        gint64      fallback);
+
+int _nm_utils_ascii_str_to_bool(const char *str, int default_value);
+
+/*****************************************************************************/
+
+extern char _nm_utils_to_string_buffer[2096];
+
+void     nm_utils_to_string_buffer_init(char **buf, gsize *len);
+gboolean nm_utils_to_string_buffer_init_null(gconstpointer obj, char **buf, gsize *len);
+
+/*****************************************************************************/
+
+typedef struct {
+    unsigned    flag;
+    const char *name;
+} NMUtilsFlags2StrDesc;
+
+#define NM_UTILS_FLAGS2STR(f, n) \
+    {                            \
+        .flag = f, .name = "" n, \
+    }
+
+#define NM_UTILS_FLAGS2STR_DEFINE(fcn_name, flags_type, ...)                    \
+    const char *fcn_name(flags_type flags, char *buf, gsize len)                \
+    {                                                                           \
+        static const NMUtilsFlags2StrDesc descs[] = {__VA_ARGS__};              \
+        G_STATIC_ASSERT(sizeof(flags_type) <= sizeof(unsigned));                \
+                                                                                \
+        return nm_utils_flags2str(descs, G_N_ELEMENTS(descs), flags, buf, len); \
+    }                                                                           \
+    _NM_DUMMY_STRUCT_FOR_TRAILING_SEMICOLON
+
+const char *nm_utils_flags2str(const NMUtilsFlags2StrDesc *descs,
+                               gsize                       n_descs,
+                               unsigned                    flags,
+                               char *                      buf,
+                               gsize                       len);
+
+/*****************************************************************************/
+
+#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_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);                        \
+        if (len) {                                                         \
+            const char *s = NULL;                                          \
+            switch (val) {                                                 \
+                (void) 0, __VA_ARGS__(void) 0;                             \
+            };                                                             \
+            if (s)                                                         \
+                g_strlcpy(buf, s, len);                                    \
+            else                                                           \
+                g_snprintf(buf, len, "(%" int_fmt ")", val);               \
+        }                                                                  \
+        return buf;                                                        \
+    }                                                                      \
+    _NM_DUMMY_STRUCT_FOR_TRAILING_SEMICOLON
+
+#define NM_UTILS_ENUM2STR_DEFINE(fcn_name, lookup_type, ...) \
+    NM_UTILS_ENUM2STR_DEFINE_FULL(fcn_name, lookup_type, "d", __VA_ARGS__)
+
+/*****************************************************************************/
+
+#define _nm_g_slice_free_fcn_define(mem_size)                              \
+    static inline void _nm_g_slice_free_fcn_##mem_size(gpointer mem_block) \
+    {                                                                      \
+        g_slice_free1(mem_size, mem_block);                                \
+    }
+
+_nm_g_slice_free_fcn_define(1) _nm_g_slice_free_fcn_define(2) _nm_g_slice_free_fcn_define(4)
+    _nm_g_slice_free_fcn_define(8) _nm_g_slice_free_fcn_define(10) _nm_g_slice_free_fcn_define(12)
+        _nm_g_slice_free_fcn_define(16) _nm_g_slice_free_fcn_define(32)
+
+#define nm_g_slice_free_fcn1(mem_size)                                                        \
+    ({                                                                                        \
+        void (*_fcn)(gpointer);                                                               \
+                                                                                              \
+        /* If mem_size is a compile time constant, the compiler
+         * will be able to optimize this. Hence, you don't want
+         * to call this with a non-constant size argument. */                               \
+        G_STATIC_ASSERT_EXPR(((mem_size) == 1) || ((mem_size) == 2) || ((mem_size) == 4)      \
+                             || ((mem_size) == 8) || ((mem_size) == 10) || ((mem_size) == 12) \
+                             || ((mem_size) == 16) || ((mem_size) == 32));                    \
+        switch ((mem_size)) {                                                                 \
+        case 1:                                                                               \
+            _fcn = _nm_g_slice_free_fcn_1;                                                    \
+            break;                                                                            \
+        case 2:                                                                               \
+            _fcn = _nm_g_slice_free_fcn_2;                                                    \
+            break;                                                                            \
+        case 4:                                                                               \
+            _fcn = _nm_g_slice_free_fcn_4;                                                    \
+            break;                                                                            \
+        case 8:                                                                               \
+            _fcn = _nm_g_slice_free_fcn_8;                                                    \
+            break;                                                                            \
+        case 10:                                                                              \
+            _fcn = _nm_g_slice_free_fcn_10;                                                   \
+            break;                                                                            \
+        case 12:                                                                              \
+            _fcn = _nm_g_slice_free_fcn_12;                                                   \
+            break;                                                                            \
+        case 16:                                                                              \
+            _fcn = _nm_g_slice_free_fcn_16;                                                   \
+            break;                                                                            \
+        case 32:                                                                              \
+            _fcn = _nm_g_slice_free_fcn_32;                                                   \
+            break;                                                                            \
+        default:                                                                              \
+            g_assert_not_reached();                                                           \
+            _fcn = NULL;                                                                      \
+            break;                                                                            \
+        }                                                                                     \
+        _fcn;                                                                                 \
+    })
+
+/**
+ * nm_g_slice_free_fcn:
+ * @type: type argument for sizeof() operator that you would
+ *   pass to g_slice_new().
+ *
+ * Returns: a function pointer with GDestroyNotify signature
+ *   for g_slice_free(type,*).
+ *
+ * Only certain types are implemented. You'll get a compile time
+ * error for the wrong types. */
+#define nm_g_slice_free_fcn(type) (nm_g_slice_free_fcn1(sizeof(type)))
+
+#define nm_g_slice_free_fcn_gint64 (nm_g_slice_free_fcn(gint64))
+
+/*****************************************************************************/
+
+/* Like g_error_matches() however:
+ * - as macro it is always inlined.
+ * - the @domain is usually a error quark getter function that cannot
+ *   be inlined. This macro calls the getter only if there is an error (lazy).
+ * - accept a list of allowed codes, instead of only one.
+ */
+#define nm_g_error_matches(error, err_domain, ...)                                        \
+    ({                                                                                    \
+        const GError *const _error = (error);                                             \
+                                                                                          \
+        _error && _error->domain == (err_domain) && NM_IN_SET(_error->code, __VA_ARGS__); \
+    })
+
+            static inline void nm_g_set_error_take(GError **error, GError *error_take)
+{
+    if (!error_take)
+        g_return_if_reached();
+    if (!error) {
+        g_error_free(error_take);
+        return;
+    }
+    if (*error) {
+        g_error_free(error_take);
+        g_return_if_reached();
+    }
+    *error = error_take;
+}
+
+#define nm_g_set_error_take_lazy(error, error_take_lazy)    \
+    G_STMT_START                                            \
+    {                                                       \
+        GError **_error = (error);                          \
+                                                            \
+        if (_error)                                         \
+            nm_g_set_error_take(_error, (error_take_lazy)); \
+    }                                                       \
+    G_STMT_END
+
+/**
+ * NMUtilsError:
+ * @NM_UTILS_ERROR_UNKNOWN: unknown or unclassified error
+ * @NM_UTILS_ERROR_CANCELLED_DISPOSING: when disposing an object that has
+ *   pending asynchronous operations, the operation is cancelled with this
+ *   error reason. Depending on the usage, this might indicate a bug because
+ *   usually the target object should stay alive as long as there are pending
+ *   operations.
+ * @NM_UTILS_ERROR_NOT_READY: the failure is related to being currently
+ *   not ready to perform the operation.
+ *
+ * @NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE: used for a very particular
+ *   purpose during nm_device_check_connection_compatible() to indicate that
+ *   the profile does not match the device already because their type differs.
+ *   That is, there is a fundamental reason of trying to check a profile that
+ *   cannot possibly match on this device.
+ * @NM_UTILS_ERROR_CONNECTION_AVAILABLE_UNMANAGED_DEVICE: used for a very particular
+ *   purpose during nm_device_check_connection_available(), to indicate that the
+ *   device is not available because it is unmanaged.
+ * @NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY: the profile is currently not
+ *   available/compatible with the device, but this may be only temporary.
+ *
+ * @NM_UTILS_ERROR_SETTING_MISSING: the setting is missing
+ *
+ * @NM_UTILS_ERROR_INVALID_ARGUMENT: invalid argument.
+ */
+typedef enum {
+    NM_UTILS_ERROR_UNKNOWN = 0,         /*< nick=Unknown >*/
+    NM_UTILS_ERROR_CANCELLED_DISPOSING, /*< nick=CancelledDisposing >*/
+    NM_UTILS_ERROR_INVALID_ARGUMENT,    /*< nick=InvalidArgument >*/
+    NM_UTILS_ERROR_NOT_READY,           /*< nick=NotReady >*/
+
+    NM_UTILS_ERROR_AMBIGUOUS, /*< nick=Ambiguous >*/
+
+    /* the following codes have a special meaning and are exactly used for
+     * nm_device_check_connection_compatible() and nm_device_check_connection_available().
+     *
+     * Actually, their meaning is not very important (so, don't think too
+     * hard about the name of these error codes). What is important, is their
+     * relative order (i.e. the integer value of the codes). When manager
+     * searches for a suitable device, it will check all devices whether
+     * a profile can be activated. If they all fail, it will pick the error
+     * message from the device that returned the *highest* error code,
+     * in the hope that this message makes the most sense for the caller.
+     * */
+    NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+    NM_UTILS_ERROR_CONNECTION_AVAILABLE_UNMANAGED_DEVICE,
+    NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+    NM_UTILS_ERROR_CONNECTION_AVAILABLE_DISALLOWED,
+
+    NM_UTILS_ERROR_SETTING_MISSING,
+
+} NMUtilsError;
+
+#define NM_UTILS_ERROR (nm_utils_error_quark())
+GQuark nm_utils_error_quark(void);
+
+GQuark nm_manager_error_quark(void);
+#define _NM_MANAGER_ERROR (nm_manager_error_quark())
+
+#define _NM_MANAGER_ERROR_UNKNOWN_LOG_LEVEL  10
+#define _NM_MANAGER_ERROR_UNKNOWN_LOG_DOMAIN 11
+
+void nm_utils_error_set_cancelled(GError **error, gboolean is_disposing, const char *instance_name);
+
+static inline GError *
+nm_utils_error_new_cancelled(gboolean is_disposing, const char *instance_name)
+{
+    GError *error = NULL;
+
+    nm_utils_error_set_cancelled(&error, is_disposing, instance_name);
+    return error;
+}
+
+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);
+
+static inline void
+nm_utils_error_set_literal(GError **error, int error_code, const char *literal)
+{
+    g_set_error_literal(error, NM_UTILS_ERROR, error_code, literal);
+}
+
+#define nm_utils_error_set(error, error_code, ...)                           \
+    G_STMT_START                                                             \
+    {                                                                        \
+        if (NM_NARG(__VA_ARGS__) == 1) {                                     \
+            g_set_error_literal((error),                                     \
+                                NM_UTILS_ERROR,                              \
+                                (error_code),                                \
+                                _NM_UTILS_MACRO_FIRST(__VA_ARGS__));         \
+        } else {                                                             \
+            g_set_error((error), NM_UTILS_ERROR, (error_code), __VA_ARGS__); \
+        }                                                                    \
+    }                                                                        \
+    G_STMT_END
+
+#define nm_utils_error_set_errno(error, errsv, fmt, ...)                                          \
+    G_STMT_START                                                                                  \
+    {                                                                                             \
+        char _bstrerr[NM_STRERROR_BUFSIZE];                                                       \
+                                                                                                  \
+        g_set_error((error),                                                                      \
+                    NM_UTILS_ERROR,                                                               \
+                    NM_UTILS_ERROR_UNKNOWN,                                                       \
+                    fmt,                                                                          \
+                    ##__VA_ARGS__,                                                                \
+                    nm_strerror_native_r(                                                         \
+                        ({                                                                        \
+                            const int _errsv = (errsv);                                           \
+                                                                                                  \
+                            (_errsv >= 0 ? _errsv                                                 \
+                                         : (G_UNLIKELY(_errsv == G_MININT) ? G_MAXINT : -errsv)); \
+                        }),                                                                       \
+                        _bstrerr,                                                                 \
+                        sizeof(_bstrerr)));                                                       \
+    }                                                                                             \
+    G_STMT_END
+
+#define nm_utils_error_new(error_code, ...)                                                      \
+    ((NM_NARG(__VA_ARGS__) == 1)                                                                 \
+         ? g_error_new_literal(NM_UTILS_ERROR, (error_code), _NM_UTILS_MACRO_FIRST(__VA_ARGS__)) \
+         : g_error_new(NM_UTILS_ERROR, (error_code), __VA_ARGS__))
+
+/*****************************************************************************/
+
+gboolean nm_g_object_set_property(GObject *     object,
+                                  const char *  property_name,
+                                  const GValue *value,
+                                  GError **     error);
+
+gboolean nm_g_object_set_property_string(GObject *   object,
+                                         const char *property_name,
+                                         const char *value,
+                                         GError **   error);
+
+gboolean nm_g_object_set_property_string_static(GObject *   object,
+                                                const char *property_name,
+                                                const char *value,
+                                                GError **   error);
+
+gboolean nm_g_object_set_property_string_take(GObject *   object,
+                                              const char *property_name,
+                                              char *      value,
+                                              GError **   error);
+
+gboolean nm_g_object_set_property_boolean(GObject *   object,
+                                          const char *property_name,
+                                          gboolean    value,
+                                          GError **   error);
+
+gboolean nm_g_object_set_property_char(GObject *   object,
+                                       const char *property_name,
+                                       gint8       value,
+                                       GError **   error);
+
+gboolean nm_g_object_set_property_uchar(GObject *   object,
+                                        const char *property_name,
+                                        guint8      value,
+                                        GError **   error);
+
+gboolean
+nm_g_object_set_property_int(GObject *object, const char *property_name, int value, GError **error);
+
+gboolean nm_g_object_set_property_int64(GObject *   object,
+                                        const char *property_name,
+                                        gint64      value,
+                                        GError **   error);
+
+gboolean nm_g_object_set_property_uint(GObject *   object,
+                                       const char *property_name,
+                                       guint       value,
+                                       GError **   error);
+
+gboolean nm_g_object_set_property_uint64(GObject *   object,
+                                         const char *property_name,
+                                         guint64     value,
+                                         GError **   error);
+
+gboolean nm_g_object_set_property_flags(GObject *   object,
+                                        const char *property_name,
+                                        GType       gtype,
+                                        guint       value,
+                                        GError **   error);
+
+gboolean nm_g_object_set_property_enum(GObject *   object,
+                                       const char *property_name,
+                                       GType       gtype,
+                                       int         value,
+                                       GError **   error);
+
+GParamSpec *nm_g_object_class_find_property_from_gtype(GType gtype, const char *property_name);
+
+/*****************************************************************************/
+
+#define _NM_G_PARAM_SPEC_CAST(param_spec, _value_type, _c_type)              \
+    ({                                                                       \
+        const GParamSpec *const _param_spec = (param_spec);                  \
+                                                                             \
+        nm_assert(!_param_spec || _param_spec->value_type == (_value_type)); \
+        ((const _c_type *) _param_spec);                                     \
+    })
+
+#define NM_G_PARAM_SPEC_CAST_BOOLEAN(param_spec) \
+    _NM_G_PARAM_SPEC_CAST(param_spec, G_TYPE_BOOLEAN, GParamSpecBoolean)
+#define NM_G_PARAM_SPEC_CAST_UINT(param_spec) \
+    _NM_G_PARAM_SPEC_CAST(param_spec, G_TYPE_UINT, GParamSpecUInt)
+#define NM_G_PARAM_SPEC_CAST_UINT64(param_spec) \
+    _NM_G_PARAM_SPEC_CAST(param_spec, G_TYPE_UINT64, GParamSpecUInt64)
+#define NM_G_PARAM_SPEC_CAST_STRING(param_spec) \
+    _NM_G_PARAM_SPEC_CAST(param_spec, G_TYPE_STRING, GParamSpecString)
+
+#define NM_G_PARAM_SPEC_GET_DEFAULT_BOOLEAN(param_spec) \
+    (NM_G_PARAM_SPEC_CAST_BOOLEAN(NM_ENSURE_NOT_NULL(param_spec))->default_value)
+#define NM_G_PARAM_SPEC_GET_DEFAULT_UINT(param_spec) \
+    (NM_G_PARAM_SPEC_CAST_UINT(NM_ENSURE_NOT_NULL(param_spec))->default_value)
+#define NM_G_PARAM_SPEC_GET_DEFAULT_UINT64(param_spec) \
+    (NM_G_PARAM_SPEC_CAST_UINT64(NM_ENSURE_NOT_NULL(param_spec))->default_value)
+#define NM_G_PARAM_SPEC_GET_DEFAULT_STRING(param_spec) \
+    (NM_G_PARAM_SPEC_CAST_STRING(NM_ENSURE_NOT_NULL(param_spec))->default_value)
+
+/*****************************************************************************/
+
+GType nm_g_type_find_implementing_class_for_property(GType gtype, const char *pname);
+
+/*****************************************************************************/
+
+typedef enum {
+    NM_UTILS_STR_UTF8_SAFE_FLAG_NONE = 0,
+
+    /* This flag only has an effect during escaping.
+     *
+     * It will backslash escape ascii characters according to nm_ascii_is_ctrl_or_del(). */
+    NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL = 0x0001,
+
+    /* This flag only has an effect during escaping.
+     *
+     * It will backslash escape ascii characters according to nm_ascii_is_non_ascii(). */
+    NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII = 0x0002,
+
+    /* This flag only has an effect during escaping to ensure we
+     * don't leak secrets in memory. Note that during unescape we
+     * know the maximum result size from the beginning, and no
+     * reallocation happens. Thus, unescape always avoids leaking
+     * secrets already. */
+    NM_UTILS_STR_UTF8_SAFE_FLAG_SECRET = 0x0004,
+
+    /* This flag only has an effect during unescaping. It means
+     * that non-escaped whitespaces (g_ascii_isspace()) will be
+     * stripped from the front and end of the string. Note that
+     * this flag is only useful for gracefully accepting user input
+     * with spaces. With this flag, escape and unescape may no longer
+     * yield the original input. */
+    NM_UTILS_STR_UTF8_SAFE_UNESCAPE_STRIP_SPACES = 0x0008,
+} NMUtilsStrUtf8SafeFlags;
+
+const char *nm_utils_buf_utf8safe_escape(gconstpointer           buf,
+                                         gssize                  buflen,
+                                         NMUtilsStrUtf8SafeFlags flags,
+                                         char **                 to_free);
+char *
+nm_utils_buf_utf8safe_escape_cp(gconstpointer buf, gssize buflen, NMUtilsStrUtf8SafeFlags flags);
+const char *
+nm_utils_buf_utf8safe_escape_bytes(GBytes *bytes, NMUtilsStrUtf8SafeFlags flags, char **to_free);
+gconstpointer nm_utils_buf_utf8safe_unescape(const char *            str,
+                                             NMUtilsStrUtf8SafeFlags flags,
+                                             gsize *                 out_len,
+                                             gpointer *              to_free);
+
+const char *
+nm_utils_str_utf8safe_escape(const char *str, NMUtilsStrUtf8SafeFlags flags, char **to_free);
+const char *
+nm_utils_str_utf8safe_unescape(const char *str, NMUtilsStrUtf8SafeFlags flags, char **to_free);
+
+char *nm_utils_str_utf8safe_escape_cp(const char *str, NMUtilsStrUtf8SafeFlags flags);
+char *nm_utils_str_utf8safe_unescape_cp(const char *str, NMUtilsStrUtf8SafeFlags flags);
+
+char *nm_utils_str_utf8safe_escape_take(char *str, NMUtilsStrUtf8SafeFlags flags);
+
+GVariant *nm_g_variant_singleton_b(gboolean value);
+GVariant *nm_g_variant_singleton_u_0(void);
+GVariant *nm_g_variant_singleton_s_empty(void);
+GVariant *nm_g_variant_singleton_aLsvI(void);
+GVariant *nm_g_variant_singleton_aLsaLsvII(void);
+GVariant *nm_g_variant_singleton_aaLsvI(void);
+
+static inline void
+nm_g_variant_unref_floating(GVariant *var)
+{
+    /* often a function wants to keep a reference to an input variant.
+     * It uses g_variant_ref_sink() to either increase the ref-count,
+     * or take ownership of a possibly floating reference.
+     *
+     * If the function doesn't actually want to do anything with the
+     * input variant, it still must make sure that a passed in floating
+     * reference is consumed. Hence, this helper which:
+     *
+     *   - does nothing if @var is not floating
+     *   - unrefs (consumes) @var if it is floating. */
+    if (g_variant_is_floating(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 GVariant *
+nm_g_variant_new_ay(const guint8 *data, gsize len)
+{
+    return g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, data, len, 1);
+}
+
+static inline GVariant *
+nm_g_variant_new_au(const guint32 *data, gsize len)
+{
+    return g_variant_new_fixed_array(G_VARIANT_TYPE_UINT32, data, len, sizeof(guint32));
+}
+
+static inline GVariant *
+nm_g_variant_new_ay_inaddr(int addr_family, gconstpointer addr)
+{
+    return nm_g_variant_new_ay(addr ?: &nm_ip_addr_zero, nm_utils_addr_family_to_size(addr_family));
+}
+
+static inline GVariant *
+nm_g_variant_new_ay_in4addr(in_addr_t addr)
+{
+    return nm_g_variant_new_ay_inaddr(AF_INET, &addr);
+}
+
+static inline GVariant *
+nm_g_variant_new_ay_in6addr(const struct in6_addr *addr)
+{
+    return nm_g_variant_new_ay_inaddr(AF_INET6, addr);
+}
+
+static inline void
+nm_g_variant_builder_add_sv(GVariantBuilder *builder, const char *key, GVariant *val)
+{
+    g_variant_builder_add(builder, "{sv}", key, val);
+}
+
+static inline void
+nm_g_variant_builder_add_sv_bytearray(GVariantBuilder *builder,
+                                      const char *     key,
+                                      const guint8 *   arr,
+                                      gsize            len)
+{
+    g_variant_builder_add(builder, "{sv}", key, nm_g_variant_new_ay(arr, len));
+}
+
+static inline void
+nm_g_variant_builder_add_sv_uint32(GVariantBuilder *builder, const char *key, guint32 val)
+{
+    nm_g_variant_builder_add_sv(builder, key, g_variant_new_uint32(val));
+}
+
+static inline void
+nm_g_variant_builder_add_sv_str(GVariantBuilder *builder, const char *key, const char *str)
+{
+    nm_g_variant_builder_add_sv(builder, key, g_variant_new_string(str));
+}
+
+static inline void
+nm_g_source_destroy_and_unref(GSource *source)
+{
+    g_source_destroy(source);
+    g_source_unref(source);
+}
+
+#define nm_clear_g_source_inst(ptr) (nm_clear_pointer((ptr), nm_g_source_destroy_and_unref))
+
+NM_AUTO_DEFINE_FCN0(GSource *, _nm_auto_destroy_and_unref_gsource, nm_g_source_destroy_and_unref);
+#define nm_auto_destroy_and_unref_gsource nm_auto(_nm_auto_destroy_and_unref_gsource)
+
+NM_AUTO_DEFINE_FCN0(GMainContext *, _nm_auto_pop_gmaincontext, g_main_context_pop_thread_default);
+#define nm_auto_pop_gmaincontext nm_auto(_nm_auto_pop_gmaincontext)
+
+static inline void
+nm_g_main_context_pop_and_unref(GMainContext *context)
+{
+    g_main_context_pop_thread_default(context);
+    g_main_context_unref(context);
+}
+
+NM_AUTO_DEFINE_FCN0(GMainContext *,
+                    _nm_auto_pop_and_unref_gmaincontext,
+                    nm_g_main_context_pop_and_unref);
+#define nm_auto_pop_and_unref_gmaincontext nm_auto(_nm_auto_pop_and_unref_gmaincontext)
+
+static inline gboolean
+nm_source_func_unref_gobject(gpointer user_data)
+{
+    nm_assert(G_IS_OBJECT(user_data));
+    g_object_unref(user_data);
+    return G_SOURCE_REMOVE;
+}
+
+extern GSource *_nm_g_source_sentinel[1];
+
+GSource *_nm_g_source_sentinel_get_init(GSource **p_source);
+
+/* Get a GSource sentinel (dummy instance). This instance should never be
+ * attached to a GMainContext. The only currently known purpose is to use it
+ * as dummy value instead of an infinity timeout. That is, if we configurably
+ * want to schedule a timeout that might be infinity, we might set the GSource
+ * instance to nm_g_source_sentinel_get(). On this instance, we still may
+ * call g_source_ref(), g_source_unref() and g_source_destroy(). But nothing
+ * else. */
+#define nm_g_source_sentinel_get(idx)                                         \
+    ({                                                                        \
+        GSource *_s;                                                          \
+                                                                              \
+        G_STATIC_ASSERT((idx) >= 0);                                          \
+        G_STATIC_ASSERT((idx) < G_N_ELEMENTS(_nm_g_source_sentinel));         \
+                                                                              \
+        _s = g_atomic_pointer_get(&_nm_g_source_sentinel[idx]);               \
+        if (G_UNLIKELY(!_s))                                                  \
+            _s = _nm_g_source_sentinel_get_init(&_nm_g_source_sentinel[idx]); \
+                                                                              \
+        _s;                                                                   \
+    })
+
+GSource *nm_g_idle_source_new(int            priority,
+                              GSourceFunc    func,
+                              gpointer       user_data,
+                              GDestroyNotify destroy_notify);
+
+GSource *nm_g_timeout_source_new(guint          timeout_msec,
+                                 int            priority,
+                                 GSourceFunc    func,
+                                 gpointer       user_data,
+                                 GDestroyNotify destroy_notify);
+
+GSource *nm_g_timeout_source_new_seconds(guint          timeout_sec,
+                                         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,
+                                     gpointer       user_data,
+                                     GDestroyNotify notify);
+
+static inline GSource *
+nm_g_source_attach(GSource *source, GMainContext *context)
+{
+    g_source_attach(source, context);
+    return source;
+}
+
+static inline GSource *
+nm_g_idle_add_source(GSourceFunc func, gpointer user_data)
+{
+    /* G convenience function to attach a new timeout source to the default GMainContext.
+     * In that sense it's very similar to g_idle_add() except that it returns a
+     * reference to the new source.  */
+    return nm_g_source_attach(nm_g_idle_source_new(G_PRIORITY_DEFAULT, func, user_data, NULL),
+                              NULL);
+}
+
+static inline GSource *
+nm_g_timeout_add_source(guint timeout_msec, GSourceFunc func, gpointer user_data)
+{
+    /* G convenience function to attach a new timeout source to the default GMainContext.
+     * In that sense it's very similar to g_timeout_add() except that it returns a
+     * reference to the new source.  */
+    return nm_g_source_attach(
+        nm_g_timeout_source_new(timeout_msec, G_PRIORITY_DEFAULT, func, user_data, NULL),
+        NULL);
+}
+
+static inline GSource *
+nm_g_timeout_add_source_seconds(guint timeout_sec, GSourceFunc func, gpointer user_data)
+{
+    /* G convenience function to attach a new timeout source to the default GMainContext.
+     * In that sense it's very similar to g_timeout_add_seconds() except that it returns a
+     * reference to the new source.  */
+    return nm_g_source_attach(
+        nm_g_timeout_source_new_seconds(timeout_sec, G_PRIORITY_DEFAULT, func, user_data, NULL),
+        NULL);
+}
+
+static inline GSource *
+nm_g_timeout_add_source_approx(guint       timeout_msec,
+                               guint       timeout_sec_threshold,
+                               GSourceFunc func,
+                               gpointer    user_data)
+{
+    GSource *source;
+
+    /* If timeout_msec is larger or equal than a threshold, then we use g_timeout_source_new_seconds()
+     * instead. */
+    if (timeout_msec / 1000u >= timeout_sec_threshold)
+        source = nm_g_timeout_source_new_seconds(timeout_msec / 1000u,
+                                                 G_PRIORITY_DEFAULT,
+                                                 func,
+                                                 user_data,
+                                                 NULL);
+    else
+        source = nm_g_timeout_source_new(timeout_msec, G_PRIORITY_DEFAULT, func, user_data, NULL);
+    return nm_g_source_attach(source, NULL);
+}
+
+NM_AUTO_DEFINE_FCN0(GMainContext *, _nm_auto_unref_gmaincontext, g_main_context_unref);
+#define nm_auto_unref_gmaincontext nm_auto(_nm_auto_unref_gmaincontext)
+
+static inline GMainContext *
+nm_g_main_context_push_thread_default(GMainContext *context)
+{
+    /* This function is to work together with nm_auto_pop_gmaincontext. */
+    if (G_UNLIKELY(!context))
+        context = g_main_context_default();
+    g_main_context_push_thread_default(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)
+{
+    GMainContext *cur_context;
+
+    cur_context = g_main_context_get_thread_default();
+    if (cur_context == context)
+        return NULL;
+
+    if (G_UNLIKELY(!cur_context)) {
+        cur_context = g_main_context_default();
+        if (cur_context == context)
+            return NULL;
+    } else if (G_UNLIKELY(!context)) {
+        context = g_main_context_default();
+        if (cur_context == context)
+            return NULL;
+    }
+
+    g_main_context_push_thread_default(context);
+    return context;
+}
+
+/*****************************************************************************/
+
+static inline int
+nm_utf8_collate0(const char *a, const char *b)
+{
+    if (!a)
+        return !b ? 0 : -1;
+    if (!b)
+        return 1;
+    return g_utf8_collate(a, b);
+}
+
+int nm_strcmp_with_data(gconstpointer a, gconstpointer b, gpointer user_data);
+int nm_strcmp_p_with_data(gconstpointer a, gconstpointer b, gpointer user_data);
+int nm_strcmp0_p_with_data(gconstpointer a, gconstpointer b, gpointer user_data);
+int nm_strcmp_ascii_case_with_data(gconstpointer a, gconstpointer b, gpointer user_data);
+int nm_cmp_uint32_p_with_data(gconstpointer p_a, gconstpointer p_b, gpointer user_data);
+int nm_cmp_int2ptr_p_with_data(gconstpointer p_a, gconstpointer p_b, gpointer user_data);
+
+/*****************************************************************************/
+
+typedef struct {
+    const char *name;
+} NMUtilsNamedEntry;
+
+typedef struct {
+    union {
+        NMUtilsNamedEntry named_entry;
+        const char *      name;
+        char *            name_mutable;
+    };
+    union {
+        const char *value_str;
+        char *      value_str_mutable;
+        gpointer    value_ptr;
+    };
+} NMUtilsNamedValue;
+
+#define NM_UTILS_NAMED_VALUE_INIT(n, v) \
+    {                                   \
+        .name = (n), .value_ptr = (v)   \
+    }
+
+NMUtilsNamedValue *
+nm_utils_named_values_from_strdict_full(GHashTable *        hash,
+                                        guint *             out_len,
+                                        GCompareDataFunc    compare_func,
+                                        gpointer            user_data,
+                                        NMUtilsNamedValue * provided_buffer,
+                                        guint               provided_buffer_len,
+                                        NMUtilsNamedValue **out_allocated_buffer);
+
+#define nm_utils_named_values_from_strdict(hash, out_len, array, out_allocated_buffer) \
+    nm_utils_named_values_from_strdict_full((hash),                                    \
+                                            (out_len),                                 \
+                                            nm_strcmp_p_with_data,                     \
+                                            NULL,                                      \
+                                            (array),                                   \
+                                            G_N_ELEMENTS(array),                       \
+                                            (out_allocated_buffer))
+
+gssize nm_utils_named_value_list_find(const NMUtilsNamedValue *arr,
+                                      gsize                    len,
+                                      const char *             name,
+                                      gboolean                 sorted);
+
+gboolean nm_utils_named_value_list_is_sorted(const NMUtilsNamedValue *arr,
+                                             gsize                    len,
+                                             gboolean                 accept_duplicates,
+                                             GCompareDataFunc         compare_func,
+                                             gpointer                 user_data);
+
+void nm_utils_named_value_list_sort(NMUtilsNamedValue *arr,
+                                    gsize              len,
+                                    GCompareDataFunc   compare_func,
+                                    gpointer           user_data);
+
+void nm_utils_named_value_clear_with_g_free(NMUtilsNamedValue *val);
+
+/*****************************************************************************/
+
+gpointer *nm_utils_hash_keys_to_array(GHashTable *     hash,
+                                      GCompareDataFunc compare_func,
+                                      gpointer         user_data,
+                                      guint *          out_len);
+
+gpointer *nm_utils_hash_values_to_array(GHashTable *     hash,
+                                        GCompareDataFunc compare_func,
+                                        gpointer         user_data,
+                                        guint *          out_len);
+
+static inline const char **
+nm_utils_strdict_get_keys(const GHashTable *hash, gboolean sorted, guint *out_length)
+{
+    return (const char **) nm_utils_hash_keys_to_array((GHashTable *) hash,
+                                                       sorted ? nm_strcmp_p_with_data : NULL,
+                                                       NULL,
+                                                       out_length);
+}
+
+gboolean nm_utils_hashtable_equal(const GHashTable *a,
+                                  const GHashTable *b,
+                                  gboolean          treat_null_as_empty,
+                                  GEqualFunc        equal_func);
+
+gboolean nm_utils_hashtable_cmp_equal(const GHashTable *a,
+                                      const GHashTable *b,
+                                      GCompareDataFunc  cmp_values,
+                                      gpointer          user_data);
+
+static inline gboolean
+nm_utils_hashtable_same_keys(const GHashTable *a, const GHashTable *b)
+{
+    return nm_utils_hashtable_cmp_equal(a, b, NULL, NULL);
+}
+
+int nm_utils_hashtable_cmp(const GHashTable *a,
+                           const GHashTable *b,
+                           gboolean          do_fast_precheck,
+                           GCompareDataFunc  cmp_keys,
+                           GCompareDataFunc  cmp_values,
+                           gpointer          user_data);
+
+char **nm_utils_strv_make_deep_copied(const char **strv);
+
+char **nm_utils_strv_make_deep_copied_n(const char **strv, gsize len);
+
+static inline char **
+nm_utils_strv_make_deep_copied_nonnull(const char **strv)
+{
+    return nm_utils_strv_make_deep_copied(strv) ?: g_new0(char *, 1);
+}
+
+char **_nm_utils_strv_dup(const char *const *strv, gssize len, gboolean deep_copied);
+
+#define nm_utils_strv_dup(strv, len, deep_copied) \
+    _nm_utils_strv_dup(NM_CAST_STRV_CC(strv), (len), (deep_copied))
+
+const char **_nm_utils_strv_dup_packed(const char *const *strv, gssize len);
+
+#define nm_utils_strv_dup_packed(strv, len) _nm_utils_strv_dup_packed(NM_CAST_STRV_CC(strv), (len))
+
+#define nm_utils_strv_dup_shallow_maybe_a(alloca_maxlen, strv, len, to_free)       \
+    ({                                                                             \
+        const char *const *const _strv    = NM_CAST_STRV_CC(strv);                 \
+        const gssize             _len     = (len);                                 \
+        const char **            _result  = NULL;                                  \
+        const char ***const      _to_free = (to_free);                             \
+                                                                                   \
+        G_STATIC_ASSERT_EXPR((alloca_maxlen) <= 500u / sizeof(const char *));      \
+        G_STATIC_ASSERT_EXPR((alloca_maxlen) > 0u);                                \
+        nm_assert(_to_free && !*_to_free);                                         \
+                                                                                   \
+        if (_len >= 0 || _strv) {                                                  \
+            const gsize _l = (_len < 0) ? NM_PTRARRAY_LEN(_strv) : ((gsize) _len); \
+                                                                                   \
+            if (G_LIKELY(_l < (alloca_maxlen))) {                                  \
+                _result = g_newa(const char *, _l + 1);                            \
+            } else {                                                               \
+                _result   = g_new(const char *, _l + 1);                           \
+                *_to_free = _result;                                               \
+            }                                                                      \
+            if (_l > 0)                                                            \
+                memcpy(_result, _strv, _l * sizeof(const char *));                 \
+            _result[_l] = NULL;                                                    \
+        }                                                                          \
+                                                                                   \
+        _result;                                                                   \
+    })
+
+/*****************************************************************************/
+
+GSList *nm_utils_g_slist_find_str(const GSList *list, const char *needle);
+
+int nm_utils_g_slist_strlist_cmp(const GSList *a, const GSList *b);
+
+char *nm_utils_g_slist_strlist_join(const GSList *a, const char *separator);
+
+/*****************************************************************************/
+
+static inline guint
+nm_g_array_len(const GArray *arr)
+{
+    return arr ? arr->len : 0u;
+}
+
+static inline void
+nm_g_array_unref(GArray *arr)
+{
+    if (arr)
+        g_array_unref(arr);
+}
+
+#define nm_g_array_append_new(arr, type)   \
+    ({                                     \
+        GArray *const _arr = (arr);        \
+        guint         _len;                \
+                                           \
+        nm_assert(_arr);                   \
+        _len = _arr->len;                  \
+        nm_assert(_len < G_MAXUINT);       \
+        g_array_set_size(_arr, _len + 1u); \
+        &g_array_index(arr, type, _len);   \
+    })
+
+/*****************************************************************************/
+
+static inline GPtrArray *
+nm_g_ptr_array_ref(GPtrArray *arr)
+{
+    return arr ? g_ptr_array_ref(arr) : NULL;
+}
+
+static inline void
+nm_g_ptr_array_unref(GPtrArray *arr)
+{
+    if (arr)
+        g_ptr_array_unref(arr);
+}
+
+#define nm_g_ptr_array_set(pdst, val)                              \
+    ({                                                             \
+        GPtrArray **_pdst    = (pdst);                             \
+        GPtrArray * _val     = (val);                              \
+        gboolean    _changed = FALSE;                              \
+                                                                   \
+        nm_assert(_pdst);                                          \
+                                                                   \
+        if (*_pdst != _val) {                                      \
+            _nm_unused gs_unref_ptrarray GPtrArray *_old = *_pdst; \
+                                                                   \
+            *_pdst   = nm_g_ptr_array_ref(_val);                   \
+            _changed = TRUE;                                       \
+        }                                                          \
+        _changed;                                                  \
+    })
+
+#define nm_g_ptr_array_set_take(pdst, val)                         \
+    ({                                                             \
+        GPtrArray **_pdst    = (pdst);                             \
+        GPtrArray * _val     = (val);                              \
+        gboolean    _changed = FALSE;                              \
+                                                                   \
+        nm_assert(_pdst);                                          \
+                                                                   \
+        if (*_pdst != _val) {                                      \
+            _nm_unused gs_unref_ptrarray GPtrArray *_old = *_pdst; \
+                                                                   \
+            *_pdst   = _val;                                       \
+            _changed = TRUE;                                       \
+        } else {                                                   \
+            nm_g_ptr_array_unref(_val);                            \
+        }                                                          \
+        _changed;                                                  \
+    })
+
+static inline guint
+nm_g_ptr_array_len(const GPtrArray *arr)
+{
+    return arr ? arr->len : 0u;
+}
+
+static inline gpointer *
+nm_g_ptr_array_pdata(const GPtrArray *arr)
+{
+    return arr ? arr->pdata : NULL;
+}
+
+GPtrArray *_nm_g_ptr_array_copy(GPtrArray *    array,
+                                GCopyFunc      func,
+                                gpointer       user_data,
+                                GDestroyNotify element_free_func);
+
+/**
+ * nm_g_ptr_array_copy:
+ * @array: the #GPtrArray to clone.
+ * @func: the copy function.
+ * @user_data: the user data for the copy function
+ * @element_free_func: the free function of the elements. @array MUST have
+ *   the same element_free_func. This argument is only used on older
+ *   glib, that doesn't support g_ptr_array_copy().
+ *
+ * This is a replacement for g_ptr_array_copy(), which is not available
+ * before glib 2.62. Since GPtrArray does not allow to access the internal
+ * element_free_func, we cannot add a compatibility implementation of g_ptr_array_copy()
+ * and the user must provide a suitable destroy function.
+ *
+ * Note that the @element_free_func MUST correspond to free function set in @array.
+ */
+#if GLIB_CHECK_VERSION(2, 62, 0)
+    #define nm_g_ptr_array_copy(array, func, user_data, element_free_func)            \
+        ({                                                                            \
+            _nm_unused GDestroyNotify const _element_free_func = (element_free_func); \
+                                                                                      \
+            G_GNUC_BEGIN_IGNORE_DEPRECATIONS;                                         \
+            g_ptr_array_copy((array), (func), (user_data));                           \
+            G_GNUC_END_IGNORE_DEPRECATIONS;                                           \
+        })
+#else
+    #define nm_g_ptr_array_copy(array, func, user_data, element_free_func) \
+        _nm_g_ptr_array_copy((array), (func), (user_data), (element_free_func))
+#endif
+
+/*****************************************************************************/
+
+static inline GHashTable *
+nm_g_hash_table_ref(GHashTable *hash)
+{
+    return hash ? g_hash_table_ref(hash) : NULL;
+}
+
+static inline void
+nm_g_hash_table_unref(GHashTable *hash)
+{
+    if (hash)
+        g_hash_table_unref(hash);
+}
+
+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_contains(GHashTable *hash, gconstpointer key)
+{
+    return hash ? g_hash_table_contains(hash, key) : FALSE;
+}
+
+static inline gboolean
+nm_g_hash_table_remove(GHashTable *hash, gconstpointer key)
+{
+    return hash ? g_hash_table_remove(hash, key) : FALSE;
+}
+
+/*****************************************************************************/
+
+gboolean nm_utils_ptrarray_is_sorted(gconstpointer *  list,
+                                     gsize            len,
+                                     gboolean         require_strict,
+                                     GCompareDataFunc cmpfcn,
+                                     gpointer         user_data);
+
+gssize nm_utils_ptrarray_find_binary_search(gconstpointer *  list,
+                                            gsize            len,
+                                            gconstpointer    needle,
+                                            GCompareDataFunc cmpfcn,
+                                            gpointer         user_data);
+
+gssize nm_utils_ptrarray_find_binary_search_range(gconstpointer *  list,
+                                                  gsize            len,
+                                                  gconstpointer    needle,
+                                                  GCompareDataFunc cmpfcn,
+                                                  gpointer         user_data,
+                                                  gssize *         out_idx_first,
+                                                  gssize *         out_idx_last);
+
+#define nm_utils_strv_find_binary_search(strv, len, needle)           \
+    ({                                                                \
+        const char *const *const _strv   = NM_CAST_STRV_CC(strv);     \
+        const gsize              _len    = (len);                     \
+        const char *const        _needle = (needle);                  \
+                                                                      \
+        nm_assert(_len == 0 || _strv);                                \
+        nm_assert(_needle);                                           \
+                                                                      \
+        nm_utils_ptrarray_find_binary_search((gconstpointer *) _strv, \
+                                             _len,                    \
+                                             _needle,                 \
+                                             nm_strcmp_with_data,     \
+                                             NULL);                   \
+    })
+
+gssize nm_utils_array_find_binary_search(gconstpointer    list,
+                                         gsize            elem_size,
+                                         gsize            len,
+                                         gconstpointer    needle,
+                                         GCompareDataFunc cmpfcn,
+                                         gpointer         user_data);
+
+gssize nm_utils_ptrarray_find_first(gconstpointer *list, gssize len, gconstpointer needle);
+
+/*****************************************************************************/
+
+void _nm_utils_strv_sort(const char **strv, gssize len);
+#define nm_utils_strv_sort(strv, len) _nm_utils_strv_sort(NM_CAST_STRV_MC(strv), len)
+
+int
+_nm_utils_strv_cmp_n(const char *const *strv1, gssize len1, const char *const *strv2, gssize len2);
+
+#define nm_utils_strv_cmp_n(strv1, len1, strv2, len2) \
+    _nm_utils_strv_cmp_n(NM_CAST_STRV_CC(strv1), (len1), NM_CAST_STRV_CC(strv2), (len2))
+
+#define nm_utils_strv_equal(strv1, strv2) (nm_utils_strv_cmp_n((strv1), -1, (strv2), -1) == 0)
+
+/*****************************************************************************/
+
+#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_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);
+
+/*****************************************************************************/
+
+#define NM_DEFINE_GDBUS_ARG_INFO_FULL(name_, ...) \
+    ((GDBusArgInfo *) (&((const GDBusArgInfo){.ref_count = -1, .name = name_, __VA_ARGS__})))
+
+#define NM_DEFINE_GDBUS_ARG_INFO(name_, a_signature) \
+    NM_DEFINE_GDBUS_ARG_INFO_FULL(name_, .signature = a_signature, )
+
+#define NM_DEFINE_GDBUS_ARG_INFOS(...)            \
+    ((GDBusArgInfo **) ((const GDBusArgInfo *[]){ \
+        __VA_ARGS__ NULL,                         \
+    }))
+
+#define NM_DEFINE_GDBUS_PROPERTY_INFO(name_, ...) \
+    ((GDBusPropertyInfo *) (&(                    \
+        (const GDBusPropertyInfo){.ref_count = -1, .name = name_, __VA_ARGS__})))
+
+#define NM_DEFINE_GDBUS_PROPERTY_INFO_READABLE(name_, m_signature) \
+    NM_DEFINE_GDBUS_PROPERTY_INFO(name_,                           \
+                                  .signature = m_signature,        \
+                                  .flags     = G_DBUS_PROPERTY_INFO_FLAGS_READABLE, )
+
+#define NM_DEFINE_GDBUS_PROPERTY_INFOS(...)                 \
+    ((GDBusPropertyInfo **) ((const GDBusPropertyInfo *[]){ \
+        __VA_ARGS__ NULL,                                   \
+    }))
+
+#define NM_DEFINE_GDBUS_SIGNAL_INFO_INIT(name_, ...) \
+    {                                                \
+        .ref_count = -1, .name = name_, __VA_ARGS__  \
+    }
+
+#define NM_DEFINE_GDBUS_SIGNAL_INFO(name_, ...) \
+    ((GDBusSignalInfo *) (&(                    \
+        (const GDBusSignalInfo) NM_DEFINE_GDBUS_SIGNAL_INFO_INIT(name_, __VA_ARGS__))))
+
+#define NM_DEFINE_GDBUS_SIGNAL_INFOS(...)               \
+    ((GDBusSignalInfo **) ((const GDBusSignalInfo *[]){ \
+        __VA_ARGS__ NULL,                               \
+    }))
+
+#define NM_DEFINE_GDBUS_METHOD_INFO_INIT(name_, ...) \
+    {                                                \
+        .ref_count = -1, .name = name_, __VA_ARGS__  \
+    }
+
+#define NM_DEFINE_GDBUS_METHOD_INFO(name_, ...) \
+    ((GDBusMethodInfo *) (&(                    \
+        (const GDBusMethodInfo) NM_DEFINE_GDBUS_METHOD_INFO_INIT(name_, __VA_ARGS__))))
+
+#define NM_DEFINE_GDBUS_METHOD_INFOS(...)               \
+    ((GDBusMethodInfo **) ((const GDBusMethodInfo *[]){ \
+        __VA_ARGS__ NULL,                               \
+    }))
+
+#define NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(name_, ...) \
+    {                                                   \
+        .ref_count = -1, .name = name_, __VA_ARGS__     \
+    }
+
+#define NM_DEFINE_GDBUS_INTERFACE_INFO(name_, ...) \
+    ((GDBusInterfaceInfo *) (&(                    \
+        (const GDBusInterfaceInfo) NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(name_, __VA_ARGS__))))
+
+#define NM_DEFINE_GDBUS_INTERFACE_VTABLE(...) \
+    ((GDBusInterfaceVTable *) (&((const GDBusInterfaceVTable){__VA_ARGS__})))
+
+/*****************************************************************************/
+
+guint64 nm_utils_get_start_time_for_pid(pid_t pid, char *out_state, pid_t *out_ppid);
+
+static inline gboolean
+nm_utils_process_state_is_dead(char pstate)
+{
+    /* "/proc/[pid]/stat" returns a state as the 3rd fields (see `man 5 proc`).
+     * Some of these states indicate the process is effectively dead (or a zombie).
+     */
+    return NM_IN_SET(pstate, 'Z', 'x', 'X');
+}
+
+/*****************************************************************************/
+
+typedef struct _NMUtilsUserData NMUtilsUserData;
+
+NMUtilsUserData *_nm_utils_user_data_pack(int nargs, gconstpointer *args);
+
+#define nm_utils_user_data_pack(...) \
+    _nm_utils_user_data_pack(NM_NARG(__VA_ARGS__), (gconstpointer[]){__VA_ARGS__})
+
+void _nm_utils_user_data_unpack(NMUtilsUserData *user_data, int nargs, ...);
+
+#define nm_utils_user_data_unpack(user_data, ...) \
+    _nm_utils_user_data_unpack(user_data, NM_NARG(__VA_ARGS__), __VA_ARGS__)
+
+/*****************************************************************************/
+
+typedef void (*NMUtilsInvokeOnIdleCallback)(gpointer user_data, GCancellable *cancellable);
+
+void nm_utils_invoke_on_idle(GCancellable *              cancellable,
+                             NMUtilsInvokeOnIdleCallback callback,
+                             gpointer                    callback_user_data);
+
+void nm_utils_invoke_on_timeout(guint                       timeout_msec,
+                                GCancellable *              cancellable,
+                                NMUtilsInvokeOnIdleCallback callback,
+                                gpointer                    callback_user_data);
+
+/*****************************************************************************/
+
+GSource *nm_utils_g_main_context_create_integrate_source(GMainContext *internal);
+
+/*****************************************************************************/
+
+static inline GPtrArray *
+nm_strv_ptrarray_ensure(GPtrArray **p_arr)
+{
+    nm_assert(p_arr);
+
+    if (G_UNLIKELY(!*p_arr))
+        *p_arr = g_ptr_array_new_with_free_func(g_free);
+
+    return *p_arr;
+}
+
+static inline const char *const *
+nm_strv_ptrarray_get_unsafe(GPtrArray *arr, guint *out_len)
+{
+    /* warning: the GPtrArray is not NULL terminated. So, it
+     * isn't really a strv array (sorry the misnomer). That's why
+     * the function is potentially "unsafe" and you must provide a
+     * out_len parameter. */
+    if (!arr || arr->len == 0) {
+        *out_len = 0;
+        return NULL;
+    }
+    *out_len = arr->len;
+    return (const char *const *) arr->pdata;
+}
+
+static inline GPtrArray *
+nm_strv_ptrarray_clone(const GPtrArray *src, gboolean null_if_empty)
+{
+    if (!src || (null_if_empty && src->len == 0))
+        return NULL;
+    return nm_g_ptr_array_copy((GPtrArray *) src, nm_copy_func_g_strdup, NULL, g_free);
+}
+
+static inline void
+nm_strv_ptrarray_add_string_take(GPtrArray *cmd, char *str)
+{
+    nm_assert(cmd);
+    nm_assert(str);
+
+    g_ptr_array_add(cmd, str);
+}
+
+static inline void
+nm_strv_ptrarray_add_string_dup(GPtrArray *cmd, const char *str)
+{
+    nm_strv_ptrarray_add_string_take(cmd, g_strdup(str));
+}
+
+#define nm_strv_ptrarray_add_string_concat(cmd, ...) \
+    nm_strv_ptrarray_add_string_take((cmd), g_strconcat(__VA_ARGS__, NULL))
+
+#define nm_strv_ptrarray_add_string_printf(cmd, ...) \
+    nm_strv_ptrarray_add_string_take((cmd), g_strdup_printf(__VA_ARGS__))
+
+#define nm_strv_ptrarray_add_int(cmd, val) \
+    nm_strv_ptrarray_add_string_take((cmd), nm_strdup_int(val))
+
+static inline void
+nm_strv_ptrarray_take_gstring(GPtrArray *cmd, GString **gstr)
+{
+    nm_assert(gstr && *gstr);
+
+    nm_strv_ptrarray_add_string_take(cmd, g_string_free(g_steal_pointer(gstr), FALSE));
+}
+
+static inline gssize
+nm_strv_ptrarray_find_first(const GPtrArray *strv, const char *str)
+{
+    if (!strv)
+        return -1;
+    return nm_utils_strv_find_first((char **) strv->pdata, strv->len, str);
+}
+
+static inline gboolean
+nm_strv_ptrarray_contains(const GPtrArray *strv, const char *str)
+{
+    return nm_strv_ptrarray_find_first(strv, str) >= 0;
+}
+
+static inline int
+nm_strv_ptrarray_cmp(const GPtrArray *a, const GPtrArray *b)
+{
+    /* nm_utils_strv_cmp_n() will treat NULL and empty arrays the same.
+     * That means, an empty strv array can both be represented by NULL
+     * and an array of length zero.
+     * If you need to distinguish between these case, do that yourself. */
+    return nm_utils_strv_cmp_n((const char *const *) nm_g_ptr_array_pdata(a),
+                               nm_g_ptr_array_len(a),
+                               (const char *const *) nm_g_ptr_array_pdata(b),
+                               nm_g_ptr_array_len(b));
+}
+
+/*****************************************************************************/
+
+int nm_utils_getpagesize(void);
+
+/*****************************************************************************/
+
+extern const char _nm_hexchar_table_lower[16];
+extern const char _nm_hexchar_table_upper[16];
+
+static inline char
+nm_hexchar(int x, gboolean upper_case)
+{
+    return upper_case ? _nm_hexchar_table_upper[x & 15] : _nm_hexchar_table_lower[x & 15];
+}
+
+static inline gboolean
+nm_ascii_is_ctrl(char ch)
+{
+    /* 0 to ' '-1 is the C0 range.
+     *
+     * Other ranges may also be considered control characters, but NOT
+     * CONSIDERED by this function. For example:
+     *   - DEL (127) is also a control character.
+     *   - SP (' ', 0x20) is also considered a control character.
+     *   - DEL+1 (0x80) to 0x9F is C1 range.
+     *   - NBSP (0xA0) and SHY (0xAD) are ISO 8859 special characters
+     */
+    return ((guchar) ch) < ' ';
+}
+
+static inline gboolean
+nm_ascii_is_ctrl_or_del(char ch)
+{
+    return ((guchar) ch) < ' ' || ch == 127;
+}
+
+static inline gboolean
+nm_ascii_is_non_ascii(char ch)
+{
+    return ((guchar) ch) > 127;
+}
+
+static inline gboolean
+nm_ascii_is_regular(char ch)
+{
+    /* same as(!nm_ascii_is_ctrl_or_del(ch) && !nm_ascii_is_non_ascii(ch)) */
+    return ch >= ' ' && ch < 127;
+}
+
+char *nm_utils_bin2hexstr_full(gconstpointer addr,
+                               gsize         length,
+                               char          delimiter,
+                               gboolean      upper_case,
+                               char *        out);
+
+#define nm_utils_bin2hexstr_a(addr, length, delimiter, upper_case, str_to_free)               \
+    ({                                                                                        \
+        gconstpointer _addr        = (addr);                                                  \
+        gsize         _length      = (length);                                                \
+        char          _delimiter   = (delimiter);                                             \
+        char **       _str_to_free = (str_to_free);                                           \
+        char *        _s;                                                                     \
+        gsize         _s_len;                                                                 \
+                                                                                              \
+        nm_assert(_str_to_free);                                                              \
+                                                                                              \
+        _s_len = _length == 0 ? 1u : (_delimiter == '\0' ? _length * 2u + 1u : _length * 3u); \
+        if (_s_len < 100)                                                                     \
+            _s = g_alloca(_s_len);                                                            \
+        else {                                                                                \
+            _s            = g_malloc(_s_len);                                                 \
+            *_str_to_free = _s;                                                               \
+        }                                                                                     \
+        nm_utils_bin2hexstr_full(_addr, _length, _delimiter, (upper_case), _s);               \
+    })
+
+static inline const char *
+nm_ether_addr_to_string(const NMEtherAddr *ether_addr, char sbuf[static(sizeof(NMEtherAddr) * 3)])
+{
+    nm_assert(ether_addr);
+    nm_assert(sbuf);
+
+    return nm_utils_bin2hexstr_full(ether_addr, sizeof(NMEtherAddr), ':', TRUE, sbuf);
+}
+
+#define nm_ether_addr_to_string_a(ether_addr) \
+    nm_ether_addr_to_string((ether_addr), g_alloca(sizeof(NMEtherAddr) * 3))
+
+guint8 *nm_utils_hexstr2bin_full(const char *hexstr,
+                                 gboolean    allow_0x_prefix,
+                                 gboolean    delimiter_required,
+                                 gboolean    hexdigit_pairs_required,
+                                 const char *delimiter_candidates,
+                                 gsize       required_len,
+                                 guint8 *    buffer,
+                                 gsize       buffer_len,
+                                 gsize *     out_len);
+
+#define nm_utils_hexstr2bin_buf(hexstr,               \
+                                allow_0x_prefix,      \
+                                delimiter_required,   \
+                                delimiter_candidates, \
+                                buffer)               \
+    nm_utils_hexstr2bin_full((hexstr),                \
+                             (allow_0x_prefix),       \
+                             (delimiter_required),    \
+                             FALSE,                   \
+                             (delimiter_candidates),  \
+                             G_N_ELEMENTS(buffer),    \
+                             (buffer),                \
+                             G_N_ELEMENTS(buffer),    \
+                             NULL)
+
+guint8 *nm_utils_hexstr2bin_alloc(const char *hexstr,
+                                  gboolean    allow_0x_prefix,
+                                  gboolean    delimiter_required,
+                                  const char *delimiter_candidates,
+                                  gsize       required_len,
+                                  gsize *     out_len);
+
+/**
+ * _nm_utils_hwaddr_aton:
+ * @asc: the ASCII representation of a hardware address
+ * @buffer: buffer to store the result into. Must have
+ *   at least a size of @buffer_length.
+ * @buffer_length: the length of the input buffer @buffer.
+ *   The result must fit into that buffer, otherwise
+ *   the function fails and returns %NULL.
+ * @out_length: the output length in case of success.
+ *
+ * Parses @asc and converts it to binary form in @buffer.
+ * Bytes in @asc can be separated by colons (:), or hyphens (-), but not mixed.
+ *
+ * It is like nm_utils_hwaddr_aton(), but contrary to that it
+ * can parse addresses of any length. That is, you don't need
+ * to know the length before-hand.
+ *
+ * Return value: @buffer, or %NULL if @asc couldn't be parsed.
+ */
+static inline guint8 *
+_nm_utils_hwaddr_aton(const char *asc, gpointer buffer, gsize buffer_length, gsize *out_length)
+{
+    g_return_val_if_fail(asc, NULL);
+    g_return_val_if_fail(buffer, NULL);
+    g_return_val_if_fail(buffer_length > 0, NULL);
+    g_return_val_if_fail(out_length, NULL);
+
+    return nm_utils_hexstr2bin_full(asc,
+                                    FALSE,
+                                    TRUE,
+                                    FALSE,
+                                    ":-",
+                                    0,
+                                    buffer,
+                                    buffer_length,
+                                    out_length);
+}
+
+static inline guint8 *
+_nm_utils_hwaddr_aton_exact(const char *asc, gpointer buffer, gsize buffer_length)
+{
+    g_return_val_if_fail(asc, NULL);
+    g_return_val_if_fail(buffer, NULL);
+    g_return_val_if_fail(buffer_length > 0, NULL);
+
+    return nm_utils_hexstr2bin_full(asc,
+                                    FALSE,
+                                    TRUE,
+                                    FALSE,
+                                    ":-",
+                                    buffer_length,
+                                    buffer,
+                                    buffer_length,
+                                    NULL);
+}
+
+static inline const char *
+_nm_utils_hwaddr_ntoa(gconstpointer addr,
+                      gsize         addr_len,
+                      gboolean      upper_case,
+                      char *        buf,
+                      gsize         buf_len)
+{
+    g_return_val_if_fail(addr, NULL);
+    g_return_val_if_fail(addr_len > 0, NULL);
+    g_return_val_if_fail(buf, NULL);
+    if (buf_len < addr_len * 3)
+        g_return_val_if_reached(NULL);
+
+    return nm_utils_bin2hexstr_full(addr, addr_len, ':', upper_case, buf);
+}
+
+#define _nm_utils_hwaddr_ntoa_maybe_a(addr, addr_len, buf_to_free)                       \
+    ({                                                                                   \
+        gconstpointer const _addr        = (addr);                                       \
+        const gsize         _addr_len    = (addr_len);                                   \
+        char **const        _buf_to_free = (buf_to_free);                                \
+                                                                                         \
+        nm_utils_bin2hexstr_full(                                                        \
+            _addr,                                                                       \
+            _addr_len,                                                                   \
+            ':',                                                                         \
+            TRUE,                                                                        \
+            nm_malloc_maybe_a(3 * 20, _addr_len ? (_addr_len * 3u) : 1u, _buf_to_free)); \
+    })
+
+/*****************************************************************************/
+
+#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;                                                                   \
+        }                                                                                      \
+    }                                                                                          \
+    _NM_DUMMY_STRUCT_FOR_TRAILING_SEMICOLON
+
+#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,
+              gpointer            source_tag,
+              GAsyncReadyCallback callback,
+              gpointer            callback_data)
+{
+    GTask *task;
+
+    task = g_task_new(source_object, cancellable, callback, callback_data);
+    if (source_tag)
+        g_task_set_source_tag(task, source_tag);
+    return task;
+}
+
+static inline gboolean
+nm_g_task_is_valid(gpointer task, gpointer source_object, gpointer source_tag)
+{
+    return g_task_is_valid(task, source_object) && g_task_get_source_tag(task) == source_tag;
+}
+
+guint nm_utils_parse_debug_string(const char *string, const GDebugKey *keys, guint nkeys);
+
+/*****************************************************************************/
+
+static inline gboolean
+nm_utils_strdup_reset(char **dst, const char *src)
+{
+    char *old;
+
+    nm_assert(dst);
+
+    if (nm_streq0(*dst, src))
+        return FALSE;
+    old  = *dst;
+    *dst = g_strdup(src);
+    g_free(old);
+    return TRUE;
+}
+
+static inline gboolean
+nm_utils_strdup_reset_take(char **dst, char *src)
+{
+    char *old;
+
+    nm_assert(dst);
+    nm_assert(src != *dst);
+
+    if (nm_streq0(*dst, src)) {
+        if (src)
+            g_free(src);
+        return FALSE;
+    }
+    old  = *dst;
+    *dst = src;
+    g_free(old);
+    return TRUE;
+}
+
+void nm_indirect_g_free(gpointer arg);
+
+/*****************************************************************************/
+
+void nm_utils_ifname_cpy(char *dst, const char *name);
+
+typedef enum {
+    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);
+
+gboolean nm_utils_ifname_valid(const char *name, NMUtilsIfaceType type, GError **error);
+
+/*****************************************************************************/
+
+static inline GArray *
+nm_strvarray_ensure(GArray **p)
+{
+    if (!*p) {
+        *p = g_array_new(TRUE, FALSE, sizeof(char *));
+        g_array_set_clear_func(*p, nm_indirect_g_free);
+    }
+    return *p;
+}
+
+static inline void
+nm_strvarray_add(GArray *array, const char *str)
+{
+    char *s;
+
+    s = g_strdup(str);
+    g_array_append_val(array, s);
+}
+
+static inline const char *
+nm_strvarray_get_idx(GArray *array, guint idx)
+{
+    nm_assert(array);
+    nm_assert(idx < array->len);
+
+    return g_array_index(array, const char *, idx);
+}
+
+static inline const char *const *
+nm_strvarray_get_strv_non_empty(GArray *arr, guint *length)
+{
+    if (!arr || arr->len == 0) {
+        NM_SET_OUT(length, 0);
+        return NULL;
+    }
+
+    NM_SET_OUT(length, arr->len);
+    return &g_array_index(arr, const char *, 0);
+}
+
+static inline char **
+nm_strvarray_get_strv_non_empty_dup(GArray *arr, guint *length)
+{
+    const char *const *strv;
+
+    if (!arr || arr->len == 0) {
+        NM_SET_OUT(length, 0);
+        return NULL;
+    }
+
+    NM_SET_OUT(length, arr->len);
+    strv = &g_array_index(arr, const char *, 0);
+    return nm_utils_strv_dup(strv, arr->len, TRUE);
+}
+
+static inline const char *const *
+nm_strvarray_get_strv(GArray **arr, guint *length)
+{
+    if (!*arr) {
+        NM_SET_OUT(length, 0);
+        return (const char *const *) arr;
+    }
+
+    NM_SET_OUT(length, (*arr)->len);
+    return &g_array_index(*arr, const char *, 0);
+}
+
+static inline void
+nm_strvarray_set_strv(GArray **array, const char *const *strv)
+{
+    gs_unref_array GArray *array_old = NULL;
+
+    array_old = g_steal_pointer(array);
+
+    if (!strv || !strv[0])
+        return;
+
+    nm_strvarray_ensure(array);
+    for (; strv[0]; strv++)
+        nm_strvarray_add(*array, strv[0]);
+}
+
+static inline gssize
+nm_strvarray_find_first(GArray *strv, const char *needle)
+{
+    guint i;
+
+    nm_assert(needle);
+
+    if (strv) {
+        for (i = 0; i < strv->len; i++) {
+            if (nm_streq(needle, g_array_index(strv, const char *, i)))
+                return i;
+        }
+    }
+    return -1;
+}
+
+static inline gboolean
+nm_strvarray_remove_first(GArray *strv, const char *needle)
+{
+    gssize idx;
+
+    nm_assert(needle);
+
+    idx = nm_strvarray_find_first(strv, needle);
+    if (idx < 0)
+        return FALSE;
+    g_array_remove_index(strv, idx);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+struct _NMVariantAttributeSpec {
+    char *              name;
+    const GVariantType *type;
+    bool                v4 : 1;
+    bool                v6 : 1;
+    bool                no_value : 1;
+    bool                consumes_rest : 1;
+    char                str_type;
+};
+
+typedef struct _NMVariantAttributeSpec NMVariantAttributeSpec;
+
+void _nm_utils_format_variant_attributes_full(GString *                            str,
+                                              const NMUtilsNamedValue *            values,
+                                              guint                                num_values,
+                                              const NMVariantAttributeSpec *const *spec,
+                                              char                                 attr_separator,
+                                              char key_value_separator);
+
+char *_nm_utils_format_variant_attributes(GHashTable *                         attributes,
+                                          const NMVariantAttributeSpec *const *spec,
+                                          char                                 attr_separator,
+                                          char                                 key_value_separator);
+
+/*****************************************************************************/
+
+gboolean nm_utils_is_localhost(const char *name);
+
+gboolean nm_utils_is_specific_hostname(const char *name);
+
+char *   nm_utils_uid_to_name(uid_t uid);
+gboolean nm_utils_name_to_uid(const char *name, uid_t *out_uid);
+
+/*****************************************************************************/
+
+double nm_utils_exp10(gint16 e);
+
+/*****************************************************************************/
+
+gboolean _nm_utils_is_empty_ssid_arr(const guint8 *ssid, gsize len);
+gboolean _nm_utils_is_empty_ssid_gbytes(GBytes *ssid);
+char *   _nm_utils_ssid_to_string_arr(const guint8 *ssid, gsize len);
+char *   _nm_utils_ssid_to_string_gbytes(GBytes *ssid);
+
+/*****************************************************************************/
+
+gboolean    nm_utils_is_valid_path_component(const char *name);
+const char *NM_ASSERT_VALID_PATH_COMPONENT(const char *name);
+
+#define NM_UTILS_SYSCTL_IP_CONF_PATH_BUFSIZE 100
+
+const char *
+nm_utils_sysctl_ip_conf_path(int addr_family, char *buf, const char *ifname, const char *property);
+
+gboolean nm_utils_sysctl_ip_conf_is_path(int         addr_family,
+                                         const char *path,
+                                         const char *ifname,
+                                         const char *property);
+
+/*****************************************************************************/
+
+void nm_crypto_md5_hash(const guint8 *salt,
+                        gsize         salt_len,
+                        const guint8 *password,
+                        gsize         password_len,
+                        guint8 *      buffer,
+                        gsize         buflen);
+
+/*****************************************************************************/
+
+char *nm_utils_get_process_exit_status_desc(int status);
+
+/*****************************************************************************/
+
+void nm_utils_thread_local_register_destroy(gpointer tls_data, GDestroyNotify destroy_notify);
+
+#endif /* __NM_SHARED_UTILS_H__ */
diff --git a/src/libnm-glib-aux/nm-str-buf.h b/src/libnm-glib-aux/nm-str-buf.h
new file mode 100644
index 00000000..7a7f580c
--- /dev/null
+++ b/src/libnm-glib-aux/nm-str-buf.h
@@ -0,0 +1,514 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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(const NMStrBuf *strbuf)
+{
+    nm_assert(strbuf);
+    nm_assert((!!strbuf->_priv_str) == (strbuf->_priv_allocated > 0));
+    nm_assert(strbuf->_priv_len <= strbuf->_priv_allocated);
+}
+
+static inline NMStrBuf
+NM_STR_BUF_INIT(gsize allocated, gboolean do_bzero_mem)
+{
+    NMStrBuf strbuf = {
+        ._priv_str          = allocated ? g_malloc(allocated) : NULL,
+        ._priv_allocated    = allocated,
+        ._priv_len          = 0,
+        ._priv_do_bzero_mem = do_bzero_mem,
+    };
+
+    return strbuf;
+}
+
+static inline void
+nm_str_buf_init(NMStrBuf *strbuf, gsize len, bool do_bzero_mem)
+{
+    nm_assert(strbuf);
+    *strbuf = NM_STR_BUF_INIT(len, 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);
+    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 shrunk memory will be cleared, if
+ *   do_bzero_mem is set. This should be usually set to %TRUE, unless
+ *   you know that the shrunk 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_repeated(NMStrBuf *strbuf, char ch, guint len)
+{
+    if (len > 0) {
+        nm_str_buf_maybe_expand(strbuf, ((gsize) len) + 1u, FALSE);
+        do {
+            strbuf->_priv_str[strbuf->_priv_len++] = ch;
+        } while (--len > 0);
+    }
+}
+
+static inline void
+_nm_str_buf_append_c_1(NMStrBuf *strbuf, char ch)
+{
+    strbuf->_priv_str[strbuf->_priv_len++] = ch;
+}
+
+#define _nm_str_buf_append_c_2(strbuf, ch0, ch1) \
+    _nm_str_buf_append_c_1(strbuf, (ch0));       \
+    _nm_str_buf_append_c_1(strbuf, (ch1))
+#define _nm_str_buf_append_c_3(strbuf, ch0, ...) \
+    _nm_str_buf_append_c_1(strbuf, (ch0));       \
+    _nm_str_buf_append_c_2(strbuf, __VA_ARGS__)
+#define _nm_str_buf_append_c_4(strbuf, ch0, ...) \
+    _nm_str_buf_append_c_1(strbuf, (ch0));       \
+    _nm_str_buf_append_c_3(strbuf, __VA_ARGS__)
+
+#define _nm_str_buf_append_c_n2(strbuf, n, ...)             \
+    G_STMT_START                                            \
+    {                                                       \
+        NMStrBuf *_strbuf = strbuf;                         \
+                                                            \
+        nm_str_buf_maybe_expand(_strbuf, (n) + 1, FALSE);   \
+        {                                                   \
+            _nm_str_buf_append_c_##n(_strbuf, __VA_ARGS__); \
+        };                                                  \
+    }                                                       \
+    G_STMT_END
+
+#define _nm_str_buf_append_c_n1(strbuf, n, ...) _nm_str_buf_append_c_n2(strbuf, n, __VA_ARGS__)
+
+#define nm_str_buf_append_c(strbuf, ...) \
+    _nm_str_buf_append_c_n1((strbuf), NM_NARG(__VA_ARGS__), __VA_ARGS__)
+
+static inline void
+nm_str_buf_append_c_hex(NMStrBuf *strbuf, char ch, gboolean upper_case)
+{
+    nm_str_buf_append_c(strbuf,
+                        nm_hexchar(((guchar) ch) >> 4, upper_case),
+                        nm_hexchar((guchar) ch, upper_case));
+}
+
+static inline void
+nm_str_buf_append_unichar(NMStrBuf *strbuf, gunichar wc)
+{
+    int l;
+
+    nm_str_buf_maybe_expand(strbuf, 6 + 1, FALSE);
+    l = g_unichar_to_utf8(wc, &strbuf->_priv_str[strbuf->_priv_len]);
+    nm_assert(l > 0 && l <= 6);
+    strbuf->_priv_len += (gsize) l;
+}
+
+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 char *
+nm_str_buf_append_len0(NMStrBuf *strbuf, const char *str, gsize len)
+{
+    _nm_str_buf_assert(strbuf);
+
+    /* this is basically like nm_str_buf_append_len() and
+     * nm_str_buf_get_str() in one. */
+
+    nm_str_buf_maybe_expand(strbuf, len + 1u, FALSE);
+    if (len > 0) {
+        memcpy(&strbuf->_priv_str[strbuf->_priv_len], str, len);
+        strbuf->_priv_len += len;
+    }
+    strbuf->_priv_str[strbuf->_priv_len] = '\0';
+    return strbuf->_priv_str;
+}
+
+static inline void
+nm_str_buf_append(NMStrBuf *strbuf, const char *str)
+{
+    nm_assert(str);
+
+    nm_str_buf_append_len(strbuf, str, strlen(str));
+}
+
+static inline char *
+nm_str_buf_append0(NMStrBuf *strbuf, const char *str)
+{
+    nm_assert(str);
+
+    return nm_str_buf_append_len0(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 NMStrBuf *
+nm_str_buf_append_required_delimiter(NMStrBuf *strbuf, char delimiter)
+{
+    _nm_str_buf_assert(strbuf);
+
+    /* appends the @delimiter if it is required (that is, if the
+     * string is not empty). */
+    if (strbuf->len > 0)
+        nm_str_buf_append_c(strbuf, delimiter);
+    return strbuf;
+}
+
+static inline void
+nm_str_buf_append_dirty(NMStrBuf *strbuf, gsize len)
+{
+    _nm_str_buf_assert(strbuf);
+
+    /* this append @len bytes to the buffer, but it does not
+     * initialize them! */
+    if (len > 0) {
+        nm_str_buf_maybe_expand(strbuf, len, FALSE);
+        strbuf->_priv_len += len;
+    }
+}
+
+static inline void
+nm_str_buf_append_c_len(NMStrBuf *strbuf, char ch, gsize len)
+{
+    _nm_str_buf_assert(strbuf);
+
+    if (len > 0) {
+        nm_str_buf_maybe_expand(strbuf, len, FALSE);
+        memset(&strbuf->_priv_str[strbuf->_priv_len], ch, len);
+        strbuf->_priv_len += len;
+    }
+}
+
+/*****************************************************************************/
+
+static inline NMStrBuf *
+nm_str_buf_reset(NMStrBuf *strbuf)
+{
+    _nm_str_buf_assert(strbuf);
+
+    if (strbuf->_priv_len > 0) {
+        if (strbuf->_priv_do_bzero_mem) {
+            /* we only clear the memory that we wrote to. */
+            nm_explicit_bzero(strbuf->_priv_str, strbuf->_priv_len);
+        }
+        strbuf->_priv_len = 0;
+    }
+
+    return strbuf;
+}
+
+/*****************************************************************************/
+
+/* Calls nm_utils_escaped_tokens_escape() on @str and appends the
+ * result to @strbuf. */
+static inline void
+nm_utils_escaped_tokens_escape_strbuf(const char *str, const char *delimiters, NMStrBuf *strbuf)
+{
+    gs_free char *str_to_free = NULL;
+
+    nm_assert(str);
+
+    nm_str_buf_append(strbuf, nm_utils_escaped_tokens_escape(str, delimiters, &str_to_free));
+}
+
+/* Calls nm_utils_escaped_tokens_escape_unnecessary() on @str and appends the
+ * string to @strbuf. */
+static inline void
+nm_utils_escaped_tokens_escape_strbuf_assert(const char *str,
+                                             const char *delimiters,
+                                             NMStrBuf *  strbuf)
+{
+    nm_str_buf_append(strbuf, nm_utils_escaped_tokens_escape_unnecessary(str, delimiters));
+}
+
+/*****************************************************************************/
+
+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.
+ *   If currently no buffer is allocated, this will return %NULL.
+ */
+static inline char *
+nm_str_buf_get_str(NMStrBuf *strbuf)
+{
+    _nm_str_buf_assert(strbuf);
+
+    if (!strbuf->_priv_str)
+        return NULL;
+
+    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;
+}
+
+static inline char *
+nm_str_buf_get_str_at_unsafe(NMStrBuf *strbuf, gsize index)
+{
+    _nm_str_buf_assert(strbuf);
+
+    /* it is acceptable to ask for a pointer at the end of the buffer -- even
+     * if there is no data there. The caller is anyway required to take care
+     * of the length (that's the "unsafe" part), and in that case, the length
+     * is merely zero. */
+    nm_assert(index <= strbuf->allocated);
+
+    if (!strbuf->_priv_str)
+        return NULL;
+
+    return &strbuf->_priv_str[index];
+}
+
+static inline char
+nm_str_buf_get_char(const NMStrBuf *strbuf, gsize index)
+{
+    _nm_str_buf_assert(strbuf);
+    nm_assert(index < strbuf->allocated);
+    return strbuf->_priv_str[index];
+}
+
+/**
+ * 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().
+ *   Note that if no string is allocated yet (after nm_str_buf_init() with
+ *   length zero), this will return %NULL. */
+static inline char *
+nm_str_buf_finalize(NMStrBuf *strbuf, gsize *out_len)
+{
+    _nm_str_buf_assert(strbuf);
+
+    NM_SET_OUT(out_len, strbuf->_priv_len);
+
+    if (!strbuf->_priv_str)
+        return NULL;
+
+    nm_str_buf_maybe_expand(strbuf, 1, TRUE);
+    strbuf->_priv_str[strbuf->_priv_len] = '\0';
+
+    /* 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);
+}
+
+static inline GBytes *
+nm_str_buf_finalize_to_gbytes(NMStrBuf *strbuf)
+{
+    char *s;
+    gsize l;
+
+    /* this always returns a non-NULL, newly allocated GBytes instance.
+     * The data buffer always has an additional NUL character after
+     * the data, and the data is allocated with malloc.
+     *
+     * That means, the caller who takes ownership of the GBytes can
+     * safely modify the content of the buffer (including the additional
+     * NUL sentinel). */
+    s = nm_str_buf_finalize(strbuf, &l);
+    return g_bytes_new_take(s ?: g_new0(char, 1), l);
+}
+
+/**
+ * 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)
+
+static inline gboolean
+nm_str_buf_utf8_validate(NMStrBuf *strbuf)
+{
+    _nm_str_buf_assert(strbuf);
+    return strbuf->_priv_len == 0 || g_utf8_validate(strbuf->_priv_str, strbuf->_priv_len, NULL);
+}
+
+#endif /* __NM_STR_BUF_H__ */
diff --git a/src/libnm-glib-aux/nm-test-utils.h b/src/libnm-glib-aux/nm-test-utils.h
new file mode 100644
index 00000000..ff8eb606
--- /dev/null
+++ b/src/libnm-glib-aux/nm-test-utils.h
@@ -0,0 +1,2860 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#ifndef __NM_TEST_UTILS_H__
+#define __NM_TEST_UTILS_H__
+
+/*******************************************************************************
+ * HOWTO run tests.
+ *
+ * Our tests (make check) include this header-only file nm-test-utils.h.
+ *
+ * You should always include this header *as last*. Reason is, that depending on
+ * previous includes, functionality will be enabled.
+ *
+ * Logging:
+ *   In tests, nm-logging redirects to glib logging. By default, glib suppresses all debug
+ *   messages unless you set G_MESSAGES_DEBUG. To enable debug logging, you can explicitly set
+ *   G_MESSAGES_DEBUG. Otherwise, nm-test will set G_MESSAGES_DEBUG=all in debug mode (see below).
+ *   For nm-logging, you can configure the log-level and domains via NMTST_DEBUG environment
+ *   variable.
+ *
+ * Assert-logging:
+ *   Some tests assert against logged messages (g_test_expect_message()).
+ *   By specifying no-expect-message in NMTST_DEBUG, you can disable assert logging
+ *   and g_test_assert_expected_messages() will not fail.
+ *
+ * NMTST_SEED_RAND environment variable:
+ *   Tests that use random numbers from nmtst_get_rand() get seeded at each start.
+ *   You can specify the seed by setting NMTST_SEED_RAND to a particular number or empty ("")
+ *   for a random one. If NMTST_SEED_RAND is not set (default) a stable seed gets chosen.
+ *   Tests will print the seed to stdout, so that you know which one was chosen or generated.
+ *
+ *
+ * NMTST_DEBUG environment variable:
+ *
+ * "debug", "no-debug": when at test is run in debug mode, it might behave differently,
+ *   depending on the test. See nmtst_is_debug().
+ *   Known differences:
+ *    - a test might leave the logging level unspecified. In this case, running in
+ *      debug mode, will turn on DEBUG logging, otherwise WARN logging only.
+ *    - if G_MESSAGES_DEBUG is unset, nm-test will set G_MESSAGES_DEBUG=all
+ *      for tests that don't do assert-logging.
+ *   Debug mode is determined as follows (highest priority first):
+ *    - command line option --debug/--no-debug
+ *    - NMTST_DEBUG=debug/no-debug
+ *    - setting NMTST_DEBUG implies debugging turned on
+ *    - g_test_verbose()
+ *
+ * "no-expect-message": for tests that would assert against log messages, disable
+ *   those asserts.
+ *
+ * "log-level=LEVEL", "log-domains=DOMAIN": reset the log level and domain for tests.
+ *    It only has an effect for nm-logging messages.
+ *    This has no effect if the test asserts against logging (unless no-expect-message),
+ *    otherwise, changing the logging would break tests.
+ *    If you set the level to DEBUG or TRACE, it also sets G_MESSAGES_DEBUG=all (unless
+ *    in assert-logging mode and unless G_MESSAGES_DEBUG is already defined).
+ *
+ * "TRACE", this is shorthand for "log-level=TRACE".
+ *
+ * "D", this is shorthand for "log-level=TRACE,no-expect-message".
+ *
+ * "sudo-cmd=PATH": when running root tests as normal user, the test will execute
+ *   itself by invoking sudo at PATH.
+ *   For example
+ *     NMTST_DEBUG="sudo-cmd=$PWD/tools/test-sudo-wrapper.sh" make -C src/platform/tests/ check
+ *
+ * "slow|quick|thorough": enable/disable long-running tests. This sets nmtst_test_quick().
+ *   Whether long-running tests are enabled is determined as follows (highest priority first):
+ *     - specifying the value in NMTST_DEBUG has highest priority
+ *     - respect g_test_quick(), if the command line contains '-mslow', '-mquick', '-mthorough'.
+ *     - use compile time default (CFLAGS=-DNMTST_TEST_QUICK=TRUE)
+ *     - enable slow tests by default
+ *
+ * "p=PATH"|"s=PATH": passes the path to g_test_init() as "-p" and "-s", respectively.
+ *   Unfortunately, these options conflict with "--tap" which our makefile passes to the
+ *   tests, thus it's only useful outside of `make check`.
+ *
+ *******************************************************************************/
+
+#if defined(NM_ASSERT_NO_MSG) && NM_ASSERT_NO_MSG
+    #undef g_return_if_fail_warning
+    #undef g_assertion_message_expr
+#endif
+
+#include <arpa/inet.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+/*****************************************************************************/
+
+#define NMTST_G_RETURN_MSG_S(expr) "*: assertion '" NM_ASSERT_G_RETURN_EXPR(expr) "' failed"
+#define NMTST_G_RETURN_MSG(expr)   NMTST_G_RETURN_MSG_S(#expr)
+
+/*****************************************************************************/
+
+/* general purpose functions that have no dependency on other nmtst functions */
+
+#define nmtst_assert_error(error, expect_error_domain, expect_error_code, expect_error_pattern) \
+    G_STMT_START                                                                                \
+    {                                                                                           \
+        GError *    _error                = (error);                                            \
+        GQuark      _expect_error_domain  = (expect_error_domain);                              \
+        const char *_expect_error_pattern = (expect_error_pattern);                             \
+                                                                                                \
+        if (_expect_error_domain)                                                               \
+            g_assert_error(_error, _expect_error_domain, (expect_error_code));                  \
+        else                                                                                    \
+            g_assert(_error);                                                                   \
+        g_assert(_error->message);                                                              \
+        if (_expect_error_pattern                                                               \
+            && !g_pattern_match_simple(_expect_error_pattern, _error->message)) {               \
+            g_error("%s:%d: error message does not have expected pattern '%s'. Instead it is "  \
+                    "'%s' (%s, %d)",                                                            \
+                    __FILE__,                                                                   \
+                    __LINE__,                                                                   \
+                    _expect_error_pattern,                                                      \
+                    _error->message,                                                            \
+                    g_quark_to_string(_error->domain),                                          \
+                    _error->code);                                                              \
+        }                                                                                       \
+    }                                                                                           \
+    G_STMT_END
+
+#define NMTST_WAIT(max_wait_ms, wait)                                                         \
+    ({                                                                                        \
+        gboolean     _not_expired             = TRUE;                                         \
+        const gint64 nmtst_wait_start_us      = g_get_monotonic_time();                       \
+        const gint64 nmtst_wait_duration_us   = (max_wait_ms) *1000L;                         \
+        const gint64 nmtst_wait_end_us        = nmtst_wait_start_us + nmtst_wait_duration_us; \
+        gint64       _nmtst_wait_remaining_us = nmtst_wait_duration_us;                       \
+        int          _nmtst_wait_iteration    = 0;                                            \
+                                                                                              \
+        while (TRUE) {                                                                        \
+            _nm_unused const gint64 nmtst_wait_remaining_us = _nmtst_wait_remaining_us;       \
+            _nm_unused int          nmtst_wait_iteration    = _nmtst_wait_iteration++;        \
+                                                                                              \
+            {wait};                                                                           \
+            _nmtst_wait_remaining_us = (nmtst_wait_end_us - g_get_monotonic_time());          \
+            if (_nmtst_wait_remaining_us <= 0) {                                              \
+                _not_expired = FALSE;                                                         \
+                break;                                                                        \
+            }                                                                                 \
+        }                                                                                     \
+        _not_expired;                                                                         \
+    })
+
+#define NMTST_WAIT_ASSERT(max_wait_ms, wait)  \
+    G_STMT_START                              \
+    {                                         \
+        if (!(NMTST_WAIT(max_wait_ms, wait))) \
+            g_assert_not_reached();           \
+    }                                         \
+    G_STMT_END
+
+#define nmtst_assert_nonnull(command)          \
+    ({                                         \
+        typeof(*(command)) *_ptr = (command);  \
+                                               \
+        g_assert(_ptr && (TRUE || (command))); \
+        _ptr;                                  \
+    })
+
+#define nmtst_assert_success(success, error) \
+    G_STMT_START                             \
+    {                                        \
+        g_assert_no_error(error);            \
+        g_assert((success));                 \
+    }                                        \
+    G_STMT_END
+
+#define nmtst_assert_no_success(success, error) \
+    G_STMT_START                                \
+    {                                           \
+        g_assert(error);                        \
+        g_assert(!(success));                   \
+    }                                           \
+    G_STMT_END
+
+/*****************************************************************************/
+
+/* Our nm-error error numbers use negative values to signal failure.
+ * A non-negative value signals success. Hence, the correct way for checking
+ * is always (r < 0) vs. (r >= 0). Never (r == 0).
+ *
+ * For assertions in tests, we also want to assert that no positive values
+ * are returned. For a lot of functions, positive return values are unexpected
+ * and a bug. This macro evaluates @r to success or failure, while asserting
+ * that @r is not positive. */
+#define NMTST_NM_ERR_SUCCESS(r)         \
+    ({                                  \
+        const int _r = (r);             \
+                                        \
+        if (_r >= 0)                    \
+            g_assert_cmpint(_r, ==, 0); \
+        (_r >= 0);                      \
+    })
+
+/*****************************************************************************/
+
+struct __nmtst_internal {
+    GRand *  rand0;
+    guint32  rand_seed;
+    GRand *  rand;
+    gboolean is_debug;
+    gboolean assert_logging;
+    gboolean no_expect_message;
+    gboolean test_quick;
+    gboolean test_tap_log;
+    char *   sudo_cmd;
+    char **  orig_argv;
+};
+
+extern struct __nmtst_internal __nmtst_internal;
+
+#define NMTST_DEFINE()                                        \
+    struct __nmtst_internal __nmtst_internal = {0};           \
+                                                              \
+    __attribute__((destructor)) static void _nmtst_exit(void) \
+    {                                                         \
+        __nmtst_internal.assert_logging = FALSE;              \
+        g_test_assert_expected_messages();                    \
+        nmtst_free();                                         \
+    }
+
+static inline gboolean
+nmtst_initialized(void)
+{
+    return !!__nmtst_internal.rand0;
+}
+
+#define __NMTST_LOG(cmd, ...)                                                                  \
+    G_STMT_START                                                                               \
+    {                                                                                          \
+        g_assert(nmtst_initialized());                                                         \
+        if (!__nmtst_internal.assert_logging || __nmtst_internal.no_expect_message) {          \
+            cmd(__VA_ARGS__);                                                                  \
+        } else {                                                                               \
+            printf(_NM_UTILS_MACRO_FIRST(__VA_ARGS__) "\n" _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+        }                                                                                      \
+    }                                                                                          \
+    G_STMT_END
+
+/* split the string inplace at specific delimiters, allowing escaping with '\\'.
+ * Returns a zero terminated array of pointers into @str.
+ *
+ * The caller must g_free() the returned argv array.
+ **/
+static inline char **
+nmtst_str_split(char *str, const char *delimiters)
+{
+    const char *d;
+    GArray *    result = g_array_sized_new(TRUE, FALSE, sizeof(char *), 3);
+
+    g_assert(str);
+    g_assert(delimiters && !strchr(delimiters, '\\'));
+
+    while (*str) {
+        gsize i = 0, j = 0;
+
+        while (TRUE) {
+            char c = str[i];
+
+            if (c == '\0') {
+                str[j++] = 0;
+                break;
+            } else if (c == '\\') {
+                str[j++] = str[++i];
+                if (!str[i])
+                    break;
+            } else {
+                for (d = delimiters; *d; d++) {
+                    if (c == *d) {
+                        str[j++] = 0;
+                        i++;
+                        goto BREAK_INNER_LOOPS;
+                    }
+                }
+                str[j++] = c;
+            }
+            i++;
+        }
+
+BREAK_INNER_LOOPS:
+        g_array_append_val(result, str);
+        str = &str[i];
+    }
+
+    return (char **) g_array_free(result, FALSE);
+}
+
+/* free instances allocated by nmtst (especially nmtst_init()) on shutdown
+ * to release memory. After nmtst_free(), the test is uninitialized again. */
+static inline void
+nmtst_free(void)
+{
+    if (!nmtst_initialized())
+        return;
+
+    g_rand_free(__nmtst_internal.rand0);
+    if (__nmtst_internal.rand)
+        g_rand_free(__nmtst_internal.rand);
+    g_free(__nmtst_internal.sudo_cmd);
+    g_strfreev(__nmtst_internal.orig_argv);
+
+    memset(&__nmtst_internal, 0, sizeof(__nmtst_internal));
+}
+
+static inline void
+_nmtst_log_handler(const char *   log_domain,
+                   GLogLevelFlags log_level,
+                   const char *   message,
+                   gpointer       user_data)
+{
+    g_print("%s\n", message);
+}
+
+static inline void
+__nmtst_init(int *       argc,
+             char ***    argv,
+             gboolean    assert_logging,
+             const char *log_level,
+             const char *log_domains,
+             gboolean *  out_set_logging)
+{
+    const char *      nmtst_debug;
+    gboolean          is_debug    = FALSE;
+    char *            c_log_level = NULL, *c_log_domains = NULL;
+    char *            sudo_cmd       = NULL;
+    GArray *          debug_messages = g_array_new(TRUE, FALSE, sizeof(char *));
+    int               i;
+    gboolean          no_expect_message = FALSE;
+    gboolean          _out_set_logging;
+    gboolean          test_quick         = FALSE;
+    gboolean          test_quick_set     = FALSE;
+    gboolean          test_quick_argv    = FALSE;
+    gs_unref_ptrarray GPtrArray *p_tests = NULL;
+    gs_unref_ptrarray GPtrArray *s_tests = NULL;
+
+    if (!out_set_logging)
+        out_set_logging = &_out_set_logging;
+    *out_set_logging = FALSE;
+
+    g_assert(!nmtst_initialized());
+
+    g_assert(!((!!argc) ^ (!!argv)));
+    g_assert(!argc || (g_strv_length(*argv) == *argc));
+    g_assert(!assert_logging || (!log_level && !log_domains));
+
+#ifdef __NETWORKMANAGER_UTILS_H__
+    if (!nm_utils_get_testing_initialized())
+        _nm_utils_set_testing(_NM_UTILS_TEST_GENERAL);
+#endif
+
+    if (argc)
+        __nmtst_internal.orig_argv = g_strdupv(*argv);
+
+    __nmtst_internal.assert_logging = !!assert_logging;
+
+    nm_g_type_init();
+
+    is_debug = g_test_verbose();
+
+    nmtst_debug = g_getenv("NMTST_DEBUG");
+    if (nmtst_debug) {
+        char **d_argv, **i_argv, *nmtst_debug_copy;
+
+        /* By setting then NMTST_DEBUG variable, @is_debug is set automatically.
+         * This can be reverted with no-debug (on command line or environment variable). */
+        is_debug = TRUE;
+
+        nmtst_debug_copy = g_strdup(nmtst_debug);
+        d_argv           = nmtst_str_split(nmtst_debug_copy, ",; \t\r\n");
+
+        for (i_argv = d_argv; *i_argv; i_argv++) {
+            const char *debug = *i_argv;
+
+            if (!g_ascii_strcasecmp(debug, "debug"))
+                is_debug = TRUE;
+            else if (!g_ascii_strcasecmp(debug, "no-debug")) {
+                /* when specifying the NMTST_DEBUG variable, we set is_debug to true. Use this flag to disable this
+                 * (e.g. for only setting the log-level, but not is_debug). */
+                is_debug = FALSE;
+            } else if (!g_ascii_strncasecmp(debug, "log-level=", strlen("log-level="))) {
+                g_free(c_log_level);
+                log_level = c_log_level = g_strdup(&debug[strlen("log-level=")]);
+            } else if (!g_ascii_strcasecmp(debug, "D")) {
+                /* shorthand for "log-level=TRACE,no-expect-message" */
+                g_free(c_log_level);
+                log_level = c_log_level = g_strdup("TRACE");
+                no_expect_message       = TRUE;
+            } else if (!g_ascii_strcasecmp(debug, "TRACE")) {
+                g_free(c_log_level);
+                log_level = c_log_level = g_strdup("TRACE");
+            } else if (!g_ascii_strncasecmp(debug, "log-domains=", strlen("log-domains="))) {
+                g_free(c_log_domains);
+                log_domains = c_log_domains = g_strdup(&debug[strlen("log-domains=")]);
+            } else if (!g_ascii_strncasecmp(debug, "sudo-cmd=", strlen("sudo-cmd="))) {
+                g_free(sudo_cmd);
+                sudo_cmd = g_strdup(&debug[strlen("sudo-cmd=")]);
+            } else if (!g_ascii_strcasecmp(debug, "no-expect-message")) {
+                no_expect_message = TRUE;
+            } else if (!g_ascii_strncasecmp(debug, "p=", strlen("p="))) {
+                if (!p_tests)
+                    p_tests = g_ptr_array_new_with_free_func(g_free);
+                g_ptr_array_add(p_tests, g_strdup(&debug[strlen("p=")]));
+            } else if (!g_ascii_strncasecmp(debug, "s=", strlen("s="))) {
+                if (!s_tests)
+                    s_tests = g_ptr_array_new_with_free_func(g_free);
+                g_ptr_array_add(s_tests, g_strdup(&debug[strlen("s=")]));
+            } else if (!g_ascii_strcasecmp(debug, "slow")
+                       || !g_ascii_strcasecmp(debug, "thorough")) {
+                test_quick     = FALSE;
+                test_quick_set = TRUE;
+            } else if (!g_ascii_strcasecmp(debug, "quick")) {
+                test_quick     = TRUE;
+                test_quick_set = TRUE;
+            } else {
+                char *msg =
+                    g_strdup_printf(">>> nmtst: ignore unrecognized NMTST_DEBUG option \"%s\"",
+                                    debug);
+
+                g_array_append_val(debug_messages, msg);
+            }
+        }
+
+        g_free(d_argv);
+        g_free(nmtst_debug_copy);
+    }
+
+    if (__nmtst_internal.orig_argv) {
+        char **a = __nmtst_internal.orig_argv;
+
+        for (; *a; a++) {
+            if (!g_ascii_strcasecmp(*a, "--debug"))
+                is_debug = TRUE;
+            else if (!g_ascii_strcasecmp(*a, "--no-debug"))
+                is_debug = FALSE;
+            else if (!strcmp(*a, "-m=slow") || !strcmp(*a, "-m=thorough") || !strcmp(*a, "-m=quick")
+                     || (!strcmp(*a, "-m") && *(a + 1)
+                         && (!strcmp(*(a + 1), "quick") || !strcmp(*(a + 1), "slow")
+                             || !strcmp(*(a + 1), "thorough"))))
+                test_quick_argv = TRUE;
+            else if (strcmp(*a, "--tap") == 0)
+                __nmtst_internal.test_tap_log = TRUE;
+        }
+    }
+
+    if (!argc || g_test_initialized()) {
+        if (p_tests || s_tests) {
+            char *msg = g_strdup_printf(
+                ">>> nmtst: ignore -p and -s options for test which calls g_test_init() itself");
+
+            g_array_append_val(debug_messages, msg);
+        }
+    } else {
+        /* We're intentionally assigning a value to static variables
+         * s_tests_x and p_tests_x without using it afterwards, just
+         * so that valgrind doesn't complain about the leak. */
+        NM_PRAGMA_WARNING_DISABLE("-Wunused-but-set-variable")
+
+        /* g_test_init() is a variadic function, so we cannot pass it
+         * (variadic) arguments. If you need to pass additional parameters,
+         * call nmtst_init() with argc==NULL and call g_test_init() yourself. */
+
+        /* g_test_init() sets g_log_set_always_fatal() for G_LOG_LEVEL_WARNING
+         * and G_LOG_LEVEL_CRITICAL. So, beware that the test will fail if you
+         * have any WARN or ERR log messages -- unless you g_test_expect_message(). */
+        GPtrArray *    arg_array   = g_ptr_array_new();
+        gs_free char **arg_array_c = NULL;
+        int            arg_array_n, j;
+        static char ** s_tests_x, **p_tests_x;
+
+        if (*argc) {
+            for (i = 0; i < *argc; i++)
+                g_ptr_array_add(arg_array, (*argv)[i]);
+        } else
+            g_ptr_array_add(arg_array, "./test");
+
+        if (test_quick_set && !test_quick_argv)
+            g_ptr_array_add(arg_array, "-m=quick");
+
+        if (!__nmtst_internal.test_tap_log) {
+            for (i = 0; p_tests && i < p_tests->len; i++) {
+                g_ptr_array_add(arg_array, "-p");
+                g_ptr_array_add(arg_array, p_tests->pdata[i]);
+            }
+            for (i = 0; s_tests && i < s_tests->len; i++) {
+                g_ptr_array_add(arg_array, "-s");
+                g_ptr_array_add(arg_array, s_tests->pdata[i]);
+            }
+        } else if (p_tests || s_tests) {
+            char *msg = g_strdup_printf(">>> nmtst: ignore -p and -s options for tap-tests");
+
+            g_array_append_val(debug_messages, msg);
+        }
+
+        g_ptr_array_add(arg_array, NULL);
+
+        arg_array_n = arg_array->len - 1;
+        arg_array_c = (char **) g_ptr_array_free(arg_array, FALSE);
+
+        g_test_init(&arg_array_n, &arg_array_c, NULL);
+
+        if (*argc > 1) {
+            /* collaps argc/argv by removing the arguments detected
+             * by g_test_init(). */
+            for (i = 1, j = 1; i < *argc; i++) {
+                if ((*argv)[i] == arg_array_c[j])
+                    j++;
+                else
+                    (*argv)[i] = NULL;
+            }
+            for (i = 1, j = 1; i < *argc; i++) {
+                if ((*argv)[i]) {
+                    (*argv)[j++] = (*argv)[i];
+                    if (i >= j)
+                        (*argv)[i] = NULL;
+                }
+            }
+            *argc = j;
+        }
+
+        /* we must "leak" the test paths because they are not cloned by g_test_init(). */
+        if (!__nmtst_internal.test_tap_log) {
+            if (p_tests) {
+                p_tests_x = (char **) g_ptr_array_free(p_tests, FALSE);
+                p_tests   = NULL;
+            }
+            if (s_tests) {
+                s_tests_x = (char **) g_ptr_array_free(s_tests, FALSE);
+                s_tests   = NULL;
+            }
+        }
+
+        NM_PRAGMA_WARNING_REENABLE
+    }
+
+    if (test_quick_set)
+        __nmtst_internal.test_quick = test_quick;
+    else if (test_quick_argv)
+        __nmtst_internal.test_quick = g_test_quick();
+    else {
+#ifdef NMTST_TEST_QUICK
+        __nmtst_internal.test_quick = NMTST_TEST_QUICK;
+#else
+        __nmtst_internal.test_quick = FALSE;
+#endif
+    }
+
+    __nmtst_internal.is_debug          = is_debug;
+    __nmtst_internal.rand0             = g_rand_new_with_seed(0);
+    __nmtst_internal.sudo_cmd          = sudo_cmd;
+    __nmtst_internal.no_expect_message = no_expect_message;
+
+    if (!log_level && log_domains) {
+        /* if the log level is not specified (but the domain is), we assume
+         * the caller wants to set it depending on is_debug */
+        log_level = is_debug ? "DEBUG" : "WARN";
+    }
+
+    if (!__nmtst_internal.assert_logging) {
+        gboolean success = TRUE;
+#ifdef _NMTST_INSIDE_CORE
+        success          = nm_logging_setup(log_level, log_domains, NULL, NULL);
+        *out_set_logging = TRUE;
+#endif
+        g_assert(success);
+#if GLIB_CHECK_VERSION(2, 34, 0)
+        if (__nmtst_internal.no_expect_message)
+            g_log_set_always_fatal(G_LOG_FATAL_MASK);
+#else
+        /* g_test_expect_message() is a NOP, so allow any messages */
+        g_log_set_always_fatal(G_LOG_FATAL_MASK);
+#endif
+    } else if (__nmtst_internal.no_expect_message) {
+        /* We have a test that would be assert_logging, but the user specified no_expect_message.
+         * This transforms g_test_expect_message() into a NOP, but we also have to relax
+         * g_log_set_always_fatal(), which was set by g_test_init(). */
+        g_log_set_always_fatal(G_LOG_FATAL_MASK);
+#ifdef _NMTST_INSIDE_CORE
+        if (c_log_domains || c_log_level) {
+            /* Normally, tests with assert_logging do not overwrite the logging level/domains because
+             * the logging statements are part of the assertions. But if the test is run with
+             * no-expect-message *and* the logging is set explicitly via environment variables,
+             * we still reset the logging. */
+            gboolean success;
+
+            success          = nm_logging_setup(log_level, log_domains, NULL, NULL);
+            *out_set_logging = TRUE;
+            g_assert(success);
+        }
+#endif
+    } else {
+#if GLIB_CHECK_VERSION(2, 34, 0)
+        /* We were called not to set logging levels. This means, that the user
+         * expects to assert against (all) messages.
+         * Any uncaught message on >debug level is fatal. */
+        g_log_set_always_fatal(G_LOG_LEVEL_MASK & ~G_LOG_LEVEL_DEBUG);
+#else
+        /* g_test_expect_message() is a NOP, so allow any messages */
+        g_log_set_always_fatal(G_LOG_FATAL_MASK);
+#endif
+    }
+
+    if ((!__nmtst_internal.assert_logging
+         || (__nmtst_internal.assert_logging && __nmtst_internal.no_expect_message))
+        && (is_debug
+            || (c_log_level
+                && (!g_ascii_strcasecmp(c_log_level, "DEBUG")
+                    || !g_ascii_strcasecmp(c_log_level, "TRACE"))))
+        && !g_getenv("G_MESSAGES_DEBUG")) {
+        /* if we are @is_debug or @log_level=="DEBUG" and
+         * G_MESSAGES_DEBUG is unset, we set G_MESSAGES_DEBUG=all.
+         * To disable this default behaviour, set G_MESSAGES_DEBUG='' */
+
+        /* Note that g_setenv is not thread safe, but you should anyway call
+         * nmtst_init() at the very start. */
+        g_setenv("G_MESSAGES_DEBUG", "all", TRUE);
+    }
+
+    /* "tc" is in /sbin, which might not be in $PATH of a regular user. Unconditionally
+     * add "/bin" and "/sbin" to $PATH for all tests. */
+    {
+        static char *path_new;
+        const char * path_old;
+
+        g_assert(!path_new);
+
+        path_old = g_getenv("PATH");
+        path_new = g_strjoin("",
+                             path_old ?: "",
+                             (nm_str_is_empty(path_old) ? "" : ":"),
+                             "/bin:/sbin",
+                             NULL);
+        g_setenv("PATH", path_new, TRUE);
+    }
+
+    /* Delay messages until we setup logging. */
+    for (i = 0; i < debug_messages->len; i++)
+        __NMTST_LOG(g_message, "%s", g_array_index(debug_messages, const char *, i));
+
+    g_strfreev((char **) g_array_free(debug_messages, FALSE));
+    g_free(c_log_level);
+    g_free(c_log_domains);
+
+#ifdef __NETWORKMANAGER_UTILS_H__
+    /* ensure that monotonic timestamp is called (because it initially logs a line) */
+    nm_utils_get_monotonic_timestamp_sec();
+#endif
+
+#ifdef NM_UTILS_H
+    {
+        gs_free_error GError *error = NULL;
+
+        if (!nm_utils_init(&error))
+            g_assert_not_reached();
+        g_assert_no_error(error);
+    }
+#endif
+
+    g_log_set_handler(G_LOG_DOMAIN,
+                      G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION,
+                      _nmtst_log_handler,
+                      NULL);
+}
+
+#ifndef _NMTST_INSIDE_CORE
+static inline void
+nmtst_init(int *argc, char ***argv, gboolean assert_logging)
+{
+    __nmtst_init(argc, argv, assert_logging, NULL, NULL, NULL);
+}
+#endif
+
+static inline gboolean
+nmtst_is_debug(void)
+{
+    g_assert(nmtst_initialized());
+    return __nmtst_internal.is_debug;
+}
+
+static inline gboolean
+nmtst_test_quick(void)
+{
+    g_assert(nmtst_initialized());
+    return __nmtst_internal.test_quick;
+}
+
+#if GLIB_CHECK_VERSION(2, 34, 0)
+    #undef g_test_expect_message
+    #define g_test_expect_message(...)                                                   \
+        G_STMT_START                                                                     \
+        {                                                                                \
+            g_assert(nmtst_initialized());                                               \
+            if (__nmtst_internal.assert_logging && __nmtst_internal.no_expect_message) { \
+                g_debug("nmtst: assert-logging: g_test_expect_message %s",               \
+                        G_STRINGIFY((__VA_ARGS__)));                                     \
+            } else {                                                                     \
+                G_GNUC_BEGIN_IGNORE_DEPRECATIONS                                         \
+                g_test_expect_message(__VA_ARGS__);                                      \
+                G_GNUC_END_IGNORE_DEPRECATIONS                                           \
+            }                                                                            \
+        }                                                                                \
+        G_STMT_END
+    #undef g_test_assert_expected_messages_internal
+    #define g_test_assert_expected_messages_internal(domain, file, line, func)                   \
+        G_STMT_START                                                                             \
+        {                                                                                        \
+            const char *_domain = (domain);                                                      \
+            const char *_file   = (file);                                                        \
+            const char *_func   = (func);                                                        \
+            int         _line   = (line);                                                        \
+                                                                                                 \
+            if (__nmtst_internal.assert_logging && __nmtst_internal.no_expect_message)           \
+                g_debug("nmtst: assert-logging: g_test_assert_expected_messages(%s, %s:%d, %s)", \
+                        _domain ?: "",                                                           \
+                        _file ?: "",                                                             \
+                        _line,                                                                   \
+                        _func ?: "");                                                            \
+                                                                                                 \
+            G_GNUC_BEGIN_IGNORE_DEPRECATIONS                                                     \
+            g_test_assert_expected_messages_internal(_domain, _file, _line, _func);              \
+            G_GNUC_END_IGNORE_DEPRECATIONS                                                       \
+        }                                                                                        \
+        G_STMT_END
+#endif
+
+#define NMTST_EXPECT(domain, level, msg) g_test_expect_message(domain, level, msg)
+
+#define NMTST_EXPECT_LIBNM(level, msg) NMTST_EXPECT("nm", level, msg)
+
+#define NMTST_EXPECT_LIBNM_WARNING(msg)  NMTST_EXPECT_LIBNM(G_LOG_LEVEL_WARNING, msg)
+#define NMTST_EXPECT_LIBNM_CRITICAL(msg) NMTST_EXPECT_LIBNM(G_LOG_LEVEL_CRITICAL, msg)
+
+/*****************************************************************************/
+
+typedef struct _NmtstTestData NmtstTestData;
+
+typedef void (*NmtstTestHandler)(const NmtstTestData *test_data);
+
+struct _NmtstTestData {
+    union {
+        const char *testpath;
+        char *      _testpath;
+    };
+    gsize            n_args;
+    gpointer *       args;
+    NmtstTestHandler _func_setup;
+    GTestDataFunc    _func_test;
+    NmtstTestHandler _func_teardown;
+};
+
+static inline void
+_nmtst_test_data_unpack(const NmtstTestData *test_data, gsize n_args, ...)
+{
+    gsize     i;
+    va_list   ap;
+    gpointer *p;
+
+    g_assert(test_data);
+    g_assert_cmpint(n_args, ==, test_data->n_args);
+
+    va_start(ap, n_args);
+    for (i = 0; i < n_args; i++) {
+        p = va_arg(ap, gpointer *);
+
+        if (p)
+            *p = test_data->args[i];
+    }
+    va_end(ap);
+}
+#define nmtst_test_data_unpack(test_data, ...) \
+    _nmtst_test_data_unpack(test_data, NM_NARG(__VA_ARGS__), ##__VA_ARGS__)
+
+static inline void
+_nmtst_test_data_free(gpointer data)
+{
+    NmtstTestData *test_data = data;
+
+    g_assert(test_data);
+
+    g_free(test_data->_testpath);
+    g_free(test_data);
+}
+
+static inline void
+_nmtst_test_run(gconstpointer data)
+{
+    const NmtstTestData *test_data = data;
+
+    if (test_data->_func_setup)
+        test_data->_func_setup(test_data);
+
+    test_data->_func_test(test_data);
+
+    if (test_data->_func_teardown)
+        test_data->_func_teardown(test_data);
+}
+
+static inline void
+_nmtst_add_test_func_full(const char *     testpath,
+                          GTestDataFunc    func_test,
+                          NmtstTestHandler func_setup,
+                          NmtstTestHandler func_teardown,
+                          gsize            n_args,
+                          ...)
+{
+    gsize          i;
+    NmtstTestData *data;
+    va_list        ap;
+
+    g_assert(testpath && testpath[0]);
+    g_assert(func_test);
+
+    data = g_malloc0(sizeof(NmtstTestData) + (sizeof(gpointer) * (n_args + 1)));
+
+    data->_testpath      = g_strdup(testpath);
+    data->_func_test     = func_test;
+    data->_func_setup    = func_setup;
+    data->_func_teardown = func_teardown;
+    data->n_args         = n_args;
+    data->args           = (gpointer) &data[1];
+    va_start(ap, n_args);
+    for (i = 0; i < n_args; i++)
+        data->args[i] = va_arg(ap, gpointer);
+    data->args[i] = NULL;
+    va_end(ap);
+
+    g_test_add_data_func_full(testpath, data, _nmtst_test_run, _nmtst_test_data_free);
+}
+#define nmtst_add_test_func_full(testpath, func_test, func_setup, func_teardown, ...) \
+    _nmtst_add_test_func_full(testpath,                                               \
+                              func_test,                                              \
+                              func_setup,                                             \
+                              func_teardown,                                          \
+                              NM_NARG(__VA_ARGS__),                                   \
+                              ##__VA_ARGS__)
+#define nmtst_add_test_func(testpath, func_test, ...) \
+    nmtst_add_test_func_full(testpath, func_test, NULL, NULL, ##__VA_ARGS__)
+
+/*****************************************************************************/
+
+static inline GRand *
+nmtst_get_rand0(void)
+{
+    g_assert(nmtst_initialized());
+    return __nmtst_internal.rand0;
+}
+
+static inline GRand *
+nmtst_get_rand(void)
+{
+    g_assert(nmtst_initialized());
+
+    if (G_UNLIKELY(!__nmtst_internal.rand)) {
+        guint32     seed;
+        const char *str = g_getenv("NMTST_SEED_RAND");
+
+        if (!str) {
+            /* No NMTST_SEED_RAND. Pick a stable one. */
+            seed                  = 0;
+            __nmtst_internal.rand = g_rand_new_with_seed(seed);
+        } else if (str[0] == '\0') {
+            /* NMTST_SEED_RAND is set but empty. Pick a random one. */
+            __nmtst_internal.rand = g_rand_new();
+
+            seed = g_rand_int(__nmtst_internal.rand);
+            g_rand_set_seed(__nmtst_internal.rand, seed);
+        } else {
+            /* NMTST_SEED_RAND is set. Use it as a seed. */
+            char * s;
+            gint64 i;
+
+            i = g_ascii_strtoll(str, &s, 0);
+            g_assert(s[0] == '\0' && i >= 0 && i < G_MAXUINT32);
+
+            seed                  = i;
+            __nmtst_internal.rand = g_rand_new_with_seed(seed);
+        }
+        __nmtst_internal.rand_seed = seed;
+
+        g_print("\nnmtst: initialize nmtst_get_rand() with NMTST_SEED_RAND=%u\n", seed);
+    }
+    return __nmtst_internal.rand;
+}
+
+static inline guint32
+nmtst_get_rand_uint32(void)
+{
+    return g_rand_int(nmtst_get_rand());
+}
+
+static inline guint64
+nmtst_get_rand_uint64(void)
+{
+    GRand *rand = nmtst_get_rand();
+
+    return (((guint64) g_rand_int(rand))) | (((guint64) g_rand_int(rand)) << 32);
+}
+
+static inline guint
+nmtst_get_rand_uint(void)
+{
+    G_STATIC_ASSERT_EXPR((sizeof(guint) == sizeof(guint32) || (sizeof(guint) == sizeof(guint64))));
+    if (sizeof(guint32) == sizeof(guint))
+        return nmtst_get_rand_uint32();
+    return nmtst_get_rand_uint64();
+}
+
+static inline gsize
+nmtst_get_rand_size(void)
+{
+    G_STATIC_ASSERT_EXPR((sizeof(gsize) == sizeof(guint32) || (sizeof(gsize) == sizeof(guint64))));
+    if (sizeof(gsize) == sizeof(guint32))
+        return nmtst_get_rand_uint32();
+    return nmtst_get_rand_uint64();
+}
+
+static inline gboolean
+nmtst_get_rand_bool(void)
+{
+    return nmtst_get_rand_uint32() % 2;
+}
+
+static inline gboolean
+nmtst_get_rand_one_case_in(guint32 num)
+{
+    /* num=1 doesn't make much sense, because it will always return %TRUE.
+     * Still accept it, it might be that @num is calculated, so 1 might be
+     * a valid edge case. */
+    g_assert(num > 0);
+
+    return (nmtst_get_rand_uint32() % num) == 0;
+}
+
+static inline gpointer
+nmtst_rand_buf(GRand *rand, gpointer buffer, gsize buffer_length)
+{
+    guint32 v;
+    guint8 *b = buffer;
+
+    if (!buffer_length)
+        return buffer;
+
+    g_assert(buffer);
+
+    if (!rand)
+        rand = nmtst_get_rand();
+
+    for (; buffer_length >= sizeof(guint32);
+         buffer_length -= sizeof(guint32), b += sizeof(guint32)) {
+        v = g_rand_int(rand);
+        memcpy(b, &v, sizeof(guint32));
+    }
+    if (buffer_length > 0) {
+        v = g_rand_int(rand);
+        do {
+            *(b++) = v & 0xFF;
+            v >>= 8;
+        } while (--buffer_length > 0);
+    }
+    return buffer;
+}
+
+#define _nmtst_rand_select(uniq, v0, ...)                                                     \
+    ({                                                                                        \
+        typeof(v0) NM_UNIQ_T(UNIQ, uniq)[1 + NM_NARG(__VA_ARGS__)] = {(v0), __VA_ARGS__};     \
+                                                                                              \
+        NM_UNIQ_T(UNIQ, uniq)[nmtst_get_rand_uint32() % G_N_ELEMENTS(NM_UNIQ_T(UNIQ, uniq))]; \
+    })
+
+#define nmtst_rand_select(...) _nmtst_rand_select(NM_UNIQ, __VA_ARGS__)
+
+#define nmtst_rand_select_str(x, ...) nmtst_rand_select((const char *) (x), ##__VA_ARGS__)
+
+static inline void *
+nmtst_rand_perm(GRand *rand, void *dst, const void *src, gsize elmt_size, gsize n_elmt)
+{
+    gsize i, j;
+    char *p_, *pj;
+    char *bu;
+
+    g_assert(dst);
+    g_assert(elmt_size > 0);
+    g_assert(n_elmt < G_MAXINT32);
+
+    if (n_elmt == 0)
+        return dst;
+
+    if (src && dst != src)
+        memcpy(dst, src, elmt_size * n_elmt);
+
+    if (!rand)
+        rand = nmtst_get_rand();
+
+    bu = g_slice_alloc(elmt_size);
+
+    p_ = dst;
+    for (i = n_elmt; i > 1; i--) {
+        j = g_rand_int_range(rand, 0, i);
+
+        if (j != 0) {
+            pj = &p_[j * elmt_size];
+
+            /* swap */
+            memcpy(bu, p_, elmt_size);
+            memcpy(p_, pj, elmt_size);
+            memcpy(pj, bu, elmt_size);
+        }
+        p_ += elmt_size;
+    }
+
+    g_slice_free1(elmt_size, bu);
+    return dst;
+}
+
+static inline const char **
+nmtst_rand_perm_strv(const char *const *strv)
+{
+    const char **res;
+    gsize        n;
+
+    if (!strv)
+        return NULL;
+
+    /* this returns a (scrambled) SHALLOW copy of the strv array! */
+
+    n   = NM_PTRARRAY_LEN(strv);
+    res = (const char **) (nm_utils_strv_dup(strv, n, FALSE) ?: g_new0(char *, 1));
+    nmtst_rand_perm(NULL, res, res, sizeof(char *), n);
+    return res;
+}
+
+static inline GSList *
+nmtst_rand_perm_gslist(GRand *rand, GSList *list)
+{
+    GSList *result;
+    guint   l;
+
+    if (!rand)
+        rand = nmtst_get_rand();
+
+    /* no need for an efficient implementation :) */
+
+    result = 0;
+    for (l = g_slist_length(list); l > 0; l--) {
+        GSList *tmp;
+
+        tmp = g_slist_nth(list, g_rand_int(rand) % l);
+        g_assert(tmp);
+
+        list   = g_slist_remove_link(list, tmp);
+        result = g_slist_concat(tmp, result);
+    }
+    g_assert(!list);
+    return result;
+}
+
+static inline void
+nmtst_stable_rand(guint64 seed, gpointer buf, gsize len)
+{
+    const guint64 C = 1442695040888963407llu;
+    const guint64 A = 6364136223846793005llu;
+    guint8 *      b;
+    union {
+        guint8  a[sizeof(guint64)];
+        guint64 n;
+    } n;
+
+    /* We want a stable random generator that is in our control and does not
+     * depend on glibc/glib versions.
+     * Use a linear congruential generator (x[n+1] = (A * x[n] + C) % M)
+     * https://en.wikipedia.org/wiki/Linear_congruential_generator
+     *
+     * We choose (Knuth’s LCG MMIX)
+     *   A = 6364136223846793005llu
+     *   C = 1442695040888963407llu
+     *   M = 2^64
+     */
+
+    g_assert(len == 0 || buf);
+
+    n.n = seed;
+    b   = buf;
+    for (; len > 0; len--, b++) {
+        n.n = (A * n.n + C);
+
+        /* let's combine the 64 bits randomness in one byte. By xor-ing, it's
+         * also independent of endianness. */
+        b[0] = n.a[0] ^ n.a[1] ^ n.a[2] ^ n.a[3] ^ n.a[4] ^ n.a[5] ^ n.a[6] ^ n.a[7];
+    }
+}
+
+/*****************************************************************************/
+
+/**
+ * nmtst_get_rand_word_length:
+ * @rand: (allow-none): #GRand instance or %NULL to use the singleton.
+ *
+ * Returns: a random integer >= 0, that most frequently is somewhere between
+ * 0 and 16, but (with decreasing) probability, it can be larger. This can
+ * be used when we generate random input for unit tests.
+ */
+static inline guint
+nmtst_get_rand_word_length(GRand *rand)
+{
+    guint n;
+
+    if (!rand)
+        rand = nmtst_get_rand();
+
+    n = 0;
+    while (TRUE) {
+        guint32 rnd = g_rand_int(rand);
+        guint   probability;
+
+        /* The following python code implements a random sample with this
+         * distribution:
+         *
+         *    def random_histogram(n_tries, scale = None):
+         *        def probability(n_tok):
+         *            import math
+         *            return max(2, math.floor(100 / (2*(n_tok+1))))
+         *        def n_tokens():
+         *            import random
+         *            n_tok = 0
+         *            while True:
+         *                if random.randint(0, 0xFFFFFFFF) % probability(n_tok) == 0:
+         *                   return n_tok
+         *                n_tok += 1
+         *        hist = []
+         *        i = 0;
+         *        while i < n_tries:
+         *            n_tok = n_tokens()
+         *            while n_tok >= len(hist):
+         *                hist.append(0)
+         *            hist[n_tok] = hist[n_tok] + 1
+         *            i += 1
+         *        if scale is not None:
+         *            hist = list([round(x / n_tries * scale) for x in hist])
+         *        return hist
+         *
+         * For example, random_histogram(n_tries = 1000000, scale = 1000) may give
+         *
+         *   IDX:  [ 0,  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]
+         *   SEEN: [20, 39, 59, 73, 80, 91, 92, 90, 91, 73, 73, 54, 55, 36, 24, 16, 16,  8,  4,  2,  1,  1,  0,  0,  0,  0,  0,  0,  0,  0]
+         *
+         * which give a sense of the probability with this individual results are returned.
+         */
+        probability = NM_MAX(2u, (100u / (2u * (n + 1u))));
+        if ((rnd % probability) == 0)
+            return n;
+        n++;
+    }
+}
+
+/*****************************************************************************/
+
+static inline gboolean
+nmtst_g_source_assert_not_called(gpointer user_data)
+{
+    g_assert_not_reached();
+    return G_SOURCE_CONTINUE;
+}
+
+static inline gboolean
+nmtst_g_source_nop(gpointer user_data)
+{
+    g_assert(!user_data);
+    return G_SOURCE_CONTINUE;
+}
+
+static inline gboolean
+nmtst_g_source_set_boolean_true(gpointer user_data)
+{
+    gboolean *ptr = user_data;
+
+    g_assert(ptr);
+    g_assert(!*ptr);
+    *ptr = TRUE;
+    return G_SOURCE_CONTINUE;
+}
+
+/*****************************************************************************/
+
+static inline gboolean
+_nmtst_main_loop_run_timeout(gpointer user_data)
+{
+    GMainLoop **p_loop = user_data;
+
+    g_assert(p_loop && *p_loop);
+    g_main_loop_quit(g_steal_pointer(p_loop));
+    return G_SOURCE_REMOVE;
+}
+
+static inline gboolean
+nmtst_main_loop_run(GMainLoop *loop, guint timeout_msec)
+{
+    nm_auto_unref_gsource GSource *source = NULL;
+    GMainLoop *                    loopx  = loop;
+
+    if (timeout_msec > 0) {
+        source = g_timeout_source_new(timeout_msec);
+        g_source_set_callback(source, _nmtst_main_loop_run_timeout, &loopx, NULL);
+        g_source_attach(source, g_main_loop_get_context(loop));
+    }
+
+    g_main_loop_run(loop);
+
+    if (source)
+        g_source_destroy(source);
+
+    /* if the timeout was reached, return FALSE. */
+    return loopx != NULL;
+}
+
+#define nmtst_main_loop_run_assert(loop, timeout_msec)    \
+    G_STMT_START                                          \
+    {                                                     \
+        if (!nmtst_main_loop_run((loop), (timeout_msec))) \
+            g_assert_not_reached();                       \
+    }                                                     \
+    G_STMT_END
+
+static inline void
+_nmtst_main_loop_quit_on_notify(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    GMainLoop *loop = user_data;
+
+    g_assert(G_IS_OBJECT(object));
+    g_assert(loop);
+
+    g_main_loop_quit(loop);
+}
+#define nmtst_main_loop_quit_on_notify ((GCallback) _nmtst_main_loop_quit_on_notify)
+
+#define nmtst_main_context_iterate_until_full(context, timeout_msec, poll_msec, condition)      \
+    ({                                                                                          \
+        nm_auto_destroy_and_unref_gsource GSource *_source_timeout = NULL;                      \
+        nm_auto_destroy_and_unref_gsource GSource *_source_poll    = NULL;                      \
+        GMainContext *                             _context        = (context);                 \
+        gboolean                                   _had_timeout    = FALSE;                     \
+        typeof(timeout_msec)                       _timeout_msec0  = (timeout_msec);            \
+        typeof(poll_msec)                          _poll_msec0     = (poll_msec);               \
+        gint64                                     _timeout_msec   = _timeout_msec0;            \
+        guint                                      _poll_msec      = _poll_msec0;               \
+                                                                                                \
+        g_assert_cmpint(_timeout_msec0, ==, _timeout_msec);                                     \
+        g_assert_cmpint(_poll_msec0, ==, _poll_msec);                                           \
+                                                                                                \
+        _source_timeout = g_timeout_source_new(NM_CLAMP(_timeout_msec, 0, (gint64) G_MAXUINT)); \
+        g_source_set_callback(_source_timeout,                                                  \
+                              nmtst_g_source_set_boolean_true,                                  \
+                              &_had_timeout,                                                    \
+                              NULL);                                                            \
+        g_source_attach(_source_timeout, _context);                                             \
+                                                                                                \
+        if (_poll_msec > 0) {                                                                   \
+            _source_poll = g_timeout_source_new(_poll_msec);                                    \
+            g_source_set_callback(_source_poll, nmtst_g_source_nop, NULL, NULL);                \
+            g_source_attach(_source_poll, _context);                                            \
+        }                                                                                       \
+                                                                                                \
+        while (TRUE) {                                                                          \
+            if (condition)                                                                      \
+                break;                                                                          \
+            g_main_context_iteration(_context, TRUE);                                           \
+            if (_had_timeout)                                                                   \
+                break;                                                                          \
+        }                                                                                       \
+                                                                                                \
+        !_had_timeout;                                                                          \
+    })
+
+#define nmtst_main_context_iterate_until(context, timeout_msec, condition) \
+    nmtst_main_context_iterate_until_full((context), (timeout_msec), 0, condition)
+
+#define nmtst_main_context_iterate_until_assert_full(context, timeout_msec, poll_msec, condition) \
+    G_STMT_START                                                                                  \
+    {                                                                                             \
+        if (!nmtst_main_context_iterate_until_full((context),                                     \
+                                                   (timeout_msec),                                \
+                                                   (poll_msec),                                   \
+                                                   condition))                                    \
+            g_assert(FALSE &&#condition);                                                         \
+    }                                                                                             \
+    G_STMT_END
+
+#define nmtst_main_context_iterate_until_assert(context, timeout_msec, condition) \
+    nmtst_main_context_iterate_until_assert_full((context), (timeout_msec), 0, condition)
+
+/*****************************************************************************/
+
+static inline void
+nmtst_main_context_assert_no_dispatch(GMainContext *context, guint timeout_msec)
+{
+    nm_auto_destroy_and_unref_gsource GSource *source      = NULL;
+    gboolean                                   timeout_hit = FALSE;
+
+    source = g_timeout_source_new(timeout_msec);
+    g_source_set_callback(source, nmtst_g_source_set_boolean_true, &timeout_hit, NULL);
+    g_source_attach(source, context);
+
+    while (g_main_context_iteration(context, TRUE)) {
+        if (timeout_hit)
+            return;
+        g_assert_not_reached();
+    }
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    GMainLoop *_main_loop;
+    union {
+        GSList *          _list;
+        const void *const is_waiting;
+    };
+} NMTstContextBusyWatcherData;
+
+static inline void
+_nmtst_context_busy_watcher_add_cb(gpointer data, GObject *where_the_object_was)
+{
+    NMTstContextBusyWatcherData *watcher_data = data;
+    GSList *                     l;
+
+    g_assert(watcher_data);
+
+    l = g_slist_find(watcher_data->_list, where_the_object_was);
+    g_assert(l);
+
+    watcher_data->_list = g_slist_delete_link(watcher_data->_list, l);
+    if (!watcher_data->_list)
+        g_main_loop_quit(watcher_data->_main_loop);
+}
+
+static inline void
+nmtst_context_busy_watcher_add(NMTstContextBusyWatcherData *watcher_data, GObject *object)
+{
+    g_assert(watcher_data);
+    g_assert(G_IS_OBJECT(object));
+
+    if (!watcher_data->_main_loop) {
+        watcher_data->_main_loop = g_main_loop_new(g_main_context_get_thread_default(), FALSE);
+        g_assert(!watcher_data->_list);
+    } else {
+        g_assert(g_main_loop_get_context(watcher_data->_main_loop)
+                 == (g_main_context_get_thread_default() ?: g_main_context_default()));
+    }
+
+    g_object_weak_ref(object, _nmtst_context_busy_watcher_add_cb, watcher_data);
+    watcher_data->_list = g_slist_prepend(watcher_data->_list, object);
+}
+
+static inline void
+nmtst_context_busy_watcher_wait(NMTstContextBusyWatcherData *watcher_data)
+{
+    g_assert(watcher_data);
+
+    if (!watcher_data->_main_loop) {
+        g_assert(!watcher_data->_list);
+        return;
+    }
+
+    if (watcher_data->_list) {
+        if (!nmtst_main_loop_run(watcher_data->_main_loop, 5000))
+            g_error("timeout running mainloop waiting for GObject to destruct");
+    }
+
+    g_assert(!watcher_data->_list);
+    nm_clear_pointer(&watcher_data->_main_loop, g_main_loop_unref);
+}
+
+/*****************************************************************************/
+
+static inline const char *
+nmtst_get_sudo_cmd(void)
+{
+    g_assert(nmtst_initialized());
+    return __nmtst_internal.sudo_cmd;
+}
+
+static inline void
+nmtst_reexec_sudo(void)
+{
+    char * str;
+    char **argv;
+    int    i;
+    int    errsv;
+
+    g_assert(nmtst_initialized());
+    g_assert(__nmtst_internal.orig_argv);
+
+    if (!__nmtst_internal.sudo_cmd)
+        return;
+
+    str = g_strjoinv(" ", __nmtst_internal.orig_argv);
+    __NMTST_LOG(g_message, ">> exec %s %s", __nmtst_internal.sudo_cmd, str);
+
+    argv    = g_new0(char *, 1 + g_strv_length(__nmtst_internal.orig_argv) + 1);
+    argv[0] = __nmtst_internal.sudo_cmd;
+    for (i = 0; __nmtst_internal.orig_argv[i]; i++)
+        argv[i + 1] = __nmtst_internal.orig_argv[i];
+
+    execvp(__nmtst_internal.sudo_cmd, argv);
+
+    errsv = errno;
+    g_error(">> exec %s failed: %d - %s",
+            __nmtst_internal.sudo_cmd,
+            errsv,
+            nm_strerror_native(errsv));
+}
+
+/*****************************************************************************/
+
+static inline gsize
+nmtst_find_all_indexes(gpointer *elements,
+                       gsize     n_elements,
+                       gpointer *needles,
+                       gsize     n_needles,
+                       gboolean (*equal_fcn)(gpointer element, gpointer needle, gpointer user_data),
+                       gpointer user_data,
+                       gssize * out_idx)
+{
+    gsize i, j, k;
+    gsize found = 0;
+
+    for (i = 0; i < n_needles; i++) {
+        gssize idx = -1;
+
+        for (j = 0; j < n_elements; j++) {
+            /* no duplicates */
+            for (k = 0; k < i; k++) {
+                if (out_idx[k] == j)
+                    goto next;
+            }
+
+            if (equal_fcn(elements[j], needles[i], user_data)) {
+                idx = j;
+                break;
+            }
+next:;
+        }
+
+        out_idx[i] = idx;
+        if (idx >= 0)
+            found++;
+    }
+
+    return found;
+}
+
+/*****************************************************************************/
+
+#define __define_nmtst_static(NUM, SIZE)                                   \
+    static inline const char *nmtst_static_##SIZE##_##NUM(const char *str) \
+    {                                                                      \
+        gsize                        l;                                    \
+        static _nm_thread_local char buf[SIZE];                            \
+                                                                           \
+        if (!str)                                                          \
+            return NULL;                                                   \
+        l = g_strlcpy(buf, str, sizeof(buf));                              \
+        g_assert(l < sizeof(buf));                                         \
+        return buf;                                                        \
+    }
+__define_nmtst_static(01, 1024) __define_nmtst_static(02, 1024) __define_nmtst_static(03, 1024)
+#undef __define_nmtst_static
+
+#if defined(__NM_UTILS_H__) || defined(NM_UTILS_H)
+
+    #define NMTST_UUID_INIT(uuid)                                          \
+        gs_free char *    _nmtst_hidden_##uuid = nm_utils_uuid_generate(); \
+        const char *const uuid                 = _nmtst_hidden_##uuid
+
+    static inline const char *nmtst_uuid_generate(void)
+{
+    static _nm_thread_local char u[37];
+    gs_free char *               m = NULL;
+
+    m = nm_utils_uuid_generate();
+    g_assert(m && strlen(m) == sizeof(u) - 1);
+    memcpy(u, m, sizeof(u));
+    return u;
+}
+
+#endif
+
+#define nmtst_assert_str_has_substr(str, substr)                                                  \
+    G_STMT_START                                                                                  \
+    {                                                                                             \
+        const char *__str    = (str);                                                             \
+        const char *__substr = (substr);                                                          \
+                                                                                                  \
+        g_assert(__str);                                                                          \
+        g_assert(__substr);                                                                       \
+        if (strstr(__str, __substr) == NULL)                                                      \
+            g_error("%s:%d: Expects \"%s\" but got \"%s\"", __FILE__, __LINE__, __substr, __str); \
+    }                                                                                             \
+    G_STMT_END
+
+static inline in_addr_t
+nmtst_inet4_from_string(const char *str)
+{
+    in_addr_t addr;
+    int       success;
+
+    if (!str)
+        return 0;
+
+    success = inet_pton(AF_INET, str, &addr);
+
+    g_assert(success == 1);
+
+    return addr;
+}
+
+static inline const struct in6_addr *
+nmtst_inet6_from_string(const char *str)
+{
+    static _nm_thread_local struct in6_addr addr;
+    int                                     success;
+
+    if (!str)
+        addr = in6addr_any;
+    else {
+        success = inet_pton(AF_INET6, str, &addr);
+        g_assert(success == 1);
+    }
+
+    return &addr;
+}
+
+static inline gconstpointer
+nmtst_inet_from_string(int addr_family, const char *str)
+{
+    if (addr_family == AF_INET) {
+        static in_addr_t a;
+
+        a = nmtst_inet4_from_string(str);
+        return &a;
+    }
+    if (addr_family == AF_INET6)
+        return nmtst_inet6_from_string(str);
+
+    g_assert_not_reached();
+    return NULL;
+}
+
+static inline const char *
+nmtst_inet_to_string(int addr_family, gconstpointer addr)
+{
+    static _nm_thread_local char buf[NM_CONST_MAX(INET6_ADDRSTRLEN, INET_ADDRSTRLEN)];
+
+    g_assert(NM_IN_SET(addr_family, AF_INET, AF_INET6));
+    g_assert(addr);
+
+    if (inet_ntop(addr_family, addr, buf, sizeof(buf)) != buf)
+        g_assert_not_reached();
+
+    return buf;
+}
+
+static inline const char *
+nmtst_inet4_to_string(in_addr_t addr)
+{
+    return nmtst_inet_to_string(AF_INET, &addr);
+}
+
+static inline const char *
+nmtst_inet6_to_string(const struct in6_addr *addr)
+{
+    return nmtst_inet_to_string(AF_INET6, addr);
+}
+
+static inline void
+_nmtst_assert_ip4_address(const char *file, int line, in_addr_t addr, const char *str_expected)
+{
+    if (nmtst_inet4_from_string(str_expected) != addr) {
+        char buf[100];
+
+        g_error("%s:%d: Unexpected IPv4 address: expected %s, got %s",
+                file,
+                line,
+                str_expected ?: "0.0.0.0",
+                inet_ntop(AF_INET, &addr, buf, sizeof(buf)));
+    }
+}
+#define nmtst_assert_ip4_address(addr, str_expected) \
+    _nmtst_assert_ip4_address(__FILE__, __LINE__, addr, str_expected)
+
+static inline void
+_nmtst_assert_ip6_address(const char *           file,
+                          int                    line,
+                          const struct in6_addr *addr,
+                          const char *           str_expected)
+{
+    struct in6_addr any = in6addr_any;
+
+    if (!addr)
+        addr = &any;
+
+    if (memcmp(nmtst_inet6_from_string(str_expected), addr, sizeof(*addr)) != 0) {
+        char buf[100];
+
+        g_error("%s:%d: Unexpected IPv6 address: expected %s, got %s",
+                file,
+                line,
+                str_expected ?: "::",
+                inet_ntop(AF_INET6, addr, buf, sizeof(buf)));
+    }
+}
+#define nmtst_assert_ip6_address(addr, str_expected) \
+    _nmtst_assert_ip6_address(__FILE__, __LINE__, addr, str_expected)
+
+#define nmtst_assert_ip_address(addr_family, addr, str_expected)                        \
+    G_STMT_START                                                                        \
+    {                                                                                   \
+        if (NM_IS_IPv4(addr_family))                                                    \
+            nmtst_assert_ip4_address(*((const in_addr_t *) (addr)), (str_expected));    \
+        else                                                                            \
+            nmtst_assert_ip6_address((const struct in6_addr *) (addr), (str_expected)); \
+    }                                                                                   \
+    G_STMT_END
+
+#define nmtst_spawn_sync(working_directory, standard_out, standard_err, assert_exit_status, ...) \
+    __nmtst_spawn_sync(working_directory,                                                        \
+                       standard_out,                                                             \
+                       standard_err,                                                             \
+                       assert_exit_status,                                                       \
+                       ##__VA_ARGS__,                                                            \
+                       NULL)
+static inline int __nmtst_spawn_sync(const char *working_directory,
+                                     char **     standard_out,
+                                     char **     standard_err,
+                                     int         assert_exit_status,
+                                     ...) G_GNUC_NULL_TERMINATED;
+static inline int
+__nmtst_spawn_sync(const char *working_directory,
+                   char **     standard_out,
+                   char **     standard_err,
+                   int         assert_exit_status,
+                   ...)
+{
+    int        exit_status = 0;
+    GError *   error       = NULL;
+    char *     arg;
+    va_list    va_args;
+    GPtrArray *argv = g_ptr_array_new();
+    gboolean   success;
+
+    va_start(va_args, assert_exit_status);
+    while ((arg = va_arg(va_args, char *)))
+        g_ptr_array_add(argv, arg);
+    va_end(va_args);
+
+    g_assert(argv->len >= 1);
+    g_ptr_array_add(argv, NULL);
+
+    success = g_spawn_sync(working_directory,
+                           (char **) argv->pdata,
+                           NULL,
+                           0 /*G_SPAWN_DEFAULT*/,
+                           NULL,
+                           NULL,
+                           standard_out,
+                           standard_err,
+                           &exit_status,
+                           &error);
+    if (!success)
+        g_error("nmtst_spawn_sync(%s): %s", ((char **) argv->pdata)[0], error->message);
+    g_assert(!error);
+
+    g_assert(!standard_out || *standard_out);
+    g_assert(!standard_err || *standard_err);
+
+    if (assert_exit_status != -1) {
+        /* exit status is a guint8 on success. Set @assert_exit_status to -1
+         * not to check for the exit status. */
+        g_assert(WIFEXITED(exit_status));
+        g_assert_cmpint(WEXITSTATUS(exit_status), ==, assert_exit_status);
+    }
+
+    g_ptr_array_free(argv, TRUE);
+    return exit_status;
+}
+
+/*****************************************************************************/
+
+static inline char *
+nmtst_file_resolve_relative_path(const char *rel, const char *cwd)
+{
+    gs_free char *cwd_free = NULL;
+
+    g_assert(rel && *rel);
+
+    if (g_path_is_absolute(rel))
+        return g_strdup(rel);
+
+    if (!cwd)
+        cwd = cwd_free = g_get_current_dir();
+    return g_build_filename(cwd, rel, NULL);
+}
+
+static inline char *
+nmtst_file_get_contents(const char *filename)
+{
+    GError * error = NULL;
+    gboolean success;
+    char *   contents = NULL;
+    gsize    len;
+
+    success = g_file_get_contents(filename, &contents, &len, &error);
+    nmtst_assert_success(success && contents, error);
+    g_assert_cmpint(strlen(contents), ==, len);
+    return contents;
+}
+
+#define nmtst_file_set_contents_size(filename, content, size)                 \
+    G_STMT_START                                                              \
+    {                                                                         \
+        GError *    _error = NULL;                                            \
+        gboolean    _success;                                                 \
+        const char *_content = (content);                                     \
+        gssize      _size    = (size);                                        \
+                                                                              \
+        g_assert(_content);                                                   \
+                                                                              \
+        if (_size < 0) {                                                      \
+            g_assert(_size == -1);                                            \
+            _size = strlen(_content);                                         \
+        }                                                                     \
+                                                                              \
+        _success = g_file_set_contents((filename), _content, _size, &_error); \
+        nmtst_assert_success(_success, _error);                               \
+    }                                                                         \
+    G_STMT_END
+
+#define nmtst_file_set_contents(filename, content) \
+    nmtst_file_set_contents_size(filename, content, -1)
+
+/*****************************************************************************/
+
+static inline void
+nmtst_file_unlink_if_exists(const char *name)
+{
+    int errsv;
+
+    g_assert(name && name[0]);
+
+    if (unlink(name) != 0) {
+        errsv = errno;
+        if (errsv != ENOENT)
+            g_error("nmtst_file_unlink_if_exists(%s): failed with %s",
+                    name,
+                    nm_strerror_native(errsv));
+    }
+}
+
+static inline void
+nmtst_file_unlink(const char *name)
+{
+    int errsv;
+
+    g_assert(name && name[0]);
+
+    if (unlink(name) != 0) {
+        errsv = errno;
+        g_error("nmtst_file_unlink(%s): failed with %s", name, nm_strerror_native(errsv));
+    }
+}
+
+static inline void
+_nmtst_auto_unlinkfile(char **p_name)
+{
+    if (*p_name) {
+        nmtst_file_unlink(*p_name);
+        nm_clear_g_free(p_name);
+    }
+}
+
+#define nmtst_auto_unlinkfile nm_auto(_nmtst_auto_unlinkfile)
+
+/*****************************************************************************/
+
+static inline void
+_nmtst_assert_resolve_relative_path_equals(const char *f1,
+                                           const char *f2,
+                                           const char *file,
+                                           int         line)
+{
+    gs_free char *p1 = NULL, *p2 = NULL;
+
+    p1 = nmtst_file_resolve_relative_path(f1, NULL);
+    p2 = nmtst_file_resolve_relative_path(f2, NULL);
+    g_assert(p1 && *p1);
+
+    /* Fixme: later we might need to coalesce repeated '/', "./", and "../".
+     * For now, it's good enough. */
+    if (g_strcmp0(p1, p2) != 0)
+        g_error("%s:%d : filenames don't match \"%s\" vs. \"%s\" // \"%s\" - \"%s\"",
+                file,
+                line,
+                f1,
+                f2,
+                p1,
+                p2);
+}
+#define nmtst_assert_resolve_relative_path_equals(f1, f2) \
+    _nmtst_assert_resolve_relative_path_equals(f1, f2, __FILE__, __LINE__);
+
+/*****************************************************************************/
+
+#ifdef __NETWORKMANAGER_LOGGING_H__
+
+    #define NMTST_EXPECT_NM(level, msg) NMTST_EXPECT("NetworkManager", level, msg)
+
+    #define NMTST_EXPECT_NM_ERROR(msg) NMTST_EXPECT_NM(G_LOG_LEVEL_MESSAGE, "*<error> [*] " msg)
+    #define NMTST_EXPECT_NM_WARN(msg)  NMTST_EXPECT_NM(G_LOG_LEVEL_MESSAGE, "*<warn>  [*] " msg)
+    #define NMTST_EXPECT_NM_INFO(msg)  NMTST_EXPECT_NM(G_LOG_LEVEL_INFO, "*<info>  [*] " msg)
+    #define NMTST_EXPECT_NM_DEBUG(msg) NMTST_EXPECT_NM(G_LOG_LEVEL_DEBUG, "*<debug> [*] " msg)
+    #define NMTST_EXPECT_NM_TRACE(msg) NMTST_EXPECT_NM(G_LOG_LEVEL_DEBUG, "*<trace> [*] " msg)
+
+static inline void
+nmtst_init_with_logging(int *argc, char ***argv, const char *log_level, const char *log_domains)
+{
+    __nmtst_init(argc, argv, FALSE, log_level, log_domains, NULL);
+}
+static inline void
+nmtst_init_assert_logging(int *argc, char ***argv, const char *log_level, const char *log_domains)
+{
+    gboolean set_logging;
+
+    __nmtst_init(argc, argv, TRUE, NULL, NULL, &set_logging);
+
+    if (!set_logging) {
+        gboolean success;
+
+        success = nm_logging_setup(log_level, log_domains, NULL, NULL);
+        g_assert(success);
+    }
+}
+
+#endif
+
+/*****************************************************************************/
+
+#ifdef __NETWORKMANAGER_LOGGING_H__
+static inline gpointer
+nmtst_logging_disable(gboolean always)
+{
+    gpointer p;
+
+    g_assert(nmtst_initialized());
+    if (!always && __nmtst_internal.no_expect_message) {
+        /* The caller does not want to @always suppress logging. Instead,
+         * the caller wants to suppress unexpected log messages that would
+         * fail assertions (since we possibly assert against all unexpected
+         * log messages).
+         *
+         * If the test is run with no-expect-message, then don't suppress
+         * the loggings, because they also wouldn't fail assertions. */
+        return NULL;
+    }
+
+    p = g_memdup(_nm_logging_enabled_state, sizeof(_nm_logging_enabled_state));
+    memset(_nm_logging_enabled_state, 0, sizeof(_nm_logging_enabled_state));
+    return p;
+}
+
+static inline void
+nmtst_logging_reenable(gpointer old_state)
+{
+    g_assert(nmtst_initialized());
+    if (old_state) {
+        memcpy(_nm_logging_enabled_state, old_state, sizeof(_nm_logging_enabled_state));
+        g_free(old_state);
+    }
+}
+#endif
+
+/*****************************************************************************/
+
+#ifdef NM_SETTING_IP_CONFIG_H
+static inline void
+nmtst_setting_ip_config_add_address(NMSettingIPConfig *s_ip, const char *address, guint prefix)
+{
+    NMIPAddress *addr;
+    int          family;
+
+    g_assert(s_ip);
+
+    if (nm_utils_ipaddr_is_valid(AF_INET, address))
+        family = AF_INET;
+    else if (nm_utils_ipaddr_is_valid(AF_INET6, address))
+        family = AF_INET6;
+    else
+        g_assert_not_reached();
+
+    addr = nm_ip_address_new(family, address, prefix, NULL);
+    g_assert(addr);
+    g_assert(nm_setting_ip_config_add_address(s_ip, addr));
+    nm_ip_address_unref(addr);
+}
+
+static inline void
+nmtst_setting_ip_config_add_route(NMSettingIPConfig *s_ip,
+                                  const char *       dest,
+                                  guint              prefix,
+                                  const char *       next_hop,
+                                  gint64             metric)
+{
+    NMIPRoute *route;
+    int        family;
+
+    g_assert(s_ip);
+
+    if (nm_utils_ipaddr_is_valid(AF_INET, dest))
+        family = AF_INET;
+    else if (nm_utils_ipaddr_is_valid(AF_INET6, dest))
+        family = AF_INET6;
+    else
+        g_assert_not_reached();
+
+    route = nm_ip_route_new(family, dest, prefix, next_hop, metric, NULL);
+    g_assert(route);
+    g_assert(nm_setting_ip_config_add_route(s_ip, route));
+    nm_ip_route_unref(route);
+}
+
+static inline void
+nmtst_assert_route_attribute_string(NMIPRoute *route, const char *name, const char *value)
+{
+    GVariant *variant;
+
+    variant = nm_ip_route_get_attribute(route, name);
+    g_assert(variant);
+    g_assert(g_variant_is_of_type(variant, G_VARIANT_TYPE_STRING));
+    g_assert_cmpstr(g_variant_get_string(variant, NULL), ==, value);
+}
+
+static inline void
+nmtst_assert_route_attribute_byte(NMIPRoute *route, const char *name, guchar value)
+{
+    GVariant *variant;
+
+    variant = nm_ip_route_get_attribute(route, name);
+    g_assert(variant);
+    g_assert(g_variant_is_of_type(variant, G_VARIANT_TYPE_BYTE));
+    g_assert_cmpint(g_variant_get_byte(variant), ==, value);
+}
+
+static inline void
+nmtst_assert_route_attribute_uint32(NMIPRoute *route, const char *name, guint32 value)
+{
+    GVariant *variant;
+
+    variant = nm_ip_route_get_attribute(route, name);
+    g_assert(variant);
+    g_assert(g_variant_is_of_type(variant, G_VARIANT_TYPE_UINT32));
+    g_assert_cmpint(g_variant_get_uint32(variant), ==, value);
+}
+
+static inline void
+nmtst_assert_route_attribute_boolean(NMIPRoute *route, const char *name, gboolean value)
+{
+    GVariant *variant;
+
+    variant = nm_ip_route_get_attribute(route, name);
+    g_assert(variant);
+    g_assert(g_variant_is_of_type(variant, G_VARIANT_TYPE_BOOLEAN));
+    g_assert_cmpint(g_variant_get_boolean(variant), ==, value);
+}
+#endif /* NM_SETTING_IP_CONFIG_H */
+
+#if (defined(__NM_SIMPLE_CONNECTION_H__) && defined(__NM_SETTING_CONNECTION_H__)) \
+    || (defined(NM_CONNECTION_H))
+
+static inline NMConnection *
+nmtst_clone_connection(NMConnection *connection)
+{
+    g_assert(NM_IS_CONNECTION(connection));
+
+    #if defined(__NM_SIMPLE_CONNECTION_H__)
+    return nm_simple_connection_new_clone(connection);
+    #else
+    return nm_connection_duplicate(connection);
+    #endif
+}
+
+static inline NMConnection *
+nmtst_create_minimal_connection(const char *          id,
+                                const char *          uuid,
+                                const char *          type,
+                                NMSettingConnection **out_s_con)
+{
+    NMConnection *       con;
+    NMSetting *          s_base = NULL;
+    NMSettingConnection *s_con;
+    gs_free char *       uuid_free = NULL;
+
+    g_assert(id);
+
+    if (uuid)
+        g_assert(nm_utils_is_uuid(uuid));
+    else
+        uuid = uuid_free = nm_utils_uuid_generate();
+
+    if (type) {
+        GType type_g;
+
+    #if defined(__NM_SIMPLE_CONNECTION_H__)
+        type_g = nm_setting_lookup_type(type);
+    #else
+        type_g = nm_connection_lookup_setting_type(type);
+    #endif
+
+        g_assert(type_g != G_TYPE_INVALID);
+
+        s_base = g_object_new(type_g, NULL);
+        g_assert(NM_IS_SETTING(s_base));
+    }
+
+    #if defined(__NM_SIMPLE_CONNECTION_H__)
+    con = nm_simple_connection_new();
+    #else
+    con = nm_connection_new();
+    #endif
+
+    g_assert(con);
+
+    s_con = NM_SETTING_CONNECTION(nm_setting_connection_new());
+
+    g_assert(s_con);
+
+    g_object_set(s_con,
+                 NM_SETTING_CONNECTION_ID,
+                 id,
+                 NM_SETTING_CONNECTION_UUID,
+                 uuid,
+                 NM_SETTING_CONNECTION_TYPE,
+                 type,
+                 NULL);
+    nm_connection_add_setting(con, NM_SETTING(s_con));
+
+    if (s_base)
+        nm_connection_add_setting(con, s_base);
+
+    if (out_s_con)
+        *out_s_con = s_con;
+    return con;
+}
+
+static inline gboolean
+_nmtst_connection_normalize_v(NMConnection *connection, va_list args)
+{
+    GError *    error = NULL;
+    gboolean    success;
+    gboolean    was_modified = FALSE;
+    GHashTable *parameters   = NULL;
+    const char *p_name;
+
+    g_assert(NM_IS_CONNECTION(connection));
+
+    while ((p_name = va_arg(args, const char *))) {
+        if (!parameters)
+            parameters = g_hash_table_new(g_str_hash, g_str_equal);
+        g_hash_table_insert(parameters, (gpointer *) p_name, va_arg(args, gpointer));
+    }
+
+    success = nm_connection_normalize(connection, parameters, &was_modified, &error);
+    g_assert_no_error(error);
+    g_assert(success);
+
+    if (parameters)
+        g_hash_table_destroy(parameters);
+
+    return was_modified;
+}
+
+static inline gboolean
+_nmtst_connection_normalize(NMConnection *connection, ...)
+{
+    gboolean was_modified;
+    va_list  args;
+
+    va_start(args, connection);
+    was_modified = _nmtst_connection_normalize_v(connection, args);
+    va_end(args);
+
+    return was_modified;
+}
+    #define nmtst_connection_normalize(connection, ...) \
+        _nmtst_connection_normalize(connection, ##__VA_ARGS__, NULL)
+
+static inline NMConnection *
+_nmtst_connection_duplicate_and_normalize(NMConnection *connection, ...)
+{
+    va_list args;
+
+    connection = nmtst_clone_connection(connection);
+
+    va_start(args, connection);
+    _nmtst_connection_normalize_v(connection, args);
+    va_end(args);
+
+    return connection;
+}
+    #define nmtst_connection_duplicate_and_normalize(connection, ...) \
+        _nmtst_connection_duplicate_and_normalize(connection, ##__VA_ARGS__, NULL)
+
+static inline void
+nmtst_assert_connection_equals(NMConnection *a,
+                               gboolean      normalize_a,
+                               NMConnection *b,
+                               gboolean      normalize_b)
+{
+    gboolean        compare;
+    gs_unref_object NMConnection *a2           = NULL;
+    gs_unref_object NMConnection *b2           = NULL;
+    GHashTable *                  out_settings = NULL;
+
+    g_assert(NM_IS_CONNECTION(a));
+    g_assert(NM_IS_CONNECTION(b));
+
+    if (normalize_a)
+        a = a2 = nmtst_connection_duplicate_and_normalize(a);
+    if (normalize_b)
+        b = b2 = nmtst_connection_duplicate_and_normalize(b);
+
+    compare = nm_connection_diff(a, b, NM_SETTING_COMPARE_FLAG_EXACT, &out_settings);
+    if (!compare || out_settings) {
+        const char *   name, *pname;
+        GHashTable *   setting;
+        GHashTableIter iter, iter2;
+
+        __NMTST_LOG(g_message, ">>> ASSERTION nmtst_assert_connection_equals() fails");
+        if (out_settings) {
+            g_hash_table_iter_init(&iter, out_settings);
+            while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &setting)) {
+                __NMTST_LOG(g_message, ">>> differences in setting '%s':", name);
+
+                g_hash_table_iter_init(&iter2, setting);
+                while (g_hash_table_iter_next(&iter2, (gpointer *) &pname, NULL))
+                    __NMTST_LOG(g_message, ">>> differences in setting '%s.%s'", name, pname);
+            }
+        }
+
+    #ifdef __NM_KEYFILE_INTERNAL_H__
+        {
+            nm_auto_unref_keyfile GKeyFile *kf_a = NULL, *kf_b = NULL;
+            gs_free char *                  str_a = NULL, *str_b = NULL;
+
+            kf_a = nm_keyfile_write(a, NM_KEYFILE_HANDLER_FLAGS_NONE, NULL, NULL, NULL);
+            kf_b = nm_keyfile_write(b, NM_KEYFILE_HANDLER_FLAGS_NONE, NULL, NULL, NULL);
+
+            if (kf_a)
+                str_a = g_key_file_to_data(kf_a, NULL, NULL);
+            if (kf_b)
+                str_b = g_key_file_to_data(kf_b, NULL, NULL);
+
+            __NMTST_LOG(g_message,
+                        ">>> Connection A as kf (*WARNING: keyfile representation might not show "
+                        "the difference*):\n%s",
+                        str_a);
+            __NMTST_LOG(g_message,
+                        ">>> Connection B as kf (*WARNING: keyfile representation might not show "
+                        "the difference*):\n%s",
+                        str_b);
+        }
+    #endif
+    }
+    g_assert(compare);
+    g_assert(!out_settings);
+
+    compare = nm_connection_compare(a, b, NM_SETTING_COMPARE_FLAG_EXACT);
+    g_assert(compare);
+}
+
+static inline void
+nmtst_assert_connection_verifies(NMConnection *con)
+{
+    /* assert that the connection does verify, it might be normaliziable or not */
+    GError * error = NULL;
+    gboolean success;
+
+    g_assert(NM_IS_CONNECTION(con));
+
+    success = nm_connection_verify(con, &error);
+    g_assert_no_error(error);
+    g_assert(success);
+}
+
+static inline void
+nmtst_assert_connection_verifies_without_normalization(NMConnection *con)
+{
+    /* assert that the connection verifies and does not need any normalization */
+    GError *        error = NULL;
+    gboolean        success;
+    gboolean        was_modified        = FALSE;
+    gs_unref_object NMConnection *clone = NULL;
+
+    clone = nmtst_clone_connection(con);
+
+    nmtst_assert_connection_verifies(con);
+
+    success = nm_connection_normalize(clone, NULL, &was_modified, &error);
+    g_assert_no_error(error);
+    g_assert(success);
+    nmtst_assert_connection_equals(con, FALSE, clone, FALSE);
+    g_assert(!was_modified);
+}
+
+static inline void
+nmtst_assert_connection_verifies_and_normalizable(NMConnection *con)
+{
+    /* assert that the connection does verify, but normalization still modifies it */
+    GError *        error = NULL;
+    gboolean        success;
+    gboolean        was_modified        = FALSE;
+    gs_unref_object NMConnection *clone = NULL;
+
+    clone = nmtst_clone_connection(con);
+
+    nmtst_assert_connection_verifies(con);
+
+    success = nm_connection_normalize(clone, NULL, &was_modified, &error);
+    g_assert_no_error(error);
+    g_assert(success);
+    g_assert(was_modified);
+
+    /* again! */
+    nmtst_assert_connection_verifies_without_normalization(clone);
+}
+
+static inline void
+nmtst_assert_connection_verifies_after_normalization(NMConnection *con,
+                                                     GQuark        expect_error_domain,
+                                                     int           expect_error_code)
+{
+    /* assert that the connection does not verify, but normalization does fix it */
+    GError *        error = NULL;
+    gboolean        success;
+    gboolean        was_modified        = FALSE;
+    gs_unref_object NMConnection *clone = NULL;
+
+    clone = nmtst_clone_connection(con);
+
+    success = nm_connection_verify(con, &error);
+    nmtst_assert_error(error, expect_error_domain, expect_error_code, NULL);
+    g_assert(!success);
+    g_clear_error(&error);
+
+    success = nm_connection_normalize(clone, NULL, &was_modified, &error);
+    g_assert_no_error(error);
+    g_assert(success);
+    g_assert(was_modified);
+
+    /* again! */
+    nmtst_assert_connection_verifies_without_normalization(clone);
+}
+
+static inline void
+nmtst_assert_connection_unnormalizable(NMConnection *con,
+                                       GQuark        expect_error_domain,
+                                       int           expect_error_code)
+{
+    /* assert that the connection does not verify, and it cannot be fixed by normalization */
+
+    GError *        error = NULL;
+    gboolean        success;
+    gboolean        was_modified        = FALSE;
+    gs_unref_object NMConnection *clone = NULL;
+
+    clone = nmtst_clone_connection(con);
+
+    success = nm_connection_verify(con, &error);
+    nmtst_assert_error(error, expect_error_domain, expect_error_code, NULL);
+    g_assert(!success);
+    g_clear_error(&error);
+
+    success = nm_connection_normalize(clone, NULL, &was_modified, &error);
+    nmtst_assert_error(error, expect_error_domain, expect_error_code, NULL);
+    g_assert(!success);
+    g_assert(!was_modified);
+    nmtst_assert_connection_equals(con, FALSE, clone, FALSE);
+    g_clear_error(&error);
+}
+
+static inline void
+nmtst_assert_setting_verifies(NMSetting *setting)
+{
+    /* assert that the setting verifies without an error */
+
+    GError * error = NULL;
+    gboolean success;
+
+    g_assert(NM_IS_SETTING(setting));
+
+    success = nm_setting_verify(setting, NULL, &error);
+    g_assert_no_error(error);
+    g_assert(success);
+}
+
+    #if defined(__NM_SIMPLE_CONNECTION_H__) && NM_CHECK_VERSION(1, 10, 0) \
+        && (!defined(NM_VERSION_MAX_ALLOWED) || NM_VERSION_MAX_ALLOWED >= NM_VERSION_1_10)
+static inline void
+_nmtst_assert_connection_has_settings(NMConnection *connection,
+                                      gboolean      has_at_least,
+                                      gboolean      has_at_most,
+                                      ...)
+{
+    gs_unref_hashtable GHashTable *names = NULL;
+    gs_free NMSetting **settings         = NULL;
+    va_list             ap;
+    const char *        name;
+    guint               i, len;
+    gs_unref_ptrarray GPtrArray *names_arr = NULL;
+
+    g_assert(NM_IS_CONNECTION(connection));
+
+    names     = g_hash_table_new(g_str_hash, g_str_equal);
+    names_arr = g_ptr_array_new();
+
+    va_start(ap, has_at_most);
+    while ((name = va_arg(ap, const char *))) {
+        if (!nm_g_hash_table_add(names, (gpointer) name))
+            g_assert_not_reached();
+        g_ptr_array_add(names_arr, (gpointer) name);
+    }
+    va_end(ap);
+
+    g_ptr_array_add(names_arr, NULL);
+
+    settings = nm_connection_get_settings(connection, &len);
+    for (i = 0; i < len; i++) {
+        if (!g_hash_table_remove(names, nm_setting_get_name(settings[i])) && has_at_most) {
+            g_error(
+                "nmtst_assert_connection_has_settings(): has setting \"%s\" which is not expected",
+                nm_setting_get_name(settings[i]));
+        }
+    }
+    if (g_hash_table_size(names) > 0 && has_at_least) {
+        gs_free char *       expected_str   = g_strjoinv(" ", (char **) names_arr->pdata);
+        gs_free const char **settings_names = NULL;
+        gs_free char *       has_str        = NULL;
+
+        settings_names = g_new0(const char *, len + 1);
+        for (i = 0; i < len; i++)
+            settings_names[i] = nm_setting_get_name(settings[i]);
+        has_str = g_strjoinv(" ", (char **) settings_names);
+
+        g_error("nmtst_assert_connection_has_settings(): the setting lacks %u expected settings "
+                "(expected: [%s] vs. has: [%s])",
+                g_hash_table_size(names),
+                expected_str,
+                has_str);
+    }
+}
+        #define nmtst_assert_connection_has_settings(connection, ...) \
+            _nmtst_assert_connection_has_settings((connection), TRUE, TRUE, __VA_ARGS__, NULL)
+        #define nmtst_assert_connection_has_settings_at_least(connection, ...) \
+            _nmtst_assert_connection_has_settings((connection), TRUE, FALSE, __VA_ARGS__, NULL)
+        #define nmtst_assert_connection_has_settings_at_most(connection, ...) \
+            _nmtst_assert_connection_has_settings((connection), FALSE, TRUE, __VA_ARGS__, NULL)
+    #endif
+
+static inline void
+nmtst_assert_setting_verify_fails(NMSetting *setting,
+                                  GQuark     expect_error_domain,
+                                  int        expect_error_code)
+{
+    /* assert that the setting verification fails */
+
+    GError * error = NULL;
+    gboolean success;
+
+    g_assert(NM_IS_SETTING(setting));
+
+    success = nm_setting_verify(setting, NULL, &error);
+    nmtst_assert_error(error, expect_error_domain, expect_error_code, NULL);
+    g_assert(!success);
+    g_clear_error(&error);
+}
+
+static inline void
+nmtst_assert_setting_is_equal(gconstpointer /* const NMSetting * */ a,
+                              gconstpointer /* const NMSetting * */ b,
+                              NMSettingCompareFlags                 flags)
+{
+    gs_unref_hashtable GHashTable *hash = NULL;
+    guint32                        r    = nmtst_get_rand_uint32();
+
+    g_assert(NM_IS_SETTING(a));
+    g_assert(NM_IS_SETTING(b));
+
+    if (NM_FLAGS_HAS(r, 0x4))
+        NM_SWAP(&a, &b);
+
+    g_assert(nm_setting_compare((NMSetting *) a, (NMSetting *) b, flags));
+
+    if (NM_FLAGS_HAS(r, 0x8))
+        NM_SWAP(&a, &b);
+
+    g_assert(nm_setting_diff((NMSetting *) a, (NMSetting *) b, flags, NM_FLAGS_HAS(r, 0x1), &hash));
+    g_assert(!hash);
+}
+#endif
+
+#ifdef __NM_SETTING_PRIVATE_H__
+static inline NMSetting *
+nmtst_assert_setting_dbus_new(GType gtype, GVariant *variant)
+{
+    NMSetting *   setting;
+    gs_free_error GError *error = NULL;
+
+    g_assert(g_type_is_a(gtype, NM_TYPE_SETTING));
+    g_assert(gtype != NM_TYPE_SETTING);
+    g_assert(variant);
+    g_assert(g_variant_is_of_type(variant, NM_VARIANT_TYPE_SETTING));
+
+    setting =
+        _nm_setting_new_from_dbus(gtype, variant, NULL, NM_SETTING_PARSE_FLAGS_STRICT, &error);
+    nmtst_assert_success(setting, error);
+    return setting;
+}
+
+static inline void
+nmtst_assert_setting_dbus_roundtrip(gconstpointer /* const NMSetting * */ setting)
+{
+    gs_unref_object NMSetting *setting2 = NULL;
+    gs_unref_variant GVariant *variant  = NULL;
+
+    g_assert(NM_IS_SETTING(setting));
+
+    variant  = _nm_setting_to_dbus((NMSetting *) setting, NULL, NM_CONNECTION_SERIALIZE_ALL, NULL);
+    setting2 = nmtst_assert_setting_dbus_new(G_OBJECT_TYPE(setting), variant);
+    nmtst_assert_setting_is_equal(setting, setting2, NM_SETTING_COMPARE_FLAG_EXACT);
+}
+#endif
+
+#ifdef __NM_UTILS_H__
+static inline void
+nmtst_assert_hwaddr_equals(gconstpointer hwaddr1,
+                           gssize        hwaddr1_len,
+                           const char *  expected,
+                           const char *  file,
+                           int           line)
+{
+    guint8      buf2[NM_UTILS_HWADDR_LEN_MAX];
+    gsize       hwaddr2_len = 1;
+    const char *p;
+    gboolean    success;
+
+    g_assert(hwaddr1_len > 0 && hwaddr1_len <= NM_UTILS_HWADDR_LEN_MAX);
+
+    g_assert(expected);
+    for (p = expected; *p; p++) {
+        if (*p == ':' || *p == '-')
+            hwaddr2_len++;
+    }
+    g_assert(hwaddr2_len <= NM_UTILS_HWADDR_LEN_MAX);
+    g_assert(nm_utils_hwaddr_aton(expected, buf2, hwaddr2_len));
+
+    /* Manually check the entire hardware address instead of using
+     * nm_utils_hwaddr_matches() because that function doesn't compare
+     * entire InfiniBand addresses for various (legitimate) reasons.
+     */
+    success = (hwaddr1_len == hwaddr2_len);
+    if (success)
+        success = !memcmp(hwaddr1, buf2, hwaddr1_len);
+    if (!success) {
+        g_error("assert: %s:%d: hwaddr '%s' (%zd) expected, but got %s (%zd)",
+                file,
+                line,
+                expected,
+                hwaddr2_len,
+                nm_utils_hwaddr_ntoa(hwaddr1, hwaddr1_len),
+                hwaddr1_len);
+    }
+}
+    #define nmtst_assert_hwaddr_equals(hwaddr1, hwaddr1_len, expected) \
+        nmtst_assert_hwaddr_equals(hwaddr1, hwaddr1_len, expected, __FILE__, __LINE__)
+#endif
+
+#if defined(__NM_SIMPLE_CONNECTION_H__) && defined(__NM_SETTING_CONNECTION_H__) \
+    && defined(__NM_KEYFILE_INTERNAL_H__)
+
+static inline NMConnection *
+nmtst_create_connection_from_keyfile(const char *keyfile_str, const char *full_filename)
+{
+    nm_auto_unref_keyfile GKeyFile *keyfile = NULL;
+    gs_free_error GError *error             = NULL;
+    gboolean              success;
+    NMConnection *        con;
+    gs_free char *        filename = g_path_get_basename(full_filename);
+    gs_free char *        base_dir = g_path_get_dirname(full_filename);
+
+    g_assert(keyfile_str);
+    g_assert(full_filename && full_filename[0] == '/');
+
+    keyfile = g_key_file_new();
+    success = g_key_file_load_from_data(keyfile,
+                                        keyfile_str,
+                                        strlen(keyfile_str),
+                                        G_KEY_FILE_NONE,
+                                        &error);
+    nmtst_assert_success(success, error);
+
+    con = nm_keyfile_read(keyfile, base_dir, NM_KEYFILE_HANDLER_FLAGS_NONE, NULL, NULL, &error);
+    nmtst_assert_success(NM_IS_CONNECTION(con), error);
+
+    nm_keyfile_read_ensure_id(con, filename);
+    nm_keyfile_read_ensure_uuid(con, full_filename);
+
+    nmtst_connection_normalize(con);
+
+    return con;
+}
+
+#endif
+
+#ifdef __NM_CONNECTION_H__
+
+static inline GVariant *
+_nmtst_variant_new_vardict(int dummy, ...)
+{
+    GVariantBuilder builder;
+    va_list         ap;
+    const char *    name;
+    GVariant *      variant;
+
+    g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
+
+    va_start(ap, dummy);
+    while ((name = va_arg(ap, const char *))) {
+        variant = va_arg(ap, GVariant *);
+        g_variant_builder_add(&builder, "{sv}", name, variant);
+    }
+    va_end(ap);
+
+    return g_variant_builder_end(&builder);
+}
+    #define nmtst_variant_new_vardict(...) _nmtst_variant_new_vardict(0, __VA_ARGS__, NULL)
+
+    #define nmtst_assert_variant_is_of_type(variant, type)     \
+        G_STMT_START                                           \
+        {                                                      \
+            GVariant *_variantx = (variant);                   \
+                                                               \
+            g_assert(_variantx);                               \
+            g_assert(g_variant_is_of_type(_variantx, (type))); \
+        }                                                      \
+        G_STMT_END
+
+    #define nmtst_assert_variant_uint32(variant, val)                         \
+        G_STMT_START                                                          \
+        {                                                                     \
+            GVariant *_variant = (variant);                                   \
+                                                                              \
+            nmtst_assert_variant_is_of_type(_variant, G_VARIANT_TYPE_UINT32); \
+            g_assert_cmpint(g_variant_get_uint32(_variant), ==, (val));       \
+        }                                                                     \
+        G_STMT_END
+
+    #define nmtst_assert_variant_string(variant, str)                         \
+        G_STMT_START                                                          \
+        {                                                                     \
+            gsize       _l;                                                   \
+            GVariant *  _variant = (variant);                                 \
+            const char *_str     = (str);                                     \
+                                                                              \
+            nmtst_assert_variant_is_of_type(_variant, G_VARIANT_TYPE_STRING); \
+            g_assert(_str);                                                   \
+            g_assert_cmpstr(g_variant_get_string(_variant, &_l), ==, _str);   \
+            g_assert_cmpint(_l, ==, strlen(_str));                            \
+        }                                                                     \
+        G_STMT_END
+
+    #ifdef __NM_SHARED_UTILS_H__
+        #define _nmtst_assert_variant_bytestring_cmp_str(_ptr, _ptr2, _len)                      \
+            G_STMT_START                                                                         \
+            {                                                                                    \
+                if (memcmp(_ptr2, _ptr, _len) != 0) {                                            \
+                    gs_free char *_x1 = NULL;                                                    \
+                    gs_free char *_x2 = NULL;                                                    \
+                    const char *  _xx1;                                                          \
+                    const char *  _xx2;                                                          \
+                                                                                                 \
+                    _xx1 = nm_utils_buf_utf8safe_escape(_ptr,                                    \
+                                                        _len,                                    \
+                                                        NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL, \
+                                                        &_x1);                                   \
+                    _xx2 = nm_utils_buf_utf8safe_escape(_ptr2,                                   \
+                                                        _len,                                    \
+                                                        NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL, \
+                                                        &_x2);                                   \
+                    g_assert_cmpstr(_xx1, ==, _xx2);                                             \
+                    g_assert_not_reached();                                                      \
+                }                                                                                \
+            }                                                                                    \
+            G_STMT_END
+    #else
+        #define _nmtst_assert_variant_bytestring_cmp_str(_ptr, _ptr2, _len) \
+            G_STMT_START {}                                                 \
+            G_STMT_END
+    #endif
+
+    #define nmtst_assert_variant_bytestring(variant, ptr, len)                    \
+        G_STMT_START                                                              \
+        {                                                                         \
+            GVariant *    _variant = (variant);                                   \
+            gconstpointer _ptr     = (ptr);                                       \
+            gconstpointer _ptr2;                                                  \
+            gsize         _len = (len);                                           \
+            gsize         _len2;                                                  \
+                                                                                  \
+            nmtst_assert_variant_is_of_type(_variant, G_VARIANT_TYPE_BYTESTRING); \
+            _ptr2 = g_variant_get_fixed_array(_variant, &_len2, 1);               \
+            g_assert_cmpint(_len2, ==, _len);                                     \
+            if (_len != 0 && _ptr) {                                              \
+                _nmtst_assert_variant_bytestring_cmp_str(_ptr, _ptr2, _len);      \
+                g_assert_cmpmem(_ptr2, _len2, _ptr, _len);                        \
+            }                                                                     \
+        }                                                                         \
+        G_STMT_END
+
+typedef enum {
+    NMTST_VARIANT_EDITOR_CONNECTION,
+    NMTST_VARIANT_EDITOR_SETTING,
+    NMTST_VARIANT_EDITOR_PROPERTY
+} NmtstVariantEditorPhase;
+
+    #define NMTST_VARIANT_EDITOR(__connection_variant, __code)                         \
+        G_STMT_START                                                                   \
+        {                                                                              \
+            GVariantIter            __connection_iter, *__setting_iter;                \
+            GVariantBuilder         __connection_builder, __setting_builder;           \
+            const char *            __cur_setting_name, *__cur_property_name;          \
+            GVariant *              __property_val;                                    \
+            NmtstVariantEditorPhase __phase;                                           \
+                                                                                       \
+            g_variant_builder_init(&__connection_builder, NM_VARIANT_TYPE_CONNECTION); \
+            g_variant_iter_init(&__connection_iter, __connection_variant);             \
+                                                                                       \
+            __phase             = NMTST_VARIANT_EDITOR_CONNECTION;                     \
+            __cur_setting_name  = NULL;                                                \
+            __cur_property_name = NULL;                                                \
+            __code;                                                                    \
+            while (g_variant_iter_next(&__connection_iter,                             \
+                                       "{&sa{sv}}",                                    \
+                                       &__cur_setting_name,                            \
+                                       &__setting_iter)) {                             \
+                g_variant_builder_init(&__setting_builder, NM_VARIANT_TYPE_SETTING);   \
+                __phase             = NMTST_VARIANT_EDITOR_SETTING;                    \
+                __cur_property_name = NULL;                                            \
+                __code;                                                                \
+                                                                                       \
+                while (__cur_setting_name                                              \
+                       && g_variant_iter_next(__setting_iter,                          \
+                                              "{&sv}",                                 \
+                                              &__cur_property_name,                    \
+                                              &__property_val)) {                      \
+                    __phase = NMTST_VARIANT_EDITOR_PROPERTY;                           \
+                    __code;                                                            \
+                                                                                       \
+                    if (__cur_property_name) {                                         \
+                        g_variant_builder_add(&__setting_builder,                      \
+                                              "{sv}",                                  \
+                                              __cur_property_name,                     \
+                                              __property_val);                         \
+                    }                                                                  \
+                    g_variant_unref(__property_val);                                   \
+                }                                                                      \
+                                                                                       \
+                if (__cur_setting_name)                                                \
+                    g_variant_builder_add(&__connection_builder,                       \
+                                          "{sa{sv}}",                                  \
+                                          __cur_setting_name,                          \
+                                          &__setting_builder);                         \
+                else                                                                   \
+                    g_variant_builder_clear(&__setting_builder);                       \
+                g_variant_iter_free(__setting_iter);                                   \
+            }                                                                          \
+                                                                                       \
+            g_variant_unref(__connection_variant);                                     \
+                                                                                       \
+            __connection_variant = g_variant_builder_end(&__connection_builder);       \
+        }                                                                              \
+        G_STMT_END;
+
+    #define NMTST_VARIANT_ADD_SETTING(__setting_name, __setting_variant) \
+        G_STMT_START                                                     \
+        {                                                                \
+            if (__phase == NMTST_VARIANT_EDITOR_CONNECTION)              \
+                g_variant_builder_add(&__connection_builder,             \
+                                      "{s@a{sv}}",                       \
+                                      __setting_name,                    \
+                                      __setting_variant);                \
+        }                                                                \
+        G_STMT_END
+
+    #define NMTST_VARIANT_DROP_SETTING(__setting_name)                           \
+        G_STMT_START                                                             \
+        {                                                                        \
+            if (__phase == NMTST_VARIANT_EDITOR_SETTING && __cur_setting_name) { \
+                if (!strcmp(__cur_setting_name, __setting_name))                 \
+                    __cur_setting_name = NULL;                                   \
+            }                                                                    \
+        }                                                                        \
+        G_STMT_END
+
+    #define NMTST_VARIANT_ADD_PROPERTY(__setting_name, __property_name, __format_string, __value) \
+        G_STMT_START                                                                              \
+        {                                                                                         \
+            if (__phase == NMTST_VARIANT_EDITOR_SETTING) {                                        \
+                if (!strcmp(__cur_setting_name, __setting_name)) {                                \
+                    g_variant_builder_add(&__setting_builder,                                     \
+                                          "{sv}",                                                 \
+                                          __property_name,                                        \
+                                          g_variant_new(__format_string, __value));               \
+                }                                                                                 \
+            }                                                                                     \
+        }                                                                                         \
+        G_STMT_END
+
+    #define NMTST_VARIANT_DROP_PROPERTY(__setting_name, __property_name)           \
+        G_STMT_START                                                               \
+        {                                                                          \
+            if (__phase == NMTST_VARIANT_EDITOR_PROPERTY && __cur_property_name) { \
+                if (!strcmp(__cur_setting_name, __setting_name)                    \
+                    && !strcmp(__cur_property_name, __property_name))              \
+                    __cur_property_name = NULL;                                    \
+            }                                                                      \
+        }                                                                          \
+        G_STMT_END
+
+    #define NMTST_VARIANT_CHANGE_PROPERTY(__setting_name,                                          \
+                                          __property_name,                                         \
+                                          __format_string,                                         \
+                                          __value)                                                 \
+        G_STMT_START                                                                               \
+        {                                                                                          \
+            NMTST_VARIANT_DROP_PROPERTY(__setting_name, __property_name);                          \
+            NMTST_VARIANT_ADD_PROPERTY(__setting_name, __property_name, __format_string, __value); \
+        }                                                                                          \
+        G_STMT_END
+
+#endif /* __NM_CONNECTION_H__ */
+
+static inline GVariant *
+nmtst_variant_from_string(const GVariantType *variant_type, const char *variant_str)
+{
+    GVariant *variant;
+    GError *  error = NULL;
+
+    g_assert(variant_type);
+    g_assert(variant_str);
+
+    variant = g_variant_parse(variant_type, variant_str, NULL, NULL, &error);
+    nmtst_assert_success(variant, error);
+    return variant;
+}
+
+/*****************************************************************************/
+
+static inline void
+nmtst_keyfile_assert_data(GKeyFile *kf, const char *data, gssize data_len)
+{
+    nm_auto_unref_keyfile GKeyFile *kf2 = NULL;
+    gs_free_error GError *error         = NULL;
+    gs_free char *        d1            = NULL;
+    gs_free char *        d2            = NULL;
+    gboolean              success;
+    gsize                 d1_len;
+    gsize                 d2_len;
+
+    g_assert(kf);
+    g_assert(data || data_len == 0);
+    g_assert(data_len >= -1);
+
+    d1 = g_key_file_to_data(kf, &d1_len, &error);
+    nmtst_assert_success(d1, error);
+
+    if (data_len == -1) {
+        g_assert_cmpint(strlen(d1), ==, d1_len);
+        data_len = strlen(data);
+        g_assert_cmpstr(d1, ==, data);
+    }
+
+    g_assert_cmpmem(d1, d1_len, data, (gsize) data_len);
+
+    /* also check that we can re-generate the same keyfile from the data. */
+
+    kf2     = g_key_file_new();
+    success = g_key_file_load_from_data(kf2, d1, d1_len, G_KEY_FILE_NONE, &error);
+    nmtst_assert_success(success, error);
+
+    d2 = g_key_file_to_data(kf2, &d2_len, &error);
+    nmtst_assert_success(d2, error);
+
+    g_assert_cmpmem(d2, d2_len, d1, d1_len);
+}
+
+static inline gssize
+nmtst_keyfile_get_num_keys(GKeyFile *keyfile, const char *group_name)
+{
+    gs_strfreev char **keys     = NULL;
+    gs_free_error GError *error = NULL;
+    gsize                 l     = 0;
+
+    g_assert(keyfile);
+    g_assert(group_name);
+
+    if (!g_key_file_has_group(keyfile, group_name))
+        return -1;
+
+    keys = g_key_file_get_keys(keyfile, group_name, &l, &error);
+
+    nmtst_assert_success(keys, error);
+
+    g_assert_cmpint(NM_PTRARRAY_LEN(keys), ==, l);
+
+    return l;
+}
+
+/*****************************************************************************/
+
+#if defined(NM_SETTING_IP_CONFIG_H) && defined(__NM_SHARED_UTILS_H__)
+
+static inline NMIPAddress *
+nmtst_ip_address_new(int addr_family, const char *str)
+{
+    NMIPAddr     addr;
+    int          plen;
+    GError *     error = NULL;
+    NMIPAddress *a;
+
+    if (!nm_utils_parse_inaddr_prefix_bin(addr_family, str, &addr_family, &addr, &plen))
+        g_assert_not_reached();
+
+    if (plen == -1)
+        plen = addr_family == AF_INET ? 32 : 128;
+
+    a = nm_ip_address_new_binary(addr_family, &addr, plen, &error);
+    nmtst_assert_success(a, error);
+    return a;
+}
+
+#endif
+
+/*****************************************************************************/
+
+#endif /* __NM_TEST_UTILS_H__ */
diff --git a/src/libnm-glib-aux/nm-time-utils.c b/src/libnm-glib-aux/nm-time-utils.c
new file mode 100644
index 00000000..f30e6a19
--- /dev/null
+++ b/src/libnm-glib-aux/nm-time-utils.c
@@ -0,0 +1,328 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-time-utils.h"
+
+#include "nm-logging-fwd.h"
+
+/*****************************************************************************/
+
+typedef struct {
+    /* the offset to the native clock, in seconds. */
+    gint64    offset_sec;
+    clockid_t clk_id;
+} GlobalState;
+
+static const GlobalState *volatile p_global_state;
+
+static const GlobalState *
+_t_init_global_state(void)
+{
+    static GlobalState global_state = {};
+    static gsize       init_once    = 0;
+    const GlobalState *p;
+    clockid_t          clk_id;
+    struct timespec    tp;
+    gint64             offset_sec;
+    int                r;
+
+    clk_id = CLOCK_BOOTTIME;
+    r      = clock_gettime(clk_id, &tp);
+    if (r == -1 && errno == EINVAL) {
+        clk_id = CLOCK_MONOTONIC;
+        r      = clock_gettime(clk_id, &tp);
+    }
+
+    /* 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_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_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_sec()
+     * wraps (~68 years).
+     **/
+    offset_sec = (-((gint64) tp.tv_sec)) + 1;
+
+    if (!g_once_init_enter(&init_once)) {
+        /* there was a race. We expect the pointer to be fully initialized now. */
+        p = g_atomic_pointer_get(&p_global_state);
+        g_assert(p);
+        return p;
+    }
+
+    global_state.offset_sec = offset_sec;
+    global_state.clk_id     = clk_id;
+    p                       = &global_state;
+    g_atomic_pointer_set(&p_global_state, p);
+    g_once_init_leave(&init_once, 1);
+
+    _nm_utils_monotonic_timestamp_initialized(&tp, p->offset_sec, p->clk_id == CLOCK_BOOTTIME);
+
+    return p;
+}
+
+#define _t_get_global_state()                         \
+    ({                                                \
+        const GlobalState *_p;                        \
+                                                      \
+        _p = g_atomic_pointer_get(&p_global_state);   \
+        (G_LIKELY(_p) ? _p : _t_init_global_state()); \
+    })
+
+#define _t_clock_gettime_eval(p, tp)                                          \
+    ({                                                                        \
+        struct timespec *const   _tp = (tp);                                  \
+        const GlobalState *const _p2 = (p);                                   \
+        int                      _r;                                          \
+                                                                              \
+        nm_assert(_tp);                                                       \
+                                                                              \
+        _r = clock_gettime(_p2->clk_id, _tp);                                 \
+                                                                              \
+        nm_assert(_r == 0);                                                   \
+        nm_assert(_tp->tv_nsec >= 0 && _tp->tv_nsec < NM_UTILS_NSEC_PER_SEC); \
+                                                                              \
+        _p2;                                                                  \
+    })
+
+#define _t_clock_gettime(tp) _t_clock_gettime_eval(_t_get_global_state(), tp);
+
+/*****************************************************************************/
+
+/**
+ * nm_utils_get_monotonic_timestamp_nsec:
+ *
+ * Returns: a monotonically increasing time stamp in nanoseconds,
+ * starting at an unspecified offset. See clock_gettime(), %CLOCK_BOOTTIME.
+ *
+ * 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_*sec functions return the same
+ * timestamp but in different scales (nsec, usec, msec, sec).
+ **/
+gint64
+nm_utils_get_monotonic_timestamp_nsec(void)
+{
+    const GlobalState *p;
+    struct timespec    tp;
+
+    p = _t_clock_gettime(&tp);
+
+    /* Although the result will always be positive, we return a signed
+     * 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_NSEC_PER_SEC + tp.tv_nsec;
+}
+
+/**
+ * nm_utils_get_monotonic_timestamp_usec:
+ *
+ * Returns: a monotonically increasing time stamp in microseconds,
+ * starting at an unspecified offset. See clock_gettime(), %CLOCK_BOOTTIME.
+ *
+ * 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_*sec functions return the same
+ * timestamp but in different scales (nsec, usec, msec, sec).
+ **/
+gint64
+nm_utils_get_monotonic_timestamp_usec(void)
+{
+    const GlobalState *p;
+    struct timespec    tp;
+
+    p = _t_clock_gettime(&tp);
+
+    /* Although the result will always be positive, we return a signed
+     * integer, which makes it easier to calculate time differences (when
+     * you want to subtract signed values).
+     **/
+    return (((gint64) tp.tv_sec) + p->offset_sec) * ((gint64) G_USEC_PER_SEC)
+           + (tp.tv_nsec / (NM_UTILS_NSEC_PER_SEC / G_USEC_PER_SEC));
+}
+
+/**
+ * nm_utils_get_monotonic_timestamp_msec:
+ *
+ * Returns: a monotonically increasing time stamp in milliseconds,
+ * starting at an unspecified offset. See clock_gettime(), %CLOCK_BOOTTIME.
+ *
+ * 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_*sec functions return the same
+ * timestamp but in different scales (nsec, usec, msec, sec).
+ **/
+gint64
+nm_utils_get_monotonic_timestamp_msec(void)
+{
+    const GlobalState *p;
+    struct timespec    tp;
+
+    p = _t_clock_gettime(&tp);
+
+    /* Although the result will always be positive, we return a signed
+     * integer, which makes it easier to calculate time differences (when
+     * you want to subtract signed values).
+     **/
+    return (((gint64) tp.tv_sec) + p->offset_sec) * ((gint64) 1000)
+           + (tp.tv_nsec / (NM_UTILS_NSEC_PER_SEC / 1000));
+}
+
+/**
+ * nm_utils_get_monotonic_timestamp_sec:
+ *
+ * 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_*sec functions return the same
+ * timestamp but in different scales (nsec, usec, msec, sec).
+ **/
+gint32
+nm_utils_get_monotonic_timestamp_sec(void)
+{
+    const GlobalState *p;
+    struct timespec    tp;
+
+    p = _t_clock_gettime(&tp);
+
+    return (((gint64) tp.tv_sec) + p->offset_sec);
+}
+
+/**
+ * nm_utils_monotonic_timestamp_as_boottime:
+ * @timestamp: the monotonic-timestamp that should be converted into CLOCK_BOOTTIME.
+ * @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_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
+ *   the application is running). However, it might make sense to calculate a timestamp from
+ *   before the application was running, hence negative @timestamp is allowed. The result
+ *   in that case might also be a negative timestamp (in CLOCK_BOOTTIME), which would indicate
+ *   that the timestamp lies in the past before the machine was booted.
+ *
+ * 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_nsec_per_tick)
+{
+    const GlobalState *p;
+    gint64             offset;
+
+    /* 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
+     * value (because he has no idea what these timestamps would be). That would be a bug. */
+    nm_assert(g_atomic_pointer_get(&p_global_state));
+
+    p = _t_get_global_state();
+
+    nm_assert(p->offset_sec <= 0);
+
+    /* calculate the offset of monotonic-timestamp to boottime. offset_s is <= 1. */
+    offset = p->offset_sec * (NM_UTILS_NSEC_PER_SEC / timestamp_nsec_per_tick);
+
+    nm_assert(offset <= 0 && offset > G_MININT64);
+
+    /* check for overflow (note that offset is non-positive). */
+    g_return_val_if_fail(timestamp < G_MAXINT64 + offset, G_MAXINT64);
+
+    return timestamp - offset;
+}
+
+/**
+ * nm_utils_monotonic_timestamp_from_boottime:
+ * @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_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_NSEC_PER_SEC/1000, etc.
+ *
+ * Returns: the same timestamp in monotonic timestamp scale.
+ *
+ * Note that commonly monotonic timestamps are positive. But they may not
+ * be positive in this case. That's when boottime is taken from a time before
+ * the monotonic timestamps started counting. So, that means a zero or negative
+ * value is still a valid timestamp.
+ *
+ * This is the inverse of nm_utils_monotonic_timestamp_as_boottime().
+ */
+gint64
+nm_utils_monotonic_timestamp_from_boottime(guint64 boottime, gint64 timestamp_nsec_per_tick)
+{
+    const GlobalState *p;
+    gint64             offset;
+
+    /* 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();
+
+    nm_assert(p->offset_sec <= 0);
+
+    /* calculate the offset of monotonic-timestamp to boottime. offset_s is <= 1. */
+    offset = p->offset_sec * (NM_UTILS_NSEC_PER_SEC / timestamp_nsec_per_tick);
+
+    nm_assert(offset <= 0 && offset > G_MININT64);
+
+    /* check for overflow (note that offset is non-positive). */
+    g_return_val_if_fail(boottime < G_MAXINT64, G_MAXINT64);
+
+    return (gint64) boottime + offset;
+}
+
+gint64
+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_nsec(&tp);
+}
+
+gint64
+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_msec(&tp);
+}
diff --git a/src/libnm-glib-aux/nm-time-utils.h b/src/libnm-glib-aux/nm-time-utils.h
new file mode 100644
index 00000000..3c3e935f
--- /dev/null
+++ b/src/libnm-glib-aux/nm-time-utils.h
@@ -0,0 +1,47 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#ifndef __NM_TIME_UTILS_H__
+#define __NM_TIME_UTILS_H__
+
+#include <time.h>
+
+static inline gint64
+nm_utils_timespec_to_nsec(const struct timespec *ts)
+{
+    return (((gint64) ts->tv_sec) * ((gint64) NM_UTILS_NSEC_PER_SEC)) + ((gint64) ts->tv_nsec);
+}
+
+static inline gint64
+nm_utils_timespec_to_msec(const struct timespec *ts)
+{
+    return (((gint64) ts->tv_sec) * ((gint64) 1000))
+           + (((gint64) ts->tv_nsec) / ((gint64) NM_UTILS_NSEC_PER_SEC / 1000));
+}
+
+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_msec_cached(gint64 *cache_now)
+{
+    return (*cache_now) ?: (*cache_now = nm_utils_get_monotonic_timestamp_msec());
+}
+
+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/src/libnm-glib-aux/nm-uuid.c b/src/libnm-glib-aux/nm-uuid.c
new file mode 100644
index 00000000..2c6e218a
--- /dev/null
+++ b/src/libnm-glib-aux/nm-uuid.c
@@ -0,0 +1,430 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-glib-aux/nm-default-glib.h"
+
+#include "nm-uuid.h"
+
+#include "libnm-glib-aux/nm-random-utils.h"
+#include "libnm-glib-aux/nm-str-buf.h"
+
+/*****************************************************************************/
+
+char *
+nm_uuid_unparse_case(const NMUuid *uuid, char out_str[static 37], gboolean upper_case)
+{
+    char *s;
+    int   i;
+
+    nm_assert(uuid);
+    nm_assert(out_str);
+
+    s = out_str;
+    for (i = 0; i < 16; i++) {
+        const guint8 c = uuid->uuid[i];
+
+        if (NM_IN_SET(i, 4, 6, 8, 10))
+            *(s++) = '-';
+        *(s++) = nm_hexchar(c >> 4, upper_case);
+        *(s++) = nm_hexchar(c, upper_case);
+    }
+    *s = '\0';
+
+    return out_str;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_uuid_parse_full(const char *str, NMUuid *out_uuid, gboolean *out_is_normalized)
+{
+    NMUuid   uuid;
+    guint8 * p;
+    int      i;
+    gboolean is_normalized = TRUE;
+
+    nm_assert(str);
+
+    p = uuid.uuid;
+
+    for (i = 0; TRUE;) {
+        int v0;
+        int v1;
+
+        if (NM_IN_SET(i, 8, 13, 18, 23)) {
+            if (str[i] != '-')
+                return FALSE;
+            i++;
+            continue;
+        }
+
+        if (i == 36) {
+            if (str[i] != '\0')
+                return FALSE;
+
+            NM_SET_OUT(out_is_normalized, is_normalized);
+            NM_SET_OUT(out_uuid, uuid);
+            return TRUE;
+        }
+
+#define _hexchar(ch, out_is_normalized2)                                    \
+    ({                                                                      \
+        const char _ch     = (ch);                                          \
+        int        _result = -1;                                            \
+                                                                            \
+        if (_ch >= '0') {                                                   \
+            if (_ch <= '9')                                                 \
+                _result = (_ch - '0');                                      \
+            else if (_ch >= 'A') {                                          \
+                if (_ch <= 'F') {                                           \
+                    *(out_is_normalized2) = FALSE;                          \
+                    _result               = ((int) _ch) + (10 - (int) 'A'); \
+                } else if (_ch >= 'a' && _ch <= 'f')                        \
+                    _result = ((int) _ch) + (10 - (int) 'a');               \
+            }                                                               \
+        }                                                                   \
+                                                                            \
+        _result;                                                            \
+    })
+
+        v0 = _hexchar(str[i++], &is_normalized);
+        if (v0 < 0)
+            return FALSE;
+        v1 = _hexchar(str[i++], &is_normalized);
+        if (v1 < 0)
+            return FALSE;
+
+        *(p++) = (v0 << 4) + v1;
+    }
+}
+
+/*****************************************************************************/
+
+NMUuid *
+nm_uuid_generate_random(NMUuid *out_uuid)
+{
+    nm_assert(out_uuid);
+
+    /* https://tools.ietf.org/html/rfc4122#section-4.4 */
+
+    /* See also, systemd's id128_make_v4_uuid() */
+
+    /* nm_utils_random_bytes() is supposed to try hard to give good
+     * randomness. If it fails, it still makes an effort to fill
+     * random data into the buffer. There is not much we can do about
+     * that case, except making sure that it does not happen in the
+     * first place. */
+    nm_utils_random_bytes(out_uuid, sizeof(*out_uuid));
+
+    /* Set the four most significant bits (bits 12 through 15) of the
+     * time_hi_and_version field to the 4-bit version number from
+     * Section 4.1.3. */
+    out_uuid->uuid[6] = (out_uuid->uuid[6] & 0x0Fu) | 0x40u;
+
+    /* Set the two most significant bits (bits 6 and 7) of the
+     * clock_seq_hi_and_reserved to zero and one, respectively. */
+    out_uuid->uuid[8] = (out_uuid->uuid[8] & 0x3Fu) | 0x80u;
+
+    return out_uuid;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_uuid_is_normalized_full(const char *str)
+{
+    /* The only reason why this exists is that nm_uuid_is_normalized() is an inline function.
+     * If you need to forward declare the function, that won't work.
+     *
+     * Usually, you wouldn't use this variant! */
+    return nm_uuid_is_normalized(str);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_uuid_is_valid_nmlegacy()
+ * @str: the string to check whether it's a valid UUID.
+ *
+ * Note that this does not perform a strict check.
+ * Instead, it checks a more relaxed format (including
+ * non valid UUID strings). This is for backward compatibility,
+ * where older code did not perform a strict check.
+ *
+ * Returns: %TRUE, if the string is a valid legacy format.
+ *   This may not be a valid UUID!
+ */
+gboolean
+nm_uuid_is_valid_nmlegacy(const char *str)
+{
+    const char *p          = str;
+    int         num_dashes = 0;
+
+    if (!p)
+        return FALSE;
+
+    while (*p) {
+        if (*p == '-')
+            num_dashes++;
+        else if (!g_ascii_isxdigit(*p))
+            return FALSE;
+        p++;
+    }
+
+    /* While we accept here bogus strings as UUIDs, they must contain only
+     * hexdigits and '-', and they must be eithr 36 or 40 chars long. */
+
+    if ((num_dashes == 4) && (p - str == 36))
+        return TRUE;
+
+    /* Backwards compat for older configurations */
+    if ((num_dashes == 0) && (p - str == 40))
+        return TRUE;
+
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_uuid_is_valid_nm(const char *str,
+                    gboolean *  out_normalized,
+                    char *      out_normalized_str /* [static 37] */)
+{
+    NMUuid   uuid;
+    gboolean is_normalized;
+
+    /* @out_normalized_str is only set, if normalization was necessary
+     * and possible. The caller cannot request @out_normalized_str, without
+     * also getting @out_normalized. */
+    nm_assert(!out_normalized_str || out_normalized);
+
+    if (!str)
+        return FALSE;
+
+    if (nm_uuid_parse_full(str, &uuid, &is_normalized)) {
+        /* Note that:
+         *   @is_normalized means that "str" contains a normalized UUID
+         *   @out_normalized: indicates whether str requires normalization
+         *     and whether @out_normalized_str was set to contain the normalized
+         *     UUID.
+         * With this, we get the slightly odd assignment: */
+        NM_SET_OUT(out_normalized, !is_normalized);
+
+        if (!is_normalized && out_normalized_str) {
+            /* we need to normalize the UUID */
+            nm_uuid_unparse(&uuid, out_normalized_str);
+        }
+
+        /* regardless whether normalization was necessary, the UUID is
+         * essentially valid. */
+        return TRUE;
+    }
+
+    if (nm_uuid_is_valid_nmlegacy(str)) {
+        /* This is not a valid UUID, but something that we used to
+         * accept according to nm_uuid_is_valid_nmlegacy().
+         *
+         * Normalize it by sha1 hashing the string. Upper case characters
+         * are made lower case first. */
+        NM_SET_OUT(out_normalized, TRUE);
+        if (out_normalized_str) {
+            char str_lower[40 + 1];
+            int  i;
+
+            nm_assert(strlen(str) < G_N_ELEMENTS(str_lower));
+
+            /* normalize first to lower-case. */
+            g_strlcpy(str_lower, str, sizeof(str_lower));
+            for (i = 0; str_lower[i]; i++)
+                str_lower[i] = g_ascii_tolower(str_lower[i]);
+
+            /* The namespace UUID is chosen randomly. */
+            nm_uuid_generate_from_string(&uuid,
+                                         str_lower,
+                                         -1,
+                                         NM_UUID_TYPE_VERSION5,
+                                         "4e72f709-ca95-4405-9053-1f43294a618c");
+            nm_uuid_unparse(&uuid, out_normalized_str);
+        }
+        return TRUE;
+    }
+
+    /* UUID is not valid. */
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_uuid_is_null(const NMUuid *uuid)
+{
+    int i;
+
+    if (!uuid)
+        return TRUE;
+
+    for (i = 0; i < (int) G_N_ELEMENTS(uuid->uuid); i++) {
+        if (uuid->uuid[i])
+            return FALSE;
+    }
+    return TRUE;
+}
+
+char *
+nm_uuid_generate_random_str(char buf[static 37])
+{
+    NMUuid uuid;
+
+    nm_assert(buf);
+
+    nm_uuid_generate_random(&uuid);
+    return nm_uuid_unparse(&uuid, buf);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_uuid_generate_from_string:
+ * @uuid: the UUID to update inplace. This function cannot
+ *   fail to succeed.
+ * @s: a string to use as the seed for the UUID
+ * @slen: if negative, treat @s as zero terminated C string.
+ *   Otherwise, assume the length as given (and allow @s to be
+ *   non-null terminated or contain '\0').
+ * @uuid_type: a type identifier which UUID format to generate.
+ * @type_args: additional arguments, depending on the uuid_type
+ *
+ * For a given @s, this function will always return the same UUID.
+ *
+ * Returns: the input @uuid. This function cannot fail.
+ **/
+NMUuid *
+nm_uuid_generate_from_string(NMUuid *    uuid,
+                             const char *s,
+                             gssize      slen,
+                             NMUuidType  uuid_type,
+                             gpointer    type_args)
+{
+    g_return_val_if_fail(uuid, FALSE);
+    g_return_val_if_fail(slen == 0 || s, FALSE);
+
+    if (slen < 0)
+        slen = s ? strlen(s) : 0;
+
+    switch (uuid_type) {
+    case NM_UUID_TYPE_LEGACY:
+        g_return_val_if_fail(!type_args, NULL);
+        nm_crypto_md5_hash(NULL, 0, (guint8 *) s, slen, (guint8 *) uuid, sizeof(*uuid));
+        break;
+    case NM_UUID_TYPE_VERSION3:
+    case NM_UUID_TYPE_VERSION5:
+    {
+        NMUuid ns_uuid;
+
+        if (type_args) {
+            /* type_args can be a name space UUID. Interpret it as (char *) */
+            if (!nm_uuid_parse(type_args, &ns_uuid))
+                g_return_val_if_reached(NULL);
+        } else
+            ns_uuid = (NMUuid){};
+
+        if (uuid_type == NM_UUID_TYPE_VERSION3) {
+            nm_crypto_md5_hash((guint8 *) s,
+                               slen,
+                               (guint8 *) &ns_uuid,
+                               sizeof(ns_uuid),
+                               (guint8 *) uuid,
+                               sizeof(*uuid));
+        } else {
+            nm_auto_free_checksum GChecksum *sum = NULL;
+            union {
+                guint8 sha1[NM_UTILS_CHECKSUM_LENGTH_SHA1];
+                NMUuid uuid;
+            } digest;
+
+            sum = g_checksum_new(G_CHECKSUM_SHA1);
+            g_checksum_update(sum, (guchar *) &ns_uuid, sizeof(ns_uuid));
+            g_checksum_update(sum, (guchar *) s, slen);
+            nm_utils_checksum_get_digest(sum, digest.sha1);
+
+            G_STATIC_ASSERT_EXPR(sizeof(digest.sha1) > sizeof(digest.uuid));
+            *uuid = digest.uuid;
+        }
+
+        uuid->uuid[6] = (uuid->uuid[6] & 0x0F) | (uuid_type << 4);
+        uuid->uuid[8] = (uuid->uuid[8] & 0x3F) | 0x80;
+        break;
+    }
+    default:
+        g_return_val_if_reached(NULL);
+    }
+
+    return uuid;
+}
+
+/**
+ * nm_uuid_generate_from_string_str:
+ * @s: a string to use as the seed for the UUID
+ * @slen: if negative, treat @s as zero terminated C string.
+ *   Otherwise, assume the length as given (and allow @s to be
+ *   non-null terminated or contain '\0').
+ * @uuid_type: a type identifier which UUID format to generate.
+ * @type_args: additional arguments, depending on the uuid_type
+ *
+ * For a given @s, this function will always return the same UUID.
+ *
+ * Returns: a newly allocated UUID suitable for use as the #NMSettingConnection
+ * object's #NMSettingConnection:id: property
+ **/
+char *
+nm_uuid_generate_from_string_str(const char *s,
+                                 gssize      slen,
+                                 NMUuidType  uuid_type,
+                                 gpointer    type_args)
+{
+    NMUuid uuid;
+
+    nm_uuid_generate_from_string(&uuid, s, slen, uuid_type, type_args);
+    return nm_uuid_unparse(&uuid, g_new(char, 37));
+}
+
+/**
+ * nm_uuid_generate_from_strings:
+ * @string1: a variadic list of strings. Must be NULL terminated.
+ *
+ * Returns a variant3 UUID based on the concatenated C strings.
+ * It does not simply concatenate them, but also includes the
+ * terminating '\0' character. For example "a", "b", gives
+ * "a\0b\0".
+ *
+ * This has the advantage, that the following invocations
+ * all give different UUIDs: (NULL), (""), ("",""), ("","a"), ("a",""),
+ * ("aa"), ("aa", ""), ("", "aa"), ...
+ */
+char *
+nm_uuid_generate_from_strings(const char *string1, ...)
+{
+    if (!string1)
+        return nm_uuid_generate_from_string_str(NULL, 0, NM_UUID_TYPE_VERSION3, NM_UUID_NS1);
+
+    {
+        nm_auto_str_buf NMStrBuf str = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_104, FALSE);
+        va_list                  args;
+        const char *             s;
+
+        nm_str_buf_append_len(&str, string1, strlen(string1) + 1u);
+
+        va_start(args, string1);
+        s = va_arg(args, const char *);
+        while (s) {
+            nm_str_buf_append_len(&str, s, strlen(s) + 1u);
+            s = va_arg(args, const char *);
+        }
+        va_end(args);
+
+        return nm_uuid_generate_from_string_str(nm_str_buf_get_str_unsafe(&str),
+                                                str.len,
+                                                NM_UUID_TYPE_VERSION3,
+                                                NM_UUID_NS1);
+    }
+}
diff --git a/src/libnm-glib-aux/nm-uuid.h b/src/libnm-glib-aux/nm-uuid.h
new file mode 100644
index 00000000..10302bd8
--- /dev/null
+++ b/src/libnm-glib-aux/nm-uuid.h
@@ -0,0 +1,100 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NM_UUID_H__
+#define __NM_UUID_H__
+
+typedef struct _NMUuid {
+    guint8 uuid[16];
+} NMUuid;
+
+char *nm_uuid_unparse_case(const NMUuid *uuid, char out_str[static 37], gboolean upper_case);
+
+static inline char *
+nm_uuid_unparse(const NMUuid *uuid, char out_str[static 37])
+{
+    return nm_uuid_unparse_case(uuid, out_str, FALSE);
+}
+
+gboolean nm_uuid_parse_full(const char *str, NMUuid *out_uuid, gboolean *out_is_normalized);
+
+static inline NMUuid *
+nm_uuid_parse(const char *str, NMUuid *out_uuid)
+{
+    nm_assert(out_uuid);
+
+    if (!nm_uuid_parse_full(str, out_uuid, NULL))
+        return NULL;
+    return out_uuid;
+}
+
+NMUuid *nm_uuid_generate_random(NMUuid *out_uuid);
+
+gboolean nm_uuid_is_null(const NMUuid *uuid);
+
+/*****************************************************************************/
+
+static inline gboolean
+nm_uuid_is_valid(const char *str)
+{
+    return str && nm_uuid_parse_full(str, NULL, NULL);
+}
+
+gboolean nm_uuid_is_normalized_full(const char *str);
+
+static inline gboolean
+nm_uuid_is_normalized(const char *str)
+{
+    gboolean is_normalized;
+
+    return str && nm_uuid_parse_full(str, NULL, &is_normalized) && is_normalized;
+}
+
+/*****************************************************************************/
+
+gboolean nm_uuid_is_valid_nmlegacy(const char *str);
+
+gboolean nm_uuid_is_valid_nm(const char *str,
+                             gboolean *  out_normalized,
+                             char *      out_normalized_str /* [static 37] */);
+
+/*****************************************************************************/
+
+char *nm_uuid_generate_random_str(char buf[static 37]);
+
+#define nm_uuid_generate_random_str_arr(buf)                                    \
+    ({                                                                          \
+        G_STATIC_ASSERT(sizeof(buf) == G_N_ELEMENTS(buf) && sizeof(buf) >= 37); \
+        nm_uuid_generate_random_str(buf);                                       \
+    })
+
+#define nm_uuid_generate_random_str_a() (nm_uuid_generate_random_str(g_alloca(37)))
+
+#define nm_uuid_generate_random_str_malloc() (nm_uuid_generate_random_str(g_new(char, 37)))
+
+/*****************************************************************************/
+
+typedef enum {
+    NM_UUID_TYPE_LEGACY   = 0,
+    NM_UUID_TYPE_VERSION3 = 3,
+    NM_UUID_TYPE_VERSION5 = 5,
+} NMUuidType;
+
+NMUuid *nm_uuid_generate_from_string(NMUuid *    uuid,
+                                     const char *s,
+                                     gssize      slen,
+                                     NMUuidType  uuid_type,
+                                     gpointer    type_args);
+
+char *nm_uuid_generate_from_string_str(const char *s,
+                                       gssize      slen,
+                                       NMUuidType  uuid_type,
+                                       gpointer    type_args);
+
+/* arbitrarily chosen namespace UUID for nm_uuid_generate_from_strings() */
+#define NM_UUID_NS1 "b425e9fb-7598-44b4-9e3b-5a2e3aaa4905"
+
+char *nm_uuid_generate_from_strings(const char *string1, ...) G_GNUC_NULL_TERMINATED;
+
+/*****************************************************************************/
+
+#endif /* __NM_UUID_H__ */
diff --git a/src/libnm-glib-aux/nm-value-type.h b/src/libnm-glib-aux/nm-value-type.h
new file mode 100644
index 00000000..f1cce480
--- /dev/null
+++ b/src/libnm-glib-aux/nm-value-type.h
@@ -0,0 +1,239 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019 Red Hat, Inc.
+ */
+
+#ifndef __NM_VALUE_TYPE_H__
+#define __NM_VALUE_TYPE_H__
+
+typedef enum _nm_packed {
+    NM_VALUE_TYPE_UNSPEC = 1,
+    NM_VALUE_TYPE_BOOL   = 2,
+    NM_VALUE_TYPE_INT32  = 3,
+    NM_VALUE_TYPE_INT    = 4,
+    NM_VALUE_TYPE_INT64  = 5,
+    NM_VALUE_TYPE_UINT64 = 6,
+    NM_VALUE_TYPE_STRING = 7,
+} NMValueType;
+
+/*****************************************************************************/
+
+#ifdef NM_VALUE_TYPE_DEFINE_FUNCTIONS
+
+typedef union {
+    bool        v_bool;
+    gint32      v_int32;
+    gint64      v_int64;
+    guint64     v_uint64;
+    int         v_int;
+    const char *v_string;
+
+    /* for convenience, also let the union contain other pointer types. These are
+     * for NM_VALUE_TYPE_UNSPEC. */
+    gconstpointer *  v_ptr;
+    const GPtrArray *v_ptrarray;
+
+} NMValueTypUnion;
+
+    /* Set the NMValueTypUnion. You can also assign the member directly.
+ * The only purpose of this is that it also returns a pointer to the
+ * union. So, you can do
+ *
+ *   ptr = NM_VALUE_TYP_UNION_SET (&value_typ_union_storage, v_bool, TRUE);
+ */
+    #define NM_VALUE_TYP_UNION_SET(_arg, _type, _val) \
+        ({                                            \
+            NMValueTypUnion *const _arg2 = (_arg);    \
+                                                      \
+            *_arg2 = (NMValueTypUnion){               \
+                ._type = (_val),                      \
+            };                                        \
+            _arg2;                                    \
+        })
+
+typedef struct {
+    bool            has;
+    NMValueTypUnion val;
+} NMValueTypUnioMaybe;
+
+    #define NM_VALUE_TYP_UNIO_MAYBE_SET(_arg, _type, _val) \
+        ({                                                 \
+            NMValueTypUnioMaybe *const _arg2 = (_arg);     \
+                                                           \
+            *_arg2 = (NMValueTypUnioMaybe){                \
+                .has       = TRUE,                         \
+                .val._type = (_val),                       \
+            };                                             \
+            _arg2;                                         \
+        })
+
+/*****************************************************************************/
+
+static inline int
+nm_value_type_cmp(NMValueType value_type, gconstpointer p_a, gconstpointer p_b)
+{
+    switch (value_type) {
+    case NM_VALUE_TYPE_BOOL:
+        NM_CMP_DIRECT(*((const bool *) p_a), *((const bool *) p_b));
+        return 0;
+    case NM_VALUE_TYPE_INT32:
+        NM_CMP_DIRECT(*((const gint32 *) p_a), *((const gint32 *) p_b));
+        return 0;
+    case NM_VALUE_TYPE_INT:
+        NM_CMP_DIRECT(*((const int *) p_a), *((const int *) p_b));
+        return 0;
+    case NM_VALUE_TYPE_INT64:
+        NM_CMP_DIRECT(*((const gint64 *) p_a), *((const gint64 *) p_b));
+        return 0;
+    case NM_VALUE_TYPE_UINT64:
+        NM_CMP_DIRECT(*((const guint64 *) p_a), *((const guint64 *) p_b));
+        return 0;
+    case NM_VALUE_TYPE_STRING:
+        return nm_strcmp0(*((const char *const *) p_a), *((const char *const *) p_b));
+    case NM_VALUE_TYPE_UNSPEC:
+        break;
+    }
+    nm_assert_not_reached();
+    return 0;
+}
+
+static inline gboolean
+nm_value_type_equal(NMValueType value_type, gconstpointer p_a, gconstpointer p_b)
+{
+    return nm_value_type_cmp(value_type, p_a, p_b) == 0;
+}
+
+static inline void
+nm_value_type_copy(NMValueType value_type, gpointer dst, gconstpointer src)
+{
+    switch (value_type) {
+    case NM_VALUE_TYPE_BOOL:
+        (*((bool *) dst) = *((const bool *) src));
+        return;
+    case NM_VALUE_TYPE_INT32:
+        (*((gint32 *) dst) = *((const gint32 *) src));
+        return;
+    case NM_VALUE_TYPE_INT:
+        (*((int *) dst) = *((const int *) src));
+        return;
+    case NM_VALUE_TYPE_INT64:
+        (*((gint64 *) dst) = *((const gint64 *) src));
+        return;
+    case NM_VALUE_TYPE_UINT64:
+        (*((guint64 *) dst) = *((const guint64 *) src));
+        return;
+    case NM_VALUE_TYPE_STRING:
+        /* self assignment safe! */
+        if (*((char **) dst) != *((const char *const *) src)) {
+            g_free(*((char **) dst));
+            *((char **) dst) = g_strdup(*((const char *const *) src));
+        }
+        return;
+    case NM_VALUE_TYPE_UNSPEC:
+        break;
+    }
+    nm_assert_not_reached();
+}
+
+static inline void
+nm_value_type_get_from_variant(NMValueType value_type,
+                               gpointer    dst,
+                               GVariant *  variant,
+                               gboolean    clone)
+{
+    switch (value_type) {
+    case NM_VALUE_TYPE_BOOL:
+        *((bool *) dst) = g_variant_get_boolean(variant);
+        return;
+    case NM_VALUE_TYPE_INT32:
+        *((gint32 *) dst) = g_variant_get_int32(variant);
+        return;
+    case NM_VALUE_TYPE_INT64:
+        *((gint64 *) dst) = g_variant_get_int64(variant);
+        return;
+    case NM_VALUE_TYPE_UINT64:
+        *((guint64 *) dst) = g_variant_get_uint64(variant);
+        return;
+    case NM_VALUE_TYPE_STRING:
+        if (clone) {
+            g_free(*((char **) dst));
+            *((char **) dst) = g_variant_dup_string(variant, NULL);
+        } else {
+            /* we don't clone the string, nor free the previous value. */
+            *((const char **) dst) = g_variant_get_string(variant, NULL);
+        }
+        return;
+
+    case NM_VALUE_TYPE_INT:
+        /* "int" also does not have a define variant type, because it's not
+         * clear how many bits we would need. */
+
+        /* fall-through */
+    case NM_VALUE_TYPE_UNSPEC:
+        break;
+    }
+    nm_assert_not_reached();
+}
+
+static inline GVariant *
+nm_value_type_to_variant(NMValueType value_type, gconstpointer src)
+{
+    const char *v_string;
+
+    switch (value_type) {
+    case NM_VALUE_TYPE_BOOL:
+        return g_variant_new_boolean(*((const bool *) src));
+    case NM_VALUE_TYPE_INT32:
+        return g_variant_new_int32(*((const gint32 *) src));
+    case NM_VALUE_TYPE_INT64:
+        return g_variant_new_int64(*((const gint64 *) src));
+    case NM_VALUE_TYPE_UINT64:
+        return g_variant_new_uint64(*((const guint64 *) src));
+    case NM_VALUE_TYPE_STRING:
+        v_string = *((const char *const *) src);
+        return v_string ? g_variant_new_string(v_string) : NULL;
+
+    case NM_VALUE_TYPE_INT:
+        /* "int" also does not have a define variant type, because it's not
+         * clear how many bits we would need. */
+
+        /* fall-through */
+    case NM_VALUE_TYPE_UNSPEC:
+        break;
+    }
+    nm_assert_not_reached();
+    return NULL;
+}
+
+static inline const GVariantType *
+nm_value_type_get_variant_type(NMValueType value_type)
+{
+    switch (value_type) {
+    case NM_VALUE_TYPE_BOOL:
+        return G_VARIANT_TYPE_BOOLEAN;
+    case NM_VALUE_TYPE_INT32:
+        return G_VARIANT_TYPE_INT32;
+    case NM_VALUE_TYPE_INT64:
+        return G_VARIANT_TYPE_INT64;
+    case NM_VALUE_TYPE_UINT64:
+        return G_VARIANT_TYPE_UINT64;
+    case NM_VALUE_TYPE_STRING:
+        return G_VARIANT_TYPE_STRING;
+
+    case NM_VALUE_TYPE_INT:
+        /* "int" also does not have a define variant type, because it's not
+         * clear how many bits we would need. */
+
+        /* fall-through */
+    case NM_VALUE_TYPE_UNSPEC:
+        break;
+    }
+    nm_assert_not_reached();
+    return NULL;
+}
+
+    /*****************************************************************************/
+
+#endif /* NM_VALUE_TYPE_DEFINE_FUNCTIONS */
+
+#endif /* __NM_VALUE_TYPE_H__ */
diff --git a/src/libnm-glib-aux/tests/meson.build b/src/libnm-glib-aux/tests/meson.build
new file mode 100644
index 00000000..0ec4ad30
--- /dev/null
+++ b/src/libnm-glib-aux/tests/meson.build
@@ -0,0 +1,55 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+exe = executable(
+  'test-shared-general',
+  'test-shared-general.c',
+  include_directories: [
+    src_inc,
+    top_inc,
+  ],
+  dependencies: [
+    glib_dep,
+  ],
+  link_with: [
+    libnm_log_null,
+    libnm_glib_aux,
+    libnm_std_aux,
+    libc_siphash,
+  ],
+)
+
+test(
+  'src/libnm-glib-aux/tests/test-shared-general',
+  test_script,
+  args: test_args + [exe.full_path()],
+  timeout: default_test_timeout,
+)
+
+if jansson_dep.found()
+  exe = executable(
+    'test-json-aux',
+    'test-json-aux.c',
+    dependencies: [
+      glib_dep,
+      jansson_dep,
+      dl_dep,
+    ],
+    include_directories: [
+      src_inc,
+      top_inc,
+    ],
+    link_with: [
+      libnm_log_null,
+      libnm_glib_aux,
+      libnm_std_aux,
+      libc_siphash,
+    ],
+  )
+
+  test(
+    'src/libnm-glib-aux/tests/test-json-aux',
+    test_script,
+    args: test_args + [exe.full_path()],
+    timeout: default_test_timeout,
+  )
+endif
diff --git a/src/libnm-glib-aux/tests/test-json-aux.c b/src/libnm-glib-aux/tests/test-json-aux.c
new file mode 100644
index 00000000..5f33f4fe
--- /dev/null
+++ b/src/libnm-glib-aux/tests/test-json-aux.c
@@ -0,0 +1,165 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-prog.h"
+
+#include <jansson.h>
+
+#include "libnm-glib-aux/nm-json-aux.h"
+
+#include "libnm-glib-aux/nm-test-utils.h"
+
+/*****************************************************************************/
+
+static void
+test_jansson(void)
+{
+    const NMJsonVt *    vt;
+    nm_auto_decref_json nm_json_t *js1 = NULL;
+    nm_auto_decref_json nm_json_t *js2 = NULL;
+
+#define _ASSERT_FIELD(type1, type2, field)                                                        \
+    G_STMT_START                                                                                  \
+    {                                                                                             \
+        G_STATIC_ASSERT_EXPR(sizeof(((type1 *) NULL)->field) == sizeof(((type2 *) NULL)->field)); \
+        G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(type1, field) == G_STRUCT_OFFSET(type2, field));     \
+    }                                                                                             \
+    G_STMT_END
+
+    G_STATIC_ASSERT_EXPR(NM_JSON_REJECT_DUPLICATES == JSON_REJECT_DUPLICATES);
+
+    G_STATIC_ASSERT_EXPR(sizeof(nm_json_type) == sizeof(json_type));
+
+    G_STATIC_ASSERT_EXPR((int) NM_JSON_OBJECT == JSON_OBJECT);
+    G_STATIC_ASSERT_EXPR((int) NM_JSON_ARRAY == JSON_ARRAY);
+    G_STATIC_ASSERT_EXPR((int) NM_JSON_STRING == JSON_STRING);
+    G_STATIC_ASSERT_EXPR((int) NM_JSON_INTEGER == JSON_INTEGER);
+    G_STATIC_ASSERT_EXPR((int) NM_JSON_REAL == JSON_REAL);
+    G_STATIC_ASSERT_EXPR((int) NM_JSON_TRUE == JSON_TRUE);
+    G_STATIC_ASSERT_EXPR((int) NM_JSON_FALSE == JSON_FALSE);
+    G_STATIC_ASSERT_EXPR((int) NM_JSON_NULL == JSON_NULL);
+
+    G_STATIC_ASSERT_EXPR(sizeof(nm_json_int_t) == sizeof(json_int_t));
+
+    G_STATIC_ASSERT_EXPR(sizeof(nm_json_t) == sizeof(json_t));
+    _ASSERT_FIELD(nm_json_t, json_t, refcount);
+    _ASSERT_FIELD(nm_json_t, json_t, type);
+
+    G_STATIC_ASSERT_EXPR(NM_JSON_ERROR_TEXT_LENGTH == JSON_ERROR_TEXT_LENGTH);
+    G_STATIC_ASSERT_EXPR(NM_JSON_ERROR_SOURCE_LENGTH == JSON_ERROR_SOURCE_LENGTH);
+
+    G_STATIC_ASSERT_EXPR(sizeof(nm_json_error_t) == sizeof(json_error_t));
+    _ASSERT_FIELD(nm_json_error_t, json_error_t, line);
+    _ASSERT_FIELD(nm_json_error_t, json_error_t, column);
+    _ASSERT_FIELD(nm_json_error_t, json_error_t, position);
+    _ASSERT_FIELD(nm_json_error_t, json_error_t, source);
+    _ASSERT_FIELD(nm_json_error_t, json_error_t, text);
+
+    vt = nm_json_vt();
+
+    g_assert(vt);
+    g_assert(vt->loaded);
+
+    js1 = vt->nm_json_loads("{ \"a\": 5 }", 0, NULL);
+    g_assert(js1);
+    nm_json_decref(vt, g_steal_pointer(&js1));
+
+    js2 = vt->nm_json_loads("{ \"a\": 6 }", 0, NULL);
+    g_assert(js2);
+
+#define CHECK_FCN(vt, fcn, nm_type, js_type)             \
+    G_STMT_START                                         \
+    {                                                    \
+        const NMJsonVt *const _vt     = (vt);            \
+        _nm_unused            nm_type = (_vt->nm_##fcn); \
+        _nm_unused            js_type = (fcn);           \
+                                                         \
+        g_assert(_vt->nm_##fcn);                         \
+        g_assert(_f_nm);                                 \
+        g_assert(_f_js);                                 \
+        g_assert(_f_nm == _vt->nm_##fcn);                \
+    }                                                    \
+    G_STMT_END
+
+    CHECK_FCN(vt, json_array, nm_json_t * (*_f_nm)(void), json_t * (*_f_js)(void) );
+    CHECK_FCN(vt,
+              json_array_append_new,
+              int (*_f_nm)(nm_json_t *, nm_json_t *),
+              int (*_f_js)(json_t *, json_t *));
+    CHECK_FCN(vt,
+              json_array_get,
+              nm_json_t * (*_f_nm)(const nm_json_t *, gsize),
+              json_t * (*_f_js)(const json_t *, size_t));
+    CHECK_FCN(vt,
+              json_array_size,
+              gsize(*_f_nm)(const nm_json_t *),
+              size_t(*_f_js)(const json_t *));
+    CHECK_FCN(vt, json_delete, void (*_f_nm)(nm_json_t *), void (*_f_js)(json_t *));
+    CHECK_FCN(vt,
+              json_dumps,
+              char *(*_f_nm)(const nm_json_t *, gsize),
+              char *(*_f_js)(const json_t *, size_t));
+    CHECK_FCN(vt, json_false, nm_json_t * (*_f_nm)(void), json_t * (*_f_js)(void) );
+    CHECK_FCN(vt, json_integer, nm_json_t * (*_f_nm)(nm_json_int_t), json_t * (*_f_js)(json_int_t));
+    CHECK_FCN(vt,
+              json_integer_value,
+              nm_json_int_t(*_f_nm)(const nm_json_t *),
+              json_int_t(*_f_js)(const json_t *));
+    CHECK_FCN(vt,
+              json_loads,
+              nm_json_t * (*_f_nm)(const char *, gsize, nm_json_error_t *),
+              json_t * (*_f_js)(const char *, size_t, json_error_t *) );
+    CHECK_FCN(vt, json_object, nm_json_t * (*_f_nm)(void), json_t * (*_f_js)(void) );
+    CHECK_FCN(vt,
+              json_object_del,
+              int (*_f_nm)(nm_json_t *, const char *),
+              int (*_f_js)(json_t *, const char *));
+    CHECK_FCN(vt,
+              json_object_get,
+              nm_json_t * (*_f_nm)(const nm_json_t *, const char *),
+              json_t * (*_f_js)(const json_t *, const char *) );
+    CHECK_FCN(vt, json_object_iter, void *(*_f_nm)(nm_json_t *), void *(*_f_js)(json_t *) );
+    CHECK_FCN(vt,
+              json_object_iter_key,
+              const char *(*_f_nm)(void *),
+              const char *(*_f_js)(void *) );
+    CHECK_FCN(vt,
+              json_object_iter_next,
+              void *(*_f_nm)(nm_json_t *, void *),
+              void *(*_f_js)(json_t *, void *) );
+    CHECK_FCN(vt, json_object_iter_value, nm_json_t * (*_f_nm)(void *), json_t * (*_f_js)(void *) );
+    CHECK_FCN(vt,
+              json_object_key_to_iter,
+              void *(*_f_nm)(const char *),
+              void *(*_f_js)(const char *) );
+    CHECK_FCN(vt,
+              json_object_set_new,
+              int (*_f_nm)(nm_json_t *, const char *, nm_json_t *),
+              int (*_f_js)(json_t *, const char *, json_t *));
+    CHECK_FCN(vt,
+              json_object_size,
+              gsize(*_f_nm)(const nm_json_t *),
+              size_t(*_f_js)(const json_t *));
+    CHECK_FCN(vt,
+              json_string,
+              nm_json_t * (*_f_nm)(const char *),
+              json_t * (*_f_js)(const char *) );
+    CHECK_FCN(vt,
+              json_string_value,
+              const char *(*_f_nm)(const nm_json_t *),
+              const char *(*_f_js)(const json_t *) );
+    CHECK_FCN(vt, json_true, nm_json_t * (*_f_nm)(void), json_t * (*_f_js)(void) );
+}
+
+/*****************************************************************************/
+
+NMTST_DEFINE();
+
+int
+main(int argc, char **argv)
+{
+    nmtst_init(&argc, &argv, TRUE);
+
+    g_test_add_func("/general/test_jansson", test_jansson);
+
+    return g_test_run();
+}
diff --git a/src/libnm-glib-aux/tests/test-shared-general.c b/src/libnm-glib-aux/tests/test-shared-general.c
new file mode 100644
index 00000000..4a7b6790
--- /dev/null
+++ b/src/libnm-glib-aux/tests/test-shared-general.c
@@ -0,0 +1,1425 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-prog.h"
+
+#include "libnm-std-aux/unaligned.h"
+#include "libnm-glib-aux/nm-random-utils.h"
+#include "libnm-glib-aux/nm-str-buf.h"
+#include "libnm-glib-aux/nm-time-utils.h"
+#include "libnm-glib-aux/nm-ref-string.h"
+
+#include "libnm-glib-aux/nm-test-utils.h"
+
+/*****************************************************************************/
+
+G_STATIC_ASSERT(NM_AF_UNSPEC == AF_UNSPEC);
+G_STATIC_ASSERT(NM_AF_INET == AF_INET);
+G_STATIC_ASSERT(NM_AF_INET6 == AF_INET6);
+
+G_STATIC_ASSERT(NM_AF_INET_SIZE == sizeof(in_addr_t));
+G_STATIC_ASSERT(NM_AF_INET_SIZE == sizeof(struct in_addr));
+G_STATIC_ASSERT(NM_AF_INET6_SIZE == sizeof(struct in6_addr));
+
+G_STATIC_ASSERT(4 == _nm_alignof(in_addr_t));
+G_STATIC_ASSERT(4 == _nm_alignof(struct in_addr));
+G_STATIC_ASSERT(4 == _nm_alignof(struct in6_addr));
+G_STATIC_ASSERT(4 == _nm_alignof(NMIPAddr));
+
+/*****************************************************************************/
+
+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_nm_utils_ip4_prefix_to_netmask(void)
+{
+    g_assert_cmpint(_nm_utils_ip4_prefix_to_netmask(0), ==, nmtst_inet4_from_string("0.0.0.0"));
+    g_assert_cmpint(_nm_utils_ip4_prefix_to_netmask(1), ==, nmtst_inet4_from_string("128.0.0.0"));
+    g_assert_cmpint(_nm_utils_ip4_prefix_to_netmask(2), ==, nmtst_inet4_from_string("192.0.0.0"));
+    g_assert_cmpint(_nm_utils_ip4_prefix_to_netmask(16),
+                    ==,
+                    nmtst_inet4_from_string("255.255.0.0"));
+    g_assert_cmpint(_nm_utils_ip4_prefix_to_netmask(24),
+                    ==,
+                    nmtst_inet4_from_string("255.255.255.0"));
+    g_assert_cmpint(_nm_utils_ip4_prefix_to_netmask(30),
+                    ==,
+                    nmtst_inet4_from_string("255.255.255.252"));
+    g_assert_cmpint(_nm_utils_ip4_prefix_to_netmask(31),
+                    ==,
+                    nmtst_inet4_from_string("255.255.255.254"));
+    g_assert_cmpint(_nm_utils_ip4_prefix_to_netmask(32),
+                    ==,
+                    nmtst_inet4_from_string("255.255.255.255"));
+    g_assert_cmpint(_nm_utils_ip4_prefix_to_netmask(33),
+                    ==,
+                    nmtst_inet4_from_string("255.255.255.255"));
+}
+
+/*****************************************************************************/
+
+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 < 500; 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();
+        gboolean hexdigit_pairs_mangled;
+        gsize    expected_strlen;
+        char *   str_hex;
+        gsize    required_len;
+        gboolean outlen_set;
+        gsize    outlen;
+        guint8 * bin2;
+        guint    i, j;
+
+        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'))));
+
+        hexdigit_pairs_mangled = FALSE;
+        if (delimiter && len > 1 && nmtst_get_rand_bool()) {
+            /* randomly convert "0?" sequences to single digits, so we can get hexdigit_pairs_required
+             * parameter. */
+            g_assert(strlen(str_hex) >= 5);
+            g_assert(str_hex[2] == delimiter);
+            i = 0;
+            j = 0;
+            for (;;) {
+                g_assert(g_ascii_isxdigit(str_hex[i]));
+                g_assert(g_ascii_isxdigit(str_hex[i + 1]));
+                g_assert(NM_IN_SET(str_hex[i + 2], delimiter, '\0'));
+                if (str_hex[i] == '0' && nmtst_get_rand_bool()) {
+                    i++;
+                    str_hex[j++]           = str_hex[i++];
+                    hexdigit_pairs_mangled = TRUE;
+                } else {
+                    str_hex[j++] = str_hex[i++];
+                    str_hex[j++] = str_hex[i++];
+                }
+                if (str_hex[i] == '\0') {
+                    str_hex[j] = '\0';
+                    break;
+                }
+                g_assert(str_hex[i] == delimiter);
+                str_hex[j++] = str_hex[i++];
+            }
+        }
+
+        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(),
+                                        !hexdigit_pairs_mangled && 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 (hexdigit_pairs_mangled) {
+            /* we mangled the hexstr to contain single digits. Trying to parse with
+             * hexdigit_pairs_required must now fail. */
+            bin2 = nm_utils_hexstr2bin_full(
+                str_hex,
+                nmtst_get_rand_bool(),
+                delimiter != '\0' && nmtst_get_rand_bool(),
+                TRUE,
+                delimiter != '\0' ? nmtst_rand_select((const char *) ":", ":-")
+                                  : nmtst_rand_select((const char *) ":", ":-", "", NULL),
+                required_len,
+                buf2,
+                len,
+                outlen_set ? &outlen : NULL);
+            g_assert(!bin2);
+        }
+
+        if (allocate)
+            g_free(str_hex);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+test_nm_ref_string(void)
+{
+    nm_auto_ref_string NMRefString *s1 = NULL;
+    NMRefString *                   s2;
+
+    g_assert(NULL == NM_REF_STRING_UPCAST(NULL));
+
+    s1 = nm_ref_string_new("hallo");
+    g_assert(s1);
+    g_assert_cmpstr(s1->str, ==, "hallo");
+    g_assert_cmpint(s1->len, ==, strlen("hallo"));
+    g_assert(s1 == NM_REF_STRING_UPCAST(s1->str));
+
+    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_32,
+         NM_UTILS_GET_NEXT_REALLOC_SIZE_32,
+         NM_UTILS_GET_NEXT_REALLOC_SIZE_32},
+        {NM_UTILS_GET_NEXT_REALLOC_SIZE_40,
+         NM_UTILS_GET_NEXT_REALLOC_SIZE_40,
+         NM_UTILS_GET_NEXT_REALLOC_SIZE_40},
+        {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_232,
+         NM_UTILS_GET_NEXT_REALLOC_SIZE_232,
+         NM_UTILS_GET_NEXT_REALLOC_SIZE_232},
+        {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_32 == 32u);
+    G_STATIC_ASSERT_EXPR(NM_UTILS_GET_NEXT_REALLOC_SIZE_40 == 40u);
+    G_STATIC_ASSERT_EXPR(NM_UTILS_GET_NEXT_REALLOC_SIZE_104 == 104u);
+    G_STATIC_ASSERT_EXPR(NM_UTILS_GET_NEXT_REALLOC_SIZE_232 == 232u);
+    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;
+            gsize       reserved_true;
+            gsize       reserved_false;
+            bool        truncated_true  = FALSE;
+            bool        truncated_false = FALSE;
+
+            if (sizeof(gsize) > 4 && requested > SIZE_MAX / 2u - 24u) {
+                reserved_false  = G_MAXSSIZE;
+                truncated_false = TRUE;
+            } else
+                reserved_false = nm_utils_get_next_realloc_size(FALSE, requested);
+
+            if (sizeof(gsize) > 4 && requested > SIZE_MAX - 0x1000u - 24u) {
+                reserved_true  = G_MAXSSIZE;
+                truncated_true = TRUE;
+            } else
+                reserved_true = nm_utils_get_next_realloc_size(TRUE, requested);
+
+            g_assert_cmpuint(reserved_true, >, 0);
+            g_assert_cmpuint(reserved_false, >, 0);
+            if (!truncated_true)
+                g_assert_cmpuint(reserved_true, >=, requested);
+            if (!truncated_false)
+                g_assert_cmpuint(reserved_false, >=, requested);
+            if (!truncated_true && !truncated_false)
+                g_assert_cmpuint(reserved_false, >=, reserved_true);
+
+            if (i < G_N_ELEMENTS(test_data)) {
+                if (!truncated_true)
+                    g_assert_cmpuint(reserved_true, ==, test_data[i].reserved_true);
+                if (!truncated_false)
+                    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 if (!reserved_false) {
+                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 if (!truncated_true) {
+                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;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+test_nm_utils_parse_next_line(void)
+{
+    const char *data;
+    const char *data0;
+    gsize       data_len;
+    const char *line_start;
+    gsize       line_len;
+    int         i_run;
+    gsize       j, k;
+
+    data     = NULL;
+    data_len = 0;
+    g_assert(!nm_utils_parse_next_line(&data, &data_len, &line_start, &line_len));
+
+    for (i_run = 0; i_run < 1000; i_run++) {
+        gs_unref_ptrarray GPtrArray *strv     = g_ptr_array_new_with_free_func(g_free);
+        gs_unref_ptrarray GPtrArray *strv2    = g_ptr_array_new_with_free_func(g_free);
+        gsize                        strv_len = nmtst_get_rand_word_length(NULL);
+        nm_auto_str_buf NMStrBuf     strbuf   = NM_STR_BUF_INIT(0, nmtst_get_rand_bool());
+
+        /* create a list of random words. */
+        for (j = 0; j < strv_len; j++) {
+            gsize    w_len = nmtst_get_rand_word_length(NULL);
+            NMStrBuf w_buf =
+                NM_STR_BUF_INIT(nmtst_get_rand_uint32() % (w_len + 1), nmtst_get_rand_bool());
+
+            for (k = 0; k < w_len; k++)
+                nm_str_buf_append_c(&w_buf, '0' + (k % 10));
+            nm_str_buf_maybe_expand(&w_buf, 1, TRUE);
+            g_ptr_array_add(strv, nm_str_buf_finalize(&w_buf, NULL));
+        }
+
+        /* join the list of random words with (random) line delimiters
+         * ("\0", "\n", "\r" or EOF). */
+        for (j = 0; j < strv_len; j++) {
+            nm_str_buf_append(&strbuf, strv->pdata[j]);
+again:
+            switch (nmtst_get_rand_uint32() % 5) {
+            case 0:
+                nm_str_buf_append_c(&strbuf, '\0');
+                break;
+            case 1:
+                if (strbuf.len > 0
+                    && (nm_str_buf_get_str_unsafe(&strbuf))[strbuf.len - 1] == '\r') {
+                    /* the previous line was empty and terminated by "\r". We
+                     * must not join with "\n". Retry. */
+                    goto again;
+                }
+                nm_str_buf_append_c(&strbuf, '\n');
+                break;
+            case 2:
+                nm_str_buf_append_c(&strbuf, '\r');
+                break;
+            case 3:
+                nm_str_buf_append(&strbuf, "\r\n");
+                break;
+            case 4:
+                /* the last word randomly is delimited or not, but not if the last
+                 * word is "". */
+                if (j + 1 < strv_len) {
+                    /* it's not the last word. Retry. */
+                    goto again;
+                }
+                g_assert(j == strv_len - 1);
+                if (((const char *) strv->pdata[j])[0] == '\0') {
+                    /* if the last word was "", we need a delimiter (to parse it back).
+                     * Retry. */
+                    goto again;
+                }
+                /* The final delimiter gets omitted. It's EOF. */
+                break;
+            }
+        }
+
+        data0 = nm_str_buf_get_str_unsafe(&strbuf);
+        if (!data0 && nmtst_get_rand_bool()) {
+            nm_str_buf_maybe_expand(&strbuf, 1, TRUE);
+            data0 = nm_str_buf_get_str_unsafe(&strbuf);
+            g_assert(data0);
+        }
+        data_len = strbuf.len;
+        g_assert((data_len > 0 && data0) || data_len == 0);
+        data = data0;
+        while (nm_utils_parse_next_line(&data, &data_len, &line_start, &line_len)) {
+            g_assert(line_start);
+            g_assert(line_start >= data0);
+            g_assert(line_start < &data0[strbuf.len]);
+            g_assert(!memchr(line_start, '\0', line_len));
+            g_ptr_array_add(strv2, g_strndup(line_start, line_len));
+        }
+        g_assert(data_len == 0);
+        if (data0)
+            g_assert(data == &data0[strbuf.len]);
+        else
+            g_assert(!data);
+
+        g_assert(nm_utils_strv_cmp_n((const char *const *) strv->pdata,
+                                     strv->len,
+                                     (const char *const *) strv2->pdata,
+                                     strv2->len)
+                 == 0);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+test_in_strset_ascii_case(void)
+{
+    const char *x;
+
+    x = NULL;
+    g_assert(NM_IN_STRSET_ASCII_CASE(x, NULL));
+    g_assert(NM_IN_STRSET_ASCII_CASE(x, NULL, "b"));
+    g_assert(!NM_IN_STRSET_ASCII_CASE(x, "b"));
+
+    x = "b";
+    g_assert(NM_IN_STRSET(x, "b"));
+    g_assert(NM_IN_STRSET_ASCII_CASE(x, "b"));
+    g_assert(!NM_IN_STRSET(x, "B"));
+    g_assert(NM_IN_STRSET_ASCII_CASE(x, "B"));
+}
+
+/*****************************************************************************/
+
+static void
+test_is_specific_hostname(void)
+{
+    g_assert(!nm_utils_is_specific_hostname(NULL));
+    g_assert(!nm_utils_is_specific_hostname(""));
+    g_assert(!nm_utils_is_specific_hostname("(none)"));
+    g_assert(nm_utils_is_specific_hostname("(NONE)"));
+
+    g_assert(!nm_utils_is_specific_hostname("localhost"));
+    g_assert(!nm_utils_is_specific_hostname("lOcalHost"));
+    g_assert(!nm_utils_is_specific_hostname("LOCALHOST"));
+
+    g_assert(!nm_utils_is_specific_hostname("LOCALHOST.localdomain"));
+
+    g_assert(nm_utils_is_specific_hostname("xlocalhost"));
+    g_assert(nm_utils_is_specific_hostname("lOcalHxost"));
+    g_assert(nm_utils_is_specific_hostname("LOCALxHOST"));
+
+    g_assert(!nm_utils_is_specific_hostname("foo.LOCALHOST"));
+    g_assert(!nm_utils_is_specific_hostname("foo.LOCALHOsT6."));
+    g_assert(!nm_utils_is_specific_hostname("foo.LOCALHOsT6.localdomain6"));
+    g_assert(!nm_utils_is_specific_hostname(".LOCALHOsT6.localdomain6"));
+    g_assert(!nm_utils_is_specific_hostname("LOCALHOsT6.localdomain6"));
+    g_assert(!nm_utils_is_specific_hostname("LOCALHOsT6.localdomain6."));
+    g_assert(nm_utils_is_specific_hostname("LOCALHOsT6.localdomain."));
+
+    g_assert(nm_utils_is_specific_hostname(" "));
+}
+
+/*****************************************************************************/
+
+static void
+test_strv_dup_packed(void)
+{
+    gs_unref_ptrarray GPtrArray *src = NULL;
+    int                          i_run;
+
+    src = g_ptr_array_new_with_free_func(g_free);
+
+    for (i_run = 0; i_run < 500; i_run++) {
+        const int            strv_len = nmtst_get_rand_word_length(NULL);
+        gs_free const char **strv_cpy = NULL;
+        const char *const *  strv_src;
+        int                  i, j;
+
+        g_ptr_array_set_size(src, 0);
+        for (i = 0; i < strv_len; i++) {
+            const int word_len = nmtst_get_rand_word_length(NULL);
+            NMStrBuf  sbuf     = NM_STR_BUF_INIT(0, nmtst_get_rand_bool());
+
+            for (j = 0; j < word_len; j++)
+                nm_str_buf_append_c(&sbuf, 'a' + (nmtst_get_rand_uint32() % 20));
+
+            g_ptr_array_add(src, nm_str_buf_finalize(&sbuf, NULL) ?: g_new0(char, 1));
+        }
+        g_ptr_array_add(src, NULL);
+
+        strv_src = (const char *const *) src->pdata;
+        g_assert(strv_src);
+        g_assert(NM_PTRARRAY_LEN(strv_src) == strv_len);
+
+        strv_cpy =
+            nm_utils_strv_dup_packed(strv_src,
+                                     nmtst_get_rand_bool() ? (gssize) strv_len : (gssize) -1);
+        if (strv_len == 0)
+            g_assert(!strv_cpy);
+        else
+            g_assert(strv_cpy);
+        g_assert(NM_PTRARRAY_LEN(strv_cpy) == strv_len);
+        if (strv_cpy)
+            g_assert(nm_utils_strv_equal(strv_cpy, strv_src));
+    }
+}
+
+/*****************************************************************************/
+
+static int
+_hash_func_cmp_direct(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+    NM_CMP_DIRECT(GPOINTER_TO_INT(a), GPOINTER_TO_INT(b));
+    return 0;
+}
+
+static void
+test_utils_hashtable_cmp(void)
+{
+    static struct {
+        int         val_i;
+        const char *val_s;
+    } vals[] = {
+        {
+            0,
+            "0",
+        },
+        {
+            1,
+            "1",
+        },
+        {
+            2,
+            "2",
+        },
+        {
+            3,
+            "3",
+        },
+        {
+            4,
+            "4",
+        },
+        {
+            5,
+            "5",
+        },
+        {
+            6,
+            "6",
+        },
+        {
+            7,
+            "7",
+        },
+        {
+            8,
+            "8",
+        },
+        {
+            9,
+            "9",
+        },
+        {
+            0,
+            "a",
+        },
+        {
+            1,
+            "a",
+        },
+        {
+            2,
+            "a",
+        },
+        {
+            3,
+            "a",
+        },
+        {
+            4,
+            "a",
+        },
+        {
+            5,
+            "a",
+        },
+        {
+            0,
+            "0",
+        },
+        {
+            0,
+            "1",
+        },
+        {
+            0,
+            "2",
+        },
+        {
+            0,
+            "3",
+        },
+        {
+            0,
+            "4",
+        },
+        {
+            0,
+            "5",
+        },
+    };
+    guint test_run;
+    int   is_num_key;
+
+    for (test_run = 0; test_run < 30; test_run++) {
+        for (is_num_key = 0; is_num_key < 2; is_num_key++) {
+            GHashFunc        func_key_hash  = is_num_key ? nm_direct_hash : nm_str_hash;
+            GEqualFunc       func_key_equal = is_num_key ? g_direct_equal : g_str_equal;
+            GCompareDataFunc func_key_cmp =
+                is_num_key ? _hash_func_cmp_direct : (GCompareDataFunc) nm_strcmp_with_data;
+            GCompareDataFunc func_val_cmp =
+                !is_num_key ? _hash_func_cmp_direct : (GCompareDataFunc) nm_strcmp_with_data;
+            gs_unref_hashtable GHashTable *h1 = NULL;
+            gs_unref_hashtable GHashTable *h2 = NULL;
+            gboolean                       has_same_keys;
+            guint                          i, n;
+
+            h1 = g_hash_table_new(func_key_hash, func_key_equal);
+            h2 = g_hash_table_new(func_key_hash, func_key_equal);
+
+            n = nmtst_get_rand_word_length(NULL);
+            for (i = 0; i < n; i++) {
+                typeof(vals[0]) *v     = &vals[nmtst_get_rand_uint32() % G_N_ELEMENTS(vals)];
+                gconstpointer    v_key = is_num_key ? GINT_TO_POINTER(v->val_i) : v->val_s;
+                gconstpointer    v_val = !is_num_key ? GINT_TO_POINTER(v->val_i) : v->val_s;
+
+                g_hash_table_insert(h1, (gpointer) v_key, (gpointer) v_val);
+                g_hash_table_insert(h2, (gpointer) v_key, (gpointer) v_val);
+            }
+
+            g_assert(nm_utils_hashtable_same_keys(h1, h2));
+            g_assert(nm_utils_hashtable_cmp_equal(h1, h2, NULL, NULL));
+            g_assert(nm_utils_hashtable_cmp_equal(h1, h2, func_val_cmp, NULL));
+            g_assert(nm_utils_hashtable_cmp(h1, h2, FALSE, func_key_cmp, NULL, NULL) == 0);
+            g_assert(nm_utils_hashtable_cmp(h1, h2, TRUE, func_key_cmp, NULL, NULL) == 0);
+            g_assert(nm_utils_hashtable_cmp(h1, h2, FALSE, func_key_cmp, func_val_cmp, NULL) == 0);
+            g_assert(nm_utils_hashtable_cmp(h1, h2, TRUE, func_key_cmp, func_val_cmp, NULL) == 0);
+
+            n             = nmtst_get_rand_word_length(NULL) + 1;
+            has_same_keys = TRUE;
+            for (i = 0; i < n; i++) {
+again:
+{
+    typeof(vals[0]) *v     = &vals[nmtst_get_rand_uint32() % G_N_ELEMENTS(vals)];
+    gconstpointer    v_key = is_num_key ? GINT_TO_POINTER(v->val_i) : v->val_s;
+    gconstpointer    v_val = !is_num_key ? GINT_TO_POINTER(v->val_i) : v->val_s;
+    gpointer         v_key2;
+    gpointer         v_val2;
+
+    if (g_hash_table_lookup_extended(h1, v_key, &v_key2, &v_val2)) {
+        g_assert(func_key_cmp(v_key, v_key2, NULL) == 0);
+        if (func_val_cmp(v_val, v_val2, NULL) == 0)
+            goto again;
+    } else
+        has_same_keys = FALSE;
+
+    g_hash_table_insert(h2, (gpointer) v_key, (gpointer) v_val);
+}
+            }
+
+            if (has_same_keys) {
+                g_assert(nm_utils_hashtable_same_keys(h1, h2));
+                g_assert(nm_utils_hashtable_cmp_equal(h1, h2, NULL, NULL));
+                g_assert(nm_utils_hashtable_cmp(h1, h2, FALSE, func_key_cmp, NULL, NULL) == 0);
+                g_assert(nm_utils_hashtable_cmp(h1, h2, TRUE, func_key_cmp, NULL, NULL) == 0);
+            } else {
+                g_assert(!nm_utils_hashtable_same_keys(h1, h2));
+                g_assert(!nm_utils_hashtable_cmp_equal(h1, h2, NULL, NULL));
+                g_assert(nm_utils_hashtable_cmp(h1, h2, FALSE, func_key_cmp, NULL, NULL) != 0);
+                g_assert(nm_utils_hashtable_cmp(h1, h2, TRUE, func_key_cmp, NULL, NULL) != 0);
+            }
+            g_assert(!nm_utils_hashtable_cmp_equal(h1, h2, func_val_cmp, NULL));
+            g_assert(nm_utils_hashtable_cmp(h1, h2, FALSE, func_key_cmp, func_val_cmp, NULL) != 0);
+            g_assert(nm_utils_hashtable_cmp(h1, h2, TRUE, func_key_cmp, func_val_cmp, NULL) != 0);
+        }
+    }
+}
+
+/*****************************************************************************/
+
+static void
+test_nm_g_source_sentinel(void)
+{
+    GSource *s1;
+    GSource *s2;
+    int      n;
+    int      i;
+    int      refs;
+
+    s1 = nm_g_source_sentinel_get(0);
+    g_assert_nonnull(s1);
+    g_assert_cmpint(g_atomic_int_get(&s1->ref_count), ==, 1);
+
+    s2 = nm_g_source_sentinel_get(0);
+    g_assert_nonnull(s2);
+    g_assert(s2 == s1);
+    g_assert_cmpint(g_atomic_int_get(&s1->ref_count), ==, 1);
+
+    n = nmtst_get_rand_uint32() % 7;
+    for (refs = 0, i = 0; i < n; i++) {
+        if (nmtst_get_rand_bool()) {
+            refs++;
+            g_source_ref(s1);
+        }
+        if (nmtst_get_rand_bool())
+            g_source_destroy(s1);
+        if (refs > 0 && nmtst_get_rand_bool()) {
+            refs--;
+            g_source_unref(s1);
+        }
+
+        if (nmtst_get_rand_bool()) {
+            s2 = nm_g_source_sentinel_get(0);
+            g_assert_cmpint(g_atomic_int_get(&s2->ref_count), >=, 1);
+            g_assert(s2 == s1);
+        }
+    }
+
+    for (; refs > 0;) {
+        if (nmtst_get_rand_bool())
+            g_source_destroy(s1);
+        if (nmtst_get_rand_bool()) {
+            refs--;
+            g_source_unref(s1);
+        }
+    }
+
+    g_assert_cmpint(g_atomic_int_get(&s1->ref_count), ==, 1);
+}
+
+/*****************************************************************************/
+
+static void
+test_nm_ascii(void)
+{
+    int i;
+
+    for (i = 0; i < 256; i++) {
+        const char ch = i;
+        gboolean   is_space;
+
+        if (ch == 127) {
+            g_assert(nm_ascii_is_ctrl_or_del(ch));
+            g_assert(!nm_ascii_is_ctrl(ch));
+        } else
+            g_assert(nm_ascii_is_ctrl_or_del(ch) == nm_ascii_is_ctrl(ch));
+        g_assert(nm_ascii_is_ctrl_or_del(ch) == g_ascii_iscntrl(ch));
+
+        g_assert(nm_ascii_is_non_ascii(ch) == (i >= 128));
+
+        g_assert(!nm_ascii_is_ctrl_or_del(ch) || !nm_ascii_is_non_ascii(ch));
+
+        g_assert((nm_ascii_is_ctrl_or_del(ch) || nm_ascii_is_regular(ch))
+                 != nm_ascii_is_non_ascii(ch));
+
+        g_assert(nm_ascii_is_regular(ch)
+                 == (!nm_ascii_is_ctrl_or_del(ch) && !nm_ascii_is_non_ascii(ch)));
+
+        is_space = g_ascii_isspace(ch);
+        if (NM_IN_SET(ch, '\t', '\n', '\f', '\r')) {
+            /* hack is-space, so that the check below works to check for regular ASCII characters. */
+            g_assert(!nm_ascii_is_regular(ch));
+            g_assert(is_space);
+            is_space = FALSE;
+        }
+        g_assert(nm_ascii_is_regular(ch)
+                 == (g_ascii_isalnum(ch) || g_ascii_isalpha(ch) || g_ascii_isdigit(ch)
+                     || g_ascii_isgraph(ch) || g_ascii_islower(ch) || g_ascii_isprint(ch)
+                     || g_ascii_ispunct(ch) || is_space || g_ascii_isupper(ch)
+                     || g_ascii_isxdigit(ch)));
+    }
+}
+
+/*****************************************************************************/
+
+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_nm_utils_ip4_prefix_to_netmask",
+                    test_nm_utils_ip4_prefix_to_netmask);
+    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);
+    g_test_add_func("/general/test_nm_utils_parse_next_line", test_nm_utils_parse_next_line);
+    g_test_add_func("/general/test_in_strset_ascii_case", test_in_strset_ascii_case);
+    g_test_add_func("/general/test_is_specific_hostname", test_is_specific_hostname);
+    g_test_add_func("/general/test_strv_dup_packed", test_strv_dup_packed);
+    g_test_add_func("/general/test_utils_hashtable_cmp", test_utils_hashtable_cmp);
+    g_test_add_func("/general/test_nm_g_source_sentinel", test_nm_g_source_sentinel);
+    g_test_add_func("/general/test_nm_ascii", test_nm_ascii);
+
+    return g_test_run();
+}