/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2004 - 2018 Red Hat, Inc.
 * Copyright (C) 2005 - 2008 Novell, Inc.
 */

#include "src/core/nm-default-daemon.h"

#include "nm-core-utils.h"

#include <fcntl.h>
#include <fnmatch.h>
#include <unistd.h>
#include <stdlib.h>
#include <resolv.h>
#include <byteswap.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <linux/if.h>
#include <linux/if_infiniband.h>
#include <net/if_arp.h>
#include <net/ethernet.h>

#include "libnm-glib-aux/nm-uuid.h"
#include "libnm-platform/nmp-base.h"
#include "libnm-std-aux/unaligned.h"
#include "libnm-glib-aux/nm-random-utils.h"
#include "libnm-glib-aux/nm-io-utils.h"
#include "libnm-glib-aux/nm-secret-utils.h"
#include "libnm-glib-aux/nm-time-utils.h"
#include "libnm-glib-aux/nm-str-buf.h"
#include "nm-utils.h"
#include "libnm-core-intern/nm-core-internal.h"
#include "nm-setting-connection.h"
#include "nm-setting-ip4-config.h"
#include "nm-setting-ip6-config.h"
#include "nm-setting-wireless.h"
#include "nm-setting-wireless-security.h"

#ifdef __NM_SD_UTILS_H__
#error \
    "nm-core-utils.c should stay independent of systemd utils. Are you looking for NetworkMangerUtils.c? "
#endif

G_STATIC_ASSERT(sizeof(NMUtilsTestFlags) <= sizeof(int));

/* we read _nm_utils_testing without memory barrier. This is thread-safe,
 * because the static variable is initialized to zero, and only reset
 * once to a non-zero value (via g_atomic_int_compare_and_exchange()).
 *
 * Since there is only one integer that contains the data, there is no
 * caching problem reading this (atomic int) variable without
 * synchronization/memory-barrier. Contrary to a double-checked locking,
 * where one needs a memory barrier to read the variable and ensure
 * that also the related data is coherent in cache. Here there is no
 * related data. */
static int _nm_utils_testing = 0;

gboolean
nm_utils_get_testing_initialized()
{
    NMUtilsTestFlags flags;

    flags = (NMUtilsTestFlags) _nm_utils_testing;
    if (flags == NM_UTILS_TEST_NONE)
        flags = (NMUtilsTestFlags) g_atomic_int_get(&_nm_utils_testing);
    return flags != NM_UTILS_TEST_NONE;
}

NMUtilsTestFlags
nm_utils_get_testing()
{
    NMUtilsTestFlags flags;

again:
    flags = (NMUtilsTestFlags) _nm_utils_testing;
    if (flags != NM_UTILS_TEST_NONE) {
        /* Flags already initialized. Return them. */
        return flags & NM_UTILS_TEST_ALL;
    }

    /* Accessing nm_utils_get_testing() causes us to set the flags to initialized.
     * Detecting running tests also based on g_test_initialized(). */
    flags = _NM_UTILS_TEST_INITIALIZED;
    if (g_test_initialized())
        flags |= _NM_UTILS_TEST_GENERAL;

    g_atomic_int_compare_and_exchange(&_nm_utils_testing, 0, (int) flags);

    /* regardless of whether we won the race of initializing _nm_utils_testing,
     * go back and read the value again. It must be non-zero by now. */
    goto again;
}

void
_nm_utils_set_testing(NMUtilsTestFlags flags)
{
    g_assert(!NM_FLAGS_ANY(flags, ~NM_UTILS_TEST_ALL));

    /* mask out everything except ALL, and always set GENERAL. */
    flags = (flags & NM_UTILS_TEST_ALL) | (_NM_UTILS_TEST_GENERAL | _NM_UTILS_TEST_INITIALIZED);

    if (!g_atomic_int_compare_and_exchange(&_nm_utils_testing, 0, (int) flags)) {
        /* We only allow setting _nm_utils_set_testing() once, before fetching the
         * value with nm_utils_get_testing(). */
        g_return_if_reached();
    }
}

/*****************************************************************************/

static GSList  *_singletons          = NULL;
static gboolean _singletons_shutdown = FALSE;

static void
_nm_singleton_instance_weak_cb(gpointer data, GObject *where_the_object_was)
{
    nm_assert(g_slist_find(_singletons, where_the_object_was));

    _singletons = g_slist_remove(_singletons, where_the_object_was);
}

static void __attribute__((destructor)) _nm_singleton_instance_destroy(void)
{
    _singletons_shutdown = TRUE;

    while (_singletons) {
        GObject *instance = _singletons->data;

        _singletons = g_slist_delete_link(_singletons, _singletons);

        g_object_weak_unref(instance, _nm_singleton_instance_weak_cb, NULL);

        if (instance->ref_count > 1) {
            nm_log_dbg(LOGD_CORE,
                       "disown %s singleton (" NM_HASH_OBFUSCATE_PTR_FMT ")",
                       G_OBJECT_TYPE_NAME(instance),
                       NM_HASH_OBFUSCATE_PTR(instance));
        }

        g_object_unref(instance);
    }
}

void
_nm_singleton_instance_register_destruction(GObject *instance)
{
    g_return_if_fail(G_IS_OBJECT(instance));

    /* Don't allow registration after shutdown. We only destroy the singletons
     * once. */
    g_return_if_fail(!_singletons_shutdown);

    g_object_weak_ref(instance, _nm_singleton_instance_weak_cb, NULL);

    _singletons = g_slist_prepend(_singletons, instance);
}

/*****************************************************************************/

gboolean
nm_ether_addr_is_valid(const NMEtherAddr *addr)
{
    static const guint8 invalid_addr[][ETH_ALEN] = {
        {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
        {0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
        {0x44, 0x44, 0x44, 0x44, 0x44, 0x44},
        {0x00, 0x30, 0xb4, 0x00, 0x00, 0x00}, /* prism54 dummy MAC */
    };
    int i;

    if (!addr)
        return FALSE;

    /* Check for multicast address */
    if (addr->ether_addr_octet[0] & 0x01u)
        return FALSE;

    for (i = 0; i < (int) G_N_ELEMENTS(invalid_addr); i++) {
        if (memcmp(addr, invalid_addr[i], ETH_ALEN) == 0)
            return FALSE;
    }

    return TRUE;
}

gboolean
nm_ether_addr_is_valid_str(const char *str)
{
    NMEtherAddr addr_bin;

    if (!str)
        return FALSE;

    if (!nm_utils_hwaddr_aton(str, &addr_bin, ETH_ALEN))
        return FALSE;

    return nm_ether_addr_is_valid(&addr_bin);
}

/*****************************************************************************/

void
nm_utils_array_remove_at_indexes(GArray *array, const guint *indexes_to_delete, gsize len)
{
    gsize elt_size;
    guint index_to_delete;
    guint i_src;
    guint mm_src, mm_dst, mm_len;
    gsize i_itd;
    guint res_length;

    g_return_if_fail(array);
    if (!len)
        return;
    g_return_if_fail(indexes_to_delete);

    elt_size = g_array_get_element_size(array);

    i_itd           = 0;
    index_to_delete = indexes_to_delete[0];
    if (index_to_delete >= array->len)
        g_return_if_reached();

    res_length = array->len - 1;

    mm_dst = index_to_delete;
    mm_src = index_to_delete;
    mm_len = 0;

    for (i_src = index_to_delete; i_src < array->len; i_src++) {
        if (i_src < index_to_delete)
            mm_len++;
        else {
            /* we require indexes_to_delete to contain non-repeated, ascending
             * indexes. Otherwise, we would need to presort the indexes. */
            while (TRUE) {
                guint dd;

                if (i_itd + 1 >= len) {
                    index_to_delete = G_MAXUINT;
                    break;
                }

                dd = indexes_to_delete[++i_itd];
                if (dd > index_to_delete) {
                    if (dd >= array->len)
                        g_warn_if_reached();
                    else {
                        g_assert(res_length > 0);
                        res_length--;
                    }
                    index_to_delete = dd;
                    break;
                }
                g_warn_if_reached();
            }

            if (mm_len) {
                memmove(&array->data[mm_dst * elt_size],
                        &array->data[mm_src * elt_size],
                        mm_len * elt_size);
                mm_dst += mm_len;
                mm_src += mm_len + 1;
                mm_len = 0;
            } else
                mm_src++;
        }
    }
    if (mm_len) {
        memmove(&array->data[mm_dst * elt_size],
                &array->data[mm_src * elt_size],
                mm_len * elt_size);
    }
    g_array_set_size(array, res_length);
}

/*****************************************************************************/

typedef struct {
    pid_t       pid;
    NMLogDomain log_domain;
    union {
        struct {
            gint64 wait_start_us;
            guint  source_timeout_kill_id;
        } async;
        struct {
            gboolean success;
            int      child_status;
        } sync;
    };
    NMUtilsKillChildAsyncCb callback;
    void                   *user_data;

    char log_name[1]; /* variable-length object, must be last element!! */
} KillChildAsyncData;

#define LOG_NAME_FMT         "kill child process '%s' (%ld)"
#define LOG_NAME_PROCESS_FMT "kill process '%s' (%ld)"
#define LOG_NAME_ARGS        log_name, (long) pid

static KillChildAsyncData *
_kc_async_data_alloc(pid_t                   pid,
                     NMLogDomain             log_domain,
                     const char             *log_name,
                     NMUtilsKillChildAsyncCb callback,
                     void                   *user_data)
{
    KillChildAsyncData *data;
    size_t              log_name_len;

    /* append the name at the end of our KillChildAsyncData. */
    log_name_len = strlen(LOG_NAME_FMT) + 20 + strlen(log_name);
    data         = g_malloc(sizeof(KillChildAsyncData) - 1 + log_name_len);
    g_snprintf(data->log_name, log_name_len, LOG_NAME_FMT, LOG_NAME_ARGS);

    data->pid        = pid;
    data->user_data  = user_data;
    data->callback   = callback;
    data->log_domain = log_domain;

    return data;
}

#define KC_EXIT_TO_STRING_BUF_SIZE 128
static const char *
_kc_exit_to_string(char *buf, int exit)
#define _kc_exit_to_string(buf, exit)                                                          \
    (G_STATIC_ASSERT_EXPR(sizeof(buf) == KC_EXIT_TO_STRING_BUF_SIZE && sizeof((buf)[0]) == 1), \
     _kc_exit_to_string(buf, exit))
{
    if (WIFEXITED(exit))
        g_snprintf(buf, KC_EXIT_TO_STRING_BUF_SIZE, "normally with status %d", WEXITSTATUS(exit));
    else if (WIFSIGNALED(exit))
        g_snprintf(buf, KC_EXIT_TO_STRING_BUF_SIZE, "by signal %d", WTERMSIG(exit));
    else
        g_snprintf(buf, KC_EXIT_TO_STRING_BUF_SIZE, "with unexpected status %d", exit);
    return buf;
}

static const char *
_kc_signal_to_string(int sig)
{
    switch (sig) {
    case 0:
        return "no signal (0)";
    case SIGKILL:
        return "SIGKILL (" G_STRINGIFY(SIGKILL) ")";
    case SIGTERM:
        return "SIGTERM (" G_STRINGIFY(SIGTERM) ")";
    default:
        return "Unexpected signal";
    }
}

#define KC_WAITED_TO_STRING 100
static const char *
_kc_waited_to_string(char *buf, gint64 wait_start_us)
#define _kc_waited_to_string(buf, wait_start_us)                                        \
    (G_STATIC_ASSERT_EXPR(sizeof(buf) == KC_WAITED_TO_STRING && sizeof((buf)[0]) == 1), \
     _kc_waited_to_string(buf, wait_start_us))
{
    g_snprintf(buf,
               KC_WAITED_TO_STRING,
               " (%ld usec elapsed)",
               (long) (nm_utils_get_monotonic_timestamp_usec() - wait_start_us));
    return buf;
}

static void
_kc_cb_watch_child(GPid pid, int status, gpointer user_data)
{
    KillChildAsyncData *data = user_data;
    char                buf_exit[KC_EXIT_TO_STRING_BUF_SIZE], buf_wait[KC_WAITED_TO_STRING];

    if (data->async.source_timeout_kill_id)
        g_source_remove(data->async.source_timeout_kill_id);

    nm_log_dbg(data->log_domain,
               "%s: terminated %s%s",
               data->log_name,
               _kc_exit_to_string(buf_exit, status),
               _kc_waited_to_string(buf_wait, data->async.wait_start_us));

    if (data->callback)
        data->callback(pid, TRUE, status, data->user_data);

    g_free(data);
}

static gboolean
_kc_cb_timeout_grace_period(void *user_data)
{
    KillChildAsyncData *data = user_data;
    int                 ret, errsv;

    data->async.source_timeout_kill_id = 0;

    if ((ret = kill(data->pid, SIGKILL)) != 0) {
        errsv = errno;
        /* ESRCH means, process does not exist or is already a zombie. */
        if (errsv != ESRCH) {
            nm_log_err(LOGD_CORE | data->log_domain,
                       "%s: kill(SIGKILL) returned unexpected return value %d: (%s, %d)",
                       data->log_name,
                       ret,
                       nm_strerror_native(errsv),
                       errsv);
        }
    } else {
        nm_log_dbg(data->log_domain,
                   "%s: process not terminated after %ld usec. Sending SIGKILL signal",
                   data->log_name,
                   (long) (nm_utils_get_monotonic_timestamp_usec() - data->async.wait_start_us));
    }

    return G_SOURCE_REMOVE;
}

static gboolean
_kc_invoke_callback_idle(gpointer user_data)
{
    KillChildAsyncData *data = user_data;

    if (data->sync.success) {
        char buf_exit[KC_EXIT_TO_STRING_BUF_SIZE];

        nm_log_dbg(data->log_domain,
                   "%s: invoke callback: terminated %s",
                   data->log_name,
                   _kc_exit_to_string(buf_exit, data->sync.child_status));
    } else
        nm_log_dbg(data->log_domain, "%s: invoke callback: killing child failed", data->log_name);

    data->callback(data->pid, data->sync.success, data->sync.child_status, data->user_data);
    g_free(data);

    return G_SOURCE_REMOVE;
}

static void
_kc_invoke_callback(pid_t                   pid,
                    NMLogDomain             log_domain,
                    const char             *log_name,
                    NMUtilsKillChildAsyncCb callback,
                    void                   *user_data,
                    gboolean                success,
                    int                     child_status)
{
    KillChildAsyncData *data;

    if (!callback)
        return;

    data                    = _kc_async_data_alloc(pid, log_domain, log_name, callback, user_data);
    data->sync.success      = success;
    data->sync.child_status = child_status;

    nm_g_idle_add(_kc_invoke_callback_idle, data);
}

/* nm_utils_kill_child_async:
 * @pid: the process id of the process to kill
 * @sig: signal to send initially. Set to 0 to send not signal.
 * @log_domain: the logging domain used for logging (LOGD_NONE to suppress logging)
 * @log_name: for logging, the name of the processes to kill
 * @wait_before_kill_msec: Waittime in milliseconds before sending %SIGKILL signal. Set this value
 * to zero, not to send %SIGKILL. If @sig is already %SIGKILL, this parameter is ignored.
 * @callback: (allow-none): callback after the child terminated. This function will always
 *   be invoked asynchronously.
 * @user_data: passed on to callback
 *
 * Uses g_child_watch_add(), so note the glib comment: if you obtain pid from g_spawn_async() or
 * g_spawn_async_with_pipes() you will need to pass %G_SPAWN_DO_NOT_REAP_CHILD as flag to the spawn
 * function for the child watching to work.
 * Also note, that you must g_source_remove() any other child watchers for @pid because glib
 * supports only one watcher per child.
 **/
void
nm_utils_kill_child_async(pid_t                   pid,
                          int                     sig,
                          NMLogDomain             log_domain,
                          const char             *log_name,
                          guint32                 wait_before_kill_msec,
                          NMUtilsKillChildAsyncCb callback,
                          void                   *user_data)
{
    int                 status = 0, errsv;
    pid_t               ret;
    KillChildAsyncData *data;
    char                buf_exit[KC_EXIT_TO_STRING_BUF_SIZE];

    g_return_if_fail(pid > 0);
    g_return_if_fail(log_name != NULL);

    /* let's see if the child already terminated... */
    ret = waitpid(pid, &status, WNOHANG);
    if (ret > 0) {
        nm_log_dbg(log_domain,
                   LOG_NAME_FMT ": process %ld already terminated %s",
                   LOG_NAME_ARGS,
                   (long) ret,
                   _kc_exit_to_string(buf_exit, status));
        _kc_invoke_callback(pid, log_domain, log_name, callback, user_data, TRUE, status);
        return;
    } else if (ret != 0) {
        errsv = errno;
        /* ECHILD means, the process is not a child/does not exist or it has SIGCHILD blocked. */
        if (errsv != ECHILD) {
            nm_log_err(LOGD_CORE | log_domain,
                       LOG_NAME_FMT ": unexpected error while waitpid: %s (%d)",
                       LOG_NAME_ARGS,
                       nm_strerror_native(errsv),
                       errsv);
            _kc_invoke_callback(pid, log_domain, log_name, callback, user_data, FALSE, -1);
            return;
        }
    }

    /* send the first signal. */
    if (kill(pid, sig) != 0) {
        errsv = errno;
        /* ESRCH means, process does not exist or is already a zombie. */
        if (errsv != ESRCH) {
            nm_log_err(LOGD_CORE | log_domain,
                       LOG_NAME_FMT ": unexpected error sending %s: %s (%d)",
                       LOG_NAME_ARGS,
                       _kc_signal_to_string(sig),
                       nm_strerror_native(errsv),
                       errsv);
            _kc_invoke_callback(pid, log_domain, log_name, callback, user_data, FALSE, -1);
            return;
        }

        /* let's try again with waitpid, probably there was a race... */
        ret = waitpid(pid, &status, 0);
        if (ret > 0) {
            nm_log_dbg(log_domain,
                       LOG_NAME_FMT ": process %ld already terminated %s",
                       LOG_NAME_ARGS,
                       (long) ret,
                       _kc_exit_to_string(buf_exit, status));
            _kc_invoke_callback(pid, log_domain, log_name, callback, user_data, TRUE, status);
        } else {
            errsv = errno;
            nm_log_err(
                LOGD_CORE | log_domain,
                LOG_NAME_FMT
                ": failed due to unexpected return value %ld by waitpid (%s, %d) after sending %s",
                LOG_NAME_ARGS,
                (long) ret,
                nm_strerror_native(errsv),
                errsv,
                _kc_signal_to_string(sig));
            _kc_invoke_callback(pid, log_domain, log_name, callback, user_data, FALSE, -1);
        }
        return;
    }

    data = _kc_async_data_alloc(pid, log_domain, log_name, callback, user_data);
    data->async.wait_start_us = nm_utils_get_monotonic_timestamp_usec();

    if (sig != SIGKILL && wait_before_kill_msec > 0) {
        data->async.source_timeout_kill_id =
            g_timeout_add(wait_before_kill_msec, _kc_cb_timeout_grace_period, data);
        nm_log_dbg(log_domain,
                   "%s: wait for process to terminate after sending %s (send SIGKILL in %ld "
                   "milliseconds)...",
                   data->log_name,
                   _kc_signal_to_string(sig),
                   (long) wait_before_kill_msec);
    } else {
        data->async.source_timeout_kill_id = 0;
        nm_log_dbg(log_domain,
                   "%s: wait for process to terminate after sending %s...",
                   data->log_name,
                   _kc_signal_to_string(sig));
    }

    g_child_watch_add(pid, _kc_cb_watch_child, data);
}

static gulong
_sleep_duration_convert_ms_to_us(guint32 sleep_duration_msec)
{
    if (sleep_duration_msec > 0) {
        guint64 x = ((guint64) sleep_duration_msec) * 1000UL;

        nm_assert(x < G_MAXULONG);
        return x;
    }
    return G_USEC_PER_SEC / 20;
}

/* nm_utils_kill_child_sync:
 * @pid: process id to kill
 * @sig: signal to sent initially. If 0, no signal is sent. If %SIGKILL, the
 * second %SIGKILL signal is not sent after @wait_before_kill_msec milliseconds.
 * @log_domain: log debug information for this domain. Errors and warnings are logged both
 * as %LOGD_CORE and @log_domain.
 * @log_name: name of the process to kill for logging.
 * @child_status: (out) (allow-none): return the exit status of the child, if no error occurred.
 * @wait_before_kill_msec: Waittime in milliseconds before sending %SIGKILL signal. Set this value
 * to zero, not to send %SIGKILL. If @sig is already %SIGKILL, this parameter has not effect.
 * @sleep_duration_msec: the synchronous function sleeps repeatedly waiting for the child to terminate.
 * Set to zero, to use the default (meaning 20 wakeups per seconds).
 *
 * Kill a child process synchronously and wait. The function first checks if the child already terminated
 * and if it did, return the exit status. Otherwise, send one @sig signal. @sig  will always be
 * sent unless the child already exited. If the child does not exit within @wait_before_kill_msec milliseconds,
 * the function will send %SIGKILL and waits for the child indefinitely. If @wait_before_kill_msec is zero, no
 * %SIGKILL signal will be sent.
 *
 * In case of error, errno is preserved to contain the last reason of failure.
 **/
gboolean
nm_utils_kill_child_sync(pid_t       pid,
                         int         sig,
                         NMLogDomain log_domain,
                         const char *log_name,
                         int        *child_status,
                         guint32     wait_before_kill_msec,
                         guint32     sleep_duration_msec)
{
    int      status = 0, errsv = 0;
    pid_t    ret;
    gboolean success     = FALSE;
    gboolean was_waiting = FALSE, send_kill = FALSE;
    char     buf_exit[KC_EXIT_TO_STRING_BUF_SIZE];
    char     buf_wait[KC_WAITED_TO_STRING];
    gint64   wait_start_us;

    g_return_val_if_fail(pid > 0, FALSE);
    g_return_val_if_fail(log_name != NULL, FALSE);

    /* check if the child process already terminated... */
    ret = waitpid(pid, &status, WNOHANG);
    if (ret > 0) {
        nm_log_dbg(log_domain,
                   LOG_NAME_FMT ": process %ld already terminated %s",
                   LOG_NAME_ARGS,
                   (long) ret,
                   _kc_exit_to_string(buf_exit, status));
        success = TRUE;
        goto out;
    } else if (ret != 0) {
        errsv = errno;
        /* ECHILD means, the process is not a child/does not exist or it has SIGCHILD blocked. */
        if (errsv != ECHILD) {
            nm_log_err(LOGD_CORE | log_domain,
                       LOG_NAME_FMT ": unexpected error while waitpid: %s (%d)",
                       LOG_NAME_ARGS,
                       nm_strerror_native(errsv),
                       errsv);
            goto out;
        }
    }

    /* send first signal @sig */
    if (kill(pid, sig) != 0) {
        errsv = errno;
        /* ESRCH means, process does not exist or is already a zombie. */
        if (errsv != ESRCH) {
            nm_log_err(LOGD_CORE | log_domain,
                       LOG_NAME_FMT ": failed to send %s: %s (%d)",
                       LOG_NAME_ARGS,
                       _kc_signal_to_string(sig),
                       nm_strerror_native(errsv),
                       errsv);
        } else {
            /* let's try again with waitpid, probably there was a race... */
            ret = waitpid(pid, &status, 0);
            if (ret > 0) {
                nm_log_dbg(log_domain,
                           LOG_NAME_FMT ": process %ld already terminated %s",
                           LOG_NAME_ARGS,
                           (long) ret,
                           _kc_exit_to_string(buf_exit, status));
                success = TRUE;
            } else {
                errsv = errno;
                nm_log_err(LOGD_CORE | log_domain,
                           LOG_NAME_FMT ": failed due to unexpected return value %ld by waitpid "
                                        "(%s, %d) after sending %s",
                           LOG_NAME_ARGS,
                           (long) ret,
                           nm_strerror_native(errsv),
                           errsv,
                           _kc_signal_to_string(sig));
            }
        }
        goto out;
    }

    wait_start_us = nm_utils_get_monotonic_timestamp_usec();

    /* wait for the process to terminated... */
    if (sig != SIGKILL) {
        gint64 wait_until, now;
        gulong sleep_time, sleep_duration_usec;
        int    loop_count = 0;

        sleep_duration_usec = _sleep_duration_convert_ms_to_us(sleep_duration_msec);
        wait_until          = wait_before_kill_msec <= 0
                                  ? 0
                                  : wait_start_us + (((gint64) wait_before_kill_msec) * 1000L);

        while (TRUE) {
            ret = waitpid(pid, &status, WNOHANG);
            if (ret > 0) {
                nm_log_dbg(log_domain,
                           LOG_NAME_FMT ": after sending %s, process %ld exited %s%s",
                           LOG_NAME_ARGS,
                           _kc_signal_to_string(sig),
                           (long) ret,
                           _kc_exit_to_string(buf_exit, status),
                           was_waiting ? _kc_waited_to_string(buf_wait, wait_start_us) : "");
                success = TRUE;
                goto out;
            }
            if (ret == -1) {
                errsv = errno;
                /* ECHILD means, the process is not a child/does not exist or it has SIGCHILD blocked. */
                if (errsv != ECHILD) {
                    nm_log_err(LOGD_CORE | log_domain,
                               LOG_NAME_FMT ": after sending %s, waitpid failed with %s (%d)%s",
                               LOG_NAME_ARGS,
                               _kc_signal_to_string(sig),
                               nm_strerror_native(errsv),
                               errsv,
                               was_waiting ? _kc_waited_to_string(buf_wait, wait_start_us) : "");
                    goto out;
                }
            }

            if (!wait_until)
                break;

            now = nm_utils_get_monotonic_timestamp_usec();
            if (now >= wait_until)
                break;

            if (!was_waiting) {
                nm_log_dbg(log_domain,
                           LOG_NAME_FMT ": waiting up to %ld milliseconds for process to terminate "
                                        "normally after sending %s...",
                           LOG_NAME_ARGS,
                           (long) MAX(wait_before_kill_msec, 0),
                           _kc_signal_to_string(sig));
                was_waiting = TRUE;
            }

            sleep_time = MIN(wait_until - now, sleep_duration_usec);
            if (loop_count < 20) {
                /* At the beginning we expect the process to die fast.
                 * Limit the sleep time, the limit doubles with every iteration. */
                sleep_time = MIN(sleep_time, (((guint64) 1) << loop_count) * G_USEC_PER_SEC / 2000);
                loop_count++;
            }
            g_usleep(sleep_time);
        }

        /* send SIGKILL, if called with @wait_before_kill_msec > 0 */
        if (wait_until) {
            nm_log_dbg(log_domain, LOG_NAME_FMT ": sending SIGKILL...", LOG_NAME_ARGS);

            send_kill = TRUE;
            if (kill(pid, SIGKILL) != 0) {
                errsv = errno;
                /* ESRCH means, process does not exist or is already a zombie. */
                if (errsv != ESRCH) {
                    nm_log_err(LOGD_CORE | log_domain,
                               LOG_NAME_FMT ": failed to send SIGKILL (after sending %s), %s (%d)",
                               LOG_NAME_ARGS,
                               _kc_signal_to_string(sig),
                               nm_strerror_native(errsv),
                               errsv);
                    goto out;
                }
            }
        }
    }

    if (!was_waiting) {
        nm_log_dbg(log_domain,
                   LOG_NAME_FMT ": waiting for process to terminate after sending %s%s...",
                   LOG_NAME_ARGS,
                   _kc_signal_to_string(sig),
                   send_kill ? " and SIGKILL" : "");
    }

    /* block until the child terminates. */
    while ((ret = waitpid(pid, &status, 0)) <= 0) {
        errsv = errno;

        if (errsv != EINTR) {
            nm_log_err(LOGD_CORE | log_domain,
                       LOG_NAME_FMT ": after sending %s%s, waitpid failed with %s (%d)%s",
                       LOG_NAME_ARGS,
                       _kc_signal_to_string(sig),
                       send_kill ? " and SIGKILL" : "",
                       nm_strerror_native(errsv),
                       errsv,
                       _kc_waited_to_string(buf_wait, wait_start_us));
            goto out;
        }
    }

    nm_log_dbg(log_domain,
               LOG_NAME_FMT ": after sending %s%s, process %ld exited %s%s",
               LOG_NAME_ARGS,
               _kc_signal_to_string(sig),
               send_kill ? " and SIGKILL" : "",
               (long) ret,
               _kc_exit_to_string(buf_exit, status),
               _kc_waited_to_string(buf_wait, wait_start_us));
    success = TRUE;
out:
    if (child_status)
        *child_status = success ? status : -1;
    errno = success ? 0 : errsv;
    return success;
}

/* nm_utils_kill_process_sync:
 * @pid: process id to kill
 * @start_time: the start time of the process to kill (as obtained by nm_utils_get_start_time_for_pid()).
 *   This is an optional argument, to avoid (somewhat) killing the wrong process as @pid
 *   might get recycled. You can pass 0, to not provide this parameter.
 * @sig: signal to sent initially. If 0, no signal is sent. If %SIGKILL, the
 *   second %SIGKILL signal is not sent after @wait_before_kill_msec milliseconds.
 * @log_domain: log debug information for this domain. Errors and warnings are logged both
 *   as %LOGD_CORE and @log_domain.
 * @log_name: name of the process to kill for logging.
 * @wait_before_kill_msec: Waittime in milliseconds before sending %SIGKILL signal. Set this value
 *   to zero, not to send %SIGKILL. If @sig is already %SIGKILL, this parameter has no effect.
 *   If @max_wait_msec is set but less then @wait_before_kill_msec, the final %SIGKILL will also
 *   not be send.
 * @sleep_duration_msec: the synchronous function sleeps repeatedly waiting for the child to terminate.
 *   Set to zero, to use the default (meaning 20 wakeups per seconds).
 * @max_wait_msec: if 0, waits indefinitely until the process is gone (or a zombie). Otherwise, this
 *   is the maximum wait time until returning. If @max_wait_msec is non-zero but smaller then @wait_before_kill_msec,
 *   we will not send a final %SIGKILL.
 *
 * Kill a non-child process synchronously and wait. This function will not return before the
 * process with PID @pid is gone, the process is a zombie, or @max_wait_msec expires.
 **/
void
nm_utils_kill_process_sync(pid_t       pid,
                           guint64     start_time,
                           int         sig,
                           NMLogDomain log_domain,
                           const char *log_name,
                           guint32     wait_before_kill_msec,
                           guint32     sleep_duration_msec,
                           guint32     max_wait_msec)
{
    int      errsv;
    guint64  start_time0;
    gint64   wait_until_sigkill, now, wait_start_us, max_wait_until;
    gulong   sleep_time, sleep_duration_usec;
    int      loop_count  = 0;
    gboolean was_waiting = FALSE;
    char     buf_wait[KC_WAITED_TO_STRING];
    char     p_state;

    g_return_if_fail(pid > 0);
    g_return_if_fail(log_name != NULL);

    start_time0 = nm_utils_get_start_time_for_pid(pid, &p_state, NULL);
    if (start_time0 == 0) {
        nm_log_dbg(log_domain,
                   LOG_NAME_PROCESS_FMT ": cannot kill process %ld because it seems already gone",
                   LOG_NAME_ARGS,
                   (long int) pid);
        return;
    }
    if (start_time != 0 && start_time != start_time0) {
        nm_log_dbg(
            log_domain,
            LOG_NAME_PROCESS_FMT
            ": don't kill process %ld because the start_time is unexpectedly %lu instead of %ld",
            LOG_NAME_ARGS,
            (long int) pid,
            (unsigned long) start_time0,
            (unsigned long) start_time);
        return;
    }

    switch (p_state) {
    case 'Z':
    case 'x':
    case 'X':
        nm_log_dbg(log_domain,
                   LOG_NAME_PROCESS_FMT
                   ": cannot kill process %ld because it is already a zombie (%c)",
                   LOG_NAME_ARGS,
                   (long int) pid,
                   p_state);
        return;
    default:
        break;
    }

    if (kill(pid, sig) != 0) {
        errsv = errno;
        /* ESRCH means, process does not exist or is already a zombie. */
        if (errsv == ESRCH) {
            nm_log_dbg(log_domain,
                       LOG_NAME_PROCESS_FMT ": failed to send %s because process seems gone",
                       LOG_NAME_ARGS,
                       _kc_signal_to_string(sig));
        } else {
            nm_log_warn(LOGD_CORE | log_domain,
                        LOG_NAME_PROCESS_FMT ": failed to send %s: %s (%d)",
                        LOG_NAME_ARGS,
                        _kc_signal_to_string(sig),
                        nm_strerror_native(errsv),
                        errsv);
        }
        return;
    }

    /* wait for the process to terminate... */

    wait_start_us = nm_utils_get_monotonic_timestamp_usec();

    sleep_duration_usec = _sleep_duration_convert_ms_to_us(sleep_duration_msec);
    if (sig != SIGKILL && wait_before_kill_msec)
        wait_until_sigkill = wait_start_us + (((gint64) wait_before_kill_msec) * 1000L);
    else
        wait_until_sigkill = 0;
    if (max_wait_msec > 0) {
        max_wait_until = wait_start_us + (((gint64) max_wait_msec) * 1000L);
        if (wait_until_sigkill > 0 && wait_until_sigkill > max_wait_msec)
            wait_until_sigkill = 0;
    } else
        max_wait_until = 0;

    while (TRUE) {
        start_time = nm_utils_get_start_time_for_pid(pid, &p_state, NULL);

        if (start_time != start_time0) {
            nm_log_dbg(log_domain,
                       LOG_NAME_PROCESS_FMT ": process is gone after sending signal %s%s",
                       LOG_NAME_ARGS,
                       _kc_signal_to_string(sig),
                       was_waiting ? _kc_waited_to_string(buf_wait, wait_start_us) : "");
            return;
        }
        switch (p_state) {
        case 'Z':
        case 'x':
        case 'X':
            nm_log_dbg(log_domain,
                       LOG_NAME_PROCESS_FMT ": process is a zombie (%c) after sending signal %s%s",
                       LOG_NAME_ARGS,
                       p_state,
                       _kc_signal_to_string(sig),
                       was_waiting ? _kc_waited_to_string(buf_wait, wait_start_us) : "");
            return;
        default:
            break;
        }

        if (kill(pid, 0) != 0) {
            errsv = errno;
            /* ESRCH means, process does not exist or is already a zombie. */
            if (errsv == ESRCH) {
                nm_log_dbg(log_domain,
                           LOG_NAME_PROCESS_FMT
                           ": process is gone or a zombie after sending signal %s%s",
                           LOG_NAME_ARGS,
                           _kc_signal_to_string(sig),
                           was_waiting ? _kc_waited_to_string(buf_wait, wait_start_us) : "");
            } else {
                nm_log_warn(LOGD_CORE | log_domain,
                            LOG_NAME_PROCESS_FMT ": failed to kill(%ld, 0): %s (%d)%s",
                            LOG_NAME_ARGS,
                            (long int) pid,
                            nm_strerror_native(errsv),
                            errsv,
                            was_waiting ? _kc_waited_to_string(buf_wait, wait_start_us) : "");
            }
            return;
        }

        sleep_time = sleep_duration_usec;
        now        = nm_utils_get_monotonic_timestamp_usec();

        if (max_wait_until != 0 && now >= max_wait_until) {
            if (wait_until_sigkill != 0) {
                /* wait_before_kill_msec is not larger then max_wait_until but we did not yet send
                 * SIGKILL. Although we already reached our timeout, we don't want to skip sending
                 * the signal. Even if we don't wait for the process to disappear. */
                nm_log_dbg(log_domain, LOG_NAME_PROCESS_FMT ": sending SIGKILL", LOG_NAME_ARGS);
                kill(pid, SIGKILL);
            }
            nm_log_warn(log_domain,
                        LOG_NAME_PROCESS_FMT
                        ": timeout %u msec waiting for process to disappear (after sending %s)%s",
                        LOG_NAME_ARGS,
                        (unsigned) max_wait_until,
                        _kc_signal_to_string(sig),
                        was_waiting ? _kc_waited_to_string(buf_wait, wait_start_us) : "");
            return;
        }

        if (wait_until_sigkill != 0) {
            if (now >= wait_until_sigkill) {
                /* Still not dead. SIGKILL now... */
                nm_log_dbg(log_domain, LOG_NAME_PROCESS_FMT ": sending SIGKILL", LOG_NAME_ARGS);
                if (kill(pid, SIGKILL) != 0) {
                    errsv = errno;
                    /* ESRCH means, process does not exist or is already a zombie. */
                    if (errsv != ESRCH) {
                        nm_log_dbg(log_domain,
                                   LOG_NAME_PROCESS_FMT ": process is gone or a zombie%s",
                                   LOG_NAME_ARGS,
                                   _kc_waited_to_string(buf_wait, wait_start_us));
                    } else {
                        nm_log_warn(LOGD_CORE | log_domain,
                                    LOG_NAME_PROCESS_FMT
                                    ": failed to send SIGKILL (after sending %s), %s (%d)%s",
                                    LOG_NAME_ARGS,
                                    _kc_signal_to_string(sig),
                                    nm_strerror_native(errsv),
                                    errsv,
                                    _kc_waited_to_string(buf_wait, wait_start_us));
                    }
                    return;
                }
                sig                = SIGKILL;
                wait_until_sigkill = 0;
                loop_count =
                    0; /* reset the loop_count. Now we really expect the process to die quickly. */
            } else
                sleep_time = MIN(wait_until_sigkill - now, sleep_duration_usec);
        }

        if (!was_waiting) {
            if (wait_until_sigkill != 0) {
                nm_log_dbg(log_domain,
                           LOG_NAME_PROCESS_FMT
                           ": waiting up to %ld milliseconds for process to disappear before "
                           "sending KILL signal after sending %s...",
                           LOG_NAME_ARGS,
                           (long) wait_before_kill_msec,
                           _kc_signal_to_string(sig));
            } else if (max_wait_until != 0) {
                nm_log_dbg(
                    log_domain,
                    LOG_NAME_PROCESS_FMT
                    ": waiting up to %ld milliseconds for process to disappear after sending %s...",
                    LOG_NAME_ARGS,
                    (long) max_wait_msec,
                    _kc_signal_to_string(sig));
            } else {
                nm_log_dbg(log_domain,
                           LOG_NAME_PROCESS_FMT
                           ": waiting for process to disappear after sending %s...",
                           LOG_NAME_ARGS,
                           _kc_signal_to_string(sig));
            }
            was_waiting = TRUE;
        }

        if (loop_count < 20) {
            /* At the beginning we expect the process to die fast.
             * Limit the sleep time, the limit doubles with every iteration. */
            sleep_time = MIN(sleep_time, (((guint64) 1) << loop_count) * G_USEC_PER_SEC / 2000);
            loop_count++;
        }
        g_usleep(sleep_time);
    }
}
#undef LOG_NAME_FMT
#undef LOG_NAME_PROCESS_FMT
#undef LOG_NAME_ARGS

const char *const NM_PATHS_DEFAULT[] = {
    PREFIX "/sbin/",
    PREFIX "/bin/",
    "/usr/local/sbin/",
    "/sbin/",
    "/usr/sbin/",
    "/usr/local/bin/",
    "/bin/",
    "/usr/bin/",
    NULL,
};

const char *
nm_utils_find_helper(const char *progname, const char *try_first, GError **error)
{
    return nm_utils_file_search_in_paths(progname,
                                         try_first,
                                         NM_PATHS_DEFAULT,
                                         G_FILE_TEST_IS_EXECUTABLE,
                                         NULL,
                                         NULL,
                                         error);
}

/*****************************************************************************/

/**
 * nm_utils_read_link_absolute:
 * @link_file: file name of the symbolic link
 * @error: error reason in case of failure
 *
 * Uses to g_file_read_link()/readlink() to read the symlink
 * and returns the result as absolute path.
 **/
char *
nm_utils_read_link_absolute(const char *link_file, GError **error)
{
    char *ln, *dirname, *ln_abs;

    ln = g_file_read_link(link_file, error);
    if (!ln)
        return NULL;
    if (g_path_is_absolute(ln))
        return ln;

    dirname = g_path_get_dirname(link_file);
    if (!g_path_is_absolute(dirname)) {
        gs_free char *current_dir = g_get_current_dir();

        /* @link_file argument was not an absolute path in the first place.
         * That actually may be a bug, because the CWD is not well defined
         * in most cases. Anyway, apparently we were able to load the file
         * even from a relative path. So, when making the link absolute, we
         * also need to prepend the CWD. */
        ln_abs = g_build_filename(current_dir, dirname, ln, NULL);
    } else
        ln_abs = g_build_filename(dirname, ln, NULL);
    g_free(dirname);
    g_free(ln);
    return ln_abs;
}

/*****************************************************************************/

#define DEVICE_TYPE_TAG                 "type:"
#define DRIVER_TAG                      "driver:"
#define DHCP_PLUGIN_TAG                 "dhcp-plugin:"
#define EXCEPT_TAG                      "except:"
#define MATCH_TAG_CONFIG_NM_VERSION     "nm-version:"
#define MATCH_TAG_CONFIG_NM_VERSION_MIN "nm-version-min:"
#define MATCH_TAG_CONFIG_NM_VERSION_MAX "nm-version-max:"
#define MATCH_TAG_CONFIG_ENV            "env:"

typedef struct {
    const char *interface_name;
    const char *device_type;
    const char *driver;
    const char *driver_version;
    const char *dhcp_plugin;
    struct {
        const char *value;
        gboolean    is_parsed;
        guint       len;
        guint8      bin[_NM_UTILS_HWADDR_LEN_MAX];
    } hwaddr;
    struct {
        const char *value;
        gboolean    is_parsed;
        guint32     a;
        guint32     b;
        guint32     c;
    } s390_subchannels;
} MatchDeviceData;

static gboolean
match_device_s390_subchannels_parse(const char *s390_subchannels,
                                    guint32    *out_a,
                                    guint32    *out_b,
                                    guint32    *out_c)
{
    char      buf[30 + 1];
    const int BUFSIZE = G_N_ELEMENTS(buf) - 1;
    guint     i       = 0;
    char     *pa = NULL, *pb = NULL, *pc = NULL;
    gint64    a, b, c;

    nm_assert(s390_subchannels);
    nm_assert(out_a);
    nm_assert(out_b);
    nm_assert(out_c);

    if (!g_ascii_isxdigit(s390_subchannels[0]))
        return FALSE;

    /* Get the first channel */
    for (i = 0; s390_subchannels[i]; i++) {
        char ch = s390_subchannels[i];

        if (!g_ascii_isxdigit(ch) && ch != '.') {
            if (ch == ',') {
                /* FIXME: currently we consider the first channel and ignore
                 * everything after the first ',' separator. Maybe we should
                 * validate all present channels? */
                break;
            }
            return FALSE; /* Invalid chars */
        }
        if (i >= BUFSIZE)
            return FALSE; /* Too long to be a subchannel */
        buf[i] = ch;
    }
    buf[i] = '\0';

    /* and grab each of its elements, there should be 3 */
    pa = &buf[0];
    pb = strchr(pa, '.');
    if (pb)
        pc = strchr(pb + 1, '.');
    if (!pb || !pc)
        return FALSE;
    *pb++ = '\0';
    *pc++ = '\0';

    a = _nm_utils_ascii_str_to_int64(pa, 16, 0, G_MAXUINT32, -1);
    if (a == -1)
        return FALSE;
    b = _nm_utils_ascii_str_to_int64(pb, 16, 0, G_MAXUINT32, -1);
    if (b == -1)
        return FALSE;
    c = _nm_utils_ascii_str_to_int64(pc, 16, 0, G_MAXUINT32, -1);
    if (c == -1)
        return FALSE;

    *out_a = (guint32) a;
    *out_b = (guint32) b;
    *out_c = (guint32) c;
    return TRUE;
}

static gboolean
match_data_s390_subchannels_eval(const char *spec_str, MatchDeviceData *match_data)
{
    guint32 a, b, c;

    if (G_UNLIKELY(!match_data->s390_subchannels.is_parsed)) {
        match_data->s390_subchannels.is_parsed = TRUE;

        if (!match_data->s390_subchannels.value
            || !match_device_s390_subchannels_parse(match_data->s390_subchannels.value,
                                                    &match_data->s390_subchannels.a,
                                                    &match_data->s390_subchannels.b,
                                                    &match_data->s390_subchannels.c)) {
            match_data->s390_subchannels.value = NULL;
            return FALSE;
        }
    } else if (!match_data->s390_subchannels.value)
        return FALSE;

    if (!match_device_s390_subchannels_parse(spec_str, &a, &b, &c))
        return FALSE;
    return match_data->s390_subchannels.a == a && match_data->s390_subchannels.b == b
           && match_data->s390_subchannels.c == c;
}

static gboolean
match_device_hwaddr_eval(const char *spec_str, MatchDeviceData *match_data)
{
    if (G_UNLIKELY(!match_data->hwaddr.is_parsed)) {
        match_data->hwaddr.is_parsed = TRUE;

        if (match_data->hwaddr.value) {
            gsize l;

            if (!_nm_utils_hwaddr_aton(match_data->hwaddr.value,
                                       match_data->hwaddr.bin,
                                       sizeof(match_data->hwaddr.bin),
                                       &l))
                g_return_val_if_reached(FALSE);
            match_data->hwaddr.len = l;
        } else
            return FALSE;
    } else if (!match_data->hwaddr.len)
        return FALSE;

    return nm_utils_hwaddr_matches(spec_str, -1, match_data->hwaddr.bin, match_data->hwaddr.len);
}

#define _MATCH_CHECK(spec_str, tag)                                        \
    ({                                                                     \
        gboolean _has = FALSE;                                             \
                                                                           \
        if (!g_ascii_strncasecmp(spec_str, ("" tag ""), NM_STRLEN(tag))) { \
            spec_str += NM_STRLEN(tag);                                    \
            _has = TRUE;                                                   \
        }                                                                  \
        _has;                                                              \
    })

static NMMatchSpecMatchType
_match_result(gboolean has_except,
              gboolean has_not_except,
              gboolean has_match,
              gboolean has_match_except)
{
    if (has_except && !has_not_except) {
        /* a match spec that only consists of a list of except matches is treated specially. */
        nm_assert(!has_match);
        if (has_match_except) {
            /* one of the "except:" matches matched. The result is an explicit
             * negative match. */
            return NM_MATCH_SPEC_NEG_MATCH;
        } else {
            /* none of the "except:" matches matched. The result is a positive match,
             * despite there being no positive match. */
            return NM_MATCH_SPEC_MATCH;
        }
    }

    if (has_match_except)
        return NM_MATCH_SPEC_NEG_MATCH;
    if (has_match)
        return NM_MATCH_SPEC_MATCH;
    return NM_MATCH_SPEC_NO_MATCH;
}

static const char *
match_except(const char *spec_str, gboolean *out_except)
{
    if (_MATCH_CHECK(spec_str, EXCEPT_TAG))
        *out_except = TRUE;
    else
        *out_except = FALSE;
    return spec_str;
}

static gboolean
match_device_eval(const char *spec_str, gboolean allow_fuzzy, MatchDeviceData *match_data)
{
    if (spec_str[0] == '*' && spec_str[1] == '\0')
        return TRUE;

    if (_MATCH_CHECK(spec_str, DEVICE_TYPE_TAG)) {
        return match_data->device_type && nm_streq(spec_str, match_data->device_type);
    }

    if (_MATCH_CHECK(spec_str, NM_MATCH_SPEC_MAC_TAG))
        return match_device_hwaddr_eval(spec_str, match_data);

    if (_MATCH_CHECK(spec_str, NM_MATCH_SPEC_INTERFACE_NAME_TAG)) {
        gboolean use_pattern = FALSE;

        if (spec_str[0] == '=')
            spec_str += 1;
        else {
            if (spec_str[0] == '~')
                spec_str += 1;
            use_pattern = TRUE;
        }

        if (match_data->interface_name) {
            if (nm_streq(spec_str, match_data->interface_name))
                return TRUE;
            if (use_pattern && g_pattern_match_simple(spec_str, match_data->interface_name))
                return TRUE;
        }
        return FALSE;
    }

    if (_MATCH_CHECK(spec_str, DRIVER_TAG)) {
        const char *t;

        if (!match_data->driver)
            return FALSE;

        /* support:
         * 1) "${DRIVER}"
         *   In this case, DRIVER may not contain a '/' character.
         *   It matches any driver version.
         * 2) "${DRIVER}/${DRIVER_VERSION}"
         *   In this case, DRIVER may contains '/' but DRIVER_VERSION
         *   may not. A '/' in DRIVER_VERSION may be replaced by '?'.
         *
         * It follows, that "${DRIVER}/""*" is like 1), but allows
         * '/' inside DRIVER.
         *
         * The fields match to what `nmcli -f GENERAL.DRIVER,GENERAL.DRIVER-VERSION device show`
         * gives. However, DRIVER matches literally, while DRIVER_VERSION is a glob
         * supporting ? and *.
         */

        t = strrchr(spec_str, '/');

        if (!t)
            return nm_streq(spec_str, match_data->driver);

        return (strncmp(spec_str, match_data->driver, t - spec_str) == 0)
               && g_pattern_match_simple(&t[1], match_data->driver_version ?: "");
    }

    if (_MATCH_CHECK(spec_str, NM_MATCH_SPEC_S390_SUBCHANNELS_TAG))
        return match_data_s390_subchannels_eval(spec_str, match_data);

    if (_MATCH_CHECK(spec_str, DHCP_PLUGIN_TAG))
        return nm_streq0(spec_str, match_data->dhcp_plugin);

    if (allow_fuzzy) {
        if (match_device_hwaddr_eval(spec_str, match_data))
            return TRUE;
        if (match_data->interface_name && nm_streq(spec_str, match_data->interface_name))
            return TRUE;
    }

    return FALSE;
}

NMMatchSpecMatchType
nm_match_spec_device(const GSList *specs,
                     const char   *interface_name,
                     const char   *device_type,
                     const char   *driver,
                     const char   *driver_version,
                     const char   *hwaddr,
                     const char   *s390_subchannels,
                     const char   *dhcp_plugin)
{
    const GSList   *iter;
    gboolean        has_match        = FALSE;
    gboolean        has_match_except = FALSE;
    gboolean        has_except       = FALSE;
    gboolean        has_not_except   = FALSE;
    const char     *spec_str;
    MatchDeviceData match_data = {
        .interface_name = interface_name,
        .device_type    = nm_str_not_empty(device_type),
        .driver         = nm_str_not_empty(driver),
        .driver_version = nm_str_not_empty(driver_version),
        .dhcp_plugin    = nm_str_not_empty(dhcp_plugin),
        .hwaddr =
            {
                .value = hwaddr,
            },
        .s390_subchannels =
            {
                .value = s390_subchannels,
            },
    };

    nm_assert(!hwaddr || nm_utils_hwaddr_valid(hwaddr, -1));

    if (!specs)
        return NM_MATCH_SPEC_NO_MATCH;

    for (iter = specs; iter; iter = iter->next) {
        gboolean except;

        spec_str = iter->data;

        if (!spec_str || !*spec_str)
            continue;

        spec_str = match_except(spec_str, &except);

        if (except)
            has_except = TRUE;
        else
            has_not_except = TRUE;

        if ((except && has_match_except) || (!except && has_match)) {
            /* evaluating the match does not give new information. Skip it. */
            continue;
        }

        if (!match_device_eval(spec_str, !except, &match_data))
            continue;

        if (except)
            has_match_except = TRUE;
        else
            has_match = TRUE;
    }

    return _match_result(has_except, has_not_except, has_match, has_match_except);
}

typedef struct {
    const char *uuid;
    const char *id;
    const char *origin;
} MatchConnectionData;

static gboolean
match_connection_eval(const char *spec_str, const MatchConnectionData *match_data)
{
    if (spec_str[0] == '*' && spec_str[1] == '\0')
        return TRUE;

    if (_MATCH_CHECK(spec_str, "id:"))
        return nm_streq0(spec_str, match_data->id);

    if (_MATCH_CHECK(spec_str, "uuid:"))
        return nm_streq0(spec_str, match_data->uuid);

    if (_MATCH_CHECK(spec_str, "origin:"))
        return nm_streq0(spec_str, match_data->origin);

    return FALSE;
}

static NMMatchSpecMatchType
match_spec_connection(const GSList *specs, const char *id, const char *uuid, const char *origin)
{
    const GSList             *iter;
    gboolean                  has_match        = FALSE;
    gboolean                  has_match_except = FALSE;
    gboolean                  has_except       = FALSE;
    gboolean                  has_not_except   = FALSE;
    const char               *spec_str;
    const MatchConnectionData match_data = {
        .id     = nm_str_not_empty(id),
        .uuid   = nm_str_not_empty(uuid),
        .origin = nm_str_not_empty(origin),
    };

    if (!specs)
        return NM_MATCH_SPEC_NO_MATCH;

    for (iter = specs; iter; iter = iter->next) {
        gboolean except;

        spec_str = iter->data;

        if (!spec_str || !*spec_str)
            continue;

        spec_str = match_except(spec_str, &except);

        if (except)
            has_except = TRUE;
        else
            has_not_except = TRUE;

        if ((except && has_match_except) || (!except && has_match)) {
            /* evaluating the match does not give new information. Skip it. */
            continue;
        }

        if (!match_connection_eval(spec_str, &match_data))
            continue;

        if (except)
            has_match_except = TRUE;
        else
            has_match = TRUE;
    }

    return _match_result(has_except, has_not_except, has_match, has_match_except);
}

int
nm_utils_connection_match_spec_list(NMConnection *connection,
                                    const GSList *specs,
                                    int           no_match_value)
{
    NMMatchSpecMatchType m;
    NMSettingUser       *s_user;
    const char          *origin = NULL;

    if (!specs)
        return no_match_value;

    s_user = _nm_connection_get_setting(connection, NM_TYPE_SETTING_USER);
    if (s_user)
        origin = nm_setting_user_get_data(s_user, NM_USER_TAG_ORIGIN);

    m = match_spec_connection(specs,
                              nm_connection_get_id(connection),
                              nm_connection_get_uuid(connection),
                              origin);
    switch (m) {
    case NM_MATCH_SPEC_MATCH:
        return TRUE;
    case NM_MATCH_SPEC_NEG_MATCH:
        return FALSE;
    case NM_MATCH_SPEC_NO_MATCH:
        return no_match_value;
    }
    nm_assert_not_reached();
    return no_match_value;
}

static gboolean
match_config_eval(const char *str, const char *tag, guint cur_nm_version)
{
    gs_free char      *s_ver        = NULL;
    gs_strfreev char **s_ver_tokens = NULL;
    int                v_maj = -1, v_min = -1, v_mic = -1;
    guint              c_maj = -1, c_min = -1, c_mic = -1;
    guint              n_tokens;

    s_ver = g_strdup(str);
    g_strstrip(s_ver);

    /* Let's be strict with the accepted format here. No funny stuff!! */

    if (s_ver[strspn(s_ver, ".0123456789")] != '\0')
        return FALSE;

    s_ver_tokens = g_strsplit(s_ver, ".", -1);
    n_tokens     = g_strv_length(s_ver_tokens);
    if (n_tokens == 0 || n_tokens > 3)
        return FALSE;

    v_maj = _nm_utils_ascii_str_to_int64(s_ver_tokens[0], 10, 0, 0xFFFF, -1);
    if (v_maj < 0)
        return FALSE;
    if (n_tokens >= 2) {
        v_min = _nm_utils_ascii_str_to_int64(s_ver_tokens[1], 10, 0, 0xFF, -1);
        if (v_min < 0)
            return FALSE;
    }
    if (n_tokens >= 3) {
        v_mic = _nm_utils_ascii_str_to_int64(s_ver_tokens[2], 10, 0, 0xFF, -1);
        if (v_mic < 0)
            return FALSE;
    }

    nm_decode_version(cur_nm_version, &c_maj, &c_min, &c_mic);

#define CHECK_AND_RETURN_FALSE(cur, val, tag, is_last_digit)        \
    G_STMT_START                                                    \
    {                                                               \
        if (!strcmp(tag, MATCH_TAG_CONFIG_NM_VERSION_MIN)) {        \
            if (cur < val)                                          \
                return FALSE;                                       \
        } else if (!strcmp(tag, MATCH_TAG_CONFIG_NM_VERSION_MAX)) { \
            if (cur > val)                                          \
                return FALSE;                                       \
        } else {                                                    \
            if (cur != val)                                         \
                return FALSE;                                       \
        }                                                           \
        if (!(is_last_digit)) {                                     \
            if (cur != val)                                         \
                return FALSE;                                       \
        }                                                           \
    }                                                               \
    G_STMT_END
    if (v_mic >= 0)
        CHECK_AND_RETURN_FALSE(c_mic, v_mic, tag, TRUE);
    if (v_min >= 0)
        CHECK_AND_RETURN_FALSE(c_min, v_min, tag, v_mic < 0);
    CHECK_AND_RETURN_FALSE(c_maj, v_maj, tag, v_min < 0);
    return TRUE;
}

NMMatchSpecMatchType
nm_match_spec_config(const GSList *specs, guint cur_nm_version, const char *env)
{
    const GSList *iter;
    gboolean      has_match        = FALSE;
    gboolean      has_match_except = FALSE;
    gboolean      has_except       = FALSE;
    gboolean      has_not_except   = FALSE;

    if (!specs)
        return NM_MATCH_SPEC_NO_MATCH;

    for (iter = specs; iter; iter = g_slist_next(iter)) {
        const char *spec_str = iter->data;
        gboolean    except;
        gboolean    v_match;

        if (!spec_str || !*spec_str)
            continue;

        spec_str = match_except(spec_str, &except);

        if (except)
            has_except = TRUE;
        else
            has_not_except = TRUE;

        if ((except && has_match_except) || (!except && has_match)) {
            /* evaluating the match does not give new information. Skip it. */
            continue;
        }

        if (_MATCH_CHECK(spec_str, MATCH_TAG_CONFIG_NM_VERSION))
            v_match = match_config_eval(spec_str, MATCH_TAG_CONFIG_NM_VERSION, cur_nm_version);
        else if (_MATCH_CHECK(spec_str, MATCH_TAG_CONFIG_NM_VERSION_MIN))
            v_match = match_config_eval(spec_str, MATCH_TAG_CONFIG_NM_VERSION_MIN, cur_nm_version);
        else if (_MATCH_CHECK(spec_str, MATCH_TAG_CONFIG_NM_VERSION_MAX))
            v_match = match_config_eval(spec_str, MATCH_TAG_CONFIG_NM_VERSION_MAX, cur_nm_version);
        else if (_MATCH_CHECK(spec_str, MATCH_TAG_CONFIG_ENV))
            v_match = env && env[0] && !strcmp(spec_str, env);
        else
            v_match = FALSE;

        if (!v_match)
            continue;

        if (except)
            has_match_except = TRUE;
        else
            has_match = TRUE;
    }

    return _match_result(has_except, has_not_except, has_match, has_match_except);
}

#undef _MATCH_CHECK

/**
 * nm_match_spec_split:
 * @value: the string of device specs
 *
 * Splits the specs from the string and returns them as individual
 * entries in a #GSList.
 *
 * It does not validate any specs, it basically just does a special
 * strsplit with ',' or ';' as separators and supporting '\\' as
 * escape character.
 *
 * Leading and trailing spaces of each entry are removed. But the user
 * can preserve them by specifying "\\s has 2 leading" or "has 2 trailing \\s".
 *
 * Specs can have a qualifier like "interface-name:". We still don't strip
 * any whitespace after the colon, so "interface-name: X" matches an interface
 * named " X".
 *
 * Returns: (transfer full): the list of device specs.
 */
GSList *
nm_match_spec_split(const char *value)
{
    char   *string_value, *p, *q0, *q;
    GSList *pieces = NULL;
    int     trailing_ws;

    if (!value || !*value)
        return NULL;

    /* Copied from glibs g_key_file_parse_value_as_string() function
     * and adjusted. */

    string_value = g_new(char, strlen(value) + 1);

    p = (char *) value;

    /* skip over leading whitespace */
    while (g_ascii_isspace(*p))
        p++;

    q0 = q      = string_value;
    trailing_ws = 0;
    while (*p) {
        if (*p == '\\') {
            p++;

            switch (*p) {
            case 's':
                *q = ' ';
                break;
            case 'n':
                *q = '\n';
                break;
            case 't':
                *q = '\t';
                break;
            case 'r':
                *q = '\r';
                break;
            case '\\':
                *q = '\\';
                break;
            case '\0':
                break;
            default:
                if (NM_IN_SET(*p, ',', ';'))
                    *q = *p;
                else {
                    *q++ = '\\';
                    *q   = *p;
                }
                break;
            }
            if (*p == '\0')
                break;
            p++;
            trailing_ws = 0;
        } else {
            *q = *p;
            if (*p == '\0')
                break;
            if (g_ascii_isspace(*p)) {
                trailing_ws++;
                p++;
            } else if (NM_IN_SET(*p, ',', ';')) {
                if (q0 < q - trailing_ws)
                    pieces = g_slist_prepend(pieces, g_strndup(q0, (q - q0) - trailing_ws));
                q0 = q + 1;
                p++;
                trailing_ws = 0;
                while (g_ascii_isspace(*p))
                    p++;
            } else
                p++;
        }
        q++;
    }

    *q = '\0';
    if (q0 < q - trailing_ws)
        pieces = g_slist_prepend(pieces, g_strndup(q0, (q - q0) - trailing_ws));
    g_free(string_value);
    return g_slist_reverse(pieces);
}

/**
 * nm_match_spec_join:
 * @specs: the device specs to join
 *
 * This is based on g_key_file_parse_string_as_value(), analog to
 * nm_match_spec_split() which is based on g_key_file_parse_value_as_string().
 *
 * Returns: (transfer full): a joined list of device specs that can be
 *   split again with nm_match_spec_split(). Note that
 *   nm_match_spec_split (nm_match_spec_join (specs)) yields the original
 *   result (which is not true the other way around because there are multiple
 *   ways to encode the same joined specs string).
 */
char *
nm_match_spec_join(GSList *specs)
{
    const char *p;
    GString    *str;

    str = g_string_new("");

    for (; specs; specs = specs->next) {
        p = specs->data;

        if (!p || !*p)
            continue;

        if (str->len > 0)
            g_string_append_c(str, ',');

        /* escape leading whitespace */
        switch (*p) {
        case ' ':
            g_string_append(str, "\\s");
            p++;
            break;
        case '\t':
            g_string_append(str, "\\t");
            p++;
            break;
        }

        for (; *p; p++) {
            switch (*p) {
            case '\n':
                g_string_append(str, "\\n");
                break;
            case '\r':
                g_string_append(str, "\\r");
                break;
            case '\\':
                g_string_append(str, "\\\\");
                break;
            case ',':
                g_string_append(str, "\\,");
                break;
            case ';':
                g_string_append(str, "\\;");
                break;
            default:
                g_string_append_c(str, *p);
                break;
            }
        }

        /* escape trailing whitespaces */
        switch (str->str[str->len - 1]) {
        case ' ':
            g_string_overwrite(str, str->len - 1, "\\s");
            break;
        case '\t':
            g_string_overwrite(str, str->len - 1, "\\t");
            break;
        }
    }

    return g_string_free(str, FALSE);
}

static void
_pattern_parse(const char  *input,
               const char **out_pattern,
               gboolean    *out_is_inverted,
               gboolean    *out_is_mandatory)
{
    gboolean is_inverted  = FALSE;
    gboolean is_mandatory = FALSE;

    if (input[0] == '&') {
        input++;
        is_mandatory = TRUE;
        if (input[0] == '!') {
            input++;
            is_inverted = TRUE;
        }
        goto out;
    }

    if (input[0] == '|') {
        input++;
        if (input[0] == '!') {
            input++;
            is_inverted = TRUE;
        }
        goto out;
    }

    if (input[0] == '!') {
        input++;
        is_inverted  = TRUE;
        is_mandatory = TRUE;
        goto out;
    }

out:
    if (input[0] == '\\')
        input++;

    *out_pattern      = input;
    *out_is_inverted  = is_inverted;
    *out_is_mandatory = is_mandatory;
}

gboolean
nm_wildcard_match_check(const char *str, const char *const *patterns, guint num_patterns)
{
    gboolean has_optional     = FALSE;
    gboolean has_any_optional = FALSE;
    guint    i;

    for (i = 0; i < num_patterns; i++) {
        gboolean    is_inverted;
        gboolean    is_mandatory;
        gboolean    match;
        const char *p;

        _pattern_parse(patterns[i], &p, &is_inverted, &is_mandatory);

        match = (fnmatch(p, str ?: "", 0) == 0);

        if (is_inverted)
            match = !match;

        if (is_mandatory) {
            if (!match)
                return FALSE;
        } else {
            has_any_optional = TRUE;
            if (match)
                has_optional = TRUE;
        }
    }

    return has_optional || !has_any_optional;
}

/*****************************************************************************/

static gboolean
_kernel_cmdline_match(const char *const *proc_cmdline, const char *pattern)
{
    if (proc_cmdline) {
        gboolean has_equal   = (!!strchr(pattern, '='));
        gsize    pattern_len = strlen(pattern);

        for (; proc_cmdline[0]; proc_cmdline++) {
            const char *c = proc_cmdline[0];

            if (has_equal) {
                /* if pattern contains '=' compare full key=value */
                if (nm_streq(c, pattern))
                    return TRUE;
                continue;
            }

            /* otherwise consider pattern as key only */
            if (strncmp(c, pattern, pattern_len) == 0 && NM_IN_SET(c[pattern_len], '\0', '='))
                return TRUE;
        }
    }

    return FALSE;
}

gboolean
nm_utils_kernel_cmdline_match_check(const char *const *proc_cmdline,
                                    const char *const *patterns,
                                    guint              num_patterns,
                                    GError           **error)
{
    gboolean has_optional     = FALSE;
    gboolean has_any_optional = FALSE;
    guint    i;

    for (i = 0; i < num_patterns; i++) {
        const char *element      = patterns[i];
        gboolean    is_inverted  = FALSE;
        gboolean    is_mandatory = FALSE;
        gboolean    match;
        const char *p;

        _pattern_parse(element, &p, &is_inverted, &is_mandatory);

        match = _kernel_cmdline_match(proc_cmdline, p);
        if (is_inverted)
            match = !match;

        if (is_mandatory) {
            if (!match) {
                nm_utils_error_set(error,
                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
                                   "device does not satisfy match.kernel-command-line property %s",
                                   patterns[i]);
                return FALSE;
            }
        } else {
            has_any_optional = TRUE;
            if (match)
                has_optional = TRUE;
        }
    }

    if (!has_optional && has_any_optional) {
        nm_utils_error_set(error,
                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
                           "device does not satisfy any match.kernel-command-line property");
        return FALSE;
    }

    return TRUE;
}

/*****************************************************************************/

/**
 * nm_utils_cmp_connection_by_autoconnect_priority:
 * @a:
 * @b:
 *
 * compare connections @a and @b for their autoconnect property
 * (with sorting the connection that has autoconnect enabled before
 * the other)
 * If they both have autoconnect enabled, sort them depending on their
 * autoconnect-priority (with the higher priority first).
 *
 * If their autoconnect/autoconnect-priority is the same, 0 is returned.
 * That is, they compare equal.
 *
 * Returns: -1, 0, or 1
 */
int
nm_utils_cmp_connection_by_autoconnect_priority(NMConnection *a, NMConnection *b)
{
    NMSettingConnection *a_s_con;
    NMSettingConnection *b_s_con;
    int                  a_ap, b_ap;
    gboolean             can_autoconnect;

    if (a == b)
        return 0;
    if (!a)
        return 1;
    if (!b)
        return -1;

    a_s_con = nm_connection_get_setting_connection(a);
    b_s_con = nm_connection_get_setting_connection(b);

    if (!a_s_con)
        return !b_s_con ? 0 : 1;
    if (!b_s_con)
        return -1;

    can_autoconnect = !!nm_setting_connection_get_autoconnect(a_s_con);
    if (can_autoconnect != (!!nm_setting_connection_get_autoconnect(b_s_con)))
        return can_autoconnect ? -1 : 1;

    if (can_autoconnect) {
        a_ap = nm_setting_connection_get_autoconnect_priority(a_s_con);
        b_ap = nm_setting_connection_get_autoconnect_priority(b_s_con);
        if (a_ap != b_ap)
            return (a_ap > b_ap) ? -1 : 1;
    }

    return 0;
}

/*****************************************************************************/

typedef struct {
    const char *name;
    NMSetting  *setting;
    NMSetting  *diff_base_setting;
    GHashTable *setting_diff;
} LogConnectionSettingData;

typedef struct {
    const char         *item_name;
    NMSettingDiffResult diff_result;
} LogConnectionSettingItem;

static int
_log_connection_sort_hashes_fcn(gconstpointer a, gconstpointer b)
{
    const LogConnectionSettingData *v1 = a;
    const LogConnectionSettingData *v2 = b;
    NMSettingPriority               p1, p2;
    NMSetting                      *s1, *s2;

    s1 = v1->setting ?: v1->diff_base_setting;
    s2 = v2->setting ?: v2->diff_base_setting;

    g_assert(s1 && s2);

    p1 = _nm_setting_get_setting_priority(s1);
    p2 = _nm_setting_get_setting_priority(s2);

    if (p1 != p2)
        return p1 > p2 ? 1 : -1;

    return strcmp(v1->name, v2->name);
}

static GArray *
_log_connection_sort_hashes(NMConnection *connection,
                            NMConnection *diff_base,
                            GHashTable   *connection_diff)
{
    GHashTableIter           iter;
    GArray                  *sorted_hashes;
    LogConnectionSettingData setting_data;

    sorted_hashes = g_array_sized_new(TRUE,
                                      FALSE,
                                      sizeof(LogConnectionSettingData),
                                      g_hash_table_size(connection_diff));

    g_hash_table_iter_init(&iter, connection_diff);
    while (g_hash_table_iter_next(&iter,
                                  (gpointer) &setting_data.name,
                                  (gpointer) &setting_data.setting_diff)) {
        setting_data.setting = nm_connection_get_setting_by_name(connection, setting_data.name);
        setting_data.diff_base_setting =
            diff_base ? nm_connection_get_setting_by_name(diff_base, setting_data.name) : NULL;
        g_assert(setting_data.setting || setting_data.diff_base_setting);
        g_array_append_val(sorted_hashes, setting_data);
    }

    g_array_sort(sorted_hashes, _log_connection_sort_hashes_fcn);
    return sorted_hashes;
}

static int
_log_connection_sort_names_fcn(gconstpointer a, gconstpointer b)
{
    const LogConnectionSettingItem *v1 = a;
    const LogConnectionSettingItem *v2 = b;

    /* we want to first show the items, that disappeared, then the one that changed and
     * then the ones that were added. */

    if ((v1->diff_result & NM_SETTING_DIFF_RESULT_IN_A)
        != (v2->diff_result & NM_SETTING_DIFF_RESULT_IN_A))
        return (v1->diff_result & NM_SETTING_DIFF_RESULT_IN_A) ? -1 : 1;
    if ((v1->diff_result & NM_SETTING_DIFF_RESULT_IN_B)
        != (v2->diff_result & NM_SETTING_DIFF_RESULT_IN_B))
        return (v1->diff_result & NM_SETTING_DIFF_RESULT_IN_B) ? 1 : -1;
    return strcmp(v1->item_name, v2->item_name);
}

static char *
_log_connection_get_property(NMSetting *setting, const char *name)
{
    GValue val = G_VALUE_INIT;
    char  *s;

    g_return_val_if_fail(setting, NULL);

    if (!NM_IS_SETTING_VPN(setting) && nm_setting_get_secret_flags(setting, name, NULL, NULL))
        return g_strdup("****");

    if (!_nm_setting_get_property(setting, name, &val))
        return g_strdup("<unknown>");

    if (G_VALUE_HOLDS_STRING(&val)) {
        const char *val_s;

        val_s = g_value_get_string(&val);
        if (!val_s) {
            /* for NULL, we want to return the unquoted string "NULL". */
            s = g_strdup("NULL");
        } else {
            char *escaped = g_strescape(val_s, "'");

            s = g_strdup_printf("'%s'", escaped);
            g_free(escaped);
        }
    } else if (G_VALUE_HOLDS_VARIANT(&val)) {
        s = g_variant_print(g_value_get_variant(&val), FALSE);
    } else {
        s = g_strdup_value_contents(&val);
        if (s == NULL)
            s = g_strdup("NULL");
        else {
            char *escaped = g_strescape(s, "'");

            g_free(s);
            s = escaped;
        }
    }
    g_value_unset(&val);
    return s;
}

static void
_log_connection_sort_names(LogConnectionSettingData *setting_data, GArray *sorted_names)
{
    GHashTableIter           iter;
    LogConnectionSettingItem item;
    gpointer                 p;

    g_array_set_size(sorted_names, 0);

    g_hash_table_iter_init(&iter, setting_data->setting_diff);
    while (g_hash_table_iter_next(&iter, (gpointer) &item.item_name, &p)) {
        item.diff_result = GPOINTER_TO_UINT(p);
        g_array_append_val(sorted_names, item);
    }

    g_array_sort(sorted_names, _log_connection_sort_names_fcn);
}

void
nm_utils_log_connection_diff(NMConnection *connection,
                             NMConnection *diff_base,
                             guint32       level,
                             guint64       domain,
                             const char   *name,
                             const char   *prefix,
                             const char   *dbus_path)
{
    GHashTable *connection_diff = NULL;
    GArray     *sorted_hashes;
    GArray     *sorted_names = NULL;
    int         i, j;
    gboolean    connection_diff_are_same;
    gboolean    print_header = TRUE;
    gboolean    print_setting_header;
    GString    *str1;

    g_return_if_fail(NM_IS_CONNECTION(connection));
    g_return_if_fail(!diff_base || (NM_IS_CONNECTION(diff_base) && diff_base != connection));

    /* For VPN setting types, this is broken, because we cannot (generically) print the content of data/secrets. Bummer... */

    if (!nm_logging_enabled(level, domain))
        return;

    if (!prefix)
        prefix = "";
    if (!name)
        name = "";

    connection_diff_are_same = nm_connection_diff(
        connection,
        diff_base,
        NM_SETTING_COMPARE_FLAG_EXACT | NM_SETTING_COMPARE_FLAG_DIFF_RESULT_NO_DEFAULT,
        &connection_diff);
    if (connection_diff_are_same) {
        const char *t1, *t2;

        t1 = nm_connection_get_connection_type(connection);
        if (diff_base) {
            t2 = nm_connection_get_connection_type(diff_base);
            nm_log(level,
                   domain,
                   NULL,
                   NULL,
                   "%sconnection '%s' (%p/%s/%s%s%s and %p/%s/%s%s%s): no difference",
                   prefix,
                   name,
                   connection,
                   G_OBJECT_TYPE_NAME(connection),
                   NM_PRINT_FMT_QUOTE_STRING(t1),
                   diff_base,
                   G_OBJECT_TYPE_NAME(diff_base),
                   NM_PRINT_FMT_QUOTE_STRING(t2));
        } else {
            nm_log(level,
                   domain,
                   NULL,
                   NULL,
                   "%sconnection '%s' (%p/%s/%s%s%s): no properties set",
                   prefix,
                   name,
                   connection,
                   G_OBJECT_TYPE_NAME(connection),
                   NM_PRINT_FMT_QUOTE_STRING(t1));
        }
        g_assert(!connection_diff);
        return;
    }

    /* FIXME: it doesn't nicely show the content of NMSettingVpn, because nm_connection_diff() does not
     * expand the hash values. */

    sorted_hashes = _log_connection_sort_hashes(connection, diff_base, connection_diff);
    if (sorted_hashes->len <= 0)
        goto out;

    sorted_names = g_array_new(FALSE, FALSE, sizeof(LogConnectionSettingItem));
    str1         = g_string_new(NULL);

    for (i = 0; i < sorted_hashes->len; i++) {
        LogConnectionSettingData *setting_data =
            &g_array_index(sorted_hashes, LogConnectionSettingData, i);

        _log_connection_sort_names(setting_data, sorted_names);
        print_setting_header = TRUE;
        for (j = 0; j < sorted_names->len; j++) {
            char                     *str_conn, *str_diff;
            LogConnectionSettingItem *item =
                &g_array_index(sorted_names, LogConnectionSettingItem, j);

            str_conn = (item->diff_result & NM_SETTING_DIFF_RESULT_IN_A)
                           ? _log_connection_get_property(setting_data->setting, item->item_name)
                           : NULL;
            str_diff =
                (item->diff_result & NM_SETTING_DIFF_RESULT_IN_B)
                    ? _log_connection_get_property(setting_data->diff_base_setting, item->item_name)
                    : NULL;

            if (print_header) {
                GError     *err_verify = NULL;
                const char *t1, *t2;

                t1 = nm_connection_get_connection_type(connection);
                if (diff_base) {
                    t2 = nm_connection_get_connection_type(diff_base);
                    nm_log(level,
                           domain,
                           NULL,
                           NULL,
                           "%sconnection '%s' (%p/%s/%s%s%s < %p/%s/%s%s%s)%s%s%s:",
                           prefix,
                           name,
                           connection,
                           G_OBJECT_TYPE_NAME(connection),
                           NM_PRINT_FMT_QUOTE_STRING(t1),
                           diff_base,
                           G_OBJECT_TYPE_NAME(diff_base),
                           NM_PRINT_FMT_QUOTE_STRING(t2),
                           NM_PRINT_FMT_QUOTED(dbus_path, " [", dbus_path, "]", ""));
                } else {
                    nm_log(level,
                           domain,
                           NULL,
                           NULL,
                           "%sconnection '%s' (%p/%s/%s%s%s):%s%s%s",
                           prefix,
                           name,
                           connection,
                           G_OBJECT_TYPE_NAME(connection),
                           NM_PRINT_FMT_QUOTE_STRING(t1),
                           NM_PRINT_FMT_QUOTED(dbus_path, " [", dbus_path, "]", ""));
                }
                print_header = FALSE;

                if (!nm_connection_verify(connection, &err_verify)) {
                    nm_log(level,
                           domain,
                           NULL,
                           NULL,
                           "%sconnection %p does not verify: %s",
                           prefix,
                           connection,
                           err_verify->message);
                    g_clear_error(&err_verify);
                }
            }
#define _NM_LOG_ALIGN "-25"
            if (print_setting_header) {
                if (diff_base) {
                    if (setting_data->setting && setting_data->diff_base_setting)
                        g_string_printf(str1,
                                        "%p < %p",
                                        setting_data->setting,
                                        setting_data->diff_base_setting);
                    else if (setting_data->diff_base_setting)
                        g_string_printf(str1, "*missing* < %p", setting_data->diff_base_setting);
                    else
                        g_string_printf(str1, "%p < *missing*", setting_data->setting);
                    nm_log(level,
                           domain,
                           NULL,
                           NULL,
                           "%s%"_NM_LOG_ALIGN
                           "s [ %s ]",
                           prefix,
                           setting_data->name,
                           str1->str);
                } else
                    nm_log(level,
                           domain,
                           NULL,
                           NULL,
                           "%s%"_NM_LOG_ALIGN
                           "s [ %p ]",
                           prefix,
                           setting_data->name,
                           setting_data->setting);
                print_setting_header = FALSE;
            }
            g_string_printf(str1, "%s.%s", setting_data->name, item->item_name);
            switch (item->diff_result
                    & (NM_SETTING_DIFF_RESULT_IN_A | NM_SETTING_DIFF_RESULT_IN_B)) {
            case NM_SETTING_DIFF_RESULT_IN_B:
                nm_log(level,
                       domain,
                       NULL,
                       NULL,
                       "%s%"_NM_LOG_ALIGN
                       "s < %s",
                       prefix,
                       str1->str,
                       str_diff ?: "NULL");
                break;
            case NM_SETTING_DIFF_RESULT_IN_A:
                nm_log(level,
                       domain,
                       NULL,
                       NULL,
                       "%s%"_NM_LOG_ALIGN
                       "s = %s",
                       prefix,
                       str1->str,
                       str_conn ?: "NULL");
                break;
            default:
                nm_log(level,
                       domain,
                       NULL,
                       NULL,
                       "%s%"_NM_LOG_ALIGN
                       "s = %s < %s",
                       prefix,
                       str1->str,
                       str_conn ?: "NULL",
                       str_diff ?: "NULL");
                break;
#undef _NM_LOG_ALIGN
            }
            g_free(str_conn);
            g_free(str_diff);
        }
    }

    g_array_free(sorted_names, TRUE);
    g_string_free(str1, TRUE);
out:
    g_hash_table_destroy(connection_diff);
    g_array_free(sorted_hashes, TRUE);
}

/*****************************************************************************/

typedef struct {
    NMUuid bin;

    /* depending on whether the string is packed or not (with/without hyphens),
     * it's 32 or 36 characters long (plus the trailing NUL).
     *
     * The difference is that boot-id is a valid RFC 4211 UUID and represented
     * as a 36 ascii string (with hyphens). The machine-id technically is not
     * a UUID, but just a 32 byte sequence of hexchars. */
    char str[37];
    bool is_fake;
} UuidData;

static UuidData *
_uuid_data_init(UuidData *uuid_data, gboolean packed, gboolean is_fake, const NMUuid *uuid)
{
    nm_assert(uuid_data);
    nm_assert(uuid);

    uuid_data->bin     = *uuid;
    uuid_data->is_fake = is_fake;
    if (packed) {
        G_STATIC_ASSERT_EXPR(sizeof(uuid_data->str) >= (sizeof(*uuid) * 2 + 1));
        nm_utils_bin2hexstr_full(uuid, sizeof(*uuid), '\0', FALSE, uuid_data->str);
    } else {
        G_STATIC_ASSERT_EXPR(sizeof(uuid_data->str) >= 37);
        nm_uuid_unparse(uuid, uuid_data->str);
    }
    return uuid_data;
}

/*****************************************************************************/

static const UuidData *
_machine_id_get(gboolean allow_fake)
{
    static const UuidData *volatile p_uuid_data;
    const UuidData *d;

again:
    d = g_atomic_pointer_get(&p_uuid_data);
    if (G_UNLIKELY(!d)) {
        static gsize    lock;
        static UuidData uuid_data;
        gs_free char   *content   = NULL;
        gboolean        is_fake   = TRUE;
        const char     *fake_type = NULL;
        NMUuid          uuid;

        /* Get the machine ID from /etc/machine-id; it's always in /etc no matter
         * where our configured SYSCONFDIR is.  Alternatively, it might be in
         * LOCALSTATEDIR /lib/dbus/machine-id.
         */
        if (nm_utils_file_get_contents(-1,
                                       "/etc/machine-id",
                                       100 * 1024,
                                       0,
                                       &content,
                                       NULL,
                                       NULL,
                                       NULL)
            || nm_utils_file_get_contents(-1,
                                          LOCALSTATEDIR "/lib/dbus/machine-id",
                                          100 * 1024,
                                          0,
                                          &content,
                                          NULL,
                                          NULL,
                                          NULL)) {
            g_strstrip(content);
            if (nm_utils_hexstr2bin_full(content,
                                         FALSE,
                                         FALSE,
                                         FALSE,
                                         NULL,
                                         16,
                                         (guint8 *) &uuid,
                                         sizeof(uuid),
                                         NULL)) {
                if (!nm_uuid_is_null(&uuid)) {
                    /* an all-zero machine-id is not valid. */
                    is_fake = FALSE;
                }
            }
        }

        if (is_fake) {
            const guint8 *seed_bin;
            const NMUuid *hash_seed;
            gsize         seed_len;

            if (!allow_fake) {
                /* we don't allow generating (and memorizing) a fake key.
                 * Signal that no valid machine-id exists. */
                return NULL;
            }

            if (nm_utils_host_id_get(&seed_bin, &seed_len)) {
                static const NMUuid u =
                    NM_UUID_INIT(ab, 08, 5f, 06, b6, 29, 46, d1, a5, 53, 84, ee, ba, 56, 83, b6);

                /* We have no valid machine-id but we have a valid secrey_key.
                 * Generate a fake machine ID by hashing the secret-key. The secret_key
                 * is commonly persisted, so it should be stable across reboots (despite
                 * having a broken system without proper machine-id).
                 *
                 * Note that we access the host-id here, which is based on secret_key.
                 * Also not that the secret_key may be generated based on the machine-id,
                 * so we have to be careful that they don't depend on each other (and
                 * no infinite recursion happens. This is done correctly, because the secret-key
                 * will call _machine_id_get(FALSE), so it won't allow accessing a fake
                 * machine-id, thus avoiding the problem. */
                fake_type = "secret-key";
                hash_seed = &u;
            } else {
                static const NMUuid u =
                    NM_UUID_INIT(7f, f0, c8, f5, 53, 99, 49, 01, ab, 63, 61, bf, 59, 4a, be, 8b);

                /* the secret-key is not valid/persistent either. That happens when we fail
                 * to read/write the secret-key to disk. Fallback to boot-id. The boot-id
                 * itself may be fake and randomly generated ad-hoc, but that is as best
                 * as it gets.  */
                seed_bin  = (const guint8 *) nm_utils_boot_id_bin();
                seed_len  = sizeof(NMUuid);
                fake_type = "boot-id";
                hash_seed = &u;
            }

            /* the fake machine-id is based on secret-key/boot-id, but we hash it
             * again, so that they are not literally the same. */
            nm_uuid_generate_from_string(&uuid,
                                         (const char *) seed_bin,
                                         seed_len,
                                         NM_UUID_TYPE_VERSION5,
                                         hash_seed);
        }

        if (!g_once_init_enter(&lock))
            goto again;

        d = _uuid_data_init(&uuid_data, TRUE, is_fake, &uuid);
        g_atomic_pointer_set(&p_uuid_data, d);
        g_once_init_leave(&lock, 1);

        if (is_fake) {
            nm_log_err(LOGD_CORE,
                       "/etc/machine-id: no valid machine-id. Use fake one based on %s: %s",
                       fake_type,
                       d->str);
        } else
            nm_log_dbg(LOGD_CORE, "/etc/machine-id: %s", d->str);
    }

    return d;
}

const char *
nm_utils_machine_id_str(void)
{
    return _machine_id_get(TRUE)->str;
}

const NMUuid *
nm_utils_machine_id_bin(void)
{
    return &_machine_id_get(TRUE)->bin;
}

gboolean
nm_utils_machine_id_is_fake(void)
{
    return _machine_id_get(TRUE)->is_fake;
}

/*****************************************************************************/

/* prefix for version2 secret key. The secret key is hashed with /etc/machine-id. */
#define SECRET_KEY_V2_PREFIX "nm-v2:"
#define SECRET_KEY_FILE      NMSTATEDIR "/secret_key"

static gboolean
_host_id_read_timestamp(gboolean      use_secret_key_file,
                        const guint8 *host_id,
                        gsize         host_id_len,
                        gint64       *out_timestamp_ns)
{
    struct stat st;
    gint64      now;
    guint64     v;

    if (use_secret_key_file && stat(SECRET_KEY_FILE, &st) == 0) {
        /* don't check for overflow or timestamps in the future. We get whatever
         * (bogus) date is on the file. */
        *out_timestamp_ns = nm_utils_timespec_to_nsec(&st.st_mtim);
        return TRUE;
    }

    /* generate a fake timestamp based on the host-id.
     *
     * This really should never happen under normal circumstances. We already
     * are in a code path, where the system has a problem (unable to get good randomness
     * and/or can't access the secret_key). In such a scenario, a fake timestamp is the
     * least of our problems.
     *
     * At least, generate something sensible so we don't have to worry about the
     * timestamp. It is wrong to worry about using a fake timestamp (which is tied to
     * the secret_key) if we are unable to access the secret_key file in the first place.
     *
     * Pick a random timestamp from the past two years. Yes, this timestamp
     * is not stable across restarts, but apparently neither is the host-id
     * nor the secret_key itself. */

#define EPOCH_TWO_YEARS (G_GINT64_CONSTANT(2 * 365 * 24 * 3600) * NM_UTILS_NSEC_PER_SEC)

    v = nm_hash_siphash42(1156657133u, host_id, host_id_len);

    now = time(NULL);
    *out_timestamp_ns =
        NM_MAX((gint64) 1,
               (now * NM_UTILS_NSEC_PER_SEC) - ((gint64) (v % ((guint64) (EPOCH_TWO_YEARS)))));
    return FALSE;
}

static const guint8 *
_host_id_hash_v2(const guint8 *seed_arr,
                 gsize         seed_len,
                 guint8       *out_digest /* 32 bytes (NM_UTILS_CHECKSUM_LENGTH_SHA256) */)
{
    nm_auto_free_checksum GChecksum *sum = g_checksum_new(G_CHECKSUM_SHA256);
    const UuidData                  *machine_id_data;
    char                             slen[100];

    /*
        (stat -c '%s' /var/lib/NetworkManager/secret_key;
         echo -n ' ';
         cat /var/lib/NetworkManager/secret_key;
         cat /etc/machine-id | tr -d '\n' | sed -n 's/[a-f0-9-]/\0/pg') | sha256sum
    */

    nm_sprintf_buf(slen, "%" G_GSIZE_FORMAT " ", seed_len);
    g_checksum_update(sum, (const guchar *) slen, strlen(slen));

    g_checksum_update(sum, (const guchar *) seed_arr, seed_len);

    machine_id_data = _machine_id_get(FALSE);
    if (machine_id_data && !machine_id_data->is_fake)
        g_checksum_update(sum, (const guchar *) machine_id_data->str, strlen(machine_id_data->str));

    nm_utils_checksum_get_digest_len(sum, out_digest, NM_UTILS_CHECKSUM_LENGTH_SHA256);
    return out_digest;
}

static gboolean
_host_id_read(guint8 **out_host_id, gsize *out_host_id_len)
{
#define SECRET_KEY_LEN 32u
    guint8                               sha256_digest[NM_UTILS_CHECKSUM_LENGTH_SHA256];
    nm_auto_clear_secret_ptr NMSecretPtr file_content = {0};
    const guint8                        *secret_arr;
    gsize                                secret_len;
    GError                              *error = NULL;
    gboolean                             success;

    if (!nm_utils_file_get_contents(-1,
                                    SECRET_KEY_FILE,
                                    10 * 1024,
                                    NM_UTILS_FILE_GET_CONTENTS_FLAG_SECRET,
                                    &file_content.str,
                                    &file_content.len,
                                    NULL,
                                    &error)) {
        if (!nm_utils_error_is_notfound(error)) {
            nm_log_warn(LOGD_CORE,
                        "secret-key: failure reading secret key in \"%s\": %s (generate new key)",
                        SECRET_KEY_FILE,
                        error->message);
        }
        g_clear_error(&error);
    } else if (file_content.len >= NM_STRLEN(SECRET_KEY_V2_PREFIX) + SECRET_KEY_LEN
               && memcmp(file_content.bin, SECRET_KEY_V2_PREFIX, NM_STRLEN(SECRET_KEY_V2_PREFIX))
                      == 0) {
        /* for this type of secret key, we require a prefix followed at least SECRET_KEY_LEN (32) bytes. We
         * (also) do that, because older versions of NetworkManager wrote exactly 32 bytes without
         * prefix, so we won't wrongly interpret such legacy keys as v2 (if they accidentally have
         * a SECRET_KEY_V2_PREFIX prefix, they'll still have the wrong size).
         *
         * Note that below we generate the random seed in base64 encoding. But that is only done
         * to write an ASCII file. There is no base64 decoding and the ASCII is hashed as-is.
         * We would accept any binary data just as well (provided a suitable prefix and at least
         * 32 bytes).
         *
         * Note that when hashing the v2 content, we also hash the prefix. There is no strong reason,
         * except that it seems simpler not to distinguish between the v2 prefix and the content.
         * It's all just part of the seed. */

        secret_arr = _host_id_hash_v2(file_content.bin, file_content.len, sha256_digest);
        secret_len = NM_UTILS_CHECKSUM_LENGTH_SHA256;
        success    = TRUE;
        nm_log_dbg(LOGD_CORE,
                   "secret-key: v2 secret key loaded from \"%s\" (%zu bytes)",
                   SECRET_KEY_FILE,
                   file_content.len);
        goto out;
    } else if (file_content.len >= 16) {
        secret_arr = file_content.bin;
        secret_len = file_content.len;
        success    = TRUE;
        nm_log_dbg(LOGD_CORE,
                   "secret-key: v1 secret key loaded from \"%s\" (%zu bytes)",
                   SECRET_KEY_FILE,
                   file_content.len);
        goto out;
    } else {
        /* the secret key is borked. Log a warning, but proceed below to generate
         * a new one. */
        nm_log_warn(LOGD_CORE,
                    "secret-key: too short secret key in \"%s\" (generate new key)",
                    SECRET_KEY_FILE);
    }

    /* generate and persist new key */
    {
#define SECRET_KEY_LEN_BASE64 ((((SECRET_KEY_LEN / 3) + 1) * 4) + 4)
        guint8 rnd_buf[SECRET_KEY_LEN];
        guint8 new_content[NM_STRLEN(SECRET_KEY_V2_PREFIX) + SECRET_KEY_LEN_BASE64];
        int    base64_state = 0;
        int    base64_save  = 0;
        gsize  len;

        success = nm_utils_random_bytes(rnd_buf, sizeof(rnd_buf));

        /* Our key is really binary data. But since we anyway generate a random seed
         * (with 32 random bytes), don't write it in binary, but instead create
         * an pure ASCII (base64) representation. Note that the ASCII will still be taken
         * as-is (no base64 decoding is done). The sole purpose is to write a ASCII file
         * instead of a binary. The content is gibberish either way. */
        memcpy(new_content, SECRET_KEY_V2_PREFIX, NM_STRLEN(SECRET_KEY_V2_PREFIX));
        len = NM_STRLEN(SECRET_KEY_V2_PREFIX);
        len += g_base64_encode_step(rnd_buf,
                                    sizeof(rnd_buf),
                                    FALSE,
                                    (char *) &new_content[len],
                                    &base64_state,
                                    &base64_save);
        len +=
            g_base64_encode_close(FALSE, (char *) &new_content[len], &base64_state, &base64_save);
        nm_assert(len <= sizeof(new_content));

        secret_arr = _host_id_hash_v2(new_content, len, sha256_digest);
        secret_len = NM_UTILS_CHECKSUM_LENGTH_SHA256;

        if (!success)
            nm_log_warn(LOGD_CORE,
                        "secret-key: failure to generate good random data for secret-key (use "
                        "non-persistent key)");
        else if (nm_utils_get_testing()) {
            /* for test code, we don't write the generated secret-key to disk. */
        } else if (!nm_utils_file_set_contents(SECRET_KEY_FILE,
                                               (const char *) new_content,
                                               len,
                                               0600,
                                               NULL,
                                               NULL,
                                               &error)) {
            nm_log_warn(
                LOGD_CORE,
                "secret-key: failure to persist secret key in \"%s\" (%s) (use non-persistent key)",
                SECRET_KEY_FILE,
                error->message);
            g_clear_error(&error);
            success = FALSE;
        } else
            nm_log_dbg(LOGD_CORE,
                       "secret-key: persist new v2 secret key to \"%s\" (%zu bytes)",
                       SECRET_KEY_FILE,
                       len);

        nm_explicit_bzero(rnd_buf, sizeof(rnd_buf));
        nm_explicit_bzero(new_content, sizeof(new_content));
    }

out:
    *out_host_id_len = secret_len;
    *out_host_id     = nm_memdup(secret_arr, secret_len);
    return success;
}

typedef struct {
    guint8 *host_id;
    gsize   host_id_len;
    gint64  timestamp_ns;
    bool    is_good : 1;
    bool    timestamp_is_good : 1;
} HostIdData;

static const HostIdData *volatile host_id_static;

static const HostIdData *
_host_id_get(void)
{
    const HostIdData *host_id;

again:
    host_id = g_atomic_pointer_get(&host_id_static);
    if (G_UNLIKELY(!host_id)) {
        static HostIdData host_id_data;
        static gsize      init_value = 0;

        if (!g_once_init_enter(&init_value))
            goto again;

        host_id_data.is_good = _host_id_read(&host_id_data.host_id, &host_id_data.host_id_len);

        host_id_data.timestamp_is_good = _host_id_read_timestamp(host_id_data.is_good,
                                                                 host_id_data.host_id,
                                                                 host_id_data.host_id_len,
                                                                 &host_id_data.timestamp_ns);
        if (!host_id_data.timestamp_is_good && host_id_data.is_good)
            nm_log_warn(LOGD_CORE, "secret-key: failure reading host timestamp (use fake one)");

        host_id = &host_id_data;
        g_atomic_pointer_set(&host_id_static, host_id);
        g_once_init_leave(&init_value, 1);
    }

    return host_id;
}

/**
 * nm_utils_host_id_get:
 * @out_host_id: (out) (transfer none): the binary host key
 * @out_host_id_len: the length of the host key.
 *
 * This returns a per-host key that depends on /var/lib/NetworkManage/secret_key
 * and (depending on the version) on /etc/machine-id. If /var/lib/NetworkManage/secret_key
 * does not exist, it will be generated and persisted for next boot.
 *
 * Returns: %TRUE, if the host key is "good". Note that this function
 *   will always succeed to return a host-key, and that this key
 *   won't change during the run of the program (no matter what).
 *   A %FALSE return possibly means, that the secret_key is not persisted
 *   to disk, and/or that it was generated with bad randomness.
 */
gboolean
nm_utils_host_id_get(const guint8 **out_host_id, gsize *out_host_id_len)
{
    const HostIdData *host_id;

    host_id          = _host_id_get();
    *out_host_id     = host_id->host_id;
    *out_host_id_len = host_id->host_id_len;
    return host_id->is_good;
}

gint64
nm_utils_host_id_get_timestamp_ns(void)
{
    return _host_id_get()->timestamp_ns;
}

static GArray    *nmtst_host_id_stack = NULL;
static GMutex     nmtst_host_id_lock;
const HostIdData *nmtst_host_id_static_0 = NULL;

void
nmtst_utils_host_id_push(const guint8 *host_id,
                         gssize        host_id_len,
                         gboolean      is_good,
                         const gint64 *timestamp_ns)
{
    NM_G_MUTEX_LOCKED(&nmtst_host_id_lock);
    gs_free char *str1_to_free = NULL;
    HostIdData   *h;

    g_assert(host_id_len >= -1);

    if (host_id_len < 0)
        host_id_len = host_id ? strlen((const char *) host_id) : 0;

    nm_log_dbg(LOGD_CORE,
               "nmtst: host-id push: \"%s\" (%zu), is-good=%d, timestamp=%" G_GINT64_FORMAT "%s",
               nm_utils_buf_utf8safe_escape(host_id,
                                            host_id_len,
                                            NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL,
                                            &str1_to_free),
               (gsize) host_id_len,
               !!is_good,
               timestamp_ns ? *timestamp_ns : 0,
               timestamp_ns ? "" : " (not-good)");

    if (!nmtst_host_id_stack) {
        nmtst_host_id_stack    = g_array_new(FALSE, FALSE, sizeof(HostIdData));
        nmtst_host_id_static_0 = g_atomic_pointer_get(&host_id_static);
    }

    h = nm_g_array_append_new(nmtst_host_id_stack, HostIdData);

    *h = (HostIdData){
        .host_id           = nm_memdup(host_id, host_id_len),
        .host_id_len       = host_id_len,
        .timestamp_ns      = timestamp_ns ? *timestamp_ns : 0,
        .is_good           = is_good,
        .timestamp_is_good = !!timestamp_ns,
    };

    g_atomic_pointer_set(&host_id_static, h);
}

void
nmtst_utils_host_id_pop(void)
{
    NM_G_MUTEX_LOCKED(&nmtst_host_id_lock);
    HostIdData *h;

    g_assert(nmtst_host_id_stack);
    g_assert(nmtst_host_id_stack->len > 0);

    nm_log_dbg(LOGD_CORE, "nmtst: host-id pop");

    h = &g_array_index(nmtst_host_id_stack, HostIdData, nmtst_host_id_stack->len - 1);

    g_free((char *) h->host_id);
    g_array_set_size(nmtst_host_id_stack, nmtst_host_id_stack->len - 1u);

    if (!g_atomic_pointer_compare_and_exchange(
            &host_id_static,
            h,
            nmtst_host_id_stack->len == 0u ? nmtst_host_id_static_0
                                           : nm_g_array_last(nmtst_host_id_stack, HostIdData)))
        g_assert_not_reached();
}

/*****************************************************************************/

static const UuidData *
_boot_id_get(void)
{
    static const UuidData *volatile p_boot_id;
    const UuidData *d;

again:
    d = g_atomic_pointer_get(&p_boot_id);
    if (G_UNLIKELY(!d)) {
        static gsize    lock;
        static UuidData boot_id;
        gs_free char   *contents = NULL;
        NMUuid          uuid;
        gboolean        is_fake = FALSE;

        nm_utils_file_get_contents(-1,
                                   "/proc/sys/kernel/random/boot_id",
                                   0,
                                   NM_UTILS_FILE_GET_CONTENTS_FLAG_NONE,
                                   &contents,
                                   NULL,
                                   NULL,
                                   NULL);
        if (!contents || !nm_uuid_parse(nm_strstrip(contents), &uuid)) {
            /* generate a random UUID instead. */
            is_fake = TRUE;
            nm_uuid_generate_random(&uuid);
        }

        if (!g_once_init_enter(&lock))
            goto again;

        d = _uuid_data_init(&boot_id, FALSE, is_fake, &uuid);
        g_atomic_pointer_set(&p_boot_id, d);
        g_once_init_leave(&lock, 1);
    }

    return d;
}

const char *
nm_utils_boot_id_str(void)
{
    return _boot_id_get()->str;
}

const NMUuid *
nm_utils_boot_id_bin(void)
{
    return &_boot_id_get()->bin;
}

/*****************************************************************************/

const char *
nm_utils_proc_cmdline(void)
{
    static const char *volatile proc_cmdline_cached = NULL;
    const char *proc_cmdline;

again:
    proc_cmdline = g_atomic_pointer_get(&proc_cmdline_cached);
    if (G_UNLIKELY(!proc_cmdline)) {
        gs_free char *str = NULL;

        g_file_get_contents("/proc/cmdline", &str, NULL, NULL);
        str = nm_str_realloc(str);

        proc_cmdline = str ?: "";
        if (!g_atomic_pointer_compare_and_exchange(&proc_cmdline_cached, NULL, proc_cmdline))
            goto again;

        g_steal_pointer(&str);
    }

    return proc_cmdline;
}

const char *const *
nm_utils_proc_cmdline_split(void)
{
    static const char *const *volatile proc_cmdline_cached = NULL;
    const char *const *proc_cmdline;

again:
    proc_cmdline = g_atomic_pointer_get(&proc_cmdline_cached);
    if (G_UNLIKELY(!proc_cmdline)) {
        gs_strfreev char **split = NULL;

        split = nm_utils_strsplit_quoted(nm_utils_proc_cmdline());
        if (!g_atomic_pointer_compare_and_exchange(&proc_cmdline_cached, NULL, (gpointer) split))
            goto again;

        proc_cmdline = (const char *const *) g_steal_pointer(&split);
    }

    return proc_cmdline;
}

/*****************************************************************************/

/**
 * nm_utils_arp_type_detect_from_hwaddrlen:
 * @hwaddr_len: the length of the hardware address in bytes.
 *
 * Detects the arp-type based on the length of the MAC address.
 * On success, this returns a (positive) value in uint16_t range,
 * like ARPHRD_ETHER or ARPHRD_INFINIBAND.
 *
 * On failure, returns a negative error code.
 *
 * Returns: the arp-type or negative value on error. */
int
nm_utils_arp_type_detect_from_hwaddrlen(gsize hwaddr_len)
{
    switch (hwaddr_len) {
    case ETH_ALEN:
        return ARPHRD_ETHER;
    case INFINIBAND_ALEN:
        return ARPHRD_INFINIBAND;
    default:
        /* Note: if you ever support anything but ethernet and infiniband,
         * make sure to look at all callers. They assert that it's one of
         * these two. */
        return -EINVAL;
    }
}

gboolean
nm_utils_arp_type_validate_hwaddr(int arp_type, const guint8 *hwaddr, gsize hwaddr_len)
{
    if (!hwaddr)
        return FALSE;

    if (arp_type == ARPHRD_ETHER) {
        G_STATIC_ASSERT(ARPHRD_ETHER >= 0 && ARPHRD_ETHER <= 0xFF);
        if (hwaddr_len != ETH_ALEN)
            return FALSE;
    } else if (arp_type == ARPHRD_INFINIBAND) {
        G_STATIC_ASSERT(ARPHRD_INFINIBAND >= 0 && ARPHRD_INFINIBAND <= 0xFF);
        if (hwaddr_len != INFINIBAND_ALEN)
            return FALSE;
    } else
        return FALSE;

    nm_assert(arp_type == nm_utils_arp_type_detect_from_hwaddrlen(hwaddr_len));
    return TRUE;
}

gboolean
nm_utils_arp_type_get_hwaddr_relevant_part(int arp_type, const guint8 **hwaddr, gsize *hwaddr_len)
{
    g_return_val_if_fail(hwaddr && hwaddr_len
                             && nm_utils_arp_type_validate_hwaddr(arp_type, *hwaddr, *hwaddr_len),
                         FALSE);

    /* for infiniband, we only consider the last 8 bytes. */
    if (arp_type == ARPHRD_INFINIBAND) {
        *hwaddr += (INFINIBAND_ALEN - 8);
        *hwaddr_len = 8;
    }

    return TRUE;
}

/*****************************************************************************/

/* Returns the "u" (universal/local) bit value for a Modified EUI-64 */
static gboolean
get_gre_eui64_u_bit(guint32 addr)
{
    static const struct {
        guint32 mask;
        guint32 result;
    } items[] = {
        {0xff000000}, {0x7f000000},       /* IPv4 loopback */
        {0xf0000000}, {0xe0000000},       /* IPv4 multicast */
        {0xffffff00}, {0xe0000000},       /* IPv4 local multicast */
        {0xffffffff}, {INADDR_BROADCAST}, /* limited broadcast */
        {0xff000000}, {0x00000000},       /* zero net */
        {0xff000000}, {0x0a000000},       /* private 10 (RFC3330) */
        {0xfff00000}, {0xac100000},       /* private 172 */
        {0xffff0000}, {0xc0a80000},       /* private 192 */
        {0xffff0000}, {0xa9fe0000},       /* IPv4 link-local */
        {0xffffff00}, {0xc0586300},       /* anycast 6-to-4 */
        {0xffffff00}, {0xc0000200},       /* test 192 */
        {0xfffe0000}, {0xc6120000},       /* test 198 */
    };
    guint i;

    for (i = 0; i < G_N_ELEMENTS(items); i++) {
        if ((addr & htonl(items[i].mask)) == htonl(items[i].result))
            return 0x00; /* "local" scope */
    }
    return 0x02; /* "universal" scope */
}

/**
 * nm_utils_get_ipv6_interface_identifier:
 * @link_type: the hardware link type
 * @hwaddr: the hardware address of the interface
 * @hwaddr_len: the length (in bytes) of @hwaddr
 * @dev_id: the device identifier, if any
 * @out_iid: on success, filled with the interface identifier; on failure
 * zeroed out
 *
 * Constructs an interface identifier in "Modified EUI-64" format which is
 * suitable for constructing IPv6 addresses.  Note that the identifier is
 * not obscured in any way (eg, RFC3041).
 *
 * Returns: %TRUE if the interface identifier could be constructed, %FALSE if
 * if could not be constructed.
 */
gboolean
nm_utils_get_ipv6_interface_identifier(NMLinkType          link_type,
                                       const guint8       *hwaddr,
                                       guint               hwaddr_len,
                                       guint               dev_id,
                                       NMUtilsIPv6IfaceId *out_iid)
{
    guint32 addr;

    g_return_val_if_fail(hwaddr != NULL, FALSE);
    g_return_val_if_fail(hwaddr_len > 0, FALSE);
    g_return_val_if_fail(out_iid != NULL, FALSE);

    out_iid->id = 0;

    switch (link_type) {
    case NM_LINK_TYPE_INFINIBAND:
        /* Use the port GUID per http://tools.ietf.org/html/rfc4391#section-8,
         * making sure to set the 'u' bit to 1.  The GUID is the lower 64 bits
         * of the IPoIB interface's hardware address.
         */
        g_return_val_if_fail(hwaddr_len == INFINIBAND_ALEN, FALSE);
        memcpy(out_iid->id_u8, hwaddr + INFINIBAND_ALEN - 8, 8);
        out_iid->id_u8[0] |= 0x02;
        return TRUE;
    case NM_LINK_TYPE_GRE:
        /* Hardware address is the network-endian IPv4 address */
        g_return_val_if_fail(hwaddr_len == 4, FALSE);
        addr              = *(guint32 *) hwaddr;
        out_iid->id_u8[0] = get_gre_eui64_u_bit(addr);
        out_iid->id_u8[1] = 0x00;
        out_iid->id_u8[2] = 0x5E;
        out_iid->id_u8[3] = 0xFE;
        memcpy(out_iid->id_u8 + 4, &addr, 4);
        return TRUE;
    case NM_LINK_TYPE_6LOWPAN:
        /* The hardware address is already 64-bit. This is the case for
         * IEEE 802.15.4 networks. */
        memcpy(out_iid->id_u8, hwaddr, sizeof(out_iid->id_u8));
        return TRUE;
    default:
        if (hwaddr_len == ETH_ALEN) {
            /* Translate 48-bit MAC address to a 64-bit Modified EUI-64.  See
             * http://tools.ietf.org/html/rfc4291#appendix-A and the Linux
             * kernel's net/ipv6/addrconf.c::ipv6_generate_eui64() function.
             */
            out_iid->id_u8[0] = hwaddr[0];
            out_iid->id_u8[1] = hwaddr[1];
            out_iid->id_u8[2] = hwaddr[2];
            if (dev_id) {
                out_iid->id_u8[3] = (dev_id >> 8) & 0xff;
                out_iid->id_u8[4] = dev_id & 0xff;
            } else {
                out_iid->id_u8[0] ^= 0x02;
                out_iid->id_u8[3] = 0xff;
                out_iid->id_u8[4] = 0xfe;
            }
            out_iid->id_u8[5] = hwaddr[3];
            out_iid->id_u8[6] = hwaddr[4];
            out_iid->id_u8[7] = hwaddr[5];
            return TRUE;
        }
        break;
    }
    return FALSE;
}

/*****************************************************************************/

char *
nm_utils_stable_id_random(void)
{
    char buf[15];

    nm_utils_random_bytes(buf, sizeof(buf));
    return g_base64_encode((guchar *) buf, sizeof(buf));
}

char *
nm_utils_stable_id_generated_complete(const char *stable_id_generated)
{
    nm_auto_free_checksum GChecksum *sum = NULL;
    guint8                           buf[NM_UTILS_CHECKSUM_LENGTH_SHA1];
    char                            *base64;

    /* for NM_UTILS_STABLE_TYPE_GENERATED we generate a possibly long string
     * by doing text-substitutions in nm_utils_stable_id_parse().
     *
     * Let's shorten the (possibly) long stable_id to something more compact. */

    g_return_val_if_fail(stable_id_generated, NULL);

    sum = g_checksum_new(G_CHECKSUM_SHA1);
    g_checksum_update(sum, (guchar *) stable_id_generated, strlen(stable_id_generated));
    nm_utils_checksum_get_digest(sum, buf);

    /* we don't care to use the sha1 sum in common hex representation.
     * Use instead base64, it's 27 chars (stripping the padding) vs.
     * 40. */

    base64 = g_base64_encode((guchar *) buf, sizeof(buf));
    nm_assert(strlen(base64) == 28);
    nm_assert(base64[27] == '=');

    base64[27] = '\0';
    return base64;
}

static void
_stable_id_append(GString *str, const char *substitution)
{
    if (!substitution)
        substitution = "";
    g_string_append_printf(str, "=%zu{%s}", strlen(substitution), substitution);
}

NMUtilsStableType
nm_utils_stable_id_parse(const char *stable_id,
                         const char *deviceid,
                         const char *hwaddr,
                         const char *bootid,
                         const char *uuid,
                         char      **out_generated)
{
    gsize    i, idx_start;
    GString *str = NULL;

    g_return_val_if_fail(out_generated, NM_UTILS_STABLE_TYPE_RANDOM);

    if (!stable_id) {
        *out_generated = NULL;
        return NM_UTILS_STABLE_TYPE_UUID;
    }

    /* the stable-id allows for some dynamic by performing text-substitutions
     * of ${...} patterns.
     *
     * At first, it looks a bit like bash parameter substitution.
     * In contrast however, the process is unambiguous so that the resulting
     * effective id differs if:
     *  - the original, untranslated stable-id differs
     *  - or any of the subsitutions differs.
     *
     * The reason for that is, for example if you specify "${CONNECTION}" in the
     * stable-id, then the resulting ID should be always(!) unique for this connection.
     * There should be no way another connection could specify any stable-id that results
     * in the same addresses to be generated (aside hash collisions).
     *
     *
     * For example: say you have a connection with UUID
     * "123e4567-e89b-12d3-a456-426655440000" which happens also to be
     * the current boot-id.
     * Then:
     *   (1) connection.stable-id = <NULL>
     *   (2) connection.stable-id = "123e4567-e89b-12d3-a456-426655440000"
     *   (3) connection.stable-id = "${CONNECTION}"
     *   (3) connection.stable-id = "${BOOT}"
     * will all generate different addresses, although in one way or the
     * other, they all mangle the uuid "123e4567-e89b-12d3-a456-426655440000".
     *
     * For example, with stable-id="${FOO}${BAR}" the substitutions
     *   - FOO="ab", BAR="c"
     *   - FOO="a",  BAR="bc"
     * should give a different effective id.
     *
     * For example, with FOO="x" and BAR="x", the stable-ids
     *   - "${FOO}${BAR}"
     *   - "${BAR}${FOO}"
     * should give a different effective id.
     */

    idx_start = 0;
    for (i = 0; stable_id[i];) {
        if (stable_id[i] != '$') {
            i++;
            continue;
        }

#define CHECK_PREFIX(prefix)                                                  \
    ({                                                                        \
        gboolean _match = FALSE;                                              \
                                                                              \
        if (g_str_has_prefix(&stable_id[i], "" prefix "")) {                  \
            _match = TRUE;                                                    \
            if (!str)                                                         \
                str = g_string_sized_new(256);                                \
            i += NM_STRLEN(prefix);                                           \
            g_string_append_len(str, &(stable_id)[idx_start], i - idx_start); \
            idx_start = i;                                                    \
        }                                                                     \
        _match;                                                               \
    })
        if (CHECK_PREFIX("${CONNECTION}"))
            _stable_id_append(str, uuid);
        else if (CHECK_PREFIX("${BOOT}"))
            _stable_id_append(str, bootid);
        else if (CHECK_PREFIX("${DEVICE}"))
            _stable_id_append(str, deviceid);
        else if (CHECK_PREFIX("${MAC}"))
            _stable_id_append(str, hwaddr);
        else if (g_str_has_prefix(&stable_id[i], "${RANDOM}")) {
            /* RANDOM makes not so much sense for cloned-mac-address
             * as the result is similar to specifying "cloned-mac-address=random".
             * It makes however sense for RFC 7217 Stable Privacy IPv6 addresses
             * where this is effectively the only way to generate a different
             * (random) host identifier for each connect.
             *
             * With RANDOM, the user can switch the lifetime of the
             * generated cloned-mac-address and IPv6 host identifier
             * by toggling only the stable-id property of the connection.
             * With RANDOM being the most short-lived, ~non-stable~ variant.
             */
            if (str)
                g_string_free(str, TRUE);
            *out_generated = NULL;
            return NM_UTILS_STABLE_TYPE_RANDOM;
        } else {
            /* The text following the '$' is not recognized as valid
             * substitution pattern. Treat it verbatim. */
            i++;

            /* Note that using unrecognized substitution patterns might
             * yield different results with future versions. Avoid that,
             * by not using '$' (except for actual substitutions) or escape
             * it as "$$" (which is guaranteed to be treated verbatim
             * in future). */
            if (stable_id[i] == '$')
                i++;
        }
    }
#undef CHECK_PREFIX

    if (!str) {
        *out_generated = NULL;
        return NM_UTILS_STABLE_TYPE_STABLE_ID;
    }

    if (idx_start < i)
        g_string_append_len(str, &stable_id[idx_start], i - idx_start);
    *out_generated = g_string_free(str, FALSE);
    return NM_UTILS_STABLE_TYPE_GENERATED;
}

/*****************************************************************************/

static gboolean
_is_reserved_ipv6_iid(const guint8 *iid)
{
    /* https://tools.ietf.org/html/rfc5453 */
    /* https://www.iana.org/assignments/ipv6-interface-ids/ipv6-interface-ids.xml */

    /* 0000:0000:0000:0000 (Subnet-Router Anycast [RFC4291]) */
    if (memcmp(iid, &nm_ip_addr_zero.addr6.s6_addr[8], 8) == 0)
        return TRUE;

    /* 0200:5EFF:FE00:0000 - 0200:5EFF:FE00:5212 (Reserved IPv6 Interface Identifiers corresponding to the IANA Ethernet Block [RFC4291])
     * 0200:5EFF:FE00:5213                       (Proxy Mobile IPv6 [RFC6543])
     * 0200:5EFF:FE00:5214 - 0200:5EFF:FEFF:FFFF (Reserved IPv6 Interface Identifiers corresponding to the IANA Ethernet Block [RFC4291]) */
    if (memcmp(iid, (const guint8[]){0x02, 0x00, 0x5E, 0xFF, 0xFE}, 5) == 0)
        return TRUE;

    /* FDFF:FFFF:FFFF:FF80 - FDFF:FFFF:FFFF:FFFF (Reserved Subnet Anycast Addresses [RFC2526]) */
    if (memcmp(iid, (const guint8[]){0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, 7) == 0) {
        if (iid[7] & 0x80)
            return TRUE;
    }

    return FALSE;
}

void
nm_utils_ipv6_addr_set_stable_privacy_with_host_id(NMUtilsStableType stable_type,
                                                   struct in6_addr  *addr,
                                                   const char       *ifname,
                                                   const char       *network_id,
                                                   guint32           dad_counter,
                                                   const guint8     *host_id,
                                                   gsize             host_id_len)
{
    nm_auto_free_checksum GChecksum *sum = NULL;
    guint8                           digest[NM_UTILS_CHECKSUM_LENGTH_SHA256];
    guint32                          tmp[2];

    nm_assert(host_id_len);
    nm_assert(network_id);

    sum = g_checksum_new(G_CHECKSUM_SHA256);

    host_id_len = MIN(host_id_len, G_MAXUINT32);

    if (stable_type != NM_UTILS_STABLE_TYPE_UUID) {
        guint8 stable_type_uint8;

        nm_assert(stable_type < (NMUtilsStableType) 255);
        stable_type_uint8 = (guint8) stable_type;

        /* Preferably, we would always like to include the stable-type,
         * but for backward compatibility reasons, we cannot for UUID.
         *
         * That is no real problem and it is still impossible to
         * force a collision here, because of how the remaining
         * fields are hashed. That is, as we also hash @host_id_len
         * and the terminating '\0' of @network_id, it is unambiguously
         * possible to revert the process and deduce the @stable_type.
         */
        g_checksum_update(sum, &stable_type_uint8, sizeof(stable_type_uint8));
    }

    g_checksum_update(sum, addr->s6_addr, 8);
    g_checksum_update(sum, (const guchar *) ifname, strlen(ifname) + 1);
    g_checksum_update(sum, (const guchar *) network_id, strlen(network_id) + 1);
    tmp[0] = htonl(dad_counter);
    tmp[1] = htonl(host_id_len);
    g_checksum_update(sum, (const guchar *) tmp, sizeof(tmp));
    g_checksum_update(sum, (const guchar *) host_id, host_id_len);
    nm_utils_checksum_get_digest(sum, digest);

    while (_is_reserved_ipv6_iid(digest)) {
        g_checksum_reset(sum);
        tmp[0] = htonl(++dad_counter);
        g_checksum_update(sum, digest, sizeof(digest));
        g_checksum_update(sum, (const guchar *) &tmp[0], sizeof(tmp[0]));
        nm_utils_checksum_get_digest(sum, digest);
    }

    memcpy(addr->s6_addr + 8, &digest[0], 8);
}

void
nm_utils_ipv6_addr_set_stable_privacy(NMUtilsStableType stable_type,
                                      struct in6_addr  *addr,
                                      const char       *ifname,
                                      const char       *network_id,
                                      guint32           dad_counter)
{
    const guint8 *host_id;
    gsize         host_id_len;

    nm_utils_host_id_get(&host_id, &host_id_len);

    nm_utils_ipv6_addr_set_stable_privacy_with_host_id(stable_type,
                                                       addr,
                                                       ifname,
                                                       network_id,
                                                       dad_counter,
                                                       host_id,
                                                       host_id_len);
}

/**
 * nm_utils_ipv6_addr_set_stable_privacy:
 *
 * Extend the address prefix with an interface identifier using the
 * RFC 7217 Stable Privacy mechanism.
 *
 * Returns: %TRUE on success, %FALSE if the address could not be generated.
 */
gboolean
nm_utils_ipv6_addr_set_stable_privacy_may_fail(NMUtilsStableType stable_type,
                                               struct in6_addr  *addr,
                                               const char       *ifname,
                                               const char       *network_id,
                                               guint32           dad_counter,
                                               GError          **error)
{
    g_return_val_if_fail(network_id, FALSE);

    if (dad_counter >= NM_STABLE_PRIVACY_RFC7217_IDGEN_RETRIES) {
        g_set_error_literal(error,
                            NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            "Too many DAD collisions");
        return FALSE;
    }

    nm_utils_ipv6_addr_set_stable_privacy(stable_type, addr, ifname, network_id, dad_counter);
    return TRUE;
}

/*****************************************************************************/

static void
_hw_addr_eth_complete(struct ether_addr *addr,
                      const char        *current_mac_address,
                      const char        *generate_mac_address_mask)
{
    struct ether_addr  mask;
    struct ether_addr  oui;
    struct ether_addr *ouis;
    gsize              ouis_len;
    guint              i;

    /* the second LSB of the first octet means
     * "globally unique, OUI enforced, BIA (burned-in-address)"
     * vs. "locally-administered". By default, set it to
     * generate locally-administered addresses.
     *
     * Maybe be overwritten by a mask below. */
    addr->ether_addr_octet[0] |= 2;

    if (!generate_mac_address_mask || !*generate_mac_address_mask)
        goto out;
    if (!_nm_utils_generate_mac_address_mask_parse(generate_mac_address_mask,
                                                   &mask,
                                                   &ouis,
                                                   &ouis_len,
                                                   NULL))
        goto out;

    nm_assert((ouis == NULL) ^ (ouis_len != 0));
    if (ouis) {
        /* g_random_int() is good enough here. It uses a static GRand instance
         * that is seeded from /dev/urandom. */
        oui = ouis[g_random_int() % ouis_len];
        g_free(ouis);
    } else {
        if (!nm_utils_hwaddr_aton(current_mac_address, &oui, ETH_ALEN))
            goto out;
    }

    for (i = 0; i < ETH_ALEN; i++) {
        const guint8 a = addr->ether_addr_octet[i];
        const guint8 o = oui.ether_addr_octet[i];
        const guint8 m = mask.ether_addr_octet[i];

        addr->ether_addr_octet[i] = (a & ~m) | (o & m);
    }

out:
    /* The LSB of the first octet must always be cleared,
     * it means Unicast vs. Multicast */
    addr->ether_addr_octet[0] &= ~1;
}

char *
nm_utils_hw_addr_gen_random_eth(const char *current_mac_address,
                                const char *generate_mac_address_mask)
{
    struct ether_addr bin_addr;

    nm_utils_random_bytes(&bin_addr, ETH_ALEN);
    _hw_addr_eth_complete(&bin_addr, current_mac_address, generate_mac_address_mask);
    return nm_utils_hwaddr_ntoa(&bin_addr, ETH_ALEN);
}

static char *
_hw_addr_gen_stable_eth(NMUtilsStableType stable_type,
                        const char       *stable_id,
                        const guint8     *host_id,
                        gsize             host_id_len,
                        const char       *ifname,
                        const char       *current_mac_address,
                        const char       *generate_mac_address_mask)
{
    nm_auto_free_checksum GChecksum *sum = NULL;
    guint32                          tmp;
    guint8                           digest[NM_UTILS_CHECKSUM_LENGTH_SHA256];
    struct ether_addr                bin_addr;
    guint8                           stable_type_uint8;

    nm_assert(stable_id);
    nm_assert(host_id);

    sum = g_checksum_new(G_CHECKSUM_SHA256);

    host_id_len = MIN(host_id_len, G_MAXUINT32);

    nm_assert(stable_type < (NMUtilsStableType) 255);
    stable_type_uint8 = stable_type;
    g_checksum_update(sum, (const guchar *) &stable_type_uint8, sizeof(stable_type_uint8));

    tmp = htonl((guint32) host_id_len);
    g_checksum_update(sum, (const guchar *) &tmp, sizeof(tmp));
    g_checksum_update(sum, (const guchar *) host_id, host_id_len);
    g_checksum_update(sum, (const guchar *) (ifname ?: ""), ifname ? (strlen(ifname) + 1) : 1);
    g_checksum_update(sum, (const guchar *) stable_id, strlen(stable_id) + 1);

    nm_utils_checksum_get_digest(sum, digest);

    memcpy(&bin_addr, digest, ETH_ALEN);
    _hw_addr_eth_complete(&bin_addr, current_mac_address, generate_mac_address_mask);
    return nm_utils_hwaddr_ntoa(&bin_addr, ETH_ALEN);
}

char *
nm_utils_hw_addr_gen_stable_eth_impl(NMUtilsStableType stable_type,
                                     const char       *stable_id,
                                     const guint8     *host_id,
                                     gsize             host_id_len,
                                     const char       *ifname,
                                     const char       *current_mac_address,
                                     const char       *generate_mac_address_mask)
{
    return _hw_addr_gen_stable_eth(stable_type,
                                   stable_id,
                                   host_id,
                                   host_id_len,
                                   ifname,
                                   current_mac_address,
                                   generate_mac_address_mask);
}

char *
nm_utils_hw_addr_gen_stable_eth(NMUtilsStableType stable_type,
                                const char       *stable_id,
                                const char       *ifname,
                                const char       *current_mac_address,
                                const char       *generate_mac_address_mask)
{
    const guint8 *host_id;
    gsize         host_id_len;

    g_return_val_if_fail(stable_id, NULL);

    nm_utils_host_id_get(&host_id, &host_id_len);

    return _hw_addr_gen_stable_eth(stable_type,
                                   stable_id,
                                   host_id,
                                   host_id_len,
                                   ifname,
                                   current_mac_address,
                                   generate_mac_address_mask);
}

/*****************************************************************************/

GBytes *
nm_utils_dhcp_client_id_mac(int arp_type, const guint8 *hwaddr, gsize hwaddr_len)
{
    guint8      *client_id_buf;
    const guint8 hwaddr_type = arp_type;

    if (!nm_utils_arp_type_get_hwaddr_relevant_part(arp_type, &hwaddr, &hwaddr_len))
        g_return_val_if_reached(NULL);

    client_id_buf    = g_malloc(hwaddr_len + 1);
    client_id_buf[0] = hwaddr_type;
    memcpy(&client_id_buf[1], hwaddr, hwaddr_len);
    return g_bytes_new_take(client_id_buf, hwaddr_len + 1);
}

#define HASH_KEY              \
    ((const guint8[16]){0x80, \
                        0x11, \
                        0x8c, \
                        0xc2, \
                        0xfe, \
                        0x4a, \
                        0x03, \
                        0xee, \
                        0x3e, \
                        0xd6, \
                        0x0c, \
                        0x6f, \
                        0x36, \
                        0x39, \
                        0x14, \
                        0x09})

/**
 * nm_utils_create_dhcp_iaid:
 * @legacy_unstable_byteorder: legacy behavior is to generate a u32 iaid which
 *   is endianness dependent. This is to preserve backward compatibility.
 *   For non-legacy behavior, the returned integer is in stable endianness,
 *   and corresponds to legacy behavior on little endian systems.
 * @interface_id: the seed for hashing when generating the ID. Usually,
 *   this is the interface name.
 * @interface_id_len: length of @interface_id
 *
 * This corresponds to systemd's dhcp_identifier_set_iaid() for generating
 * a IAID for the interface.
 *
 * Returns: the IAID in host byte order. */
guint32
nm_utils_create_dhcp_iaid(gboolean      legacy_unstable_byteorder,
                          const guint8 *interface_id,
                          gsize         interface_id_len)
{
    guint64 u64;
    guint32 u32;

    u64 = c_siphash_hash(HASH_KEY, interface_id, interface_id_len);
    u32 = (u64 & 0xffffffffu) ^ (u64 >> 32);
    if (legacy_unstable_byteorder) {
        /* legacy systemd code dhcp_identifier_set_iaid() generates the iaid
         * dependent on the host endianness. Since this function returns the IAID
         * in native-byte order, we need to account for that.
         *
         * On little endian systems, we want the legacy-behavior is identical to
         * the endianness-agnostic behavior. So, we need to swap the bytes on
         * big-endian systems.
         *
         * (https://github.com/systemd/systemd/pull/10614). */
        return htole32(u32);
    } else {
        /* we return the value as-is, in native byte order. */
        return u32;
    }
}

GBytes *
nm_utils_dhcp_client_id_duid(guint32 iaid, const guint8 *duid, gsize duid_len)
{
    struct _nm_packed {
        guint8  type;
        guint32 iaid;
        guint8  duid[];
    } * client_id;
    gsize total_size;

    /* the @duid must include the 16 bit duid-type and the data (of max 128 bytes). */
    g_return_val_if_fail(duid_len > 2 && duid_len < 128 + 2, NULL);
    g_return_val_if_fail(duid, NULL);

    total_size = sizeof(*client_id) + duid_len;

    client_id = g_malloc(total_size);

    client_id->type = 255;
    unaligned_write_be32(&client_id->iaid, iaid);
    memcpy(client_id->duid, duid, duid_len);
    return g_bytes_new_take(client_id, total_size);
}

/**
 * nm_utils_dhcp_client_id_systemd_node_specific_full:
 * @iaid: the IAID (identity association identifier) in native byte order
 * @machine_id: the binary identifier for the machine. It is hashed
 *   into the DUID. It commonly is /etc/machine-id (parsed in binary as NMUuid).
 * @machine_id_len: the length of the @machine_id.
 *
 * Systemd's sd_dhcp_client generates a default client ID (type 255, node-specific,
 * RFC 4361) if no explicit client-id is set. This function duplicates that
 * implementation and exposes it as (internal) API.
 *
 * Returns: a %GBytes of generated client-id. This function cannot fail.
 */
GBytes *
nm_utils_dhcp_client_id_systemd_node_specific_full(guint32       iaid,
                                                   const guint8 *machine_id,
                                                   gsize         machine_id_len)
{
    const guint16 DUID_TYPE_EN = 2;
    const guint32 SYSTEMD_PEN  = 43793;
    struct _nm_packed {
        guint8  type;
        guint32 iaid;
        struct _nm_packed {
            guint16 type;
            union {
                struct _nm_packed {
                    /* DUID_TYPE_EN */
                    guint32 pen;
                    uint8_t id[8];
                } en;
            };
        } duid;
    } * client_id;
    guint64 u64;

    g_return_val_if_fail(machine_id, NULL);
    g_return_val_if_fail(machine_id_len > 0, NULL);

    client_id = g_malloc(sizeof(*client_id));

    client_id->type = 255;
    unaligned_write_be32(&client_id->iaid, iaid);
    unaligned_write_be16(&client_id->duid.type, DUID_TYPE_EN);
    unaligned_write_be32(&client_id->duid.en.pen, SYSTEMD_PEN);

    u64 = htole64(c_siphash_hash(HASH_KEY, machine_id, machine_id_len));
    memcpy(client_id->duid.en.id, &u64, sizeof(client_id->duid.en.id));

    G_STATIC_ASSERT_EXPR(sizeof(*client_id) == 19);
    return g_bytes_new_take(client_id, 19);
}

GBytes *
nm_utils_dhcp_client_id_systemd_node_specific(guint32 iaid)
{
    return nm_utils_dhcp_client_id_systemd_node_specific_full(
        iaid,
        (const guint8 *) nm_utils_machine_id_bin(),
        sizeof(NMUuid));
}

/*****************************************************************************/

GBytes *
nm_utils_generate_duid_llt(int arp_type, const guint8 *hwaddr, gsize hwaddr_len, gint64 time)
{
    guint8       *arr;
    const guint16 duid_type = htons(1);
    const guint16 hw_type   = htons(arp_type);
    const guint32 duid_time = htonl(NM_MAX(0, time - NM_UTILS_EPOCH_DATETIME_200001010000));

    if (!nm_utils_arp_type_get_hwaddr_relevant_part(arp_type, &hwaddr, &hwaddr_len))
        nm_assert_not_reached();

    arr = g_new(guint8, (2u + 2u + 4u) + hwaddr_len);

    memcpy(&arr[0], &duid_type, 2);
    memcpy(&arr[2], &hw_type, 2);
    memcpy(&arr[4], &duid_time, 4);
    memcpy(&arr[8], hwaddr, hwaddr_len);

    return g_bytes_new_take(arr, (2u + 2u + 4u) + hwaddr_len);
}

GBytes *
nm_utils_generate_duid_ll(int arp_type, const guint8 *hwaddr, gsize hwaddr_len)
{
    guint8       *arr;
    const guint16 duid_type = htons(3);
    const guint16 hw_type   = htons(arp_type);

    if (!nm_utils_arp_type_get_hwaddr_relevant_part(arp_type, &hwaddr, &hwaddr_len))
        nm_assert_not_reached();

    arr = g_new(guint8, (2u + 2u) + hwaddr_len);

    memcpy(&arr[0], &duid_type, 2);
    memcpy(&arr[2], &hw_type, 2);
    memcpy(&arr[4], hwaddr, hwaddr_len);

    return g_bytes_new_take(arr, (2u + 2u) + hwaddr_len);
}

GBytes *
nm_utils_generate_duid_uuid(const NMUuid *uuid)
{
    const guint16 duid_type = htons(4);
    guint8       *duid_buffer;

    nm_assert(uuid);

    /* Generate a DHCP Unique Identifier for DHCPv6 using the
     * DUID-UUID method (see RFC 6355 section 4).  Format is:
     *
     * u16: type (DUID-UUID = 4)
     * u8[16]: UUID bytes
     */
    G_STATIC_ASSERT_EXPR(sizeof(duid_type) == 2);
    G_STATIC_ASSERT_EXPR(sizeof(*uuid) == 16);
    duid_buffer = g_malloc(18);
    memcpy(&duid_buffer[0], &duid_type, 2);
    memcpy(&duid_buffer[2], uuid, 16);
    return g_bytes_new_take(duid_buffer, 18);
}

GBytes *
nm_utils_generate_duid_from_machine_id(void)
{
    static GBytes *volatile global_duid = NULL;
    GBytes *p;

again:
    p = g_atomic_pointer_get(&global_duid);
    if (G_UNLIKELY(!p)) {
        nm_auto_free_checksum GChecksum *sum = NULL;
        const NMUuid                    *machine_id;
        union {
            guint8 sha256[NM_UTILS_CHECKSUM_LENGTH_SHA256];
            NMUuid uuid;
        } digest;

        machine_id = nm_utils_machine_id_bin();

        /* Hash the machine ID so it's not leaked to the network.
         *
         * Optimally, we would choose an use case specific seed, but for historic
         * reasons we didn't. */
        sum = g_checksum_new(G_CHECKSUM_SHA256);
        g_checksum_update(sum, (const guchar *) machine_id, sizeof(*machine_id));
        nm_utils_checksum_get_digest(sum, digest.sha256);

        G_STATIC_ASSERT_EXPR(sizeof(digest.sha256) > sizeof(digest.uuid));
        p = nm_utils_generate_duid_uuid(&digest.uuid);

        if (!g_atomic_pointer_compare_and_exchange(&global_duid, NULL, p)) {
            g_bytes_unref(p);
            goto again;
        }
    }

    return g_bytes_ref(p);
}

/*****************************************************************************/

/**
 * nm_utils_setpgid:
 * @unused: unused
 *
 * This can be passed as a child setup function to the g_spawn*() family
 * of functions, to ensure that the child is in its own process group
 * (and thus, in some situations, will not be killed when NetworkManager
 * is killed).
 */
void
nm_utils_setpgid(gpointer unused G_GNUC_UNUSED)
{
    pid_t pid;

    pid = getpid();
    setpgid(pid, pid);
}

/**
 * nm_utils_g_value_set_strv:
 * @value: a #GValue, initialized to store a #G_TYPE_STRV
 * @strings: a #GPtrArray of strings. %NULL values are not
 *   allowed.
 *
 * Converts @strings to a #GStrv and stores it in @value.
 */
void
nm_utils_g_value_set_strv(GValue *value, GPtrArray *strings)
{
    char **strv;
    guint  i;

    strv = g_new(char *, strings->len + 1);
    for (i = 0; i < strings->len; i++) {
        nm_assert(strings->pdata[i]);
        strv[i] = g_strdup(strings->pdata[i]);
    }
    strv[i] = NULL;

    g_value_take_boxed(value, strv);
}

/*****************************************************************************/

const char *
nm_utils_dnsmasq_status_to_string(int status, char *dest, gsize size)
{
    const char *msg;

    nm_utils_to_string_buffer_init(&dest, &size);

    if (status == 0)
        msg = "Success";
    else if (status == 1)
        msg = "Configuration problem";
    else if (status == 2)
        msg = "Network access problem (address in use, permissions)";
    else if (status == 3)
        msg = "Filesystem problem (missing file/directory, permissions)";
    else if (status == 4)
        msg = "Memory allocation failure";
    else if (status == 5)
        msg = "Other problem";
    else if (status >= 11) {
        g_snprintf(dest, size, "Lease script failed with error %d", status - 10);
        return dest;
    } else
        msg = "Unknown problem";

    g_snprintf(dest, size, "%s (%d)", msg, status);
    return dest;
}

/**
 * nm_utils_get_reverse_dns_domains_ip_4:
 * @addr: IP address in network order
 * @plen: prefix length
 * @domains: array for results
 *
 * Creates reverse DNS domains for the given address and prefix length, and
 * append them to @domains.
 */
void
nm_utils_get_reverse_dns_domains_ip_4(guint32 addr, guint8 plen, GPtrArray *domains)
{
    guint32 ip, ip2, mask;
    guchar *p;
    guint   octets;
    guint   i;
    gsize   len0, len;
    char   *str, *s;

    g_return_if_fail(domains);
    g_return_if_fail(plen <= 32);

    if (!plen)
        return;

    octets = (plen - 1) / 8 + 1;
    ip     = ntohl(addr);
    mask   = 0xFFFFFFFF << (32 - plen);
    ip &= mask;
    ip2 = ip;

    len0 = NM_STRLEN("in-addr.arpa") + (4 * octets) + 1;
    while ((ip2 & mask) == ip) {
        addr = htonl(ip2);
        p    = (guchar *) &addr;

        len = len0;
        str = s = g_malloc(len);
        for (i = octets; i > 0; i--)
            nm_strbuf_append(&s, &len, "%u.", p[i - 1] & 0xff);
        nm_strbuf_append_str(&s, &len, "in-addr.arpa");

        g_ptr_array_add(domains, str);

        ip2 += 1 << ((32 - plen) & ~7);
    }
}

/**
 * nm_utils_get_reverse_dns_domains_ip_6:
 * @addr: IPv6 address
 * @plen: prefix length
 * @domains: array for results
 *
 * Creates reverse DNS domains for the given address and prefix length, and
 * append them to @domains.
 */
void
nm_utils_get_reverse_dns_domains_ip_6(const struct in6_addr *ip, guint8 plen, GPtrArray *domains)
{
    struct in6_addr addr;
    guint           nibbles, bits, entries;
    int             i, j;
    gsize           len0, len;
    char           *str, *s;

    g_return_if_fail(domains);
    g_return_if_fail(plen <= 128);

    if (!plen)
        return;

    memcpy(&addr, ip, sizeof(struct in6_addr));
    nm_utils_ip6_address_clear_host_address(&addr, NULL, plen);

    /* Number of nibbles to include in domains */
    nibbles = (plen - 1) / 4 + 1;
    /* Prefix length in nibble */
    bits = plen - ((plen - 1) / 4 * 4);
    /* Number of domains */
    entries = 1 << (4 - bits);

    len0 = NM_STRLEN("ip6.arpa") + (2 * nibbles) + 1;

#define N_SHIFT(x) ((x) % 2 ? 0 : 4)

    for (i = 0; i < entries; i++) {
        len = len0;
        str = s = g_malloc(len);

        for (j = nibbles - 1; j >= 0; j--)
            nm_strbuf_append(&s, &len, "%x.", (addr.s6_addr[j / 2] >> N_SHIFT(j)) & 0xf);
        nm_strbuf_append_str(&s, &len, "ip6.arpa");

        g_ptr_array_add(domains, str);

        addr.s6_addr[(nibbles - 1) / 2] += 1 << N_SHIFT(nibbles - 1);
    }

#undef N_SHIFT
}

struct plugin_info {
    char       *path;
    struct stat st;
};

static int
read_device_factory_paths_sort_fcn(gconstpointer a, gconstpointer b)
{
    const struct plugin_info *da = a;
    const struct plugin_info *db = b;
    time_t                    ta, tb;

    ta = MAX(da->st.st_mtime, da->st.st_ctime);
    tb = MAX(db->st.st_mtime, db->st.st_ctime);

    if (ta < tb)
        return 1;
    if (ta > tb)
        return -1;
    return 0;
}

gboolean
nm_utils_validate_plugin(const char *path, struct stat *st, GError **error)
{
    g_return_val_if_fail(path, FALSE);
    g_return_val_if_fail(st, FALSE);
    g_return_val_if_fail(!error || !*error, FALSE);

    if (!S_ISREG(st->st_mode)) {
        g_set_error_literal(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "not a regular file");
        return FALSE;
    }

    if (st->st_uid != 0) {
        g_set_error_literal(error,
                            NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            "file has invalid owner (should be root)");
        return FALSE;
    }

    if (st->st_mode & (S_IWGRP | S_IWOTH | S_ISUID)) {
        g_set_error_literal(error,
                            NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            "file has invalid permissions");
        return FALSE;
    }

    return TRUE;
}

char **
nm_utils_read_plugin_paths(const char *dirname, const char *prefix)
{
    GDir       *dir;
    GError     *error = NULL;
    const char *item;
    GArray     *paths;
    char      **result;
    guint       i;

    g_return_val_if_fail(dirname, NULL);
    g_return_val_if_fail(prefix, NULL);

    dir = g_dir_open(dirname, 0, &error);
    if (!dir) {
        nm_log_warn(LOGD_CORE,
                    "device plugin: failed to open directory %s: %s",
                    dirname,
                    error->message);
        g_clear_error(&error);
        return NULL;
    }

    paths = g_array_new(FALSE, FALSE, sizeof(struct plugin_info));

    while ((item = g_dir_read_name(dir))) {
        int                errsv;
        struct plugin_info data;

        if (!g_str_has_prefix(item, prefix))
            continue;
        if (!g_str_has_suffix(item, ".so"))
            continue;

        data.path = g_build_filename(dirname, item, NULL);

        if (stat(data.path, &data.st) != 0) {
            errsv = errno;
            nm_log_warn(LOGD_CORE,
                        "plugin: skip invalid file %s (error during stat: %s)",
                        data.path,
                        nm_strerror_native(errsv));
            goto skip;
        }

        if (!nm_utils_validate_plugin(data.path, &data.st, &error)) {
            nm_log_warn(LOGD_CORE, "plugin: skip invalid file %s: %s", data.path, error->message);
            g_clear_error(&error);
            goto skip;
        }

        g_array_append_val(paths, data);
        continue;
skip:
        g_free(data.path);
    }
    g_dir_close(dir);

    /* sort filenames by modification time. */
    g_array_sort(paths, read_device_factory_paths_sort_fcn);

    result = g_new(char *, paths->len + 1);
    for (i = 0; i < paths->len; i++)
        result[i] = g_array_index(paths, struct plugin_info, i).path;
    result[i] = NULL;

    g_array_free(paths, TRUE);
    return result;
}

char *
nm_utils_format_con_diff_for_audit(GHashTable *diff)
{
    GHashTable    *setting_diff;
    char          *setting_name, *prop_name;
    GHashTableIter iter, iter2;
    GString       *str;

    str = g_string_sized_new(32);
    g_hash_table_iter_init(&iter, diff);

    while (g_hash_table_iter_next(&iter, (gpointer *) &setting_name, (gpointer *) &setting_diff)) {
        if (!setting_diff)
            continue;

        g_hash_table_iter_init(&iter2, setting_diff);

        while (g_hash_table_iter_next(&iter2, (gpointer *) &prop_name, NULL))
            g_string_append_printf(str, "%s.%s,", setting_name, prop_name);
    }

    if (str->len)
        str->str[str->len - 1] = '\0';

    return g_string_free(str, FALSE);
}

const char *
nm_utils_parse_dns_domain(const char *domain, gboolean *is_routing)
{
    g_return_val_if_fail(domain, NULL);
    g_return_val_if_fail(domain[0], NULL);

    if (domain[0] == '~') {
        domain++;
        NM_SET_OUT(is_routing, TRUE);
    } else
        NM_SET_OUT(is_routing, FALSE);

    return domain;
}

/*****************************************************************************/

static guint32
get_max_rate_ht_20(int mcs)
{
    switch (mcs) {
    case 0:
        return 6500000;
    case 1:
    case 8:
        return 13000000;
    case 2:
    case 16:
        return 19500000;
    case 3:
    case 9:
    case 24:
        return 26000000;
    case 4:
    case 10:
    case 17:
        return 39000000;
    case 5:
    case 11:
    case 25:
        return 52000000;
    case 6:
    case 18:
        return 58500000;
    case 7:
        return 65000000;
    case 12:
    case 19:
    case 26:
        return 78000000;
    case 13:
    case 27:
        return 104000000;
    case 14:
    case 20:
        return 117000000;
    case 15:
        return 130000000;
    case 21:
    case 28:
        return 156000000;
    case 22:
        return 175500000;
    case 23:
        return 195000000;
    case 29:
        return 208000000;
    case 30:
        return 234000000;
    case 31:
        return 260000000;
    }
    return 0;
}

static guint32
get_max_rate_ht_40(int mcs)
{
    switch (mcs) {
    case 0:
        return 13500000;
    case 1:
    case 8:
        return 27000000;
    case 2:
        return 40500000;
    case 3:
    case 9:
    case 24:
        return 54000000;
    case 4:
    case 10:
    case 17:
        return 81000000;
    case 5:
    case 11:
    case 25:
        return 108000000;
    case 6:
    case 18:
        return 121500000;
    case 7:
        return 135000000;
    case 12:
    case 19:
    case 26:
        return 162000000;
    case 13:
    case 27:
        return 216000000;
    case 14:
    case 20:
        return 243000000;
    case 15:
        return 270000000;
    case 16:
        return 40500000;
    case 21:
    case 28:
        return 324000000;
    case 22:
        return 364500000;
    case 23:
        return 405000000;
    case 29:
        return 432000000;
    case 30:
        return 486000000;
    case 31:
        return 540000000;
    }
    return 0;
}

static guint32
get_max_rate_vht_80_ss1(int mcs)
{
    switch (mcs) {
    case 0:
        return 29300000;
    case 1:
        return 58500000;
    case 2:
        return 87800000;
    case 3:
        return 117000000;
    case 4:
        return 175500000;
    case 5:
        return 234000000;
    case 6:
        return 263300000;
    case 7:
        return 292500000;
    case 8:
        return 351000000;
    case 9:
        return 390000000;
    }
    return 0;
}

static guint32
get_max_rate_vht_80_ss2(int mcs)
{
    switch (mcs) {
    case 0:
        return 58500000;
    case 1:
        return 117000000;
    case 2:
        return 175500000;
    case 3:
        return 234000000;
    case 4:
        return 351000000;
    case 5:
        return 468000000;
    case 6:
        return 526500000;
    case 7:
        return 585000000;
    case 8:
        return 702000000;
    case 9:
        return 780000000;
    }
    return 0;
}

static guint32
get_max_rate_vht_80_ss3(int mcs)
{
    switch (mcs) {
    case 0:
        return 87800000;
    case 1:
        return 175500000;
    case 2:
        return 263300000;
    case 3:
        return 351000000;
    case 4:
        return 526500000;
    case 5:
        return 702000000;
    case 6:
        return 0;
    case 7:
        return 877500000;
    case 8:
        return 105300000;
    case 9:
        return 117000000;
    }
    return 0;
}

static guint32
get_max_rate_vht_160_ss1(int mcs)
{
    switch (mcs) {
    case 0:
        return 58500000;
    case 1:
        return 117000000;
    case 2:
        return 175500000;
    case 3:
        return 234000000;
    case 4:
        return 351000000;
    case 5:
        return 468000000;
    case 6:
        return 526500000;
    case 7:
        return 585000000;
    case 8:
        return 702000000;
    case 9:
        return 780000000;
    }
    return 0;
}

static guint32
get_max_rate_vht_160_ss2(int mcs)
{
    switch (mcs) {
    case 0:
        return 117000000;
    case 1:
        return 234000000;
    case 2:
        return 351000000;
    case 3:
        return 468000000;
    case 4:
        return 702000000;
    case 5:
        return 936000000;
    case 6:
        return 1053000000;
    case 7:
        return 1170000000;
    case 8:
        return 1404000000;
    case 9:
        return 1560000000;
    }
    return 0;
}

static guint32
get_max_rate_vht_160_ss3(int mcs)
{
    switch (mcs) {
    case 0:
        return 175500000;
    case 1:
        return 351000000;
    case 2:
        return 526500000;
    case 3:
        return 702000000;
    case 4:
        return 1053000000;
    case 5:
        return 1404000000;
    case 6:
        return 1579500000;
    case 7:
        return 1755000000;
    case 8:
        return 2106000000;
    case 9:
        return 0;
    }
    return 0;
}

static gboolean
get_max_rate_ht(const guint8 *bytes, guint len, guint32 *out_maxrate)
{
    guint32       i;
    guint8        ht_cap_info;
    const guint8 *supported_mcs_set;
    guint32       rate;

    /* http://standards.ieee.org/getieee802/download/802.11-2012.pdf
     * https://mrncciew.com/2014/10/19/cwap-ht-capabilities-ie/
     */

    if (len != 26)
        return FALSE;

    ht_cap_info       = bytes[0];
    supported_mcs_set = &bytes[3];
    *out_maxrate      = 0;

    /* Find the maximum supported mcs rate */
    for (i = 0; i <= 76; i++) {
        const unsigned mcs_octet    = i / 8;
        const unsigned MCS_RATE_BIT = 1 << i % 8;

        if (supported_mcs_set[mcs_octet] & MCS_RATE_BIT) {
            /* Check for 40Mhz wide channel support */
            if (ht_cap_info & (1 << 1))
                rate = get_max_rate_ht_40(i);
            else
                rate = get_max_rate_ht_20(i);

            if (rate > *out_maxrate)
                *out_maxrate = rate;
        }
    }

    return TRUE;
}

static gboolean
get_max_rate_vht(const guint8 *bytes, guint len, guint32 *out_maxrate)
{
    guint32 mcs, m;
    guint8  vht_cap, tx_map;

    /* https://tda802dot11.blogspot.it/2014/10/vht-capabilities-element-vht.html
     * http://chimera.labs.oreilly.com/books/1234000001739/ch03.html#management_frames */

    if (len != 12)
        return FALSE;

    vht_cap = bytes[0];
    tx_map  = bytes[8];

    /* Check for mcs rates 8 and 9 support */
    if (tx_map & 0x2a)
        mcs = 9;
    else if (tx_map & 0x15)
        mcs = 8;
    else
        mcs = 7;

    /* Check for 160Mhz wide channel support and
     * spatial stream support */
    if (vht_cap & (1 << 2)) {
        if (tx_map & 0x30)
            m = get_max_rate_vht_160_ss3(mcs);
        else if (tx_map & 0x0C)
            m = get_max_rate_vht_160_ss2(mcs);
        else
            m = get_max_rate_vht_160_ss1(mcs);
    } else {
        if (tx_map & 0x30)
            m = get_max_rate_vht_80_ss3(mcs);
        else if (tx_map & 0x0C)
            m = get_max_rate_vht_80_ss2(mcs);
        else
            m = get_max_rate_vht_80_ss1(mcs);
    }

    *out_maxrate = m;
    return TRUE;
}

/* Management Frame Information Element IDs, ieee80211_eid */
#define WLAN_EID_HT_CAPABILITY   45
#define WLAN_EID_VHT_CAPABILITY  191
#define WLAN_EID_VENDOR_SPECIFIC 221

void
nm_wifi_utils_parse_ies(const guint8 *bytes,
                        gsize         len,
                        guint32      *out_max_rate,
                        gboolean     *out_metered,
                        gboolean     *out_owe_transition_mode)
{
    guint8  id, elem_len;
    guint32 m;

    NM_SET_OUT(out_max_rate, 0);
    NM_SET_OUT(out_metered, FALSE);
    NM_SET_OUT(out_owe_transition_mode, FALSE);

    while (len) {
        if (len < 2)
            break;

        id       = *bytes++;
        elem_len = *bytes++;
        len -= 2;

        if (elem_len > len)
            break;

        switch (id) {
        case WLAN_EID_HT_CAPABILITY:
            if (out_max_rate) {
                if (get_max_rate_ht(bytes, elem_len, &m))
                    *out_max_rate = NM_MAX(*out_max_rate, m);
            }
            break;
        case WLAN_EID_VHT_CAPABILITY:
            if (out_max_rate) {
                if (get_max_rate_vht(bytes, elem_len, &m))
                    *out_max_rate = NM_MAX(*out_max_rate, m);
            }
            break;
        case WLAN_EID_VENDOR_SPECIFIC:
            if (len == 8 && bytes[0] == 0x00 /* OUI: Microsoft */
                && bytes[1] == 0x50 && bytes[2] == 0xf2
                && bytes[3] == 0x11) /* OUI type: Network cost */
            {
                /* https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nct/ */
                NM_SET_OUT(out_metered, (bytes[4] > 1)); /* Cost level > 1 */
            }
            if (elem_len >= 10 && bytes[0] == 0x50 /* OUI: WiFi Alliance */
                && bytes[1] == 0x6f && bytes[2] == 0x9a
                && bytes[3] == 0x1c) /* OUI type: OWE Transition Mode */
                NM_SET_OUT(out_owe_transition_mode, TRUE);
            break;
        }

        len -= elem_len;
        bytes += elem_len;
    }
}

/*****************************************************************************/

guint8
nm_wifi_utils_level_to_quality(int val)
{
    if (val < 0) {
        /* Assume dBm already; rough conversion: best = -40, worst = -100 */
        val = abs(CLAMP(val, -100, -40) + 40); /* normalize to 0 */
        val = 100 - (int) ((100.0 * (double) val) / 60.0);
    } else if (val > 110 && val < 256) {
        /* assume old-style WEXT 8-bit unsigned signal level */
        val -= 256;                            /* subtract 256 to convert to dBm */
        val = abs(CLAMP(val, -100, -40) + 40); /* normalize to 0 */
        val = 100 - (int) ((100.0 * (double) val) / 60.0);
    } else {
        /* Assume signal is a "quality" percentage */
    }

    return CLAMP(val, 0, 100);
}

/*****************************************************************************/

NM_UTILS_LOOKUP_STR_DEFINE(nm_activation_type_to_string,
                           NMActivationType,
                           NM_UTILS_LOOKUP_DEFAULT_WARN("(unknown)"),
                           NM_UTILS_LOOKUP_STR_ITEM(NM_ACTIVATION_TYPE_MANAGED, "managed"),
                           NM_UTILS_LOOKUP_STR_ITEM(NM_ACTIVATION_TYPE_ASSUME, "assume"),
                           NM_UTILS_LOOKUP_STR_ITEM(NM_ACTIVATION_TYPE_EXTERNAL, "external"), );

/*****************************************************************************/

typedef struct {
    GPid     pid;
    GTask   *task;
    gulong   cancellable_id;
    GSource *child_watch_source;
    GSource *timeout_source;

    int      child_stdin;
    int      child_stdout;
    GSource *input_source;
    GSource *output_source;

    NMStrBuf in_buffer;
    NMStrBuf out_buffer;
    gsize    out_buffer_offset;
} HelperInfo;

#define _NMLOG_PREFIX_NAME "helper"
#define _NMLOG_DOMAIN      LOGD_CORE
#define _NMLOG2(level, info, ...)                                                   \
    G_STMT_START                                                                    \
    {                                                                               \
        if (nm_logging_enabled((level), (_NMLOG_DOMAIN))) {                         \
            HelperInfo *_info = (info);                                             \
                                                                                    \
            _nm_log((level),                                                        \
                    (_NMLOG_DOMAIN),                                                \
                    0,                                                              \
                    NULL,                                                           \
                    NULL,                                                           \
                    _NMLOG_PREFIX_NAME "[" NM_HASH_OBFUSCATE_PTR_FMT                \
                                       ",%d]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
                    NM_HASH_OBFUSCATE_PTR(_info),                                   \
                    _info->pid _NM_UTILS_MACRO_REST(__VA_ARGS__));                  \
        }                                                                           \
    }                                                                               \
    G_STMT_END

static void
helper_info_free(gpointer data)
{
    HelperInfo *info = data;

    nm_clear_g_source_inst(&info->child_watch_source);
    nm_clear_g_source_inst(&info->timeout_source);
    g_object_unref(info->task);

    nm_str_buf_destroy(&info->in_buffer);
    nm_str_buf_destroy(&info->out_buffer);
    nm_clear_g_source_inst(&info->input_source);
    nm_clear_g_source_inst(&info->output_source);

    if (info->child_stdout != -1)
        nm_close(info->child_stdout);
    if (info->child_stdin != -1)
        nm_close(info->child_stdin);

    if (info->pid != -1) {
        nm_assert(info->pid > 1);
        nm_utils_kill_child_async(info->pid, SIGKILL, LOGD_CORE, _NMLOG_PREFIX_NAME, 0, NULL, NULL);
    }

    g_free(info);
}

static void
helper_complete(HelperInfo *info, GError *error)
{
    if (error) {
        nm_clear_g_cancellable_disconnect(g_task_get_cancellable(info->task),
                                          &info->cancellable_id);
        g_task_return_error(info->task, error);
        helper_info_free(info);
        return;
    }

    if (info->input_source || info->output_source || info->pid != -1) {
        /* Wait that pipes are closed and process has terminated */
        return;
    }

    nm_clear_g_cancellable_disconnect(g_task_get_cancellable(info->task), &info->cancellable_id);
    g_task_return_pointer(info->task, nm_str_buf_finalize(&info->in_buffer, NULL), g_free);
    helper_info_free(info);
}

static gboolean
helper_can_write(int fd, GIOCondition condition, gpointer user_data)
{
    HelperInfo *info = user_data;
    gssize      n_written;
    int         errsv;

    if (NM_FLAGS_HAS(condition, G_IO_ERR)) {
        errsv = EIO;
        goto out_error;
    } else if (NM_FLAGS_HAS(condition, G_IO_HUP)) {
        errsv = EPIPE;
        goto out_error;
    }

    n_written = write(info->child_stdin,
                      &((nm_str_buf_get_str_unsafe(&info->out_buffer))[info->out_buffer_offset]),
                      info->out_buffer.len - info->out_buffer_offset);
    errsv     = errno;

    if (n_written < 0 && errsv != EAGAIN)
        goto out_error;

    if (n_written > 0) {
        if ((gsize) n_written >= (info->out_buffer.len - info->out_buffer_offset)) {
            nm_assert((gsize) n_written == (info->out_buffer.len - info->out_buffer_offset));
            nm_clear_g_source_inst(&info->output_source);
            nm_close(info->child_stdin);
            info->child_stdin = -1;
            return G_SOURCE_CONTINUE;
        }
        info->out_buffer_offset += (gsize) n_written;
    }

    return G_SOURCE_CONTINUE;

out_error:
    nm_clear_g_source_inst(&info->output_source);
    helper_complete(info,
                    g_error_new(NM_UTILS_ERROR,
                                NM_UTILS_ERROR_UNKNOWN,
                                "error writing to helper: %d (%s)",
                                errsv,
                                nm_strerror_native(errsv)));
    return G_SOURCE_CONTINUE;
}

static gboolean
helper_have_data(int fd, GIOCondition condition, gpointer user_data)
{
    HelperInfo *info = user_data;
    gssize      n_read;
    GError     *error = NULL;

    n_read = nm_utils_fd_read(fd, &info->in_buffer);
    _LOG2T(info, "read returns %ld", (long) n_read);

    if (n_read > 0)
        return G_SOURCE_CONTINUE;

    nm_clear_g_source_inst(&info->input_source);
    nm_close(info->child_stdout);
    info->child_stdout = -1;

    _LOG2T(info, "stdout closed");

    if (n_read < 0) {
        error = g_error_new(NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            "read from process returned %d (%s)",
                            (int) -n_read,
                            nm_strerror_native((int) -n_read));
    }

    helper_complete(info, error);
    return G_SOURCE_CONTINUE;
}

static void
helper_child_terminated(GPid pid, int status, gpointer user_data)
{
    HelperInfo   *info        = user_data;
    GError       *error       = NULL;
    gs_free char *status_desc = NULL;

    _LOG2D(info, "process %s", (status_desc = nm_utils_get_process_exit_status_desc(status)));

    info->pid = -1;
    nm_clear_g_source_inst(&info->child_watch_source);

    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
        if (!status_desc)
            status_desc = nm_utils_get_process_exit_status_desc(status);
        error =
            g_error_new(NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "helper process %s", status_desc);
    }

    helper_complete(info, error);
}

static gboolean
helper_timeout(gpointer user_data)
{
    HelperInfo *info = user_data;

    nm_clear_g_source_inst(&info->timeout_source);
    helper_complete(info, g_error_new_literal(NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "timed out"));

    return G_SOURCE_CONTINUE;
}

static void
helper_cancelled(GObject *object, gpointer user_data)
{
    HelperInfo *info  = user_data;
    GError     *error = NULL;

    nm_clear_g_signal_handler(g_task_get_cancellable(info->task), &info->cancellable_id);
    nm_utils_error_set_cancelled(&error, FALSE, NULL);
    helper_complete(info, error);
}

void
nm_utils_spawn_helper(const char *const  *args,
                      GCancellable       *cancellable,
                      GAsyncReadyCallback callback,
                      gpointer            cb_data)
{
    gs_free_error GError *error    = NULL;
    gs_free char         *commands = NULL;
    HelperInfo           *info;
    int                   fd_flags;
    const char *const    *arg;

    nm_assert(args && args[0]);

    info  = g_new(HelperInfo, 1);
    *info = (HelperInfo){
        .task         = nm_g_task_new(NULL, cancellable, nm_utils_spawn_helper, callback, cb_data),
        .child_stdin  = -1,
        .child_stdout = -1,
        .pid          = -1,
    };

    if (!g_spawn_async_with_pipes("/",
                                  (char **) NM_MAKE_STRV(LIBEXECDIR "/nm-daemon-helper"),
                                  (char **) NM_MAKE_STRV(),
                                  G_SPAWN_DO_NOT_REAP_CHILD,
                                  NULL,
                                  NULL,
                                  &info->pid,
                                  &info->child_stdin,
                                  &info->child_stdout,
                                  NULL,
                                  &error)) {
        info->child_stdin  = -1;
        info->child_stdout = -1;
        info->pid          = -1;
        g_task_return_error(info->task,
                            g_error_new(NM_UTILS_ERROR,
                                        NM_UTILS_ERROR_UNKNOWN,
                                        "error spawning nm-helper: %s",
                                        error->message));
        helper_info_free(info);
        return;
    }

    _LOG2D(info, "spawned process with args: %s", (commands = g_strjoinv(" ", (char **) args)));

    info->child_watch_source = g_child_watch_source_new(info->pid);
    g_source_set_callback(info->child_watch_source,
                          G_SOURCE_FUNC(helper_child_terminated),
                          info,
                          NULL);
    g_source_attach(info->child_watch_source, g_main_context_get_thread_default());

    info->timeout_source =
        nm_g_timeout_source_new_seconds(20, G_PRIORITY_DEFAULT, helper_timeout, info, NULL);
    g_source_attach(info->timeout_source, g_main_context_get_thread_default());

    /* Set file descriptors as non-blocking */
    fd_flags = fcntl(info->child_stdin, F_GETFD, 0);
    fcntl(info->child_stdin, F_SETFL, fd_flags | O_NONBLOCK);
    fd_flags = fcntl(info->child_stdout, F_GETFD, 0);
    fcntl(info->child_stdout, F_SETFL, fd_flags | O_NONBLOCK);

    /* Watch process stdin */
    nm_str_buf_init(&info->out_buffer, 32, TRUE);
    for (arg = args; *arg; arg++) {
        nm_str_buf_append(&info->out_buffer, *arg);
        nm_str_buf_append_c(&info->out_buffer, '\0');
    }
    info->output_source = nm_g_unix_fd_source_new(info->child_stdin,
                                                  G_IO_OUT | G_IO_ERR | G_IO_HUP,
                                                  G_PRIORITY_DEFAULT,
                                                  helper_can_write,
                                                  info,
                                                  NULL);
    g_source_attach(info->output_source, g_main_context_get_thread_default());

    /* Watch process stdout */
    nm_str_buf_init(&info->in_buffer, NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, FALSE);
    info->input_source = nm_g_unix_fd_source_new(info->child_stdout,
                                                 G_IO_IN | G_IO_ERR | G_IO_HUP,
                                                 G_PRIORITY_DEFAULT,
                                                 helper_have_data,
                                                 info,
                                                 NULL);
    g_source_attach(info->input_source, g_main_context_get_thread_default());

    if (cancellable) {
        gulong signal_id;

        signal_id = g_cancellable_connect(cancellable, G_CALLBACK(helper_cancelled), info, NULL);
        if (signal_id == 0) {
            /* the request is already cancelled. Return. */
            return;
        }
        info->cancellable_id = signal_id;
    }
}

char *
nm_utils_spawn_helper_finish(GAsyncResult *result, GError **error)
{
    GTask *task = G_TASK(result);

    nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_spawn_helper));

    return g_task_propagate_pointer(task, error);
}
