summary refs log tree commit diff
path: root/src/libnm-glib-aux
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2023-02-10 11:50:34 +0100
committerMichael Biebl <biebl@debian.org>2023-02-10 11:50:34 +0100
commit1372848511cb896b80b51ed1a3e9606bd9816631 (patch)
tree674792b9385bdef935988894b45f06b2af39f88c /src/libnm-glib-aux
parent40ec077ea305994c1fc2130add6787ca0c73e2c6 (diff)
New upstream version 1.42.0 upstream/1.42.0
Diffstat (limited to 'src/libnm-glib-aux')
-rw-r--r--src/libnm-glib-aux/meson.build2
-rw-r--r--src/libnm-glib-aux/nm-dbus-aux.c232
-rw-r--r--src/libnm-glib-aux/nm-dbus-aux.h46
-rw-r--r--src/libnm-glib-aux/nm-dedup-multi.c15
-rw-r--r--src/libnm-glib-aux/nm-dedup-multi.h20
-rw-r--r--src/libnm-glib-aux/nm-default-glib.h1
-rw-r--r--src/libnm-glib-aux/nm-glib.h52
-rw-r--r--src/libnm-glib-aux/nm-hash-utils.c2
-rw-r--r--src/libnm-glib-aux/nm-hash-utils.h57
-rw-r--r--src/libnm-glib-aux/nm-inet-utils.c529
-rw-r--r--src/libnm-glib-aux/nm-inet-utils.h389
-rw-r--r--src/libnm-glib-aux/nm-io-utils.c52
-rw-r--r--src/libnm-glib-aux/nm-io-utils.h45
-rw-r--r--src/libnm-glib-aux/nm-macros-internal.h236
-rw-r--r--src/libnm-glib-aux/nm-prioq.c323
-rw-r--r--src/libnm-glib-aux/nm-prioq.h71
-rw-r--r--src/libnm-glib-aux/nm-ref-string.c3
-rw-r--r--src/libnm-glib-aux/nm-ref-string.h19
-rw-r--r--src/libnm-glib-aux/nm-secret-utils.c16
-rw-r--r--src/libnm-glib-aux/nm-shared-utils.c675
-rw-r--r--src/libnm-glib-aux/nm-shared-utils.h692
-rw-r--r--src/libnm-glib-aux/nm-str-buf.h23
-rw-r--r--src/libnm-glib-aux/nm-test-utils.h256
-rw-r--r--src/libnm-glib-aux/nm-time-utils.c52
-rw-r--r--src/libnm-glib-aux/nm-time-utils.h36
-rw-r--r--src/libnm-glib-aux/nm-uuid.c92
-rw-r--r--src/libnm-glib-aux/nm-uuid.h21
-rw-r--r--src/libnm-glib-aux/tests/test-shared-general.c207
28 files changed, 2815 insertions, 1349 deletions
diff --git a/src/libnm-glib-aux/meson.build b/src/libnm-glib-aux/meson.build
index 4f13fd0b..afd86871 100644
--- a/src/libnm-glib-aux/meson.build
+++ b/src/libnm-glib-aux/meson.build
@@ -8,10 +8,12 @@ libnm_glib_aux = static_library(
     'nm-enum-utils.c',
     'nm-errno.c',
     'nm-hash-utils.c',
+    'nm-inet-utils.c',
     'nm-io-utils.c',
     'nm-json-aux.c',
     'nm-keyfile-aux.c',
     'nm-logging-base.c',
+    'nm-prioq.c',
     'nm-random-utils.c',
     'nm-ref-string.c',
     'nm-secret-utils.c',
diff --git a/src/libnm-glib-aux/nm-dbus-aux.c b/src/libnm-glib-aux/nm-dbus-aux.c
index 0f43ba8e..3925da55 100644
--- a/src/libnm-glib-aux/nm-dbus-aux.c
+++ b/src/libnm-glib-aux/nm-dbus-aux.c
@@ -1,6 +1,6 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 /*
- * Copyright (C) 2019 Red Hat, Inc.
+ * Copyright (C) 2015,2019 Red Hat, Inc.
  */
 
 #include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
@@ -97,6 +97,33 @@ nm_dbus_connection_call_get_all(GDBusConnection              *dbus_connection,
 }
 
 void
+nm_dbus_connection_call_get(GDBusConnection              *dbus_connection,
+                            const char                   *bus_name,
+                            const char                   *object_path,
+                            const char                   *interface_name,
+                            const char                   *property_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,
+                           "Get",
+                           g_variant_new("(s)", interface_name, property_name),
+                           G_VARIANT_TYPE("(v)"),
+                           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,
@@ -387,6 +414,12 @@ _nm_dbus_error_is(GError *error, ...)
     gs_free char *dbus_error = NULL;
     const char   *name;
     va_list       ap;
+    gboolean      found = FALSE;
+
+    /* This should only be used for "foreign" D-Bus errors (eg, errors
+     * from BlueZ or wpa_supplicant). All NetworkManager D-Bus errors
+     * should be properly mapped by gdbus to one of the domains/codes in
+     * nm-errors.h. */
 
     dbus_error = g_dbus_error_get_remote_error(error);
     if (!dbus_error)
@@ -395,13 +428,13 @@ _nm_dbus_error_is(GError *error, ...)
     va_start(ap, error);
     while ((name = va_arg(ap, const char *))) {
         if (nm_streq(dbus_error, name)) {
-            va_end(ap);
-            return TRUE;
+            found = TRUE;
+            break;
         }
     }
     va_end(ap);
 
-    return FALSE;
+    return found;
 }
 
 /*****************************************************************************/
@@ -490,3 +523,194 @@ nm_dbus_connection_call_blocking(NMDBusConnectionCallBlockingData *data, GError
 
     return g_steal_pointer(&result);
 }
+
+/*****************************************************************************/
+
+typedef struct {
+    char               *signal_name;
+    const GVariantType *signature;
+} NMDBusSignalData;
+
+static void
+dbus_signal_data_free(gpointer data, GClosure *closure)
+{
+    NMDBusSignalData *sd = data;
+
+    g_free(sd->signal_name);
+    g_slice_free(NMDBusSignalData, sd);
+}
+
+static void
+dbus_signal_meta_marshal(GClosure     *closure,
+                         GValue       *return_value,
+                         guint         n_param_values,
+                         const GValue *param_values,
+                         gpointer      invocation_hint,
+                         gpointer      marshal_data)
+{
+    NMDBusSignalData *sd = marshal_data;
+    const char       *signal_name;
+    GVariant         *parameters;
+    gs_free GValue   *closure_params_free = NULL;
+    GValue           *closure_params;
+    gsize             n_params;
+    gsize             i;
+
+    g_return_if_fail(n_param_values == 4);
+
+    signal_name = g_value_get_string(&param_values[2]);
+    parameters  = g_value_get_variant(&param_values[3]);
+
+    if (!nm_streq(signal_name, sd->signal_name))
+        return;
+
+    if (sd->signature) {
+        if (!g_variant_is_of_type(parameters, sd->signature)) {
+            g_warning("%p: got signal '%s' but parameters were of type '%s', not '%s'",
+                      g_value_get_object(&param_values[0]),
+                      signal_name,
+                      g_variant_get_type_string(parameters),
+                      g_variant_type_peek_string(sd->signature));
+            return;
+        }
+
+        n_params = g_variant_n_children(parameters) + 1;
+    } else
+        n_params = 1;
+
+    closure_params = nm_malloc0_maybe_a(240, sizeof(GValue) * n_params, &closure_params_free);
+    g_value_init(&closure_params[0], G_TYPE_OBJECT);
+    g_value_copy(&param_values[0], &closure_params[0]);
+
+    for (i = 1; i < n_params; i++) {
+        gs_unref_variant GVariant *param = NULL;
+
+        param = g_variant_get_child_value(parameters, i - 1);
+        if (g_variant_is_of_type(param, G_VARIANT_TYPE("ay"))
+            || g_variant_is_of_type(param, G_VARIANT_TYPE("aay"))) {
+            /* g_dbus_gvariant_to_gvalue() thinks 'ay' means "non-UTF-8 NUL-terminated string" */
+            g_value_init(&closure_params[i], G_TYPE_VARIANT);
+            g_value_set_variant(&closure_params[i], param);
+        } else
+            g_dbus_gvariant_to_gvalue(param, &closure_params[i]);
+    }
+
+    g_cclosure_marshal_generic(closure, NULL, n_params, closure_params, invocation_hint, NULL);
+
+    for (i = 0; i < n_params; i++)
+        g_value_unset(&closure_params[i]);
+}
+
+/**
+ * _nm_dbus_proxy_signal_connect_data:
+ * @proxy: a #GDBusProxy
+ * @signal_name: the D-Bus signal to connect to
+ * @signature: (allow-none): the signal's type signature (must be a tuple)
+ * @c_handler: the signal handler function
+ * @data: (allow-none): data to pass to @c_handler
+ * @destroy_data: (allow-none): closure destroy notify for @data
+ * @connect_flags: connection flags
+ *
+ * Connects to the D-Bus signal @signal_name on @proxy. @c_handler must be a
+ * void function whose first argument is a #GDBusProxy, followed by arguments
+ * for each element of @signature, ending with a #gpointer argument for @data.
+ *
+ * The argument types in @c_handler correspond to the types output by
+ * g_dbus_gvariant_to_gvalue(), except for 'ay' and 'aay'. In particular:
+ * - both 16-bit and 32-bit integers are passed as #int/#guint
+ * - 'as' values are passed as #GStrv (char **)
+ * - all other array, tuple, and dict types are passed as #GVariant
+ *
+ * If @signature is %NULL, then the signal's parameters will be ignored, and
+ * @c_handler should take only the #GDBusProxy and #gpointer arguments.
+ *
+ * Returns: the signal handler ID, which can be used with
+ *   g_signal_handler_remove(). Beware that because of the way the signal is
+ *   connected, you will not be able to remove it with
+ *   g_signal_handlers_disconnect_by_func(), although
+ *   g_signal_handlers_disconnect_by_data() will work correctly.
+ */
+gulong
+_nm_dbus_proxy_signal_connect_data(GDBusProxy         *proxy,
+                                   const char         *signal_name,
+                                   const GVariantType *signature,
+                                   GCallback           c_handler,
+                                   gpointer            data,
+                                   GClosureNotify      destroy_data,
+                                   GConnectFlags       connect_flags)
+{
+    NMDBusSignalData *sd;
+    GClosure         *closure;
+    gboolean          swapped = !!(connect_flags & G_CONNECT_SWAPPED);
+    gboolean          after   = !!(connect_flags & G_CONNECT_AFTER);
+
+    g_return_val_if_fail(G_IS_DBUS_PROXY(proxy), 0);
+    g_return_val_if_fail(signal_name != NULL, 0);
+    g_return_val_if_fail(signature == NULL || g_variant_type_is_tuple(signature), 0);
+    g_return_val_if_fail(c_handler != NULL, 0);
+
+    sd              = g_slice_new(NMDBusSignalData);
+    sd->signal_name = g_strdup(signal_name);
+    sd->signature   = signature;
+
+    closure = (swapped ? g_cclosure_new_swap : g_cclosure_new)(c_handler, data, destroy_data);
+    g_closure_set_marshal(closure, g_cclosure_marshal_generic);
+    g_closure_set_meta_marshal(closure, sd, dbus_signal_meta_marshal);
+    g_closure_add_finalize_notifier(closure, sd, dbus_signal_data_free);
+
+    return g_signal_connect_closure(proxy, "g-signal", closure, after);
+}
+
+/*****************************************************************************/
+
+static gboolean
+_nm_dbus_typecheck_response(GVariant *response, const GVariantType *reply_type, GError **error)
+{
+    g_return_val_if_fail(response, FALSE);
+
+    if (!reply_type)
+        return TRUE;
+    if (g_variant_is_of_type(response, reply_type))
+        return TRUE;
+
+    /* This is the same error code that g_dbus_connection_call() returns if
+     * @reply_type doesn't match.
+     */
+    g_set_error(error,
+                G_IO_ERROR,
+                G_IO_ERROR_INVALID_ARGUMENT,
+                _("Method returned type '%s', but expected '%s'"),
+                g_variant_get_type_string(response),
+                g_variant_type_peek_string(reply_type));
+    return FALSE;
+}
+
+/**
+ * _nm_dbus_proxy_call_finish:
+ * @proxy: A #GDBusProxy.
+ * @res: A #GAsyncResult obtained from the #GAsyncReadyCallback passed to
+ *   g_dbus_proxy_call().
+ * @reply_type: (allow-none): the expected type of the reply, or %NULL
+ * @error: Return location for error or %NULL.
+ *
+ * Finishes an operation started with g_dbus_proxy_call(), as with
+ * g_dbus_proxy_call_finish(), except thatif @reply_type is non-%NULL, then it
+ * will also check that the response matches that type signature, and return
+ * an error if not.
+ *
+ * Returns: %NULL if @error is set. Otherwise, a #GVariant tuple with
+ * return values. Free with g_variant_unref().
+ */
+GVariant *
+_nm_dbus_proxy_call_finish(GDBusProxy         *proxy,
+                           GAsyncResult       *res,
+                           const GVariantType *reply_type,
+                           GError            **error)
+{
+    GVariant *variant;
+
+    variant = g_dbus_proxy_call_finish(proxy, res, error);
+    if (variant && !_nm_dbus_typecheck_response(variant, reply_type, error))
+        nm_clear_pointer(&variant, g_variant_unref);
+    return variant;
+}
diff --git a/src/libnm-glib-aux/nm-dbus-aux.h b/src/libnm-glib-aux/nm-dbus-aux.h
index dfba61b0..5db79c08 100644
--- a/src/libnm-glib-aux/nm-dbus-aux.h
+++ b/src/libnm-glib-aux/nm-dbus-aux.h
@@ -147,6 +147,16 @@ void nm_dbus_connection_call_get_all(GDBusConnection              *dbus_connecti
                                      NMDBusConnectionCallDefaultCb callback,
                                      gpointer                      user_data);
 
+void nm_dbus_connection_call_get(GDBusConnection              *dbus_connection,
+                                 const char                   *bus_name,
+                                 const char                   *object_path,
+                                 const char                   *interface_name,
+                                 const char                   *property_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,
@@ -264,4 +274,40 @@ nm_g_variant_tuple_get_u(GVariant *v, guint32 *out_u)
     return FALSE;
 }
 
+/*****************************************************************************/
+
+gulong _nm_dbus_proxy_signal_connect_data(GDBusProxy         *proxy,
+                                          const char         *signal_name,
+                                          const GVariantType *signature,
+                                          GCallback           c_handler,
+                                          gpointer            data,
+                                          GClosureNotify      destroy_data,
+                                          GConnectFlags       connect_flags);
+
+/**
+ * _nm_dbus_proxy_signal_connect:
+ * @proxy: a #GDBusProxy
+ * @signal_name: the D-Bus signal to connect to
+ * @signature: the signal's type signature (must be a tuple)
+ * @c_handler: the signal handler function
+ * @data: (allow-none): data to pass to @c_handler
+ *
+ * Simplified version of _nm_dbus_proxy_signal_connect_data() with fewer arguments.
+ *
+ * Returns: the signal handler ID, as with _nm_signal_connect_data().
+ */
+#define _nm_dbus_proxy_signal_connect(proxy, name, signature, handler, data) \
+    _nm_dbus_proxy_signal_connect_data(proxy,                                \
+                                       name,                                 \
+                                       signature,                            \
+                                       handler,                              \
+                                       data,                                 \
+                                       NULL,                                 \
+                                       (GConnectFlags) 0)
+
+GVariant *_nm_dbus_proxy_call_finish(GDBusProxy         *proxy,
+                                     GAsyncResult       *res,
+                                     const GVariantType *reply_type,
+                                     GError            **error);
+
 #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
index 7d451d0d..625c4ef3 100644
--- a/src/libnm-glib-aux/nm-dedup-multi.c
+++ b/src/libnm-glib-aux/nm-dedup-multi.c
@@ -1004,12 +1004,14 @@ 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);
+    self  = g_slice_new(NMDedupMultiIndex);
+    *self = (NMDedupMultiIndex){
+        .ref_count   = 1,
+        .idx_entries = g_hash_table_new((GHashFunc) _dict_idx_entries_hash,
+                                        (GEqualFunc) _dict_idx_entries_equal),
+        .idx_objs =
+            g_hash_table_new((GHashFunc) _dict_idx_objs_hash, (GEqualFunc) _dict_idx_objs_equal),
+    };
     return self;
 }
 
@@ -1018,6 +1020,7 @@ nm_dedup_multi_index_ref(NMDedupMultiIndex *self)
 {
     g_return_val_if_fail(self, NULL);
     g_return_val_if_fail(self->ref_count > 0, NULL);
+    nm_assert(self->ref_count < G_MAXINT32);
 
     self->ref_count++;
     return self;
diff --git a/src/libnm-glib-aux/nm-dedup-multi.h b/src/libnm-glib-aux/nm-dedup-multi.h
index 0fa47422..87a2b815 100644
--- a/src/libnm-glib-aux/nm-dedup-multi.h
+++ b/src/libnm-glib-aux/nm-dedup-multi.h
@@ -37,6 +37,8 @@ typedef enum _NMDedupMultiIdxMode {
 
 /*****************************************************************************/
 
+#define _NMDedupMultiObj_Align (MAX(_nm_alignof(void *), _nm_alignof(gint64)))
+
 struct _NMDedupMultiObj {
     union {
         NMObjBaseInst               parent;
@@ -44,7 +46,7 @@ struct _NMDedupMultiObj {
     };
     NMDedupMultiIndex *_multi_idx;
     guint              _ref_count;
-};
+} _nm_align(_NMDedupMultiObj_Align);
 
 struct _NMDedupMultiObjClass {
     NMObjBaseClass parent;
@@ -101,21 +103,21 @@ nm_dedup_multi_index_obj_find(NMDedupMultiIndex                          *self,
  * 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
+ * For example, NML3ConfigData can have idx-type to put there all IPv4 Routes.
+ * This idx-type instance is private to the NML3ConfigData instance. Basically,
+ * the NML3ConfigData 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.
+ * in multiple distinct lists. NML3ConfigData doesn't do that (because instead
+ * of creating one idx-type for IPv4 and IPv6 routes, it just creates
+ * distinct idx-types, one for each object type and 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.
+ * the NMDedupMultiIdxType can partition the object and assign it to 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.
diff --git a/src/libnm-glib-aux/nm-default-glib.h b/src/libnm-glib-aux/nm-default-glib.h
index f39e16cc..18460de8 100644
--- a/src/libnm-glib-aux/nm-default-glib.h
+++ b/src/libnm-glib-aux/nm-default-glib.h
@@ -67,6 +67,7 @@
 #include "libnm-glib-aux/nm-shared-utils.h"
 #include "libnm-glib-aux/nm-errno.h"
 #include "libnm-glib-aux/nm-hash-utils.h"
+#include "libnm-glib-aux/nm-inet-utils.h"
 
 /*****************************************************************************/
 
diff --git a/src/libnm-glib-aux/nm-glib.h b/src/libnm-glib-aux/nm-glib.h
index 2dc86d7b..0436d135 100644
--- a/src/libnm-glib-aux/nm-glib.h
+++ b/src/libnm-glib-aux/nm-glib.h
@@ -45,28 +45,40 @@ __g_type_ensure(GType type)
 
 /*****************************************************************************/
 
-#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);                                      \
-        }                                                      \
-    }                                                          \
+/* glib 2.58+ defines an improved, type-safe variant of g_clear_pointer(). Reimplement
+ * that.
+ *
+ * Note that we also have nm_clear_pointer() which is similar to g_clear_pointer()
+ * and also preferred throughout. However, we do use g_clear_object(), which is
+ * implemented via g_clear_pointer(). So while there are no immediate users of
+ * g_clear_pointer() (use nm_clear_pointer() instead), there are indirect users
+ * via g_clear_object().
+ *
+ * Anyway. So the glib 2.58+ version of g_clear_pointer() is only used if GLIB_VERSION_MAX_ALLOWED
+ * is set, which we don't do.
+ *
+ * To get the type-safe variant, reimplement g_clear_pointer() below. This aims to
+ * be identical to the version of the macro in glib 2.58.
+ *
+ * Still, don't use g_clear_pointer() directly (use nm_clear_pointer()). This compat
+ * implementation exists only for g_clear_object().
+ */
+#undef g_clear_pointer
+
+#define g_clear_pointer(pp, destroy)                       \
+    G_STMT_START                                           \
+    {                                                      \
+        typeof((pp)) _pp  = (pp);                          \
+        typeof(*_pp) _ptr = *_pp;                          \
+                                                           \
+        G_STATIC_ASSERT(sizeof *(pp) == sizeof(gpointer)); \
+                                                           \
+        *_pp = NULL;                                       \
+        if (_ptr)                                          \
+            (destroy)(_ptr);                               \
+    }                                                      \
     G_STMT_END
 
-#endif
-
 /*****************************************************************************/
 
 #if !GLIB_CHECK_VERSION(2, 34, 0)
diff --git a/src/libnm-glib-aux/nm-hash-utils.c b/src/libnm-glib-aux/nm-hash-utils.c
index 941aba01..973422a1 100644
--- a/src/libnm-glib-aux/nm-hash-utils.c
+++ b/src/libnm-glib-aux/nm-hash-utils.c
@@ -106,7 +106,7 @@ nm_hash_static(guint static_seed)
      *
      * Also, ensure that we don't return zero (like for nm_hash_complete()).
      */
-    return ((*((const guint *) _get_hash_key())) ^ static_seed) ?: 3679500967u;
+    return ((*NM_CAST_ALIGN(guint, _get_hash_key())) ^ static_seed) ?: 3679500967u;
 }
 
 void
diff --git a/src/libnm-glib-aux/nm-hash-utils.h b/src/libnm-glib-aux/nm-hash-utils.h
index dffa828b..e2707fc2 100644
--- a/src/libnm-glib-aux/nm-hash-utils.h
+++ b/src/libnm-glib-aux/nm-hash-utils.h
@@ -105,15 +105,30 @@ nm_hash_update(NMHashState *state, const void *ptr, gsize n)
     c_siphash_append(&state->_state, ptr, n);
 }
 
-#define nm_hash_update_val(state, val)                \
+#define _NM_HASH_COMBINE_VALS_TYPE_OP(x, idx, op_arg) typeof(x) _v##idx;
+#define _NM_HASH_COMBINE_VALS_INIT_OP(x, idx, op_arg) ._v##idx = (x),
+
+/* 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_VA_ARGS_FOREACH(, , , _NM_HASH_COMBINE_VALS_TYPE_OP, , __VA_ARGS__) \
+    } var _nm_alignas(max_align_t) = {                                         \
+        NM_VA_ARGS_FOREACH(, , , _NM_HASH_COMBINE_VALS_INIT_OP, , __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                                      \
     {                                                 \
-        typeof(val) _val = (val);                     \
+        NM_HASH_COMBINE_VALS(_val, __VA_ARGS__);      \
                                                       \
         nm_hash_update((state), &_val, sizeof(_val)); \
     }                                                 \
     G_STMT_END
 
+#define nm_hash_update_val(state, val) nm_hash_update_vals((state), (val))
+
 #define nm_hash_update_valp(state, val) nm_hash_update((state), (val), sizeof(*(val)))
 
 static inline void
@@ -132,28 +147,6 @@ nm_hash_update_bool(NMHashState *state, bool val)
 #define nm_hash_update_bools(state, ...) \
     nm_hash_update_val(state, NM_HASH_COMBINE_BOOLS(guint8, __VA_ARGS__))
 
-#define _NM_HASH_COMBINE_VALS_TYPE_OP(x, idx, op_arg) typeof(x) _v##idx;
-#define _NM_HASH_COMBINE_VALS_INIT_OP(x, idx, op_arg) ._v##idx = (x),
-
-/* 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_VA_ARGS_FOREACH(, , , _NM_HASH_COMBINE_VALS_TYPE_OP, , __VA_ARGS__) \
-    } var _nm_alignas(max_align_t) = {                                         \
-        NM_VA_ARGS_FOREACH(, , , _NM_HASH_COMBINE_VALS_INIT_OP, , __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)
 {
@@ -206,15 +199,17 @@ guint nm_hash_ptr(gconstpointer ptr);
 guint nm_hash_str(const char *str);
 #define nm_str_hash ((guint(*)(gconstpointer str)) nm_hash_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);            \
+#define nm_hash_vals(static_seed, ...)         \
+    ({                                         \
+        NMHashState _h;                        \
+                                               \
+        nm_hash_init(&_h, (static_seed));      \
+        nm_hash_update_vals(&_h, __VA_ARGS__); \
+        nm_hash_complete(&_h);                 \
     })
 
+#define nm_hash_val(static_seed, val) nm_hash_vals((static_seed), (val))
+
 static inline guint
 nm_hash_mem(guint static_seed, const void *ptr, gsize n)
 {
diff --git a/src/libnm-glib-aux/nm-inet-utils.c b/src/libnm-glib-aux/nm-inet-utils.c
new file mode 100644
index 00000000..7f710f35
--- /dev/null
+++ b/src/libnm-glib-aux/nm-inet-utils.c
@@ -0,0 +1,529 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-inet-utils.h"
+
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+/*****************************************************************************/
+
+const NMIPAddr nm_ip_addr_zero = {};
+
+/* We use _nm_alignas(NMIPAddr) to ensure that fields for in_addr_t and
+ * struct in6_addr have all the same alignment. Ensure that this is suitable. */
+G_STATIC_ASSERT(_nm_alignof(in_addr_t) <= _nm_alignof(NMIPAddr));
+G_STATIC_ASSERT(_nm_alignof(struct in_addr) <= _nm_alignof(NMIPAddr));
+G_STATIC_ASSERT(_nm_alignof(struct in6_addr) <= _nm_alignof(NMIPAddr));
+G_STATIC_ASSERT(_nm_alignof(NMEtherAddr) <= _nm_alignof(NMIPAddr));
+
+int
+nm_ip_addr_cmp_for_sort(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+    /* This is a compare function that can be used for sorting IP addresses.
+     * Essentially, it calls memcmp(). @user_data must be GINT_TO_POINTER(addr_family).
+     * @a and @b must be either pointers to in_addr_t, struct in6_addr or NMIPAddr. */
+    return nm_ip_addr_cmp(GPOINTER_TO_INT(user_data), a, b);
+}
+
+/* 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;
+}
+
+gboolean
+nm_ip_addr_set_from_variant(int addr_family, gpointer dst, GVariant *variant, int *out_addr_family)
+{
+    gconstpointer bytes;
+    gsize         len;
+
+    g_return_val_if_fail(dst, FALSE);
+    g_return_val_if_fail(variant, FALSE);
+
+    /* This function always expects IP addresses as byte arrays ("ay"). Note that
+     * several NetworkManager API uses "u" (32 bit unsigned intergers) for IPv4 addresses.
+     * So this function won't work in those cases.
+     *
+     * Btw, using "u" for IPv4 address messes badly with the endianness (host
+     * vs network byte order). Don't do that.
+     */
+    g_return_val_if_fail(g_variant_is_of_type(variant, G_VARIANT_TYPE("ay")), FALSE);
+
+    bytes = g_variant_get_fixed_array(variant, &len, sizeof(guint8));
+
+    return nm_ip_addr_set_from_untrusted(addr_family, dst, bytes, len, out_addr_family);
+}
+
+/*****************************************************************************/
+
+guint32
+nm_ip4_addr_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_ip4_addr_get_default_prefix(in_addr_t ip)
+{
+    return nm_ip4_addr_get_default_prefix0(ip) ?: 24;
+}
+
+gboolean
+nm_ip_addr_is_site_local(int addr_family, const void *address)
+{
+    NMIPAddr a;
+
+    nm_ip_addr_set(addr_family, &a, address);
+
+    switch (addr_family) {
+    case AF_INET:
+        /* RFC1918 private addresses
+         * 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 */
+        return (a.addr4 & htonl(0xff000000)) == htonl(0x0a000000)
+               || (a.addr4 & htonl(0xfff00000)) == htonl(0xac100000)
+               || (a.addr4 & htonl(0xffff0000)) == htonl(0xc0a80000);
+    case AF_INET6:
+        /* IN6_IS_ADDR_SITELOCAL() is for deprecated fec0::/10 addresses (see rfc3879, 4.).
+         * Note that for unique local IPv6 addresses (ULA, fc00::/7) this returns false.
+         * This may or may not be a bug of this function. */
+        return IN6_IS_ADDR_SITELOCAL(&a.addr6);
+    default:
+        g_return_val_if_reached(FALSE);
+    }
+}
+
+gboolean
+nm_ip6_addr_is_ula(const struct in6_addr *address)
+{
+    /* Unique local IPv6 address (ULA) fc00::/7 */
+    return (address->s6_addr32[0] & htonl(0xfe000000u)) == htonl(0xfc000000u);
+}
+
+/*****************************************************************************/
+
+gconstpointer
+nm_ip_addr_clear_host_address(int family, gpointer dst, gconstpointer src, guint32 plen)
+{
+    NMIPAddr a;
+    NMIPAddr a2;
+
+    g_return_val_if_fail(dst, NULL);
+
+    if (!src) {
+        /* allow "self-assignment", by specifying %NULL as source. */
+        src = dst;
+    }
+
+    nm_ip_addr_set(family, &a, src);
+
+    switch (family) {
+    case AF_INET:
+        g_return_val_if_fail(plen <= 32, NULL);
+
+        a2.addr4 = nm_ip4_addr_clear_host_address(a.addr4, plen);
+        break;
+    case AF_INET6:
+        nm_ip6_addr_clear_host_address(&a2.addr6, &a.addr6, plen);
+        break;
+    default:
+        g_return_val_if_reached(NULL);
+    }
+
+    nm_ip_addr_set(family, dst, &a2);
+
+    return dst;
+}
+
+/* nm_ip6_addr_clear_host_address:
+ * @dst: destination output buffer, will contain the network part of the @src address
+ * @src: source ip6 address. If NULL, this does an in-place update of @dst.
+ *   Also, @src and @dst are allowed to be the same pointers.
+ * @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_ip6_addr_clear_host_address(struct in6_addr *dst, const struct in6_addr *src, guint32 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_ip6_addr_same_prefix_cmp(const struct in6_addr *addr_a,
+                            const struct in6_addr *addr_b,
+                            guint32                plen)
+{
+    int    nbytes;
+    guint8 va, vb, m;
+
+    if (plen >= 128) {
+        nm_assert(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;
+}
+
+/*****************************************************************************/
+
+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_inet_parse_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) {
+        NM_PRAGMA_WARNING_DISABLE_DANGLING_POINTER
+        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_inet4_ntop(addrbin.addr4, buf),
+                    error->message);
+        }
+        nm_assert(addrbin.addr4 == a);
+        NM_PRAGMA_WARNING_REENABLE
+    }
+#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_inet_parse_str(int addr_family, const char *text, char **out_addr)
+{
+    NMIPAddr addrbin;
+
+    if (!nm_inet_parse_bin(addr_family, text, &addr_family, &addrbin))
+        return FALSE;
+
+    NM_SET_OUT(out_addr, nm_inet_ntop_dup(addr_family, &addrbin));
+    return TRUE;
+}
+
+gboolean
+nm_inet_parse_with_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_inet_parse_with_prefix_str(int addr_family, const char *text, char **out_addr, int *out_prefix)
+{
+    NMIPAddr addrbin;
+
+    if (!nm_inet_parse_with_prefix_bin(addr_family, text, &addr_family, &addrbin, out_prefix))
+        return FALSE;
+
+    NM_SET_OUT(out_addr, nm_inet_ntop_dup(addr_family, &addrbin));
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_inet_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_inet_parse_bin(addr_family, str_addr, NULL, NULL);
+}
+
+gboolean
+nm_inet_is_normalized(int addr_family, const char *str_addr)
+{
+    NMIPAddr addr;
+    char     sbuf[NM_INET_ADDRSTRLEN];
+
+    nm_assert(NM_IN_SET(addr_family, AF_UNSPEC, AF_INET, AF_INET6));
+
+    if (!str_addr)
+        return FALSE;
+
+    if (!nm_inet_parse_bin(addr_family, str_addr, &addr_family, &addr))
+        return FALSE;
+
+    nm_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"), );
diff --git a/src/libnm-glib-aux/nm-inet-utils.h b/src/libnm-glib-aux/nm-inet-utils.h
new file mode 100644
index 00000000..8421929e
--- /dev/null
+++ b/src/libnm-glib-aux/nm-inet-utils.h
@@ -0,0 +1,389 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NM_INET_UTILS_H__
+#define __NM_INET_UTILS_H__
+
+typedef struct _NMIPAddr {
+    union {
+        guint8          addr_ptr[sizeof(struct in6_addr)];
+        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 }  \
+    }
+
+#define _NM_IN6ADDR_INIT(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af) \
+    {                                                                                    \
+        .s6_addr = {                                                                     \
+            (a0),                                                                        \
+            (a1),                                                                        \
+            (a2),                                                                        \
+            (a3),                                                                        \
+            (a4),                                                                        \
+            (a5),                                                                        \
+            (a6),                                                                        \
+            (a7),                                                                        \
+            (a8),                                                                        \
+            (a9),                                                                        \
+            (aa),                                                                        \
+            (ab),                                                                        \
+            (ac),                                                                        \
+            (ad),                                                                        \
+            (ae),                                                                        \
+            (af),                                                                        \
+        }                                                                                \
+    }
+
+#define NM_IN6ADDR_INIT(...) ((struct in6_addr) _NM_IN6ADDR_INIT(__VA_ARGS__))
+
+extern const NMIPAddr nm_ip_addr_zero;
+
+static inline int
+nm_ip_addr_cmp(int addr_family, gconstpointer a, gconstpointer b)
+{
+    /* Note that @a and @b are not required to be full NMIPAddr unions.
+     * Depending on @addr_family, they can also be only in_addr_t or
+     * struct in6_addr. */
+    NM_CMP_SELF(a, b);
+    NM_CMP_DIRECT_MEMCMP(a, b, nm_utils_addr_family_to_size(addr_family));
+    return 0;
+}
+
+int nm_ip_addr_cmp_for_sort(gconstpointer a, gconstpointer b, gpointer user_data);
+
+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 void
+nm_ip_addr_set(int addr_family, gpointer dst, gconstpointer src)
+{
+    nm_assert(dst);
+    nm_assert(src);
+
+    /* this MUST use memcpy() to support unaligned src/dst pointers. */
+    memcpy(dst, src, nm_utils_addr_family_to_size(addr_family));
+
+    /* Note that @dst is not necessarily a NMIPAddr, it could also be just
+     * an in_addr_t/struct in6_addr. We thus can only set the bytes that
+     * we know are present based on the address family.
+     *
+     * Using this function to initialize an NMIPAddr union (for IPv4) leaves
+     * uninitalized bytes. Avoid that by using nm_ip_addr_init() instead. */
+}
+
+static inline gboolean
+nm_ip_addr_is_null(int addr_family, gconstpointer addr)
+{
+    NMIPAddr a;
+
+    nm_ip_addr_set(addr_family, &a, addr);
+
+    if (NM_IS_IPv4(addr_family))
+        return a.addr4 == 0;
+
+    return IN6_IS_ADDR_UNSPECIFIED(&a.addr6);
+}
+
+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));
+
+    /* this MUST use memcpy() to support unaligned src/dst pointers. */
+
+    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);
+
+gboolean
+nm_ip_addr_set_from_variant(int addr_family, gpointer dst, GVariant *variant, int *out_addr_family);
+
+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]));
+}
+
+/*****************************************************************************/
+
+static inline guint32
+nm_ip4_addr_netmask_to_prefix(in_addr_t subnetmask)
+{
+    G_STATIC_ASSERT_EXPR(__SIZEOF_INT__ == 4);
+    G_STATIC_ASSERT_EXPR(sizeof(int) == 4);
+    G_STATIC_ASSERT_EXPR(sizeof(guint) == 4);
+    G_STATIC_ASSERT_EXPR(sizeof(subnetmask) == 4);
+
+    return ((subnetmask != 0u) ? (guint32) (32 - __builtin_ctz(ntohl(subnetmask))) : 0u);
+}
+
+/**
+ * nm_ip4_addr_netmask_from_prefix:
+ * @prefix: a CIDR prefix
+ *
+ * Returns: the netmask represented by the prefix, in network byte order
+ **/
+static inline in_addr_t
+nm_ip4_addr_netmask_from_prefix(guint32 prefix)
+{
+    nm_assert(prefix <= 32);
+    return prefix < 32 ? ~htonl(0xFFFFFFFFu >> prefix) : 0xFFFFFFFFu;
+}
+
+guint32 nm_ip4_addr_get_default_prefix0(in_addr_t ip);
+guint32 nm_ip4_addr_get_default_prefix(in_addr_t ip);
+
+static inline in_addr_t
+nm_ip4_addr_get_broadcast_address(in_addr_t address, guint8 plen)
+{
+    return address | ~nm_ip4_addr_netmask_from_prefix(plen);
+}
+
+gconstpointer
+nm_ip_addr_clear_host_address(int family, gpointer dst, gconstpointer src, guint32 plen);
+
+/* nm_ip4_addr_clear_host_address:
+ * @addr: source ip6 address
+ * @plen: prefix length of network
+ *
+ * returns: the input address, with the host address set to 0.
+ */
+static inline in_addr_t
+nm_ip4_addr_clear_host_address(in_addr_t addr, guint32 plen)
+{
+    return addr & nm_ip4_addr_netmask_from_prefix(plen);
+}
+
+const struct in6_addr *
+nm_ip6_addr_clear_host_address(struct in6_addr *dst, const struct in6_addr *src, guint32 plen);
+
+/*****************************************************************************/
+
+static inline int
+nm_ip4_addr_same_prefix_cmp(in_addr_t addr_a, in_addr_t addr_b, guint32 plen)
+{
+    NM_CMP_DIRECT(htonl(nm_ip4_addr_clear_host_address(addr_a, plen)),
+                  htonl(nm_ip4_addr_clear_host_address(addr_b, plen)));
+    return 0;
+}
+
+int nm_ip6_addr_same_prefix_cmp(const struct in6_addr *addr_a,
+                                const struct in6_addr *addr_b,
+                                guint32                plen);
+
+static inline gboolean
+nm_ip4_addr_same_prefix(in_addr_t addr_a, in_addr_t addr_b, guint32 plen)
+{
+    return nm_ip4_addr_same_prefix_cmp(addr_a, addr_b, plen) == 0;
+}
+
+static inline gboolean
+nm_ip6_addr_same_prefix(const struct in6_addr *addr_a, const struct in6_addr *addr_b, guint8 plen)
+{
+    return nm_ip6_addr_same_prefix_cmp(addr_a, addr_b, plen) == 0;
+}
+
+static inline int
+nm_ip_addr_same_prefix_cmp(int addr_family, gconstpointer addr_a, gconstpointer addr_b, guint8 plen)
+{
+    NMIPAddr a;
+    NMIPAddr b;
+
+    NM_CMP_SELF(addr_a, addr_b);
+
+    nm_ip_addr_set(addr_family, &a, addr_a);
+    nm_ip_addr_set(addr_family, &b, addr_b);
+
+    if (NM_IS_IPv4(addr_family))
+        return nm_ip4_addr_same_prefix_cmp(a.addr4, b.addr4, plen);
+
+    return nm_ip6_addr_same_prefix_cmp(&a.addr6, &b.addr6, plen);
+}
+
+static inline gboolean
+nm_ip_addr_same_prefix(int addr_family, gconstpointer addr_a, gconstpointer addr_b, guint8 plen)
+{
+    return nm_ip_addr_same_prefix_cmp(addr_family, addr_a, addr_b, plen) == 0;
+}
+
+#define NM_CMP_DIRECT_IP4_ADDR_SAME_PREFIX(a, b, plen) \
+    NM_CMP_RETURN(nm_ip4_addr_same_prefix_cmp((a), (b), (plen)))
+
+#define NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(a, b, plen) \
+    NM_CMP_RETURN(nm_ip6_addr_same_prefix_cmp((a), (b), (plen)))
+
+/*****************************************************************************/
+
+gboolean nm_ip_addr_is_site_local(int addr_family, const void *address);
+gboolean nm_ip6_addr_is_ula(const struct in6_addr *address);
+
+/*****************************************************************************/
+
+#define NM_IPV4LL_NETWORK   ((in_addr_t) htonl(0xA9FE0000lu)) /* 169.254.0.0 */
+#define NM_IPV4LL_NETMASK   ((in_addr_t) htonl(0xFFFF0000lu)) /* 255.255.0.0 */
+#define NM_IPV4LO_NETWORK   ((in_addr_t) htonl(0x7F000000lu)) /* 127.0.0.0 */
+#define NM_IPV4LO_NETMASK   ((in_addr_t) htonl(0xFF000000lu)) /* 255.0.0.0 */
+#define NM_IPV4LO_PREFIXLEN 8
+#define NM_IPV4LO_ADDR1     ((in_addr_t) htonl(0x7F000001lu)) /* 127.0.0.1 */
+
+static inline gboolean
+nm_ip4_addr_is_loopback(in_addr_t addr)
+{
+    /* There is also IN_LOOPBACK() in <linux/in.h>, but there the
+     * argument is in host order not `in_addr_t`. */
+    return (addr & NM_IPV4LO_NETMASK) == NM_IPV4LO_NETWORK;
+}
+
+static inline gboolean
+nm_ip4_addr_is_link_local(in_addr_t addr)
+{
+    return (addr & NM_IPV4LL_NETMASK) == NM_IPV4LL_NETWORK;
+}
+
+static inline gboolean
+nm_ip4_addr_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_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_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_inet4_ntop(in_addr_t addr, char dst[static INET_ADDRSTRLEN])
+{
+    return nm_inet_ntop(AF_INET, &addr, dst);
+}
+
+static inline const char *
+nm_inet6_ntop(const struct in6_addr *addr, char dst[static INET6_ADDRSTRLEN])
+{
+    return nm_inet_ntop(AF_INET6, addr, dst);
+}
+
+static inline char *
+nm_inet_ntop_dup(int addr_family, gconstpointer addr)
+{
+    char buf[NM_INET_ADDRSTRLEN];
+
+    return g_strdup(nm_inet_ntop(addr_family, addr, buf));
+}
+
+static inline char *
+nm_inet4_ntop_dup(in_addr_t addr)
+{
+    return nm_inet_ntop_dup(AF_INET, &addr);
+}
+
+static inline char *
+nm_inet6_ntop_dup(const struct in6_addr *addr)
+{
+    return nm_inet_ntop_dup(AF_INET6, addr);
+}
+
+/*****************************************************************************/
+
+gboolean nm_inet_parse_bin_full(int         addr_family,
+                                gboolean    accept_legacy,
+                                const char *text,
+                                int        *out_addr_family,
+                                gpointer    out_addr);
+static inline gboolean
+nm_inet_parse_bin(int addr_family, const char *text, int *out_addr_family, gpointer out_addr)
+{
+    return nm_inet_parse_bin_full(addr_family, FALSE, text, out_addr_family, out_addr);
+}
+
+gboolean nm_inet_parse_str(int addr_family, const char *text, char **out_addr);
+
+gboolean nm_inet_parse_with_prefix_bin(int         addr_family,
+                                       const char *text,
+                                       int        *out_addr_family,
+                                       gpointer    out_addr,
+                                       int        *out_prefix);
+
+gboolean
+nm_inet_parse_with_prefix_str(int addr_family, const char *text, char **out_addr, int *out_prefix);
+
+/*****************************************************************************/
+
+gboolean nm_inet_is_valid(int addr_family, const char *str_addr);
+
+gboolean nm_inet_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);
+
+/*****************************************************************************/
+
+#endif /* __NM_INET_UTILS_H__ */
diff --git a/src/libnm-glib-aux/nm-io-utils.c b/src/libnm-glib-aux/nm-io-utils.c
index 0823a16c..d1430595 100644
--- a/src/libnm-glib-aux/nm-io-utils.c
+++ b/src/libnm-glib-aux/nm-io-utils.c
@@ -12,6 +12,7 @@
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <sys/un.h>
+#include <sys/ioctl.h>
 
 #include "nm-str-buf.h"
 #include "nm-shared-utils.h"
@@ -351,6 +352,7 @@ nm_utils_file_set_contents(const char            *filename,
     int           errsv;
     gssize        s;
     int           fd;
+    int           r;
 
     g_return_val_if_fail(filename, FALSE);
     g_return_val_if_fail(contents || !length, FALSE);
@@ -414,7 +416,16 @@ nm_utils_file_set_contents(const char            *filename,
                                    tmp_name);
     }
 
-    nm_close(fd);
+    r = nm_close_with_error(fd);
+    if (r < 0) {
+        errsv = NM_ERRNO_NATIVE(-r);
+        unlink(tmp_name);
+        return _get_contents_error(error,
+                                   errsv,
+                                   out_errsv,
+                                   "failed close() after writing file %s",
+                                   tmp_name);
+    }
 
     if (rename(tmp_name, filename)) {
         errsv = NM_ERRNO_NATIVE(errno);
@@ -496,6 +507,45 @@ nm_utils_fd_read(int fd, NMStrBuf *out_string)
 
 /*****************************************************************************/
 
+/* Taken from systemd's next_datagram_size_fd(). */
+gssize
+nm_fd_next_datagram_size(int fd)
+{
+    gssize l;
+    int    k;
+
+    /* This is a bit like FIONREAD/SIOCINQ, however a bit more powerful. The difference being: recv(MSG_PEEK) will
+     * actually cause the next datagram in the queue to be validated regarding checksums, which FIONREAD doesn't
+     * do. This difference is actually of major importance as we need to be sure that the size returned here
+     * actually matches what we will read with recvmsg() next, as otherwise we might end up allocating a buffer of
+     * the wrong size. */
+
+    l = recv(fd, NULL, 0, MSG_PEEK | MSG_TRUNC);
+    if (l < 0) {
+        if (NM_IN_SET(errno, EOPNOTSUPP, EFAULT))
+            goto fallback;
+
+        return -errno;
+    }
+    if (l == 0)
+        goto fallback;
+
+    return l;
+
+fallback:
+    k = 0;
+
+    /* Some sockets (AF_PACKET) do not support null-sized recv() with MSG_TRUNC set, let's fall back to FIONREAD
+     * for them. Checksums don't matter for raw sockets anyway, hence this should be fine. */
+
+    if (ioctl(fd, FIONREAD, &k) < 0)
+        return -errno;
+
+    return (gssize) k;
+}
+
+/*****************************************************************************/
+
 typedef struct {
     GSubprocess *subprocess;
     GSource     *timeout_source;
diff --git a/src/libnm-glib-aux/nm-io-utils.h b/src/libnm-glib-aux/nm-io-utils.h
index ef015153..54018140 100644
--- a/src/libnm-glib-aux/nm-io-utils.h
+++ b/src/libnm-glib-aux/nm-io-utils.h
@@ -52,10 +52,55 @@ struct _NMStrBuf;
 
 gssize nm_utils_fd_read(int fd, struct _NMStrBuf *out_string);
 
+gssize nm_fd_next_datagram_size(int fd);
+
 struct stat;
 
 int nm_utils_file_stat(const char *filename, struct stat *out_st);
 
+/*****************************************************************************/
+
+/* From systemd's ERRNO_IS_TRANSIENT().
+ *
+ * For send()/recv() or read()/write(). */
+static inline gboolean
+NM_ERRNO_IS_TRANSIENT(int r)
+{
+    return NM_IN_SET((r < 0 ? -r : r), EAGAIN, EINTR);
+}
+
+/* From systemd's ERRNO_IS_DISCONNECT().
+ *
+ * Hint #1: ENETUNREACH happens if we try to connect to "non-existing" special IP addresses, such as ::5.
+ *
+ * Hint #2: The kernel sends e.g., EHOSTUNREACH or ENONET to userspace in some ICMP error cases.  See the
+ *          icmp_err_convert[] in net/ipv4/icmp.c in the kernel sources.
+ *
+ * Hint #3: When asynchronous connect() on TCP fails because the host never acknowledges a single packet,
+ *          kernel tells us that with ETIMEDOUT, see tcp(7). */
+static inline gboolean
+NM_ERRNO_IS_DISCONNECT(int r)
+{
+    return NM_IN_SET((r < 0 ? -r : r),
+                     ECONNABORTED,
+                     ECONNREFUSED,
+                     ECONNRESET,
+                     EHOSTDOWN,
+                     EHOSTUNREACH,
+                     ENETDOWN,
+                     ENETRESET,
+                     ENETUNREACH,
+                     ENONET,
+                     ENOPROTOOPT,
+                     ENOTCONN,
+                     EPIPE,
+                     EPROTO,
+                     ESHUTDOWN,
+                     ETIMEDOUT);
+}
+
+/*****************************************************************************/
+
 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);
diff --git a/src/libnm-glib-aux/nm-macros-internal.h b/src/libnm-glib-aux/nm-macros-internal.h
index f2a4461e..71a17e3e 100644
--- a/src/libnm-glib-aux/nm-macros-internal.h
+++ b/src/libnm-glib-aux/nm-macros-internal.h
@@ -43,6 +43,27 @@
 
 /*****************************************************************************/
 
+/* Historically, our cleanup macros come from a long gone library
+ * libgsystem, hence the "gs_" prefix. We still keep using them,
+ * although maybe we should drop them and use our respective nm_auto*
+ * macros (TODO).
+ *
+ * GLib also has g_auto() since 2.44. First of all, we still don't
+ * depend on 2.44, so we would have add compat implementations to
+ * "nm-glib.h" or bump the version.
+ * Also, they work differently (nm_auto_unref_hashtable vs g_auto(GHashTable)).
+ * If we were to switch to g_auto(), the change would be slightly more complicated
+ * than replacing one macro with another (but still easy).
+ * However, the reason for using our nm_auto* macros is that we also want cleanup
+ * macros in libnm-std-aux, which has no glib dependency. So we still would have
+ * some nm_auto* macros mixed with g_auto(). Instead, we consistently use
+ * nm_auto* macros (and the gs_* aliases).
+ *
+ * Note that c-stdaux also brings cleanup macros like _c_cleanup_(c_freep).
+ * We use c-stdaux like a proper internal library, so we could instead switch
+ * from nm_auto* macros to _c_cleanup_(). Unlike glib, c-stdaux is used by
+ * libnm-std-aux. Again, _c_cleanup_ follows a different pattern both from
+ * nm_auto* and g_auto(). */
 #define gs_free            nm_auto_g_free
 #define gs_unref_object    nm_auto_unref_object
 #define gs_unref_variant   nm_auto_unref_variant
@@ -110,6 +131,9 @@ NM_AUTO_DEFINE_FCN_VOID0(void *, _nm_auto_unref_gtypeclass, g_type_class_unref);
 NM_AUTO_DEFINE_FCN0(GByteArray *, _nm_auto_unref_bytearray, g_byte_array_unref);
 #define nm_auto_unref_bytearray nm_auto(_nm_auto_unref_bytearray)
 
+NM_AUTO_DEFINE_FCN0(GDateTime *, _nm_auto_unref_gdatetime, g_date_time_unref);
+#define nm_auto_unref_gdatetime nm_auto(_nm_auto_unref_gdatetime)
+
 static inline void
 _nm_auto_free_gstring(GString **str)
 {
@@ -236,68 +260,40 @@ NM_G_ERROR_MSG(GError *error)
 #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))))
+               const void        *: ((const type *) (obj)), \
+                     void        *: ((      type *) (obj)), \
+               const 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))))
+               const void        *: ((const type *) (obj)), \
+                     void        *: ((      type *) (obj)), \
+               const alias_type2 *: ((const type *) (obj)), \
+                     alias_type2 *: ((      type *) (obj)), \
+               const 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))))
+               const void        *: ((const type *) (obj)), \
+                     void        *: ((      type *) (obj)), \
+               const alias_type2 *: ((const type *) (obj)), \
+                     alias_type2 *: ((      type *) (obj)), \
+               const alias_type3 *: ((const type *) (obj)), \
+                     alias_type3 *: ((      type *) (obj)), \
+               const 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))))
+               const void        *: ((const type *) (obj)), \
+                     void        *: ((      type *) (obj)), \
+               const alias_type2 *: ((const type *) (obj)), \
+                     alias_type2 *: ((      type *) (obj)), \
+               const alias_type3 *: ((const type *) (obj)), \
+                     alias_type3 *: ((      type *) (obj)), \
+               const alias_type4 *: ((const type *) (obj)), \
+                     alias_type4 *: ((      type *) (obj)), \
+               const 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, ...) \
@@ -393,13 +389,7 @@ NM_G_ERROR_MSG(GError *error)
                      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)))
+                           void *: (const char *const*) (value)))
 #else
 #define NM_CAST_STRV_MC(value) ((const char **) (value))
 #define NM_CAST_STRV_CC(value) ((const char *const *) (value))
@@ -496,6 +486,17 @@ nm_strdup_not_empty(const char *str)
 }
 
 static inline char *
+nm_str_truncate(char *str)
+{
+    /* This is trivial, and is only useful in a macro, to
+     * ensure that we access the macro argument only once. */
+    nm_assert(str);
+
+    str[0] = '\0';
+    return str;
+}
+
+static inline char *
 nm_str_realloc(char *str)
 {
     gs_free char *s = str;
@@ -527,16 +528,27 @@ nm_str_realloc(char *str)
 /*****************************************************************************/
 
 /* 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()
+#undef _nm_assert_fail
+#define _nm_assert_fail(msg)                                                        \
+    G_STMT_START                                                                    \
+    {                                                                               \
+        g_assertion_message_expr(G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, msg); \
+        _nm_unreachable_code();                                                     \
+    }                                                                               \
+    G_STMT_END
+
+#undef _NM_ASSERT_FAIL_ENABLED
+#ifndef G_DISABLE_ASSERT
+#define _NM_ASSERT_FAIL_ENABLED 1
+#else
+#define _NM_ASSERT_FAIL_ENABLED 0
+#endif
 
 /* Usage:
  *
  *   if (NM_MORE_ASSERT_ONCE (5)) { extra_check (); }
  *
- * This will only run the check once, and only if NM_MORE_ASSERT is >= than
+ * This will only run the check once, and only if NM_MORE_ASSERTS is >= than
  * more_assert_level.
  */
 #define NM_MORE_ASSERT_ONCE(more_assert_level)                                                    \
@@ -652,6 +664,24 @@ nm_str_realloc(char *str)
 
 /*****************************************************************************/
 
+/* Unfortunately, G_TYPE_CHECK_INSTANCE_CAST() just does a direct cast,
+ * which can trigger a "-Wcast-align" warning, especially when casting
+ * a GObject pointer to the desired type.
+ *
+ * _NM_G_TYPE_CHECK_INSTANCE_CAST() avoids that warning.
+ *
+ * Since the entire point of G_TYPE_CHECK_INSTANCE_CAST_() is to do a
+ * runtime check on the type (with conditional assertions via G_DISABLE_CAST_CHECKS),
+ * we already assert that the gtype is right, and the alignment is also
+ * expected to be right.
+ *
+ * See https://gitlab.gnome.org/GNOME/glib/-/merge_requests/3139
+ */
+#define _NM_G_TYPE_CHECK_INSTANCE_CAST(instance, g_type, c_type) \
+    G_TYPE_CHECK_INSTANCE_CAST(((void *) (instance)), (g_type), c_type)
+
+/*****************************************************************************/
+
 static inline gpointer
 nm_g_object_ref(gpointer obj)
 {
@@ -1003,15 +1033,15 @@ nm_g_variant_equal(GVariant *a, GVariant *b)
         const typeof(flags) _flags = (flags); \
         const typeof(flags) _val   = (val);   \
                                               \
-        _flags &(~_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(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) \
@@ -1071,6 +1101,32 @@ nm_ascii_is_newline(char ch)
     return NM_IN_SET(ch, '\n', '\t');
 }
 
+static inline gboolean
+nm_ascii_is_regular_char(char ch)
+{
+    /* Checks whether "ch" is "regular", which basically
+     * means it's either a digit, a alpha, or some special
+     * characters that are suitable for base64 encoding.
+     *
+     * The meaning of what "regular" means is not well defined,
+     * but it's used to validate the keys for "ovs.external-ids"
+     * dictionary. */
+    switch (ch) {
+    case 'a' ... 'z':
+    case 'A' ... 'Z':
+    case '0' ... '9':
+    case '-':
+    case '_':
+    case '+':
+    case '/':
+    case '=':
+    case '.':
+        return TRUE;
+    default:
+        return FALSE;
+    }
+}
+
 #define nm_str_skip_leading_spaces(str)                          \
     ({                                                           \
         typeof(*(str))              *_str_sls        = (str);    \
@@ -1276,6 +1332,36 @@ nm_memdup(gconstpointer data, gsize size)
     return p;
 }
 
+static inline gpointer
+nm_memdup_nul(gconstpointer data, gsize size)
+{
+    gpointer p;
+
+    /* Like systemd's memdup_suffix0() and kernel's kmemdup_nul().
+     *
+     * This:
+     * - never returns NULL
+     * - always has one NUL byte after the @size data. Thus,
+     *   the actually allocated buffer is size+1.
+     *
+     * This is like nm_memdup() except:
+     * - never returns NULL
+     * - always returns one NUL character appended to the data.
+     *
+     * This is like g_strndup(), except
+     * - never returns NULL.
+     * - g_strndup() treats the src pointer as a NUL terminated string,
+     *   so if src is shorter than size, the rest is filled with padding.
+     *   Essentially, it uses strncpy() to copy the input which does the
+     *   truncation. If @data contains no NUL byte in teh first @size bytes,
+     *   it behaves the same as g_strndup(). */
+
+    p = g_malloc(size + 1u);
+    nm_memcpy(p, data, size);
+    ((char *) p)[size] = '\0';
+    return p;
+}
+
 #define nm_malloc_maybe_a(alloca_maxlen, bytes, to_free)  \
     ({                                                    \
         const gsize       _bytes   = (bytes);             \
diff --git a/src/libnm-glib-aux/nm-prioq.c b/src/libnm-glib-aux/nm-prioq.c
new file mode 100644
index 00000000..3448dcd9
--- /dev/null
+++ b/src/libnm-glib-aux/nm-prioq.c
@@ -0,0 +1,323 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+/*
+ * Taken from systemd's Prioq.
+ *
+ * Priority Queue
+ * The prioq object implements a priority queue. That is, it orders objects by
+ * their priority and allows O(1) access to the object with the highest
+ * priority. Insertion and removal are Θ(log n). Optionally, the caller can
+ * provide a pointer to an index which will be kept up-to-date by the prioq.
+ *
+ * The underlying algorithm used in this implementation is a Heap.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-prioq.h"
+
+#include <errno.h>
+#include <stdlib.h>
+
+/*****************************************************************************/
+
+struct _NMPrioqItem {
+    void     *data;
+    unsigned *idx;
+};
+
+/*****************************************************************************/
+
+void
+nm_prioq_init(NMPrioq *q, GCompareFunc compare_func)
+{
+    nm_assert(q);
+    nm_assert(compare_func);
+
+    *q = (NMPrioq){
+        ._priv =
+            {
+                .compare_func      = compare_func,
+                .compare_data      = NULL,
+                .compare_with_data = FALSE,
+                .items             = NULL,
+                .n_items           = 0,
+                .n_allocated       = 0,
+            },
+    };
+}
+
+void
+nm_prioq_init_with_data(NMPrioq *q, GCompareDataFunc compare_func, gpointer compare_data)
+{
+    nm_assert(q);
+    nm_assert(compare_func);
+
+    *q = (NMPrioq){
+        ._priv =
+            {
+                .compare_data_func = compare_func,
+                .compare_data      = compare_data,
+                .compare_with_data = TRUE,
+                .items             = NULL,
+                .n_items           = 0,
+                .n_allocated       = 0,
+            },
+    };
+}
+
+void
+nm_prioq_destroy(NMPrioq *q)
+{
+    if (!q || !q->_priv.compare_func)
+        return;
+
+    free(q->_priv.items);
+    q->_priv.compare_func = NULL;
+}
+
+/*****************************************************************************/
+
+static int
+compare(NMPrioq *q, unsigned a, unsigned b)
+{
+    nm_assert(q);
+    nm_assert(q->_priv.compare_func);
+    nm_assert(a != b);
+    nm_assert(a < q->_priv.n_items);
+    nm_assert(b < q->_priv.n_items);
+
+    if (q->_priv.compare_with_data) {
+        return q->_priv.compare_data_func(q->_priv.items[a].data,
+                                          q->_priv.items[b].data,
+                                          q->_priv.compare_data);
+    }
+
+    return q->_priv.compare_func(q->_priv.items[a].data, q->_priv.items[b].data);
+}
+
+static void
+swap(NMPrioq *q, unsigned j, unsigned k)
+{
+    nm_assert(q);
+    nm_assert(j < q->_priv.n_items);
+    nm_assert(k < q->_priv.n_items);
+
+    nm_assert(!q->_priv.items[j].idx || *(q->_priv.items[j].idx) == j);
+    nm_assert(!q->_priv.items[k].idx || *(q->_priv.items[k].idx) == k);
+
+    NM_SWAP(&q->_priv.items[j].data, &q->_priv.items[k].data);
+    NM_SWAP(&q->_priv.items[j].idx, &q->_priv.items[k].idx);
+
+    if (q->_priv.items[j].idx)
+        *q->_priv.items[j].idx = j;
+
+    if (q->_priv.items[k].idx)
+        *q->_priv.items[k].idx = k;
+}
+
+static unsigned
+shuffle_up(NMPrioq *q, unsigned idx)
+{
+    nm_assert(q);
+    nm_assert(idx < q->_priv.n_items);
+
+    while (idx > 0) {
+        unsigned k;
+
+        k = (idx - 1) / 2;
+
+        if (compare(q, k, idx) <= 0)
+            break;
+
+        swap(q, idx, k);
+        idx = k;
+    }
+
+    return idx;
+}
+
+static unsigned
+shuffle_down(NMPrioq *q, unsigned idx)
+{
+    nm_assert(q);
+
+    for (;;) {
+        unsigned j;
+        unsigned k;
+        unsigned s;
+
+        k = (idx + 1) * 2; /* right child */
+        j = k - 1;         /* left child */
+
+        if (j >= q->_priv.n_items)
+            break;
+
+        if (compare(q, j, idx) < 0) {
+            /* So our left child is smaller than we are, let's
+             * remember this fact */
+            s = j;
+        } else
+            s = idx;
+
+        if ((k < q->_priv.n_items) && compare(q, k, s) < 0) {
+            /* So our right child is smaller than we are, let's
+             * remember this fact */
+            s = k;
+        }
+
+        /* s now points to the smallest of the three items */
+
+        if (s == idx)
+            /* No swap necessary, we're done */
+            break;
+
+        swap(q, idx, s);
+        idx = s;
+    }
+
+    return idx;
+}
+
+void
+nm_prioq_put(NMPrioq *q, void *data, unsigned *idx)
+{
+    unsigned k;
+
+    nm_assert(q);
+
+    if (q->_priv.n_items >= q->_priv.n_allocated) {
+        q->_priv.n_allocated = NM_MAX((q->_priv.n_items + 1u) * 2u, 16u);
+        q->_priv.items       = g_renew(struct _NMPrioqItem, q->_priv.items, q->_priv.n_allocated);
+    }
+
+    k = q->_priv.n_items++;
+
+    q->_priv.items[k] = (struct _NMPrioqItem){
+        .data = data,
+        .idx  = idx,
+    };
+    if (idx)
+        *idx = k;
+
+    shuffle_up(q, k);
+}
+
+static void
+remove_item(NMPrioq *q, struct _NMPrioqItem *i)
+{
+    struct _NMPrioqItem *l;
+    unsigned             k;
+
+    nm_assert(q);
+    nm_assert(i);
+    nm_assert(q->_priv.n_items > 0);
+    nm_assert(i >= q->_priv.items);
+    nm_assert(i < &q->_priv.items[q->_priv.n_items]);
+
+    l = &q->_priv.items[q->_priv.n_items - 1u];
+
+    if (i == l) {
+        /* Last entry, let's just remove it */
+        q->_priv.n_items--;
+        return;
+    }
+
+    /* Not last entry, let's replace the last entry with
+     * this one, and reshuffle */
+    k = i - q->_priv.items;
+
+    *i = *l;
+    if (i->idx)
+        *i->idx = k;
+    q->_priv.n_items--;
+
+    k = shuffle_down(q, k);
+    shuffle_up(q, k);
+}
+
+_nm_pure static struct _NMPrioqItem *
+find_item(NMPrioq *q, void *data, unsigned *idx)
+{
+    struct _NMPrioqItem *i;
+
+    nm_assert(q);
+
+    if (q->_priv.n_items <= 0)
+        return NULL;
+
+    if (idx) {
+        if (*idx == NM_PRIOQ_IDX_NULL || *idx >= q->_priv.n_items)
+            return NULL;
+
+        i = &q->_priv.items[*idx];
+        if (i->data == data)
+            return i;
+    } else {
+        for (i = q->_priv.items; i < &q->_priv.items[q->_priv.n_items]; i++) {
+            if (i->data == data)
+                return i;
+        }
+    }
+
+    return NULL;
+}
+
+gboolean
+nm_prioq_remove(NMPrioq *q, void *data, unsigned *idx)
+{
+    struct _NMPrioqItem *i;
+
+    nm_assert(q);
+
+    i = find_item(q, data, idx);
+    if (!i)
+        return FALSE;
+
+    remove_item(q, i);
+    return TRUE;
+}
+
+gboolean
+nm_prioq_reshuffle(NMPrioq *q, void *data, unsigned *idx)
+{
+    struct _NMPrioqItem *i;
+    unsigned             k;
+
+    nm_assert(q);
+
+    i = find_item(q, data, idx);
+    if (!i)
+        return FALSE;
+
+    k = i - q->_priv.items;
+    k = shuffle_down(q, k);
+    shuffle_up(q, k);
+    return TRUE;
+}
+
+void *
+nm_prioq_peek_by_index(NMPrioq *q, unsigned idx)
+{
+    nm_assert(q);
+
+    if (idx >= q->_priv.n_items)
+        return NULL;
+
+    return q->_priv.items[idx].data;
+}
+
+void *
+nm_prioq_pop(NMPrioq *q)
+{
+    void *data;
+
+    nm_assert(q);
+
+    if (q->_priv.n_items <= 0)
+        return NULL;
+
+    data = q->_priv.items[0].data;
+    remove_item(q, &q->_priv.items[0]);
+    return data;
+}
diff --git a/src/libnm-glib-aux/nm-prioq.h b/src/libnm-glib-aux/nm-prioq.h
new file mode 100644
index 00000000..918c6447
--- /dev/null
+++ b/src/libnm-glib-aux/nm-prioq.h
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NM_PRIOQ_H__
+#define __NM_PRIOQ_H__
+
+#define NM_PRIOQ_IDX_NULL G_MAXUINT
+
+struct _NMPrioqItem;
+
+typedef struct {
+    struct {
+        union {
+            GCompareDataFunc compare_data_func;
+            GCompareFunc     compare_func;
+        };
+
+        gpointer compare_data;
+
+        struct _NMPrioqItem *items;
+
+        unsigned n_items;
+        unsigned n_allocated;
+
+        bool compare_with_data;
+    } _priv;
+} NMPrioq;
+
+#define NM_PRIOQ_ZERO             \
+    {                             \
+        ._priv = {                \
+            .compare_func = NULL, \
+        },                        \
+    }
+
+void nm_prioq_init(NMPrioq *q, GCompareFunc compare_func);
+void nm_prioq_init_with_data(NMPrioq *q, GCompareDataFunc compare_func, gpointer compare_data);
+
+void nm_prioq_destroy(NMPrioq *q);
+
+#define nm_auto_prioq nm_auto(nm_prioq_destroy)
+
+void     nm_prioq_put(NMPrioq *q, void *data, unsigned *idx);
+gboolean nm_prioq_remove(NMPrioq *q, void *data, unsigned *idx);
+gboolean nm_prioq_reshuffle(NMPrioq *q, void *data, unsigned *idx);
+
+void *nm_prioq_peek_by_index(NMPrioq *q, unsigned idx) _nm_pure;
+
+static inline void *
+nm_prioq_peek(NMPrioq *q)
+{
+    return nm_prioq_peek_by_index(q, 0);
+}
+
+void *nm_prioq_pop(NMPrioq *q);
+
+#define NM_PRIOQ_FOREACH_ITEM(q, p) for (unsigned _i = 0; (p = nm_prioq_peek_by_index(q, _i)); _i++)
+
+_nm_pure static inline unsigned
+nm_prioq_size(NMPrioq *q)
+{
+    nm_assert(q);
+    return q->_priv.n_items;
+}
+
+_nm_pure static inline gboolean
+nm_prioq_isempty(NMPrioq *q)
+{
+    return nm_prioq_size(q) <= 0;
+}
+
+#endif /* __NM_PRIOQ_H__ */
diff --git a/src/libnm-glib-aux/nm-ref-string.c b/src/libnm-glib-aux/nm-ref-string.c
index fec5d60e..27a6c938 100644
--- a/src/libnm-glib-aux/nm-ref-string.c
+++ b/src/libnm-glib-aux/nm-ref-string.c
@@ -157,8 +157,7 @@ nm_ref_string_new_len(const char *cstr, gsize len)
         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);
+        nm_memcpy((char *) rstr->str, cstr, len);
         ((char *) rstr->str)[len] = '\0';
         *((gsize *) &rstr->len)   = len;
         rstr->_ref_count          = 1;
diff --git a/src/libnm-glib-aux/nm-ref-string.h b/src/libnm-glib-aux/nm-ref-string.h
index eb3c38de..1b0cabf2 100644
--- a/src/libnm-glib-aux/nm-ref-string.h
+++ b/src/libnm-glib-aux/nm-ref-string.h
@@ -93,6 +93,23 @@ nm_ref_string_unref(NMRefString *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 gboolean
+nm_ref_string_reset(NMRefString **ptr, NMRefString *str)
+{
+    NMRefString *rstr;
+
+    nm_assert(ptr);
+
+    rstr = *ptr;
+
+    if (rstr == str)
+        return FALSE;
+
+    *ptr = nm_ref_string_ref(str);
+    nm_ref_string_unref(rstr);
+    return TRUE;
+}
+
 /*****************************************************************************/
 
 static inline const char *
@@ -139,7 +156,7 @@ nm_ref_string_equal_str(NMRefString *rstr, const char *str)
     /* We don't use streq() here, because an NMRefString might have embedded NUL characters
      * (as the length is tracked separately). The NUL terminated C string @str must not
      * compare equal to such a @rstr, thus we first explicitly check strlen(). */
-    return rstr->len == strlen(str) && (rstr->str == str || memcmp(rstr->str, str, rstr->len) == 0);
+    return rstr->str == str || (rstr->len == strlen(str) && memcmp(rstr->str, str, rstr->len) == 0);
 }
 
 static inline gboolean
diff --git a/src/libnm-glib-aux/nm-secret-utils.c b/src/libnm-glib-aux/nm-secret-utils.c
index 983b04ca..ffab2479 100644
--- a/src/libnm-glib-aux/nm-secret-utils.c
+++ b/src/libnm-glib-aux/nm-secret-utils.c
@@ -39,24 +39,10 @@ nm_explicit_bzero(void *s, gsize n)
 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);
+    nm_explicit_bzero(secret, strlen(secret));
     g_free(secret);
 }
 
diff --git a/src/libnm-glib-aux/nm-shared-utils.c b/src/libnm-glib-aux/nm-shared-utils.c
index aca7d708..702a63e9 100644
--- a/src/libnm-glib-aux/nm-shared-utils.c
+++ b/src/libnm-glib-aux/nm-shared-utils.c
@@ -3,6 +3,8 @@
  * Copyright (C) 2016 Red Hat, Inc.
  */
 
+#define NM_WANT_NM_ARRAY_FIND_BSEARCH_INLINE
+
 #include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
 
 #include "nm-shared-utils.h"
@@ -35,111 +37,6 @@ const void *const _NM_PTRARRAY_EMPTY[1] = {NULL};
 
 /*****************************************************************************/
 
-const NMIPAddr nm_ip_addr_zero = {};
-
-/* We use _nm_alignas(NMIPAddr) to ensure that fields for in_addr_t and
- * struct in6_addr have all the same alignment. Ensure that this is suitable. */
-G_STATIC_ASSERT(_nm_alignof(in_addr_t) <= _nm_alignof(NMIPAddr));
-G_STATIC_ASSERT(_nm_alignof(struct in_addr) <= _nm_alignof(NMIPAddr));
-G_STATIC_ASSERT(_nm_alignof(struct in6_addr) <= _nm_alignof(NMIPAddr));
-G_STATIC_ASSERT(_nm_alignof(NMEtherAddr) <= _nm_alignof(NMIPAddr));
-
-int
-nm_ip_addr_cmp_for_sort(gconstpointer a, gconstpointer b, gpointer user_data)
-{
-    /* This is a compare function that can be used for sorting IP addresses.
-     * Essentially, it calls memcmp(). @user_data must be GINT_TO_POINTER(addr_family).
-     * @a and @b must be either pointers to in_addr_t, struct in6_addr or NMIPAddr. */
-    return nm_ip_addr_cmp(GPOINTER_TO_INT(user_data), a, b);
-}
-
-/* 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;
-}
-
-gboolean
-nm_ip_addr_set_from_variant(int addr_family, gpointer dst, GVariant *variant, int *out_addr_family)
-{
-    gconstpointer bytes;
-    gsize         len;
-
-    g_return_val_if_fail(dst, FALSE);
-    g_return_val_if_fail(variant, FALSE);
-
-    /* This function always expects IP addressea a byte arrays ("ay"). Note that
-     * several NetworkManager API uses "u" (32 bit unsigned intergers) for IPv4 addresses.
-     * So this function won't work in those cases.
-     *
-     * Btw, using "u" for IPv4 address messes badly with the endianness (host
-     * vs network byte order). Don't do that.
-     */
-    g_return_val_if_fail(g_variant_is_of_type(variant, G_VARIANT_TYPE("ay")), FALSE);
-
-    bytes = g_variant_get_fixed_array(variant, &len, sizeof(guint8));
-
-    return nm_ip_addr_set_from_untrusted(addr_family, dst, bytes, len, out_addr_family);
-}
-
-/*****************************************************************************/
-
 G_STATIC_ASSERT(ETH_ALEN == sizeof(struct ether_addr));
 G_STATIC_ASSERT(ETH_ALEN == 6);
 G_STATIC_ASSERT(ETH_ALEN == sizeof(NMEtherAddr));
@@ -158,6 +55,7 @@ nm_ether_addr_from_string(NMEtherAddr *addr, const char *str)
 
     return addr;
 }
+
 /*****************************************************************************/
 
 /**
@@ -184,37 +82,6 @@ _nm_utils_inet6_is_token(const struct in6_addr *in6addr)
 }
 
 /**
- * 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
@@ -244,7 +111,7 @@ nm_utils_ipv6_interface_identifier_get_from_token(NMUtilsIPv6IfaceId *iid, const
 /**
  * nm_utils_inet6_interface_identifier_to_token:
  * @iid: %NMUtilsIPv6IfaceId interface identifier
- * @buf: the destination buffer of at least %NM_UTILS_INET_ADDRSTRLEN
+ * @buf: the destination buffer of at least %NM_INET_ADDRSTRLEN
  *   bytes.
  *
  * Converts the interface identifier to a string token.
@@ -261,7 +128,7 @@ nm_utils_inet6_interface_identifier_to_token(const NMUtilsIPv6IfaceId *iid,
 
     nm_assert(buf);
     nm_utils_ipv6_addr_set_interface_identifier(&i6_token, iid);
-    return _nm_utils_inet6_ntop(&i6_token, buf);
+    return nm_inet6_ntop(&i6_token, buf);
 }
 
 /*****************************************************************************/
@@ -981,293 +848,6 @@ nm_utils_flags2str(const NMUtilsFlags2StrDesc *descs,
 
 /*****************************************************************************/
 
-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:
-        /* IN6_IS_ADDR_SITELOCAL() is for deprecated fec0::/10 addresses (see rfc3879, 4.).
-         * Note that for unique local IPv6 addresses (ULA, fc00::/7) this returns false,
-         * which may or may not be a bug. */
-        return IN6_IS_ADDR_SITELOCAL(address);
-    default:
-        g_return_val_if_reached(FALSE);
-    }
-}
-
-gboolean
-nm_utils_ip6_is_ula(const struct in6_addr *address)
-{
-    /* Unique local IPv6 address (ULA) fc00::/7 */
-    return (address->s6_addr32[0] & htonl(0xfe000000u)) == htonl(0xfc000000u);
-}
-
-/*****************************************************************************/
-
-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) {
-        NM_PRAGMA_WARNING_DISABLE_DANGLING_POINTER
-        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);
-        NM_PRAGMA_WARNING_REENABLE
-    }
-#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,
@@ -1323,43 +903,6 @@ done:
 
 /*****************************************************************************/
 
-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
@@ -2587,15 +2130,16 @@ nm_strv_cleanup(char **strv, gboolean strip_whitespace, gboolean skip_empty, gbo
 /*****************************************************************************/
 
 GPtrArray *
-_nm_g_ptr_array_copy(GPtrArray     *array,
-                     GCopyFunc      func,
-                     gpointer       user_data,
-                     GDestroyNotify element_free_func)
+nm_g_ptr_array_new_clone(GPtrArray     *array,
+                         GCopyFunc      func,
+                         gpointer       user_data,
+                         GDestroyNotify element_free_func)
 {
     GPtrArray *new_array;
     guint      i;
 
     g_return_val_if_fail(array, NULL);
+    nm_assert((!!func) == (!!element_free_func));
 
     new_array = g_ptr_array_new_full(array->len, element_free_func);
     for (i = 0; i < array->len; i++) {
@@ -3522,12 +3066,12 @@ nm_utils_named_value_list_find(const NMUtilsNamedValue *arr,
 #endif
 
     if (sorted) {
-        return nm_utils_array_find_binary_search(arr,
-                                                 sizeof(NMUtilsNamedValue),
-                                                 len,
-                                                 &name,
-                                                 nm_strcmp_p_with_data,
-                                                 NULL);
+        return nm_array_find_bsearch(arr,
+                                     len,
+                                     sizeof(NMUtilsNamedValue),
+                                     &name,
+                                     nm_strcmp_p_with_data,
+                                     NULL);
     }
     for (i = 0; i < len; i++) {
         if (nm_streq(arr[i].name, name))
@@ -4215,17 +3759,19 @@ nm_utils_ptrarray_is_sorted(gconstpointer   *list,
 }
 
 gssize
-nm_utils_ptrarray_find_binary_search(gconstpointer   *list,
-                                     gsize            len,
-                                     gconstpointer    needle,
-                                     GCompareDataFunc cmpfcn,
-                                     gpointer         user_data)
-{
-    gssize imin, imax, imid;
+nm_ptrarray_find_bsearch(gconstpointer   *list,
+                         gsize            len,
+                         gconstpointer    needle,
+                         GCompareDataFunc cmpfcn,
+                         gpointer         user_data)
+{
+    gssize imax;
+    gssize imid;
+    gssize imin;
     int    cmp;
 
-    g_return_val_if_fail(list || !len, ~((gssize) 0));
-    g_return_val_if_fail(cmpfcn, ~((gssize) 0));
+    nm_assert(list || len == 0);
+    nm_assert(cmpfcn);
 
     imin = 0;
     if (len > 0) {
@@ -4252,19 +3798,24 @@ nm_utils_ptrarray_find_binary_search(gconstpointer   *list,
 }
 
 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;
+nm_ptrarray_find_bsearch_range(gconstpointer   *list,
+                               gsize            len,
+                               gconstpointer    needle,
+                               GCompareDataFunc cmpfcn,
+                               gpointer         user_data,
+                               gssize          *out_idx_first,
+                               gssize          *out_idx_last)
+{
+    gssize imax;
+    gssize imid;
+    gssize imin;
+    gssize i2max;
+    gssize i2mid;
+    gssize i2min;
     int    cmp;
 
-    g_return_val_if_fail(list || !len, ~((gssize) 0));
-    g_return_val_if_fail(cmpfcn, ~((gssize) 0));
+    nm_assert(list || len == 0);
+    nm_assert(cmpfcn);
 
     imin = 0;
     if (len > 0) {
@@ -4333,10 +3884,10 @@ nm_utils_ptrarray_find_binary_search_range(gconstpointer   *list,
 /*****************************************************************************/
 
 /**
- * nm_utils_array_find_binary_search:
+ * nm_array_find_bsearch:
  * @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
+ * @elem_size: the size in bytes of each element in the 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
@@ -4357,42 +3908,14 @@ nm_utils_ptrarray_find_binary_search_range(gconstpointer   *list,
  *   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_array_find_bsearch(gconstpointer    list,
+                      gsize            len,
+                      gsize            elem_size,
+                      gconstpointer    needle,
+                      GCompareDataFunc cmpfcn,
+                      gpointer         user_data)
+{
+    return nm_array_find_bsearch_inline(list, len, elem_size, needle, cmpfcn, user_data);
 }
 
 /*****************************************************************************/
@@ -6370,98 +5893,6 @@ _nm_utils_ssid_to_string_gbytes(GBytes *ssid)
 
 /*****************************************************************************/
 
-gconstpointer
-nm_utils_ipx_address_clear_host_address(int family, gpointer dst, gconstpointer src, guint32 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_ip6_address_clear_host_address:
- * @dst: destination output buffer, will contain the network part of the @src address
- * @src: source ip6 address. If NULL, this does an in-place update of @dst.
- *   Also, @src and @dst are allowed to be the same pointers.
- * @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,
-                                        guint32                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,
-                                     guint32                plen)
-{
-    int    nbytes;
-    guint8 va, vb, m;
-
-    if (plen >= 128) {
-        nm_assert(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/"
 
diff --git a/src/libnm-glib-aux/nm-shared-utils.h b/src/libnm-glib-aux/nm-shared-utils.h
index 53cf7f3e..083ed137 100644
--- a/src/libnm-glib-aux/nm-shared-utils.h
+++ b/src/libnm-glib-aux/nm-shared-utils.h
@@ -139,6 +139,8 @@ typedef enum {
     NM_LINK_TYPE_VETH,
     NM_LINK_TYPE_VLAN,
     NM_LINK_TYPE_VRF,
+    NM_LINK_TYPE_VTI,
+    NM_LINK_TYPE_VTI6,
     NM_LINK_TYPE_VXLAN,
     NM_LINK_TYPE_WIREGUARD,
 #define _NM_LINK_TYPE_SW_LAST NM_LINK_TYPE_WIREGUARD
@@ -198,6 +200,13 @@ typedef struct {
 
 #define NM_ETHER_ADDR_INIT(...) ((NMEtherAddr) _NM_ETHER_ADDR_INIT(__VA_ARGS__))
 
+struct _NMIPAddr;
+extern const struct _NMIPAddr nm_ip_addr_zero;
+
+/* Let's reuse nm_ip_addr_zero also for nm_ether_addr_zero. It's a union that
+ * also contains a NMEtherAddr field. */
+#define nm_ether_addr_zero (*((const NMEtherAddr *) ((gconstpointer) &nm_ip_addr_zero)))
+
 static inline int
 nm_ether_addr_cmp(const NMEtherAddr *a, const NMEtherAddr *b)
 {
@@ -212,126 +221,10 @@ nm_ether_addr_equal(const NMEtherAddr *a, const NMEtherAddr *b)
     return nm_ether_addr_cmp(a, b) == 0;
 }
 
-/*****************************************************************************/
-
-typedef struct {
-    union {
-        guint8          addr_ptr[sizeof(struct in6_addr)];
-        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)
-{
-    /* Note that @a and @b are not required to be full NMIPAddr unions.
-     * Depending on @addr_family, they can also be only in_addr_t or
-     * struct in6_addr. */
-    NM_CMP_SELF(a, b);
-    NM_CMP_DIRECT_MEMCMP(a, b, nm_utils_addr_family_to_size(addr_family));
-    return 0;
-}
-
-int nm_ip_addr_cmp_for_sort(gconstpointer a, gconstpointer b, gpointer user_data);
-
 static inline gboolean
-nm_ip_addr_equal(int addr_family, gconstpointer a, gconstpointer b)
+nm_ether_addr_is_zero(const NMEtherAddr *a)
 {
-    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 (NM_IS_IPv4(addr_family)) {
-        in_addr_t t;
-
-        /* also for in_addr_t type (AF_INET), we accept that the pointer might
-         * be unaligned. */
-        memcpy(&t, addr, sizeof(t));
-        return t == 0;
-    }
-
-    return IN6_IS_ADDR_UNSPECIFIED((const struct in6_addr *) addr);
-}
-
-static inline void
-nm_ip_addr_set(int addr_family, gpointer dst, gconstpointer src)
-{
-    nm_assert(dst);
-    nm_assert(src);
-
-    /* this MUST use memcpy() to support unaligned src/dst pointers. */
-    memcpy(dst, src, nm_utils_addr_family_to_size(addr_family));
-
-    /* Note that @dst is not necessarily a NMIPAddr, it could also be just
-     * an in_addr_t/struct in6_addr. We thus can only set the bytes that
-     * we know are present based on the address family.
-     *
-     * Using this function to initialize an NMIPAddr union (for IPv4) leaves
-     * uninitalized bytes. Avoid that by using nm_ip_addr_init() instead. */
-}
-
-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));
-
-    /* this MUST use memcpy() to support unaligned src/dst pointers. */
-
-    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);
-
-gboolean
-nm_ip_addr_set_from_variant(int addr_family, gpointer dst, GVariant *variant, int *out_addr_family);
-
-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]));
+    return nm_memeq(a, &nm_ether_addr_zero, sizeof(NMEtherAddr));
 }
 
 /*****************************************************************************/
@@ -343,6 +236,7 @@ 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*/);
 }
 
@@ -377,11 +271,36 @@ typedef struct _NMUtilsIPv6IfaceId {
         }                           \
     }
 
-void nm_utils_ipv6_addr_set_interface_identifier(struct in6_addr          *addr,
-                                                 const NMUtilsIPv6IfaceId *iid);
+/**
+ * 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.
+ */
+static inline void
+nm_utils_ipv6_addr_set_interface_identifier(struct in6_addr *addr, const NMUtilsIPv6IfaceId *iid)
+{
+    memcpy(addr->s6_addr + 8, &iid->id_u8, 8);
+}
 
-void nm_utils_ipv6_interface_identifier_get_from_addr(NMUtilsIPv6IfaceId    *iid,
-                                                      const struct in6_addr *addr);
+/**
+ * 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.
+ */
+static inline void
+nm_utils_ipv6_interface_identifier_get_from_addr(NMUtilsIPv6IfaceId    *iid,
+                                                 const struct in6_addr *addr)
+{
+    memcpy(iid, addr->s6_addr + 8, 8);
+}
 
 gboolean nm_utils_ipv6_interface_identifier_get_from_token(NMUtilsIPv6IfaceId *iid,
                                                            const char         *token);
@@ -397,219 +316,6 @@ gboolean nm_utils_get_ipv6_interface_identifier(NMLinkType          link_type,
 
 /*****************************************************************************/
 
-static inline guint32
-_nm_utils_ip4_netmask_to_prefix(in_addr_t subnetmask)
-{
-    G_STATIC_ASSERT_EXPR(__SIZEOF_INT__ == 4);
-    G_STATIC_ASSERT_EXPR(sizeof(int) == 4);
-    G_STATIC_ASSERT_EXPR(sizeof(guint) == 4);
-    G_STATIC_ASSERT_EXPR(sizeof(subnetmask) == 4);
-
-    return ((subnetmask != 0u) ? (guint32) (32 - __builtin_ctz(ntohl(subnetmask))) : 0u);
-}
-
-/**
- * _nm_utils_ip4_prefix_to_netmask:
- * @prefix: a CIDR prefix
- *
- * Returns: the netmask represented by the prefix, in network byte order
- **/
-static inline in_addr_t
-_nm_utils_ip4_prefix_to_netmask(guint32 prefix)
-{
-    nm_assert(prefix <= 32);
-    return prefix < 32 ? ~htonl(0xFFFFFFFFu >> prefix) : 0xFFFFFFFFu;
-}
-
-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, guint32 plen);
-
-/* 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.
- */
-static inline in_addr_t
-nm_utils_ip4_address_clear_host_address(in_addr_t addr, guint32 plen)
-{
-    return addr & _nm_utils_ip4_prefix_to_netmask(plen);
-}
-
-const struct in6_addr *nm_utils_ip6_address_clear_host_address(struct in6_addr       *dst,
-                                                               const struct in6_addr *src,
-                                                               guint32                plen);
-
-static inline int
-nm_utils_ip4_address_same_prefix_cmp(in_addr_t addr_a, in_addr_t addr_b, guint32 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,
-                                         guint32                plen);
-
-static inline gboolean
-nm_utils_ip4_address_same_prefix(in_addr_t addr_a, in_addr_t addr_b, guint32 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 int
-nm_utils_ip_address_same_prefix_cmp(int           addr_family,
-                                    gconstpointer addr_a,
-                                    gconstpointer addr_b,
-                                    guint8        plen)
-{
-    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_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)))
-
-/*****************************************************************************/
-
-gboolean nm_utils_ip_is_site_local(int addr_family, const void *address);
-gboolean nm_utils_ip6_is_ula(const struct in6_addr *address);
-
-/*****************************************************************************/
-
-#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_loopback(in_addr_t addr)
-{
-    /* There is also IN_LOOPBACK() in <linux/in.h>, but there the
-     * argument is in host order not `in_addr_t`. */
-    return (addr & htonl(0xFF000000u)) == htonl(0x7F000000u);
-}
-
-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);
 
 /*****************************************************************************/
@@ -1044,31 +750,6 @@ nm_utils_escaped_tokens_options_escape_val(const char *val, char **out_to_free)
 
 /*****************************************************************************/
 
-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,
@@ -1335,6 +1016,7 @@ typedef enum {
      * 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_STRICTLY_UNMANAGED_DEVICE,
     NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
     NM_UTILS_ERROR_CONNECTION_AVAILABLE_UNMANAGED_DEVICE,
     NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
@@ -1662,6 +1344,10 @@ nm_g_variant_new_au(const guint32 *data, gsize len)
     return g_variant_new_fixed_array(G_VARIANT_TYPE_UINT32, data, len, sizeof(guint32));
 }
 
+struct _NMIPAddr;
+
+extern const struct _NMIPAddr nm_ip_addr_zero;
+
 static inline GVariant *
 nm_g_variant_new_ay_inaddr(int addr_family, gconstpointer addr)
 {
@@ -2210,6 +1896,8 @@ char *nm_utils_g_slist_strlist_join(const GSList *a, const char *separator);
 static inline gpointer
 nm_g_array_data(const GArray *arr)
 {
+    /* You may want to use nm_g_array_first_p() instead, which can assert
+     * for the expected type. */
     return arr ? arr->data : NULL;
 }
 
@@ -2226,43 +1914,67 @@ nm_g_array_unref(GArray *arr)
         g_array_unref(arr);
 }
 
-#define nm_g_array_first(arr, Type)                                \
-    ({                                                             \
-        GArray *const _arr = (arr);                                \
-                                                                   \
-        nm_assert(_arr);                                           \
-        nm_assert(sizeof(Type) == g_array_get_element_size(_arr)); \
-        nm_assert(_arr->len > 0);                                  \
-                                                                   \
-        &g_array_index(arr, Type, 0);                              \
-    })
-
-#define nm_g_array_last(arr, Type)                                 \
-    ({                                                             \
-        GArray *const _arr = (arr);                                \
-                                                                   \
-        nm_assert(_arr);                                           \
-        nm_assert(sizeof(Type) == g_array_get_element_size(_arr)); \
-        nm_assert(_arr->len > 0);                                  \
-                                                                   \
-        &g_array_index(arr, Type, _arr->len - 1u);                 \
-    })
-
 /* Similar to g_array_index(). The differences are
  * - this does nm_assert() checks that the arguments are valid.
- * - returns a pointer to the element. */
-#define nm_g_array_index_p(arr, Type, idx)                            \
-    ({                                                                \
-        GArray *const _arr_55 = (arr);                                \
-        const guint   _idx_55 = (idx);                                \
-                                                                      \
-        nm_assert(_arr_55);                                           \
-        nm_assert(sizeof(Type) == g_array_get_element_size(_arr_55)); \
-        nm_assert(_idx_55 < _arr_55->len);                            \
-                                                                      \
-        &g_array_index(_arr_55, Type, _idx_55);                       \
+ * - returns a pointer to the element.
+ * - it asserts that @idx is <= arr->len. That is, it allows
+ *   to get a pointer after the data, of course, you are not
+ *   allowed to dereference in that case.
+ * - in particular, unlike nm_g_array_index(), you are allowed to call this
+ *   with "arr" NULL (for index zero) or with "arr->data" NULL
+ *   (for index zero). In that case, NULL is returned.
+ *
+ * When accessing index zero, then this returns NULL if-and-only-if
+ * "arr" is NULL or "arr->data" is NULL. In all other cases, this
+ * returns the pointer &((Type*) arr->data)[idx]. Note that the pointer
+ * may not be followed, if "idx" is equal to "arr->len". */
+#define nm_g_array_index_p(arr, Type, idx)                                                       \
+    ({                                                                                           \
+        const GArray *const _arr_55 = (arr);                                                     \
+        const guint         _idx_55 = (idx);                                                     \
+                                                                                                 \
+        nm_assert(_arr_55 || _idx_55 == 0);                                                      \
+        nm_assert(_idx_55 <= (_arr_55 ? _arr_55->len : 0u));                                     \
+        nm_assert(!_arr_55 || sizeof(Type) == g_array_get_element_size((GArray *) _arr_55));     \
+                                                                                                 \
+        ((_arr_55 && _arr_55->data) ? &(((Type *) ((gpointer) _arr_55->data))[_idx_55]) : NULL); \
     })
 
+/* Very similar to g_array_index().
+ * - nm_assert() that arguments are valid.
+ * - returns an lvalue to the element.
+ * - similar to nm_g_array_index_p(), but dereferences the pointer.
+ * - one difference to nm_g_array_index_p() is that it @idx MUST be
+ *   smaller than arr->len (unlike nm_g_array_index_p() which allows
+ *   access one element past the buffer. */
+#define nm_g_array_index(arr, Type, idx)                                         \
+    (*({                                                                         \
+        const GArray *const _arr_55 = (arr);                                     \
+        const guint         _idx_55 = (idx);                                     \
+                                                                                 \
+        nm_assert(_arr_55);                                                      \
+        nm_assert(sizeof(Type) == g_array_get_element_size((GArray *) _arr_55)); \
+        nm_assert(_idx_55 < _arr_55->len);                                       \
+                                                                                 \
+        &g_array_index((GArray *) _arr_55, Type, _idx_55);                       \
+    }))
+
+#define nm_g_array_first(arr, Type) nm_g_array_index(arr, Type, 0)
+
+#define nm_g_array_first_p(arr, Type) nm_g_array_index_p(arr, Type, 0)
+
+/* Same as g_array_index(arr, Type, arr->len-1). */
+#define nm_g_array_last(arr, Type)                                            \
+    (*({                                                                      \
+        const GArray *const _arr = (arr);                                     \
+                                                                              \
+        nm_assert(_arr);                                                      \
+        nm_assert(sizeof(Type) == g_array_get_element_size((GArray *) _arr)); \
+        nm_assert(_arr->len > 0);                                             \
+                                                                              \
+        &g_array_index((GArray *) arr, Type, _arr->len - 1u);                 \
+    }))
+
 #define nm_g_array_append_new(arr, Type)                           \
     ({                                                             \
         GArray *const _arr = (arr);                                \
@@ -2279,6 +1991,20 @@ nm_g_array_unref(GArray *arr)
         &g_array_index(arr, Type, _len);                           \
     })
 
+#define nm_g_array_append_simple(arr, val)                               \
+    G_STMT_START                                                         \
+    {                                                                    \
+        /* Similar to `g_array_append_val()`, but `g_array_append_val()`
+         * only works with lvalues. That makes sense if the value is a larger
+         * struct and you anyway have a pointer to it. It doesn't make sense
+         * if you have a list of int and want to append a number literal.
+         *
+         * nm_g_array_append_simple() is different. It depends on typeof(val)
+         * to be compatible. */ \
+        (*nm_g_array_append_new((arr), typeof(val))) = (val);            \
+    }                                                                    \
+    G_STMT_END
+
 /*****************************************************************************/
 
 static inline GPtrArray *
@@ -2342,40 +2068,29 @@ 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:
+ * nm_g_ptr_array_new_clone:
  * @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().
+ * @element_free_func: the free function of the elements. This function
+ *   must agree with the owner-ship semantics of @func.
  *
  * 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.
+ * as the caller must provide the correct element_free_func.
  *
- * Note that the @element_free_func MUST correspond to free function set in @array.
+ * So this is not the same as g_ptr_array_copy() (hence the different name) because
+ * g_ptr_array_copy() uses the free func of the source array, which we cannot access.
+ * With g_ptr_array_copy() the copy func must agree with the array's free func.
+ * Here, it must agree with the provided @element_free_func. This allows for example
+ * to do a shallow-copy without cloning the elements (which you cannot do with g_ptr_array_copy()).
  */
-#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
+GPtrArray *nm_g_ptr_array_new_clone(GPtrArray     *array,
+                                    GCopyFunc      func,
+                                    gpointer       user_data,
+                                    GDestroyNotify element_free_func);
 
 /*****************************************************************************/
 
@@ -2443,42 +2158,95 @@ gboolean nm_utils_ptrarray_is_sorted(gconstpointer   *list,
                                      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_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_ptrarray_find_bsearch(gconstpointer   *list,
+                                gsize            len,
+                                gconstpointer    needle,
+                                GCompareDataFunc cmpfcn,
+                                gpointer         user_data);
+
+gssize nm_ptrarray_find_bsearch_range(gconstpointer   *list,
+                                      gsize            len,
+                                      gconstpointer    needle,
+                                      GCompareDataFunc cmpfcn,
+                                      gpointer         user_data,
+                                      gssize          *out_idx_first,
+                                      gssize          *out_idx_last);
+
+#define nm_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_ptrarray_find_bsearch((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);
+/*****************************************************************************/
+
+#ifdef NM_WANT_NM_ARRAY_FIND_BSEARCH_INLINE
+/**
+ * nm_array_find_bsearch_inline:
+ *
+ * An inlined version of nm_array_find_bsearch(). See there.
+ * Define NM_WANT_NM_ARRAY_FIND_BSEARCH_INLINE to get it.
+ */
+_nm_always_inline static inline gssize
+nm_array_find_bsearch_inline(gconstpointer    list,
+                             gsize            len,
+                             gsize            elem_size,
+                             gconstpointer    needle,
+                             GCompareDataFunc cmpfcn,
+                             gpointer         user_data)
+{
+    gssize imax;
+    gssize imid;
+    gssize imin;
+    int    cmp;
+
+    nm_assert(list || len == 0);
+    nm_assert(cmpfcn);
+    nm_assert(elem_size > 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;
+}
+#endif
+
+gssize nm_array_find_bsearch(gconstpointer    list,
+                             gsize            len,
+                             gsize            elem_size,
+                             gconstpointer    needle,
+                             GCompareDataFunc cmpfcn,
+                             gpointer         user_data);
+
+/*****************************************************************************/
 
 gssize nm_utils_ptrarray_find_first(gconstpointer *list, gssize len, gconstpointer needle);
 
@@ -2731,7 +2499,7 @@ 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);
+    return nm_g_ptr_array_new_clone((GPtrArray *) src, nm_copy_func_g_strdup, NULL, g_free);
 }
 
 static inline void
@@ -2894,6 +2662,9 @@ nm_ether_addr_to_string(const NMEtherAddr *ether_addr, char sbuf[static(sizeof(N
 #define nm_ether_addr_to_string_a(ether_addr) \
     nm_ether_addr_to_string((ether_addr), g_alloca(sizeof(NMEtherAddr) * 3))
 
+#define nm_ether_addr_to_string_dup(ether_addr) \
+    ((char *) nm_ether_addr_to_string((ether_addr), g_malloc(sizeof(NMEtherAddr) * 3)))
+
 NMEtherAddr *nm_ether_addr_from_string(NMEtherAddr *addr, const char *str);
 
 guint8 *nm_utils_hexstr2bin_full(const char *hexstr,
@@ -3211,7 +2982,7 @@ nm_strvarray_add(GArray *array, const char *str)
 static inline const char *
 nm_strvarray_get_idx(GArray *array, guint idx)
 {
-    return *nm_g_array_index_p(array, const char *, idx);
+    return nm_g_array_index(array, const char *, idx);
 }
 
 static inline const char *const *
@@ -3310,12 +3081,12 @@ nm_strvarray_remove_first(GArray *strv, const char *needle)
 static inline int
 nm_strvarray_cmp(const GArray *a, const GArray *b)
 {
+    nm_assert(!a || sizeof(const char *const *) == g_array_get_element_size((GArray *) a));
+    nm_assert(!b || sizeof(const char *const *) == g_array_get_element_size((GArray *) b));
+
     NM_CMP_SELF(a, b);
 
-    return nm_strv_cmp_n((const char *const *) a->data,
-                         a->len,
-                         (const char *const *) b->data,
-                         b->len);
+    return nm_strv_cmp_n(nm_g_array_data(a), a->len, nm_g_array_data(b), b->len);
 }
 
 #define nm_strvarray_equal(a, b) (nm_strvarray_cmp((a), (b)) == 0)
@@ -3323,10 +3094,9 @@ nm_strvarray_cmp(const GArray *a, const GArray *b)
 static inline int
 _nm_strvarray_cmp_strv(const GArray *strv, const char *const *ss, gsize ss_len)
 {
-    return nm_strv_cmp_n(strv ? (const char *const *) strv->data : NULL,
-                         strv ? ((gssize) strv->len) : -1,
-                         ss,
-                         ss_len);
+    nm_assert(!strv || sizeof(const char *const *) == g_array_get_element_size((GArray *) strv));
+
+    return nm_strv_cmp_n(nm_g_array_data(strv), strv ? ((gssize) strv->len) : -1, ss, ss_len);
 }
 #define nm_strvarray_cmp_strv(strv, ss, ss_len) \
     _nm_strvarray_cmp_strv((strv), NM_CAST_STRV_CC(ss), (ss_len))
diff --git a/src/libnm-glib-aux/nm-str-buf.h b/src/libnm-glib-aux/nm-str-buf.h
index e8b51208..32ab8da8 100644
--- a/src/libnm-glib-aux/nm-str-buf.h
+++ b/src/libnm-glib-aux/nm-str-buf.h
@@ -447,6 +447,24 @@ nm_str_buf_get_str(NMStrBuf *strbuf)
     return strbuf->_priv_str;
 }
 
+static inline char *
+nm_str_buf_dup_str(NMStrBuf *strbuf)
+{
+    _nm_str_buf_assert(strbuf);
+
+    /* Gives almost the same as g_strdup(nm_str_buf_get_str(strbuf)). The difference
+     * is:
+     *   - unlike nm_str_buf_get_str(), it does not need to allocate
+     *     one more character to NUL terminate the string.
+     *   - it does not require an additional strlen(), because we
+     *     already know the length. */
+
+    if (!strbuf->_priv_str)
+        return NULL;
+
+    return nm_memdup_nul(strbuf->_priv_str, strbuf->_priv_len);
+}
+
 /**
  * nm_str_buf_get_str_unsafe:
  * @strbuf: the buffer
@@ -530,10 +548,7 @@ nm_str_buf_finalize(NMStrBuf *strbuf, gsize *out_len)
         char *str = g_steal_pointer(&strbuf->_priv_str);
         char *result;
 
-        result = g_new(char, strbuf->_priv_len + 1u);
-        memcpy(result, str, strbuf->_priv_len);
-        result[strbuf->_priv_len] = '\0';
-
+        result = nm_memdup_nul(str, strbuf->_priv_len);
         if (strbuf->_priv_do_bzero_mem)
             nm_explicit_bzero(str, strbuf->_priv_len);
         return result;
diff --git a/src/libnm-glib-aux/nm-test-utils.h b/src/libnm-glib-aux/nm-test-utils.h
index d338d0af..41e1a6ce 100644
--- a/src/libnm-glib-aux/nm-test-utils.h
+++ b/src/libnm-glib-aux/nm-test-utils.h
@@ -39,7 +39,7 @@
  *   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.
+ *      debug mode, will turn on TRACE 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):
@@ -85,6 +85,9 @@
 #undef g_assertion_message_expr
 #endif
 
+#undef NDEBUG
+#include <assert.h>
+
 #include <arpa/inet.h>
 #include <stdio.h>
 #include <unistd.h>
@@ -221,17 +224,24 @@
 
 /*****************************************************************************/
 
+struct __nmtst_testdata_track {
+    gpointer       data;
+    GDestroyNotify destroy_notify;
+};
+
 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;
+    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;
+    const char *testpath;
+    GArray     *testdata_track_array;
 };
 
 extern struct __nmtst_internal __nmtst_internal;
@@ -252,6 +262,62 @@ nmtst_initialized(void)
     return !!__nmtst_internal.rand0;
 }
 
+/*****************************************************************************/
+
+static inline void
+_nmtst_testdata_track_clear_func(gpointer ptr)
+{
+    struct __nmtst_testdata_track *d = ptr;
+
+    if (d->destroy_notify)
+        d->destroy_notify(d->data);
+    memset(d, 0, sizeof(*d));
+}
+
+static inline void
+_nmtst_testdata_track_add(gpointer data, GDestroyNotify destroy_notify)
+{
+    struct __nmtst_testdata_track d = {
+        .data           = data,
+        .destroy_notify = destroy_notify,
+    };
+
+    g_assert(data);
+    g_assert(destroy_notify);
+    g_assert(__nmtst_internal.testdata_track_array);
+
+    g_array_append_val(__nmtst_internal.testdata_track_array, d);
+}
+
+static inline void
+_nmtst_testdata_track_steal(gpointer data)
+{
+    struct __nmtst_testdata_track *d;
+    guint                          i;
+
+    g_assert(data);
+    g_assert(__nmtst_internal.testdata_track_array);
+
+    for (i = 0; i < __nmtst_internal.testdata_track_array->len; i++) {
+        d = &g_array_index(__nmtst_internal.testdata_track_array, struct __nmtst_testdata_track, i);
+
+        if (d->data != data)
+            continue;
+
+        d->destroy_notify = NULL;
+        g_array_remove_index_fast(__nmtst_internal.testdata_track_array, i);
+        return;
+    }
+    g_assert_not_reached();
+}
+
+static inline void
+_nmtst_testdata_track_steal_and_free(gpointer data)
+{
+    _nmtst_testdata_track_steal(data);
+    g_free(data);
+}
+
 #define __NMTST_LOG(cmd, ...)                                                                  \
     G_STMT_START                                                                               \
     {                                                                                          \
@@ -309,7 +375,7 @@ BREAK_INNER_LOOPS:
         str = &str[i];
     }
 
-    return (char **) g_array_free(result, FALSE);
+    return (char **) ((gpointer) g_array_free(result, FALSE));
 }
 
 /* free instances allocated by nmtst (especially nmtst_init()) on shutdown
@@ -320,6 +386,9 @@ nmtst_free(void)
     if (!nmtst_initialized())
         return;
 
+    g_array_set_size(__nmtst_internal.testdata_track_array, 0);
+    g_array_unref(__nmtst_internal.testdata_track_array);
+
     g_rand_free(__nmtst_internal.rand0);
     if (__nmtst_internal.rand)
         g_rand_free(__nmtst_internal.rand);
@@ -576,10 +645,14 @@ __nmtst_init(int        *argc,
     __nmtst_internal.sudo_cmd          = sudo_cmd;
     __nmtst_internal.no_expect_message = no_expect_message;
 
+    __nmtst_internal.testdata_track_array =
+        g_array_new(FALSE, FALSE, sizeof(struct __nmtst_testdata_track));
+    g_array_set_clear_func(__nmtst_internal.testdata_track_array, _nmtst_testdata_track_clear_func);
+
     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";
+        log_level = is_debug ? "TRACE" : "WARN";
     }
 
     if (!__nmtst_internal.assert_logging) {
@@ -661,9 +734,9 @@ __nmtst_init(int        *argc,
 
     /* 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));
+        __NMTST_LOG(g_message, "%s", nm_g_array_index(debug_messages, const char *, i));
 
-    g_strfreev((char **) g_array_free(debug_messages, FALSE));
+    g_strfreev((char **) ((gpointer) g_array_free(debug_messages, FALSE)));
     g_free(c_log_level);
     g_free(c_log_domains);
 
@@ -699,6 +772,12 @@ nmtst_init(int *argc, char ***argv, gboolean assert_logging)
 static inline gboolean
 nmtst_is_debug(void)
 {
+    /* This is based on the "debug"/"no-debug" flag in "$NMTST_DEBUG".
+     *
+     * If debugging is enabled, print more information. However, make sure
+     * that the test behaves still in a similar manner and that the same code
+     * path are taken where it matters (it matters for example, if the code path
+     * consumes random numbers). */
     g_assert(nmtst_initialized());
     return __nmtst_internal.is_debug;
 }
@@ -774,15 +853,12 @@ typedef struct _NmtstTestData NmtstTestData;
 typedef void (*NmtstTestHandler)(const NmtstTestData *test_data);
 
 struct _NmtstTestData {
-    union {
-        const char *testpath;
-        char       *_testpath;
-    };
+    const char      *testpath;
     gsize            n_args;
-    gpointer        *args;
     NmtstTestHandler _func_setup;
     GTestDataFunc    _func_test;
     NmtstTestHandler _func_teardown;
+    gpointer         args[];
 };
 
 static inline void
@@ -807,15 +883,20 @@ _nmtst_test_data_unpack(const NmtstTestData *test_data, gsize n_args, ...)
 #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)
+static inline const char *
+nmtst_test_get_path(void)
 {
-    NmtstTestData *test_data = data;
+    g_assert(nmtst_initialized());
+    g_assert(__nmtst_internal.testpath);
 
-    g_assert(test_data);
+    /* Similar to g_test_get_path() (which only exists since glib 2.68).
+     *
+     * This is the test name while running the test added with
+     * nmtst_add_test_func*().
+     *
+     * You are only allowed to call this from inside such a test. */
 
-    g_free(test_data->_testpath);
-    g_free(test_data);
+    return __nmtst_internal.testpath;
 }
 
 static inline void
@@ -823,6 +904,13 @@ _nmtst_test_run(gconstpointer data)
 {
     const NmtstTestData *test_data = data;
 
+    g_assert(test_data);
+    g_assert(nmtst_initialized());
+    g_assert(test_data->testpath);
+    g_assert(!__nmtst_internal.testpath);
+
+    __nmtst_internal.testpath = test_data->testpath;
+
     if (test_data->_func_setup)
         test_data->_func_setup(test_data);
 
@@ -830,6 +918,10 @@ _nmtst_test_run(gconstpointer data)
 
     if (test_data->_func_teardown)
         test_data->_func_teardown(test_data);
+
+    g_assert(__nmtst_internal.testpath);
+    g_assert(__nmtst_internal.testpath == test_data->testpath);
+    __nmtst_internal.testpath = NULL;
 }
 
 static inline void
@@ -843,26 +935,41 @@ _nmtst_add_test_func_full(const char      *testpath,
     gsize          i;
     NmtstTestData *data;
     va_list        ap;
+    gsize          testpath_len;
 
     g_assert(testpath && testpath[0]);
     g_assert(func_test);
 
-    data = g_malloc0(sizeof(NmtstTestData) + (sizeof(gpointer) * (n_args + 1)));
+    testpath_len = strlen(testpath) + 1u;
+
+    data = g_malloc(G_STRUCT_OFFSET(NmtstTestData, args)
+                    + (sizeof(gpointer) * (n_args + 1u) + testpath_len));
+
+    *data = (NmtstTestData){
+        .testpath       = (gpointer) &data->args[n_args + 1u],
+        ._func_test     = func_test,
+        ._func_setup    = func_setup,
+        ._func_teardown = func_teardown,
+        .n_args         = n_args,
+    };
 
-    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);
+    g_assert(data->testpath == (gpointer) &data->args[i + 1]);
+
+    memcpy((char *) data->testpath, testpath, testpath_len);
+
+    _nmtst_testdata_track_add(data, g_free);
+    g_test_add_data_func_full(testpath,
+                              data,
+                              _nmtst_test_run,
+                              _nmtst_testdata_track_steal_and_free);
 }
+
 #define nmtst_add_test_func_full(testpath, func_test, func_setup, func_teardown, ...) \
     _nmtst_add_test_func_full(testpath,                                               \
                               func_test,                                              \
@@ -870,6 +977,7 @@ _nmtst_add_test_func_full(const char      *testpath,
                               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__)
 
@@ -1192,6 +1300,28 @@ nmtst_get_rand_word_length(GRand *rand)
 /*****************************************************************************/
 
 static inline gboolean
+nmtst_true_once(gboolean *state, gboolean new_val)
+{
+    /* Returns only once a TRUE flag. When returning TRUE,
+     * it will be remembered in "state" and future invocations
+     * return FALSE.
+     *
+     * Also, if "new_val" is FALSE, it won't return TRUE.
+     *
+     * The point is to do an action once (depending on "new_val"),
+     * and remember it in "state".
+     */
+    if (!new_val)
+        return FALSE;
+    if (*state)
+        return FALSE;
+    *state = TRUE;
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static inline gboolean
 nmtst_g_source_assert_not_called(gpointer user_data)
 {
     g_assert_not_reached();
@@ -1564,7 +1694,7 @@ nmtst_inet4_from_string(const char *str)
 }
 
 static inline const struct in6_addr *
-nmtst_inet6_from_string(const char *str)
+nmtst_inet6_from_string_p(const char *str)
 {
     static _nm_thread_local struct in6_addr addr;
     int                                     success;
@@ -1579,6 +1709,22 @@ nmtst_inet6_from_string(const char *str)
     return &addr;
 }
 
+static inline struct in6_addr
+nmtst_inet6_from_string(const char *str)
+{
+    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)
 {
@@ -1589,7 +1735,7 @@ nmtst_inet_from_string(int addr_family, const char *str)
         return &a;
     }
     if (addr_family == AF_INET6)
-        return nmtst_inet6_from_string(str);
+        return nmtst_inet6_from_string_p(str);
 
     g_assert_not_reached();
     return NULL;
@@ -1648,7 +1794,7 @@ _nmtst_assert_ip6_address(const char            *file,
     if (!addr)
         addr = &any;
 
-    if (memcmp(nmtst_inet6_from_string(str_expected), addr, sizeof(*addr)) != 0) {
+    if (memcmp(nmtst_inet6_from_string_p(str_expected), addr, sizeof(*addr)) != 0) {
         char buf[100];
 
         g_error("%s:%d: Unexpected IPv6 address: expected %s, got %s",
@@ -1917,7 +2063,7 @@ nmtst_logging_disable(gboolean always)
         return NULL;
     }
 
-    p = g_memdup(_nm_logging_enabled_state, sizeof(_nm_logging_enabled_state));
+    p = nm_memdup(_nm_logging_enabled_state, sizeof(_nm_logging_enabled_state));
     memset(_nm_logging_enabled_state, 0, sizeof(_nm_logging_enabled_state));
     return p;
 }
@@ -1944,9 +2090,9 @@ nmtst_setting_ip_config_add_address(NMSettingIPConfig *s_ip, const char *address
 
     g_assert(s_ip);
 
-    if (nm_utils_ipaddr_is_valid(AF_INET, address))
+    if (nm_inet_is_valid(AF_INET, address))
         family = AF_INET;
-    else if (nm_utils_ipaddr_is_valid(AF_INET6, address))
+    else if (nm_inet_is_valid(AF_INET6, address))
         family = AF_INET6;
     else
         g_assert_not_reached();
@@ -1969,9 +2115,9 @@ nmtst_setting_ip_config_add_route(NMSettingIPConfig *s_ip,
 
     g_assert(s_ip);
 
-    if (nm_utils_ipaddr_is_valid(AF_INET, dest))
+    if (nm_inet_is_valid(AF_INET, dest))
         family = AF_INET;
-    else if (nm_utils_ipaddr_is_valid(AF_INET6, dest))
+    else if (nm_inet_is_valid(AF_INET6, dest))
         family = AF_INET6;
     else
         g_assert_not_reached();
@@ -2682,7 +2828,8 @@ _nmtst_variant_new_vardict(int dummy, ...)
     G_STMT_END
 #else
 #define _nmtst_assert_variant_bytestring_cmp_str(_ptr, _ptr2, _len) \
-    G_STMT_START {}                                                 \
+    G_STMT_START                                                    \
+    {}                                                              \
     G_STMT_END
 #endif
 
@@ -2913,7 +3060,7 @@ nmtst_ip_address_new(int addr_family, const char *str)
     GError      *error = NULL;
     NMIPAddress *a;
 
-    if (!nm_utils_parse_inaddr_prefix_bin(addr_family, str, &addr_family, &addr, &plen))
+    if (!nm_inet_parse_with_prefix_bin(addr_family, str, &addr_family, &addr, &plen))
         g_assert_not_reached();
 
     if (plen == -1)
@@ -2928,6 +3075,27 @@ nmtst_ip_address_new(int addr_family, const char *str)
 
 /*****************************************************************************/
 
+static inline gpointer
+nmtst_keeper_add(GPtrArray **p_arr, gpointer ptr)
+{
+    if (!p_arr) {
+        /* If not GPtrArray in/out argument is given, track the pointer
+         * via _nmtst_testdata_track_add(), which means it stays alive
+         * until the end of the test. */
+        _nmtst_testdata_track_add(ptr, g_free);
+    } else {
+        if (!*p_arr)
+            *p_arr = g_ptr_array_new_with_free_func(g_free);
+
+        g_ptr_array_add(*p_arr, ptr);
+    }
+    return ptr;
+}
+
+#define nmtst_keeper_printf(p_ptr, ...) nmtst_keeper_add((p_ptr), g_strdup_printf(__VA_ARGS__))
+
+/*****************************************************************************/
+
 #define nmtst_gbytes_from_arr(...)           \
     ({                                       \
         const guint8 _arr[] = {__VA_ARGS__}; \
diff --git a/src/libnm-glib-aux/nm-time-utils.c b/src/libnm-glib-aux/nm-time-utils.c
index f30e6a19..077d0a4c 100644
--- a/src/libnm-glib-aux/nm-time-utils.c
+++ b/src/libnm-glib-aux/nm-time-utils.c
@@ -301,8 +301,13 @@ nm_utils_monotonic_timestamp_from_boottime(guint64 boottime, gint64 timestamp_ns
 
     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);
+    if (boottime >= (guint64) G_MAXINT64) {
+        /* This indicates infinity. We keep it at such. */
+        return G_MAXINT64;
+    }
+
+    /* Note that overflow cannot happen, because bootime is non-negative, and
+     * offset is non-positive. */
 
     return (gint64) boottime + offset;
 }
@@ -318,6 +323,16 @@ nm_utils_clock_gettime_nsec(clockid_t clockid)
 }
 
 gint64
+nm_utils_clock_gettime_usec(clockid_t clockid)
+{
+    struct timespec tp;
+
+    if (clock_gettime(clockid, &tp) != 0)
+        return -NM_ERRNO_NATIVE(errno);
+    return nm_utils_timespec_to_usec(&tp);
+}
+
+gint64
 nm_utils_clock_gettime_msec(clockid_t clockid)
 {
     struct timespec tp;
@@ -326,3 +341,36 @@ nm_utils_clock_gettime_msec(clockid_t clockid)
         return -NM_ERRNO_NATIVE(errno);
     return nm_utils_timespec_to_msec(&tp);
 }
+
+/*****************************************************************************/
+
+/* Taken from systemd's map_clock_usec_internal(). */
+gint64
+nm_time_map_clock(gint64 from, gint64 from_base, gint64 to_base)
+{
+    /* Maps the time 'from' between two clocks, based on a common reference point where the first clock
+     * is at 'from_base' and the second clock at 'to_base'. Basically calculates:
+     *
+     *         from - from_base + to_base
+     *
+     * But takes care of overflows/underflows and avoids signed operations. */
+
+    if (from >= from_base) {
+        gint64 delta = from - from_base;
+
+        /* In the future */
+        if (to_base >= G_MAXINT64 - delta)
+            return G_MAXINT64;
+
+        return to_base + delta;
+
+    } else {
+        gint64 delta = from_base - from;
+
+        /* In the past */
+        if (to_base <= G_MININT64 + delta)
+            return G_MININT64;
+
+        return to_base - delta;
+    }
+}
diff --git a/src/libnm-glib-aux/nm-time-utils.h b/src/libnm-glib-aux/nm-time-utils.h
index 461d6845..d3153686 100644
--- a/src/libnm-glib-aux/nm-time-utils.h
+++ b/src/libnm-glib-aux/nm-time-utils.h
@@ -8,17 +8,38 @@
 
 #include <time.h>
 
+_nm_always_inline static inline gint64
+_nm_utils_timespec_to_xsec(const struct timespec *ts, gint64 xsec_per_sec)
+{
+    nm_assert(ts);
+
+    if (ts->tv_sec < 0 || ts->tv_nsec < 0)
+        return G_MAXINT64;
+
+    if (ts->tv_sec > ((guint64) G_MAXINT64) || ts->tv_nsec > ((guint64) G_MAXINT64)
+        || ts->tv_sec >= (G_MAXINT64 - ((gint64) ts->tv_nsec)) / xsec_per_sec)
+        return G_MAXINT64;
+
+    return (((gint64) ts->tv_sec) * xsec_per_sec)
+           + (((gint64) ts->tv_nsec) / (NM_UTILS_NSEC_PER_SEC / xsec_per_sec));
+}
+
 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);
+    return _nm_utils_timespec_to_xsec(ts, NM_UTILS_NSEC_PER_SEC);
+}
+
+static inline gint64
+nm_utils_timespec_to_usec(const struct timespec *ts)
+{
+    return _nm_utils_timespec_to_xsec(ts, NM_UTILS_USEC_PER_SEC);
 }
 
 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));
+    return _nm_utils_timespec_to_xsec(ts, NM_UTILS_MSEC_PER_SEC);
 }
 
 gint64 nm_utils_get_monotonic_timestamp_nsec(void);
@@ -36,6 +57,12 @@ nm_utils_get_monotonic_timestamp_nsec_cached(gint64 *cache_now)
 }
 
 static inline gint64
+nm_utils_get_monotonic_timestamp_usec_cached(gint64 *cache_now)
+{
+    return (*cache_now) ?: (*cache_now = nm_utils_get_monotonic_timestamp_usec());
+}
+
+static inline gint64
 nm_utils_get_monotonic_timestamp_msec_cached(gint64 *cache_now)
 {
     return (*cache_now) ?: (*cache_now = nm_utils_get_monotonic_timestamp_msec());
@@ -48,6 +75,9 @@ nm_utils_get_monotonic_timestamp_sec_cached(gint32 *cache_now)
 }
 
 gint64 nm_utils_clock_gettime_nsec(clockid_t clockid);
+gint64 nm_utils_clock_gettime_usec(clockid_t clockid);
 gint64 nm_utils_clock_gettime_msec(clockid_t clockid);
 
+gint64 nm_time_map_clock(gint64 from, gint64 from_base, gint64 to_base);
+
 #endif /* __NM_TIME_UTILS_H__ */
diff --git a/src/libnm-glib-aux/nm-uuid.c b/src/libnm-glib-aux/nm-uuid.c
index 53e8b78c..cdfa5f62 100644
--- a/src/libnm-glib-aux/nm-uuid.c
+++ b/src/libnm-glib-aux/nm-uuid.c
@@ -12,7 +12,8 @@
 const NMUuid nm_uuid_ns_zero =
     NM_UUID_INIT(00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00);
 
-/* arbitrarily chosen namespace UUID for nm_uuid_generate_from_strings() */
+/* arbitrarily chosen namespace UUID for some uses of nm_uuid_generate_from_strings_old().
+ * Try not to re-use this namespace, instead, generate a unique one. */
 const NMUuid nm_uuid_ns_1 =
     NM_UUID_INIT(b4, 25, e9, fb, 75, 98, 44, b4, 9e, 3b, 5a, 2e, 3a, aa, 49, 05);
 
@@ -406,42 +407,85 @@ nm_uuid_generate_from_string_str(const char   *s,
 }
 
 /**
- * nm_uuid_generate_from_strings:
- * @string1: a variadic list of strings. Must be NULL terminated.
+ * nm_uuid_generate_from_strings_strv:
+ * @uuid_type: the UUID type to use. Prefer version 5 unless you have
+ *   good reasons.
+ * @type_args: the namespace UUID.
+ * @strv: (allow-none): the strv list to hash. Can be NULL, in which
+ *   case the result is different from an empty array.
+ * @len: if negative, @strv is a NULL terminated array. Otherwise,
+ *   it is the length of the strv array. In the latter case it may
+ *   also contain NULL strings. The result hashes differently depending
+ *   on whether we have a NULL terminated strv array or given length.
  *
- * Returns a variant3 UUID based on the concatenated C strings.
+ * Returns a @uuid_type 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, ...)
+nm_uuid_generate_from_strings_strv(NMUuidType         uuid_type,
+                                   const NMUuid      *type_args,
+                                   const char *const *strv,
+                                   gssize             len)
 {
-    if (!string1)
-        return nm_uuid_generate_from_string_str(NULL, 0, NM_UUID_TYPE_VERSION3, &nm_uuid_ns_1);
+    nm_auto_str_buf NMStrBuf str = NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_232, TRUE);
+    gsize                    slen;
+    const char              *s;
 
-    {
-        nm_auto_str_buf NMStrBuf str = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_104, FALSE);
-        va_list                  args;
-        const char              *s;
+    if (len >= 0) {
+        gboolean has_nulls = FALSE;
+        gssize   i;
 
-        nm_str_buf_append_len(&str, string1, strlen(string1) + 1u);
+        nm_assert(len == 0 || strv);
 
-        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 *);
+        for (i = 0; i < len; i++) {
+            if (strv[i])
+                nm_str_buf_append_len(&str, strv[i], strlen(strv[i]) + 1u);
+            else
+                has_nulls = TRUE;
         }
-        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_ns_1);
+        if (has_nulls) {
+            /* We either support a NULL terminated strv array, or a ptr array of fixed
+             * length (@len argument).
+             *
+             * If there are no NULLs within the first @len strings, then the result
+             * is the same. If there are any NULL strings, we need to encode that
+             * in a unique way. We do that by appending a bitmap of the elements
+             * whether they were set, plus one 'n' character (without NUL termination).
+             * None of the other branches below hashes to that, so this will uniquely
+             * encoded the NULL strings.
+             */
+            for (i = 0; i < len; i++)
+                nm_str_buf_append_c(&str, strv[i] ? '1' : '_');
+            nm_str_buf_append_c(&str, 'n');
+        }
+        slen = str.len;
+        s    = nm_str_buf_get_str_unsafe(&str);
+    } else if (!strv) {
+        /* NULL is treated differently from an empty strv. We achieve that
+         * by using a non-empty, non-NUL terminated string (which cannot happen
+         * in the other cases). */
+        slen = 1;
+        s    = "x";
+    } else if (!strv[0]) {
+        slen = 0;
+        s    = "";
+    } else if (!strv[1]) {
+        slen = strlen(strv[0]) + 1u;
+        s    = strv[0];
+    } else {
+        /* We concatenate the NUL termiated string, including the NUL
+         * character. This way, ("a","a"), ("aa"), ("aa","") all hash
+         * differently. */
+        for (; strv[0]; strv++)
+            nm_str_buf_append_len(&str, strv[0], strlen(strv[0]) + 1u);
+        slen = str.len;
+        s    = nm_str_buf_get_str_unsafe(&str);
     }
+
+    return nm_uuid_generate_from_string_str(s, slen, uuid_type, type_args);
 }
diff --git a/src/libnm-glib-aux/nm-uuid.h b/src/libnm-glib-aux/nm-uuid.h
index 15b03a2d..b8955452 100644
--- a/src/libnm-glib-aux/nm-uuid.h
+++ b/src/libnm-glib-aux/nm-uuid.h
@@ -9,6 +9,9 @@ typedef struct _NMUuid {
 
 #define NM_UUID_INIT_ZERO() ((NMUuid){.uuid = {0}})
 
+/* Beware, the 16 macro arguments are two hex-digits, not plain numbers. The macro
+ * will automatically add the "0x". In particular, "09" is not an octal number, it's
+ * 0x09. This oddity is so that the arguments look very much like the UUID in string form. */
 #define NM_UUID_INIT(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15) \
     ((NMUuid){                                                                             \
         .uuid = {(0x##a0),                                                                 \
@@ -120,7 +123,23 @@ char *nm_uuid_generate_from_string_str(const char   *s,
                                        NMUuidType    uuid_type,
                                        const NMUuid *type_args);
 
-char *nm_uuid_generate_from_strings(const char *string1, ...) G_GNUC_NULL_TERMINATED;
+char *nm_uuid_generate_from_strings_strv(NMUuidType         uuid_type,
+                                         const NMUuid      *type_args,
+                                         const char *const *strv,
+                                         gssize             len);
+
+#define nm_uuid_generate_from_strings(uuid_type, type_args, ...)  \
+    nm_uuid_generate_from_strings_strv((uuid_type),               \
+                                       (type_args),               \
+                                       NM_MAKE_STRV(__VA_ARGS__), \
+                                       NM_NARG(__VA_ARGS__))
+
+/* Legacy function. Don't use for new code. */
+#define nm_uuid_generate_from_strings_old(...)                    \
+    nm_uuid_generate_from_strings_strv(NM_UUID_TYPE_VERSION3,     \
+                                       &nm_uuid_ns_1,             \
+                                       NM_MAKE_STRV(__VA_ARGS__), \
+                                       -1)
 
 /*****************************************************************************/
 
diff --git a/src/libnm-glib-aux/tests/test-shared-general.c b/src/libnm-glib-aux/tests/test-shared-general.c
index cfe70b58..7503dc9b 100644
--- a/src/libnm-glib-aux/tests/test-shared-general.c
+++ b/src/libnm-glib-aux/tests/test-shared-general.c
@@ -11,6 +11,7 @@
 #include "libnm-glib-aux/nm-time-utils.h"
 #include "libnm-glib-aux/nm-ref-string.h"
 #include "libnm-glib-aux/nm-io-utils.h"
+#include "libnm-glib-aux/nm-prioq.h"
 
 #include "libnm-glib-aux/nm-test-utils.h"
 
@@ -77,6 +78,9 @@ test_gpid(void)
      * the case. */
     int_ptr = &pid;
     g_assert_cmpint(*int_ptr, ==, 42);
+
+    /* also check how assert() works. */
+    assert(*int_ptr == 42);
 }
 
 /*****************************************************************************/
@@ -90,6 +94,38 @@ test_monotonic_timestamp(void)
 /*****************************************************************************/
 
 static void
+test_timespect_to(void)
+{
+    struct timespec ts;
+    int             i;
+
+    for (i = 0; i < 1000; i++) {
+        gint64 t_msec;
+        gint64 t_usec;
+        gint64 t_nsec;
+
+        nmtst_rand_buf(NULL, &ts, sizeof(ts));
+        ts.tv_sec  = llabs(ts.tv_sec % 100000);
+        ts.tv_nsec = llabs(ts.tv_nsec % NM_UTILS_NSEC_PER_SEC);
+
+        t_msec = nm_utils_timespec_to_msec(&ts);
+        t_usec = nm_utils_timespec_to_usec(&ts);
+        t_nsec = nm_utils_timespec_to_nsec(&ts);
+
+        g_assert_cmpint(t_msec, <=, t_usec / 1000);
+        g_assert_cmpint(t_msec + 1, >=, t_usec / 1000);
+
+        g_assert_cmpint(t_msec, <=, t_nsec / 1000000);
+        g_assert_cmpint(t_msec + 1, >=, t_nsec / 1000000);
+
+        g_assert_cmpint(t_usec, <=, t_nsec / 1000);
+        g_assert_cmpint(t_usec + 1, >=, t_nsec / 1000);
+    }
+}
+
+/*****************************************************************************/
+
+static void
 test_nmhash(void)
 {
     int rnd;
@@ -117,8 +153,8 @@ test_make_strv(void)
     const char *const *v2a  = NM_MAKE_STRV("a", "b");
     const char *const *v2b  = NM_MAKE_STRV("a", "b", );
     const char *const  v3[] = {
-         "a",
-         "b",
+        "a",
+        "b",
     };
     const char *const *v4b = NM_MAKE_STRV("a", _make_strv_foo(), );
 
@@ -251,37 +287,41 @@ test_nm_strndup_a(void)
 /*****************************************************************************/
 
 static void
-test_nm_utils_ip4_address_is_loopback(void)
+test_nm_ip4_addr_is_loopback(void)
 {
-    g_assert(nm_utils_ip4_address_is_loopback(nmtst_inet4_from_string("127.0.0.0")));
-    g_assert(nm_utils_ip4_address_is_loopback(nmtst_inet4_from_string("127.0.0.1")));
-    g_assert(nm_utils_ip4_address_is_loopback(nmtst_inet4_from_string("127.5.0.1")));
-    g_assert(!nm_utils_ip4_address_is_loopback(nmtst_inet4_from_string("126.5.0.1")));
-    g_assert(!nm_utils_ip4_address_is_loopback(nmtst_inet4_from_string("128.5.0.1")));
-    g_assert(!nm_utils_ip4_address_is_loopback(nmtst_inet4_from_string("129.5.0.1")));
+    g_assert(nm_ip4_addr_is_loopback(nmtst_inet4_from_string("127.0.0.0")));
+    g_assert(nm_ip4_addr_is_loopback(nmtst_inet4_from_string("127.0.0.1")));
+    g_assert(nm_ip4_addr_is_loopback(nmtst_inet4_from_string("127.5.0.1")));
+    g_assert(!nm_ip4_addr_is_loopback(nmtst_inet4_from_string("126.5.0.1")));
+    g_assert(!nm_ip4_addr_is_loopback(nmtst_inet4_from_string("128.5.0.1")));
+    g_assert(!nm_ip4_addr_is_loopback(nmtst_inet4_from_string("129.5.0.1")));
+    g_assert_cmpint(nmtst_inet4_from_string("127.0.0.0"), ==, NM_IPV4LO_NETWORK);
+    g_assert_cmpint(nmtst_inet4_from_string("127.0.0.1"), ==, NM_IPV4LO_ADDR1);
+    g_assert_cmpint(nmtst_inet4_from_string("255.0.0.0"), ==, NM_IPV4LO_NETMASK);
+    g_assert_cmpint(nm_ip4_addr_netmask_to_prefix(NM_IPV4LO_NETMASK), ==, NM_IPV4LO_PREFIXLEN);
 }
 
 /*****************************************************************************/
 
 static void
-test_nm_utils_ip4_prefix_to_netmask(void)
+test_nm_ip4_addr_netmask_from_prefix(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),
+    g_assert_cmpint(nm_ip4_addr_netmask_from_prefix(0), ==, nmtst_inet4_from_string("0.0.0.0"));
+    g_assert_cmpint(nm_ip4_addr_netmask_from_prefix(1), ==, nmtst_inet4_from_string("128.0.0.0"));
+    g_assert_cmpint(nm_ip4_addr_netmask_from_prefix(2), ==, nmtst_inet4_from_string("192.0.0.0"));
+    g_assert_cmpint(nm_ip4_addr_netmask_from_prefix(16),
                     ==,
                     nmtst_inet4_from_string("255.255.0.0"));
-    g_assert_cmpint(_nm_utils_ip4_prefix_to_netmask(24),
+    g_assert_cmpint(nm_ip4_addr_netmask_from_prefix(24),
                     ==,
                     nmtst_inet4_from_string("255.255.255.0"));
-    g_assert_cmpint(_nm_utils_ip4_prefix_to_netmask(30),
+    g_assert_cmpint(nm_ip4_addr_netmask_from_prefix(30),
                     ==,
                     nmtst_inet4_from_string("255.255.255.252"));
-    g_assert_cmpint(_nm_utils_ip4_prefix_to_netmask(31),
+    g_assert_cmpint(nm_ip4_addr_netmask_from_prefix(31),
                     ==,
                     nmtst_inet4_from_string("255.255.255.254"));
-    g_assert_cmpint(_nm_utils_ip4_prefix_to_netmask(32),
+    g_assert_cmpint(nm_ip4_addr_netmask_from_prefix(32),
                     ==,
                     nmtst_inet4_from_string("255.255.255.255"));
 }
@@ -1294,7 +1334,7 @@ test_utils_hashtable_cmp(void)
     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;
+            GEqualFunc       func_key_equal = is_num_key ? NULL : g_str_equal;
             GCompareDataFunc func_key_cmp =
                 is_num_key ? _hash_func_cmp_direct : (GCompareDataFunc) nm_strcmp_with_data;
             GCompareDataFunc func_val_cmp =
@@ -2211,6 +2251,124 @@ test_hostname_is_valid(void)
 
 /*****************************************************************************/
 
+static void
+test_inet_utils(void)
+{
+    g_assert(nm_ip_addr_is_site_local(AF_INET, nmtst_inet_from_string(AF_INET, "172.16.0.1")));
+    g_assert(nm_ip_addr_is_site_local(AF_INET, nmtst_inet_from_string(AF_INET, "172.17.0.1")));
+    g_assert(nm_ip_addr_is_site_local(AF_INET, nmtst_inet_from_string(AF_INET, "192.168.7.5")));
+    g_assert(!nm_ip_addr_is_site_local(AF_INET, nmtst_inet_from_string(AF_INET, "192.0.7.5")));
+    g_assert(nm_ip_addr_is_site_local(AF_INET6, nmtst_inet_from_string(AF_INET6, "fec0::")));
+    g_assert(!nm_ip_addr_is_site_local(AF_INET6, nmtst_inet_from_string(AF_INET6, "fc00::")));
+}
+
+/*****************************************************************************/
+
+static void
+test_garray(void)
+{
+    gs_unref_array GArray *arr = NULL;
+    int                    v;
+
+    arr = g_array_new(FALSE, FALSE, sizeof(int));
+    g_assert(nm_g_array_index_p(arr, int, 0) == (gpointer) arr->data);
+
+    v = 1;
+    g_array_append_val(arr, v);
+    g_assert(nm_g_array_index_p(arr, int, 0) == (gpointer) arr->data);
+    g_assert(nm_g_array_index_p(arr, int, 1) == ((int *) ((gpointer) arr->data)) + 1);
+    g_assert(&nm_g_array_index(arr, int, 0) == (gpointer) arr->data);
+    g_assert(&nm_g_array_first(arr, int) == (gpointer) arr->data);
+    g_assert(&nm_g_array_last(arr, int) == (gpointer) arr->data);
+    g_assert(nm_g_array_index(arr, int, 0) == 1);
+
+    v = 2;
+    g_array_append_val(arr, v);
+    g_assert(nm_g_array_index_p(arr, int, 0) == (gpointer) arr->data);
+    g_assert(nm_g_array_index_p(arr, int, 1) == ((int *) ((gpointer) arr->data)) + 1);
+    g_assert(nm_g_array_index_p(arr, int, 2) == ((int *) ((gpointer) arr->data)) + 2);
+    g_assert(&nm_g_array_index(arr, int, 0) == (gpointer) arr->data);
+    g_assert(&nm_g_array_first(arr, int) == (gpointer) arr->data);
+    g_assert(&nm_g_array_last(arr, int) == ((int *) ((gpointer) arr->data)) + 1);
+    g_assert(nm_g_array_index(arr, int, 0) == 1);
+    g_assert(nm_g_array_index(arr, int, 1) == 2);
+}
+
+/*****************************************************************************/
+
+static int
+_prioq_cmp(gconstpointer a, gconstpointer b)
+{
+    NM_CMP_DIRECT(GPOINTER_TO_UINT(a), GPOINTER_TO_UINT(b));
+    return 0;
+}
+
+static int
+_prioq_cmp_with_data(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+    return _prioq_cmp(a, b);
+}
+
+static void
+test_nm_prioq(void)
+{
+    nm_auto_prioq NMPrioq q = NM_PRIOQ_ZERO;
+    gpointer              data[200];
+    gpointer              data_pop[200];
+    guint                 data_idx[G_N_ELEMENTS(data)];
+    guint                 i;
+    guint                 n;
+    gpointer              p;
+
+    if (nmtst_get_rand_one_case_in(10))
+        return;
+
+    if (nmtst_get_rand_bool())
+        nm_prioq_init(&q, _prioq_cmp);
+    else
+        nm_prioq_init_with_data(&q, _prioq_cmp_with_data, NULL);
+
+    g_assert(nm_prioq_size(&q) == 0);
+
+    if (nmtst_get_rand_one_case_in(10))
+        return;
+
+    for (i = 0; i < G_N_ELEMENTS(data); i++) {
+        data[i]     = GUINT_TO_POINTER((nmtst_get_rand_uint32() % G_N_ELEMENTS(data)) + 1u);
+        data_idx[i] = NM_PRIOQ_IDX_NULL;
+    }
+
+    nm_prioq_put(&q, data[0], NULL);
+    g_assert(nm_prioq_size(&q) == 1);
+
+    p = nm_prioq_pop(&q);
+    g_assert(p == data[0]);
+    g_assert(nm_prioq_size(&q) == 0);
+
+    g_assert(!nm_prioq_pop(&q));
+
+    n = nmtst_get_rand_uint32() % G_N_ELEMENTS(data);
+    for (i = 0; i < n; i++)
+        nm_prioq_put(&q, data[i], &data_idx[i]);
+
+    g_assert_cmpint(nm_prioq_size(&q), ==, n);
+
+    if (nmtst_get_rand_one_case_in(10))
+        return;
+
+    for (i = 0; i < n; i++) {
+        data_pop[i] = nm_prioq_pop(&q);
+        g_assert(data_pop[i]);
+        if (i > 0)
+            g_assert(_prioq_cmp(data_pop[i - 1], data_pop[i]) <= 0);
+    }
+
+    g_assert(!nm_prioq_pop(&q));
+    g_assert(nm_prioq_size(&q) == 0);
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE();
 
 int
@@ -2221,14 +2379,14 @@ main(int argc, char **argv)
     g_test_add_func("/general/test_nm_static_assert", test_nm_static_assert);
     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_timespect_to", test_timespect_to);
     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_utils_ip4_address_is_loopback",
-                    test_nm_utils_ip4_address_is_loopback);
-    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_nm_ip4_addr_is_loopback", test_nm_ip4_addr_is_loopback);
+    g_test_add_func("/general/test_nm_ip4_addr_netmask_from_prefix",
+                    test_nm_ip4_addr_netmask_from_prefix);
     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);
@@ -2256,6 +2414,9 @@ main(int argc, char **argv)
     g_test_add_func("/general/test_path_startswith", test_path_startswith);
     g_test_add_func("/general/test_path_simplify", test_path_simplify);
     g_test_add_func("/general/test_hostname_is_valid", test_hostname_is_valid);
+    g_test_add_func("/general/test_inet_utils", test_inet_utils);
+    g_test_add_func("/general/test_garray", test_garray);
+    g_test_add_func("/general/test_nm_prioq", test_nm_prioq);
 
     return g_test_run();
 }