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

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

#include "NetworkManagerUtils.h"

#include <linux/fib_rules.h>
#include <linux/pkt_sched.h>
#include <linux/if_ether.h>

#include "libnm-glib-aux/nm-c-list.h"

#include "libnm-glib-aux/nm-uuid.h"
#include "libnm-glib-aux/nm-str-buf.h"
#include "libnm-base/nm-net-aux.h"
#include "libnm-core-aux-intern/nm-common-macros.h"
#include "nm-utils.h"
#include "nm-setting-connection.h"
#include "nm-setting-ip4-config.h"
#include "nm-setting-ip6-config.h"
#include "libnm-core-intern/nm-core-internal.h"
#include "libnm-platform/nmp-object.h"

#include "libnm-platform/nm-platform.h"
#include "libnm-platform/nm-linux-platform.h"
#include "nm-auth-utils.h"
#include "libnm-systemd-shared/nm-sd-utils-shared.h"

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

/**
 * nm_utils_get_shared_wifi_permission:
 * @connection: the NMConnection to lookup the permission.
 *
 * Returns: a static string of the wifi-permission (if any) or %NULL.
 */
const char *
nm_utils_get_shared_wifi_permission(NMConnection *connection)
{
    NMSettingWireless *        s_wifi;
    NMSettingWirelessSecurity *s_wsec;
    const char *               method;

    method = nm_utils_get_ip_config_method(connection, AF_INET);
    if (!nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_SHARED))
        return NULL;

    s_wifi = nm_connection_get_setting_wireless(connection);
    if (s_wifi) {
        s_wsec = nm_connection_get_setting_wireless_security(connection);
        if (s_wsec)
            return NM_AUTH_PERMISSION_WIFI_SHARE_PROTECTED;
        else
            return NM_AUTH_PERMISSION_WIFI_SHARE_OPEN;
    }

    return NULL;
}

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

static char *
get_new_connection_name(NMConnection *const *existing_connections,
                        const char *         preferred,
                        const char *         fallback_prefix)
{
    gs_free const char **existing_names = NULL;
    guint                i, existing_len = 0;

    g_assert(fallback_prefix);

    if (existing_connections) {
        existing_len   = NM_PTRARRAY_LEN(existing_connections);
        existing_names = g_new(const char *, existing_len);
        for (i = 0; i < existing_len; i++) {
            NMConnection *candidate;
            const char *  id;

            candidate = existing_connections[i];
            nm_assert(NM_IS_CONNECTION(candidate));

            id = nm_connection_get_id(candidate);
            nm_assert(id);

            existing_names[i] = id;

            if (preferred && nm_streq(preferred, id)) {
                /* the preferred name is already taken. Forget about it. */
                preferred = NULL;
            }
        }
        nm_assert(!existing_connections[i]);
    }

    /* Return the preferred name if it was unique */
    if (preferred)
        return g_strdup(preferred);

    /* Otherwise, find the next available unique connection name using the given
     * connection name template.
     */
    for (i = 1; TRUE; i++) {
        char *temp;

        /* TRANSLATORS: the first %s is a prefix for the connection id, such
         * as "Wired Connection" or "VPN Connection". The %d is a number
         * that is combined with the first argument to create a unique
         * connection id. */
        temp = g_strdup_printf(C_("connection id fallback", "%s %u"), fallback_prefix, i);

        if (nm_utils_strv_find_first((char **) existing_names, existing_len, temp) < 0)
            return temp;

        g_free(temp);
    }
}

static char *
get_new_connection_ifname(NMPlatform *         platform,
                          NMConnection *const *existing_connections,
                          const char *         prefix)
{
    guint i, j;

    for (i = 0; TRUE; i++) {
        char *name;

        name = g_strdup_printf("%s%d", prefix, i);

        if (nm_platform_link_get_by_ifname(platform, name))
            goto next;

        if (existing_connections) {
            for (j = 0; existing_connections[j]; j++) {
                if (nm_streq0(nm_connection_get_interface_name(existing_connections[j]), name))
                    goto next;
            }
        }

        return name;

next:
        g_free(name);
    }
}

const char *
nm_utils_get_ip_config_method(NMConnection *connection, int addr_family)
{
    NMSettingConnection *s_con;
    NMSettingIPConfig *  s_ip;
    const char *         method;

    s_con = nm_connection_get_setting_connection(connection);

    if (NM_IS_IPv4(addr_family)) {
        g_return_val_if_fail(s_con != NULL, NM_SETTING_IP4_CONFIG_METHOD_AUTO);

        s_ip = nm_connection_get_setting_ip4_config(connection);
        if (!s_ip)
            return NM_SETTING_IP4_CONFIG_METHOD_DISABLED;

        method = nm_setting_ip_config_get_method(s_ip);
        g_return_val_if_fail(method != NULL, NM_SETTING_IP4_CONFIG_METHOD_AUTO);
        return method;
    }

    g_return_val_if_fail(s_con != NULL, NM_SETTING_IP6_CONFIG_METHOD_AUTO);

    s_ip = nm_connection_get_setting_ip6_config(connection);
    if (!s_ip)
        return NM_SETTING_IP6_CONFIG_METHOD_IGNORE;

    method = nm_setting_ip_config_get_method(s_ip);
    g_return_val_if_fail(method != NULL, NM_SETTING_IP6_CONFIG_METHOD_AUTO);
    return method;
}

gboolean
nm_utils_connection_has_default_route(NMConnection *connection,
                                      int           addr_family,
                                      gboolean *    out_is_never_default)
{
    const char *       method;
    NMSettingIPConfig *s_ip;
    gboolean           is_never_default  = FALSE;
    gboolean           has_default_route = FALSE;

    g_return_val_if_fail(NM_IS_CONNECTION(connection), FALSE);
    g_return_val_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6), FALSE);

    if (!connection)
        goto out;

    s_ip = nm_connection_get_setting_ip_config(connection, addr_family);
    if (!s_ip)
        goto out;
    if (nm_setting_ip_config_get_never_default(s_ip)) {
        is_never_default = TRUE;
        goto out;
    }

    method = nm_utils_get_ip_config_method(connection, addr_family);
    if (NM_IS_IPv4(addr_family)) {
        if (NM_IN_STRSET(method,
                         NM_SETTING_IP4_CONFIG_METHOD_DISABLED,
                         NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL))
            goto out;
    } else {
        if (NM_IN_STRSET(method,
                         NM_SETTING_IP6_CONFIG_METHOD_IGNORE,
                         NM_SETTING_IP6_CONFIG_METHOD_DISABLED,
                         NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL))
            goto out;
    }

    has_default_route = TRUE;
out:
    NM_SET_OUT(out_is_never_default, is_never_default);
    return has_default_route;
}

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

void
nm_utils_complete_generic(NMPlatform *         platform,
                          NMConnection *       connection,
                          const char *         ctype,
                          NMConnection *const *existing_connections,
                          const char *         preferred_id,
                          const char *         fallback_id_prefix,
                          const char *         ifname_prefix,
                          const char *         ifname,
                          gboolean             default_enable_ipv6)
{
    NMSettingConnection *s_con;
    char *               id, *generated_ifname;
    GHashTable *         parameters;

    g_assert(fallback_id_prefix);
    g_return_if_fail(ifname_prefix == NULL || ifname == NULL);

    s_con = nm_connection_get_setting_connection(connection);
    if (!s_con) {
        s_con = (NMSettingConnection *) nm_setting_connection_new();
        nm_connection_add_setting(connection, NM_SETTING(s_con));
    }
    g_object_set(G_OBJECT(s_con), NM_SETTING_CONNECTION_TYPE, ctype, NULL);

    if (!nm_setting_connection_get_uuid(s_con)) {
        char uuid[37];

        g_object_set(G_OBJECT(s_con),
                     NM_SETTING_CONNECTION_UUID,
                     nm_uuid_generate_random_str_arr(uuid),
                     NULL);
    }

    /* Add a connection ID if absent */
    if (!nm_setting_connection_get_id(s_con)) {
        id = get_new_connection_name(existing_connections, preferred_id, fallback_id_prefix);
        g_object_set(G_OBJECT(s_con), NM_SETTING_CONNECTION_ID, id, NULL);
        g_free(id);
    }

    /* Add an interface name, if requested */
    if (ifname) {
        g_object_set(G_OBJECT(s_con), NM_SETTING_CONNECTION_INTERFACE_NAME, ifname, NULL);
    } else if (ifname_prefix && !nm_setting_connection_get_interface_name(s_con)) {
        generated_ifname = get_new_connection_ifname(platform, existing_connections, ifname_prefix);
        g_object_set(G_OBJECT(s_con), NM_SETTING_CONNECTION_INTERFACE_NAME, generated_ifname, NULL);
        g_free(generated_ifname);
    }

    /* Normalize */
    parameters = g_hash_table_new(nm_str_hash, g_str_equal);
    g_hash_table_insert(parameters,
                        NM_CONNECTION_NORMALIZE_PARAM_IP6_CONFIG_METHOD,
                        default_enable_ipv6 ? NM_SETTING_IP6_CONFIG_METHOD_AUTO
                                            : NM_SETTING_IP6_CONFIG_METHOD_IGNORE);
    nm_connection_normalize(connection, parameters, NULL, NULL);
    g_hash_table_destroy(parameters);
}

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

static GHashTable *
check_property_in_hash(GHashTable *hash, const char *s_name, const char *p_name)
{
    GHashTable *props;

    props = g_hash_table_lookup(hash, s_name);
    if (!props || !g_hash_table_lookup(props, p_name)) {
        return NULL;
    }
    return props;
}

static void
remove_from_hash(GHashTable *s_hash, GHashTable *p_hash, const char *s_name, const char *p_name)
{
    if (!p_hash)
        return;

    g_hash_table_remove(p_hash, p_name);
    if (g_hash_table_size(p_hash) == 0)
        g_hash_table_remove(s_hash, s_name);
}

static gboolean
check_ip6_method(NMConnection *orig, NMConnection *candidate, GHashTable *settings)
{
    GHashTable *       props;
    const char *       orig_ip6_method, *candidate_ip6_method;
    NMSettingIPConfig *candidate_ip6;
    gboolean           allow = FALSE;

    props = check_property_in_hash(settings,
                                   NM_SETTING_IP6_CONFIG_SETTING_NAME,
                                   NM_SETTING_IP_CONFIG_METHOD);
    if (!props)
        return TRUE;

    orig_ip6_method      = nm_utils_get_ip_config_method(orig, AF_INET6);
    candidate_ip6_method = nm_utils_get_ip_config_method(candidate, AF_INET6);
    candidate_ip6        = nm_connection_get_setting_ip6_config(candidate);

    if (nm_streq(orig_ip6_method, NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL)
        && nm_streq(candidate_ip6_method, NM_SETTING_IP6_CONFIG_METHOD_AUTO)
        && (!candidate_ip6 || nm_setting_ip_config_get_may_fail(candidate_ip6))) {
        /* If the generated connection is 'link-local' and the candidate is both 'auto'
         * and may-fail=TRUE, then the candidate is OK to use.  may-fail is included
         * in the decision because if the candidate is 'auto' but may-fail=FALSE, then
         * the connection could not possibly have been previously activated on the
         * device if the device has no non-link-local IPv6 address.
         */
        allow = TRUE;
    } else if (NM_IN_STRSET(orig_ip6_method,
                            NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL,
                            NM_SETTING_IP6_CONFIG_METHOD_DISABLED,
                            NM_SETTING_IP6_CONFIG_METHOD_AUTO)
               && nm_streq0(candidate_ip6_method, NM_SETTING_IP6_CONFIG_METHOD_IGNORE)) {
        /* If the generated connection method is 'link-local', disabled' or 'auto' and
         * the candidate method is 'ignore' we can take the connection, because NM didn't
         * simply take care of IPv6.
         */
        allow = TRUE;
    }

    if (allow) {
        remove_from_hash(settings,
                         props,
                         NM_SETTING_IP6_CONFIG_SETTING_NAME,
                         NM_SETTING_IP_CONFIG_METHOD);
    }

    return allow;
}

static int
route_compare(NMIPRoute *route1, NMIPRoute *route2, gint64 default_metric)
{
    NMIPAddr a1;
    NMIPAddr a2;
    guint64  m1;
    guint64  m2;
    int      family;
    guint    plen;

    family = nm_ip_route_get_family(route1);
    NM_CMP_DIRECT(family, nm_ip_route_get_family(route2));

    nm_assert_addr_family(family);

    plen = nm_ip_route_get_prefix(route1);
    NM_CMP_DIRECT(plen, nm_ip_route_get_prefix(route2));

    m1 = nm_ip_route_get_metric(route1);
    m2 = nm_ip_route_get_metric(route2);
    NM_CMP_DIRECT(m1 == -1 ? default_metric : m1, m2 == -1 ? default_metric : m2);

    NM_CMP_DIRECT_STRCMP0(nm_ip_route_get_next_hop(route1), nm_ip_route_get_next_hop(route2));

    if (!inet_pton(family, nm_ip_route_get_dest(route1), &a1))
        nm_assert_not_reached();
    if (!inet_pton(family, nm_ip_route_get_dest(route2), &a2))
        nm_assert_not_reached();
    nm_utils_ipx_address_clear_host_address(family, &a1, NULL, plen);
    nm_utils_ipx_address_clear_host_address(family, &a2, NULL, plen);
    NM_CMP_DIRECT_MEMCMP(&a1, &a2, nm_utils_addr_family_to_size(family));

    return 0;
}

static int
route_ptr_compare(const void *a, const void *b, gpointer metric)
{
    return route_compare(*(NMIPRoute **) a, *(NMIPRoute **) b, *((gint64 *) metric));
}

static gboolean
check_ip_routes(NMConnection *orig,
                NMConnection *candidate,
                GHashTable *  settings,
                gint64        default_metric,
                gboolean      v4)
{
    gs_free NMIPRoute **routes1 = NULL;
    NMIPRoute **        routes2;
    NMSettingIPConfig * s_ip1, *s_ip2;
    gint64              m;
    const char *        s_name;
    GHashTable *        props;
    guint               i, i1, i2, num1, num2;
    const guint8        PLEN = v4 ? 32 : 128;

    s_name = v4 ? NM_SETTING_IP4_CONFIG_SETTING_NAME : NM_SETTING_IP6_CONFIG_SETTING_NAME;

    props = check_property_in_hash(settings, s_name, NM_SETTING_IP_CONFIG_ROUTES);
    if (!props)
        return TRUE;

    s_ip1 = (NMSettingIPConfig *) nm_connection_get_setting_by_name(orig, s_name);
    s_ip2 = (NMSettingIPConfig *) nm_connection_get_setting_by_name(candidate, s_name);

    if (!s_ip1 || !s_ip2)
        return FALSE;

    num1 = nm_setting_ip_config_get_num_routes(s_ip1);
    num2 = nm_setting_ip_config_get_num_routes(s_ip2);

    routes1 = g_new(NMIPRoute *, (gsize) num1 + num2);
    routes2 = &routes1[num1];

    for (i = 0; i < num1; i++)
        routes1[i] = nm_setting_ip_config_get_route(s_ip1, i);
    for (i = 0; i < num2; i++)
        routes2[i] = nm_setting_ip_config_get_route(s_ip2, i);

    m = nm_setting_ip_config_get_route_metric(s_ip2);
    if (m != -1)
        default_metric = m;

    g_qsort_with_data(routes1, num1, sizeof(NMIPRoute *), route_ptr_compare, &default_metric);
    g_qsort_with_data(routes2, num2, sizeof(NMIPRoute *), route_ptr_compare, &default_metric);

    for (i1 = 0, i2 = 0; i2 < num2; i1++) {
        if (i1 >= num1)
            return FALSE;
        if (route_compare(routes1[i1], routes2[i2], default_metric) == 0) {
            i2++;
            continue;
        }

        /* if @orig (@routes1) contains /32 routes that are missing in @candidate,
         * we accept that.
         *
         * A /32 may have been added automatically, as a direct-route to the gateway.
         * The generated connection (@orig) would contain that route, so we shall ignore
         * it.
         *
         * Likeweise for /128 for IPv6. */
        if (nm_ip_route_get_prefix(routes1[i1]) == PLEN)
            continue;

        return FALSE;
    }

    /* check that @orig has no left-over (except host routes that we ignore). */
    for (; i1 < num1; i1++) {
        if (nm_ip_route_get_prefix(routes1[i1]) != PLEN)
            return FALSE;
    }

    remove_from_hash(settings, props, s_name, NM_SETTING_IP_CONFIG_ROUTES);
    return TRUE;
}

static gboolean
check_ip4_method(NMConnection *orig,
                 NMConnection *candidate,
                 GHashTable *  settings,
                 gboolean      device_has_carrier)
{
    GHashTable *       props;
    const char *       orig_ip4_method, *candidate_ip4_method;
    NMSettingIPConfig *candidate_ip4;

    props = check_property_in_hash(settings,
                                   NM_SETTING_IP4_CONFIG_SETTING_NAME,
                                   NM_SETTING_IP_CONFIG_METHOD);
    if (!props)
        return TRUE;

    orig_ip4_method      = nm_utils_get_ip_config_method(orig, AF_INET);
    candidate_ip4_method = nm_utils_get_ip_config_method(candidate, AF_INET);
    candidate_ip4        = nm_connection_get_setting_ip4_config(candidate);

    if (nm_streq(orig_ip4_method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED)
        && nm_streq(candidate_ip4_method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)
        && (!candidate_ip4 || nm_setting_ip_config_get_may_fail(candidate_ip4))
        && !device_has_carrier) {
        /* If the generated connection is 'disabled' (device had no IP addresses)
         * but it has no carrier, that most likely means that IP addressing could
         * not complete and thus no IP addresses were assigned.  In that case, allow
         * matching to the "auto" method.
         */
        remove_from_hash(settings,
                         props,
                         NM_SETTING_IP4_CONFIG_SETTING_NAME,
                         NM_SETTING_IP_CONFIG_METHOD);
        return TRUE;
    }
    return FALSE;
}

static gboolean
check_connection_interface_name(NMConnection *orig, NMConnection *candidate, GHashTable *settings)
{
    GHashTable *         props;
    const char *         orig_ifname, *cand_ifname;
    NMSettingConnection *s_con_orig, *s_con_cand;

    props = check_property_in_hash(settings,
                                   NM_SETTING_CONNECTION_SETTING_NAME,
                                   NM_SETTING_CONNECTION_INTERFACE_NAME);
    if (!props)
        return TRUE;

    /* If one of the interface names is NULL, we accept that connection */
    s_con_orig  = nm_connection_get_setting_connection(orig);
    s_con_cand  = nm_connection_get_setting_connection(candidate);
    orig_ifname = nm_setting_connection_get_interface_name(s_con_orig);
    cand_ifname = nm_setting_connection_get_interface_name(s_con_cand);

    if (!orig_ifname || !cand_ifname) {
        remove_from_hash(settings,
                         props,
                         NM_SETTING_CONNECTION_SETTING_NAME,
                         NM_SETTING_CONNECTION_INTERFACE_NAME);
        return TRUE;
    }
    return FALSE;
}

static gboolean
check_connection_mac_address(NMConnection *orig, NMConnection *candidate, GHashTable *settings)
{
    GHashTable *    props;
    const char *    orig_mac = NULL, *cand_mac = NULL;
    NMSettingWired *s_wired_orig, *s_wired_cand;

    props = check_property_in_hash(settings,
                                   NM_SETTING_WIRED_SETTING_NAME,
                                   NM_SETTING_WIRED_MAC_ADDRESS);
    if (!props)
        return TRUE;

    /* If one of the MAC addresses is NULL, we accept that connection */
    s_wired_orig = nm_connection_get_setting_wired(orig);
    if (s_wired_orig)
        orig_mac = nm_setting_wired_get_mac_address(s_wired_orig);

    s_wired_cand = nm_connection_get_setting_wired(candidate);
    if (s_wired_cand)
        cand_mac = nm_setting_wired_get_mac_address(s_wired_cand);

    if (!orig_mac || !cand_mac) {
        remove_from_hash(settings,
                         props,
                         NM_SETTING_WIRED_SETTING_NAME,
                         NM_SETTING_WIRED_MAC_ADDRESS);
        return TRUE;
    }
    return FALSE;
}

static gboolean
check_connection_infiniband_mac_address(NMConnection *orig,
                                        NMConnection *candidate,
                                        GHashTable *  settings)
{
    GHashTable *         props;
    const char *         orig_mac = NULL, *cand_mac = NULL;
    NMSettingInfiniband *s_infiniband_orig, *s_infiniband_cand;

    props = check_property_in_hash(settings,
                                   NM_SETTING_INFINIBAND_SETTING_NAME,
                                   NM_SETTING_INFINIBAND_MAC_ADDRESS);
    if (!props)
        return TRUE;

    /* If one of the MAC addresses is NULL, we accept that connection */
    s_infiniband_orig = nm_connection_get_setting_infiniband(orig);
    if (s_infiniband_orig)
        orig_mac = nm_setting_infiniband_get_mac_address(s_infiniband_orig);

    s_infiniband_cand = nm_connection_get_setting_infiniband(candidate);
    if (s_infiniband_cand)
        cand_mac = nm_setting_infiniband_get_mac_address(s_infiniband_cand);

    if (!orig_mac || !cand_mac) {
        remove_from_hash(settings,
                         props,
                         NM_SETTING_INFINIBAND_SETTING_NAME,
                         NM_SETTING_INFINIBAND_MAC_ADDRESS);
        return TRUE;
    }
    return FALSE;
}

static gboolean
check_connection_cloned_mac_address(NMConnection *orig,
                                    NMConnection *candidate,
                                    GHashTable *  settings)
{
    GHashTable *    props;
    const char *    orig_mac = NULL, *cand_mac = NULL;
    NMSettingWired *s_wired_orig, *s_wired_cand;

    props = check_property_in_hash(settings,
                                   NM_SETTING_WIRED_SETTING_NAME,
                                   NM_SETTING_WIRED_CLONED_MAC_ADDRESS);
    if (!props)
        return TRUE;

    /* If one of the MAC addresses is NULL, we accept that connection */
    s_wired_orig = nm_connection_get_setting_wired(orig);
    if (s_wired_orig)
        orig_mac = nm_setting_wired_get_cloned_mac_address(s_wired_orig);

    s_wired_cand = nm_connection_get_setting_wired(candidate);
    if (s_wired_cand)
        cand_mac = nm_setting_wired_get_cloned_mac_address(s_wired_cand);

    /* special cloned mac address entries are accepted. */
    if (NM_CLONED_MAC_IS_SPECIAL(orig_mac))
        orig_mac = NULL;
    if (NM_CLONED_MAC_IS_SPECIAL(cand_mac))
        cand_mac = NULL;

    if (!orig_mac || !cand_mac) {
        remove_from_hash(settings,
                         props,
                         NM_SETTING_WIRED_SETTING_NAME,
                         NM_SETTING_WIRED_CLONED_MAC_ADDRESS);
        return TRUE;
    }
    return FALSE;
}

static gboolean
check_connection_s390_props(NMConnection *orig, NMConnection *candidate, GHashTable *settings)
{
    GHashTable *    props1, *props2, *props3;
    NMSettingWired *s_wired_orig, *s_wired_cand;

    props1 = check_property_in_hash(settings,
                                    NM_SETTING_WIRED_SETTING_NAME,
                                    NM_SETTING_WIRED_S390_SUBCHANNELS);
    props2 = check_property_in_hash(settings,
                                    NM_SETTING_WIRED_SETTING_NAME,
                                    NM_SETTING_WIRED_S390_NETTYPE);
    props3 = check_property_in_hash(settings,
                                    NM_SETTING_WIRED_SETTING_NAME,
                                    NM_SETTING_WIRED_S390_OPTIONS);
    if (!props1 && !props2 && !props3)
        return TRUE;

    /* If the generated connection did not contain wired setting,
     * allow it to match to a connection with a wired setting,
     * but default (empty) s390-* properties */
    s_wired_orig = nm_connection_get_setting_wired(orig);
    s_wired_cand = nm_connection_get_setting_wired(candidate);
    if (!s_wired_orig && s_wired_cand) {
        const char *const *subchans    = nm_setting_wired_get_s390_subchannels(s_wired_cand);
        const char *       nettype     = nm_setting_wired_get_s390_nettype(s_wired_cand);
        guint32            num_options = nm_setting_wired_get_num_s390_options(s_wired_cand);

        if ((!subchans || !*subchans) && !nettype && num_options == 0) {
            remove_from_hash(settings,
                             props1,
                             NM_SETTING_WIRED_SETTING_NAME,
                             NM_SETTING_WIRED_S390_SUBCHANNELS);
            remove_from_hash(settings,
                             props2,
                             NM_SETTING_WIRED_SETTING_NAME,
                             NM_SETTING_WIRED_S390_NETTYPE);
            remove_from_hash(settings,
                             props3,
                             NM_SETTING_WIRED_SETTING_NAME,
                             NM_SETTING_WIRED_S390_OPTIONS);
            return TRUE;
        }
    }
    return FALSE;
}

static NMConnection *
check_possible_match(NMConnection *orig,
                     NMConnection *candidate,
                     GHashTable *  settings,
                     gboolean      device_has_carrier,
                     gint64        default_v4_metric,
                     gint64        default_v6_metric)
{
    g_return_val_if_fail(settings != NULL, NULL);

    if (!check_ip6_method(orig, candidate, settings))
        return NULL;

    if (!check_ip4_method(orig, candidate, settings, device_has_carrier))
        return NULL;

    if (!check_ip_routes(orig, candidate, settings, default_v4_metric, TRUE))
        return NULL;

    if (!check_ip_routes(orig, candidate, settings, default_v6_metric, FALSE))
        return NULL;

    if (!check_connection_interface_name(orig, candidate, settings))
        return NULL;

    if (!check_connection_mac_address(orig, candidate, settings))
        return NULL;

    if (!check_connection_infiniband_mac_address(orig, candidate, settings))
        return NULL;

    if (!check_connection_cloned_mac_address(orig, candidate, settings))
        return NULL;

    if (!check_connection_s390_props(orig, candidate, settings))
        return NULL;

    if (g_hash_table_size(settings) == 0)
        return candidate;
    else
        return NULL;
}

/**
 * nm_utils_match_connection:
 * @connections: a (optionally pre-sorted) list of connections from which to
 * find a matching connection to @original based on "inferrable" properties
 * @original: the #NMConnection to find a match for from @connections
 * @indicated: whether the match is already hinted/indicated. That is the
 *   case when we found the connection in the state file from a previous run.
 *   In this case, we perform a relexed check, as we have a good hint
 *   that the connection actually matches.
 * @device_has_carrier: pass %TRUE if the device that generated @original has
 * a carrier, %FALSE if not
 * @match_filter_func: a function to check whether each connection from @connections
 * should be considered for matching.  This function should return %TRUE if the
 * connection should be considered, %FALSE if the connection should be ignored
 * @match_compat_data: data pointer passed to @match_filter_func
 *
 * Checks each connection from @connections until a matching connection is found
 * considering only setting properties marked with %NM_SETTING_PARAM_INFERRABLE
 * and checking a few other characteristics like IPv6 method.  If the caller
 * desires some priority order of the connections, @connections should be
 * sorted before calling this function.
 *
 * Returns: the best #NMConnection matching @original, or %NULL if no connection
 * matches well enough.
 */
NMConnection *
nm_utils_match_connection(NMConnection *const *  connections,
                          NMConnection *         original,
                          gboolean               indicated,
                          gboolean               device_has_carrier,
                          gint64                 default_v4_metric,
                          gint64                 default_v6_metric,
                          NMUtilsMatchFilterFunc match_filter_func,
                          gpointer               match_filter_data)
{
    NMConnection *best_match = NULL;

    if (!connections)
        return NULL;

    for (; *connections; connections++) {
        NMConnection *candidate = *connections;
        GHashTable *  diffs     = NULL;

        nm_assert(NM_IS_CONNECTION(candidate));

        if (match_filter_func) {
            if (!match_filter_func(candidate, match_filter_data))
                continue;
        }

        if (indicated) {
            NMSettingConnection *s_orig, *s_cand;

            s_orig = nm_connection_get_setting_connection(original);
            s_cand = nm_connection_get_setting_connection(candidate);

            /* It is indicated that this connection matches. Assume we have
             * a match, but check for particular differences that let us
             * reject the candidate. */
            if (!nm_streq0(nm_setting_connection_get_connection_type(s_orig),
                           nm_setting_connection_get_connection_type(s_cand)))
                continue;
            if (!nm_streq0(nm_setting_connection_get_slave_type(s_orig),
                           nm_setting_connection_get_slave_type(s_cand)))
                continue;

            /* this is good enough for a match */
        } else if (!nm_connection_diff(original,
                                       candidate,
                                       NM_SETTING_COMPARE_FLAG_INFERRABLE,
                                       &diffs)) {
            if (!best_match) {
                best_match = check_possible_match(original,
                                                  candidate,
                                                  diffs,
                                                  device_has_carrier,
                                                  default_v4_metric,
                                                  default_v6_metric);
            }

            if (!best_match && nm_logging_enabled(LOGL_DEBUG, LOGD_CORE)) {
                GString *      diff_string;
                GHashTableIter s_iter, p_iter;
                gpointer       setting_name, setting;
                gpointer       property_name, value;

                diff_string = g_string_new(NULL);
                g_hash_table_iter_init(&s_iter, diffs);
                while (g_hash_table_iter_next(&s_iter, &setting_name, &setting)) {
                    g_hash_table_iter_init(&p_iter, setting);
                    while (g_hash_table_iter_next(&p_iter, &property_name, &value)) {
                        if (diff_string->len)
                            g_string_append(diff_string, ", ");
                        g_string_append_printf(diff_string,
                                               "%s.%s",
                                               (char *) setting_name,
                                               (char *) property_name);
                    }
                }

                nm_log_dbg(LOGD_CORE,
                           "Connection '%s' differs from candidate '%s' in %s",
                           nm_connection_get_id(original),
                           nm_connection_get_id(candidate),
                           diff_string->str);
                g_string_free(diff_string, TRUE);
            }

            g_hash_table_unref(diffs);
            continue;
        }

        /* Exact match */
        return candidate;
    }

    /* Best match (if any) */
    return best_match;
}

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

int
nm_match_spec_device_by_pllink(const NMPlatformLink *pllink,
                               const char *          match_device_type,
                               const char *          match_dhcp_plugin,
                               const GSList *        specs,
                               int                   no_match_value)
{
    NMMatchSpecMatchType m;

    /* we can only match by certain properties that are available on the
     * platform link (and even @pllink might be missing.
     *
     * It's still useful because of specs like "*" and "except:interface-name:eth0",
     * which match even in that case. */
    m = nm_match_spec_device(specs,
                             pllink ? pllink->name : NULL,
                             match_device_type,
                             pllink ? pllink->driver : NULL,
                             NULL,
                             NULL,
                             NULL,
                             match_dhcp_plugin);

    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;
}

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

NMPlatformRoutingRule *
nm_ip_routing_rule_to_platform(const NMIPRoutingRule *rule, NMPlatformRoutingRule *out_pl)
{
    gboolean uid_range_has;
    guint32  uid_range_start = 0;
    guint32  uid_range_end   = 0;

    nm_assert(rule);
    nm_assert(nm_ip_routing_rule_validate(rule, NULL));
    nm_assert(out_pl);

    uid_range_has = nm_ip_routing_rule_get_uid_range(rule, &uid_range_start, &uid_range_end);

    *out_pl = (NMPlatformRoutingRule){
        .addr_family = nm_ip_routing_rule_get_addr_family(rule),
        .flags       = (nm_ip_routing_rule_get_invert(rule) ? FIB_RULE_INVERT : 0),
        .priority    = nm_ip_routing_rule_get_priority(rule),
        .tos         = nm_ip_routing_rule_get_tos(rule),
        .ip_proto    = nm_ip_routing_rule_get_ipproto(rule),
        .fwmark      = nm_ip_routing_rule_get_fwmark(rule),
        .fwmask      = nm_ip_routing_rule_get_fwmask(rule),
        .sport_range =
            {
                .start = nm_ip_routing_rule_get_source_port_start(rule),
                .end   = nm_ip_routing_rule_get_source_port_end(rule),
            },
        .dport_range =
            {
                .start = nm_ip_routing_rule_get_destination_port_start(rule),
                .end   = nm_ip_routing_rule_get_destination_port_end(rule),
            },
        .src     = *(nm_ip_routing_rule_get_from_bin(rule) ?: &nm_ip_addr_zero),
        .dst     = *(nm_ip_routing_rule_get_to_bin(rule) ?: &nm_ip_addr_zero),
        .src_len = nm_ip_routing_rule_get_from_len(rule),
        .dst_len = nm_ip_routing_rule_get_to_len(rule),
        .action  = nm_ip_routing_rule_get_action(rule),
        .table   = nm_ip_routing_rule_get_table(rule),
        .suppress_prefixlen_inverse =
            ~((guint32) nm_ip_routing_rule_get_suppress_prefixlength(rule)),
        .uid_range_has = uid_range_has,
        .uid_range =
            {
                .start = uid_range_start,
                .end   = uid_range_end,
            },
    };

    nm_ip_routing_rule_get_xifname_bin(rule, TRUE, out_pl->iifname);
    nm_ip_routing_rule_get_xifname_bin(rule, FALSE, out_pl->oifname);

    return out_pl;
}

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

struct _NMShutdownWaitObjHandle {
    CList    lst;
    gpointer watched_obj;
    char *   msg_reason;
    bool     free_msg_reason : 1;
    bool     is_cancellable : 1;
};

static CList _shutdown_waitobj_lst_head;

static void
_shutdown_waitobj_unregister(NMShutdownWaitObjHandle *handle)
{
    c_list_unlink_stale(&handle->lst);
    if (handle->free_msg_reason)
        g_free(handle->msg_reason);
    g_slice_free(NMShutdownWaitObjHandle, handle);

    /* FIXME(shutdown): check whether the object list is empty, and
     * signal shutdown-complete */
}

static void
_shutdown_waitobj_cb(gpointer user_data, GObject *where_the_object_was)
{
    NMShutdownWaitObjHandle *handle = user_data;

    nm_assert(handle);
    nm_assert(handle->watched_obj == where_the_object_was);
    _shutdown_waitobj_unregister(handle);
}

/**
 * nm_shutdown_wait_obj_register_full:
 * @watched_obj: the object to watch. Takes a weak reference on the object
 *   to be notified when it gets destroyed.
 *   If wait_type is %NM_SHUTDOWN_WAIT_TYPE_HANDLE, this must be %NULL.
 * @wait_type: whether @watched_obj is just a plain GObject or a GCancellable
 *   that should be cancelled.
 * @msg_reason: a reason message, for debugging and logging purposes.
 * @free_msg_reason: if %TRUE, then ownership of @msg_reason will be taken
 *   and the string will be freed with g_free() afterwards. If %FALSE,
 *   the caller must ensure that @msg_reason string outlives the watched
 *   objects (e.g. being a static strings).
 *
 * Keep track of @watched_obj until it gets destroyed. During shutdown,
 * we wait until all watched objects are destroyed. This is useful, if
 * this object still conducts some asynchronous action, which needs to
 * complete before NetworkManager is allowed to terminate. We re-use
 * the reference-counter of @watched_obj as signal, that the object
 * is still used.
 *
 * If @wait_type is %NM_SHUTDOWN_WAIT_TYPE_CANCELLABLE, then during shutdown
 * (after %NM_SHUTDOWN_TIMEOUT_MS), the cancellable will be cancelled to notify
 * the source of the shutdown. Note that otherwise, in this mode also @watched_obj
 * is only tracked with a weak-pointer. Especially, it does not register to the
 * "cancelled" signal to automatically unregister (otherwise, you would never
 * know whether the returned NMShutdownWaitObjHandle is still valid.
 *
 * FIXME(shutdown): proper shutdown is not yet implemented, and registering
 *   an object (currently) has no effect.
 *
 * FIXME(shutdown): during shutdown, after %NM_SHUTDOWN_TIMEOUT_MS timeout, cancel
 *   all remaining %NM_SHUTDOWN_WAIT_TYPE_CANCELLABLE instances. Also, when somebody
 *   enqueues a cancellable after that point, cancel it right away on an idle handler.
 *
 * Returns: a handle to unregister the object. The caller may choose to ignore
 *   the handle, in which case, the object will be automatically unregistered,
 *   once it gets destroyed.
 *   Note that the handle is only valid as long as @watched_obj exists. If
 *   you plan to use it, ensure that you take care of not using it after
 *   destroying @watched_obj.
 */
NMShutdownWaitObjHandle *
nm_shutdown_wait_obj_register_full(gpointer           watched_obj,
                                   NMShutdownWaitType wait_type,
                                   char *             msg_reason,
                                   gboolean           free_msg_reason)
{
    NMShutdownWaitObjHandle *handle;

    if (wait_type == NM_SHUTDOWN_WAIT_TYPE_OBJECT)
        g_return_val_if_fail(G_IS_OBJECT(watched_obj), NULL);
    else if (wait_type == NM_SHUTDOWN_WAIT_TYPE_CANCELLABLE)
        g_return_val_if_fail(G_IS_CANCELLABLE(watched_obj), NULL);
    else if (wait_type == NM_SHUTDOWN_WAIT_TYPE_HANDLE)
        g_return_val_if_fail(!watched_obj, NULL);
    else
        g_return_val_if_reached(NULL);

    if (G_UNLIKELY(!_shutdown_waitobj_lst_head.next))
        c_list_init(&_shutdown_waitobj_lst_head);

    handle  = g_slice_new(NMShutdownWaitObjHandle);
    *handle = (NMShutdownWaitObjHandle){
        /* depending on @free_msg_reason, we take ownership of @msg_reason.
         * In either case, we just reference the string without cloning
         * it. */
        .watched_obj     = watched_obj,
        .msg_reason      = msg_reason,
        .free_msg_reason = free_msg_reason,
        .is_cancellable  = (wait_type == NM_SHUTDOWN_WAIT_TYPE_CANCELLABLE),
    };
    c_list_link_tail(&_shutdown_waitobj_lst_head, &handle->lst);
    if (watched_obj)
        g_object_weak_ref(watched_obj, _shutdown_waitobj_cb, handle);
    return handle;
}

void
nm_shutdown_wait_obj_unregister(NMShutdownWaitObjHandle *handle)
{
    g_return_if_fail(handle);

    nm_assert(!handle->watched_obj || G_IS_OBJECT(handle->watched_obj));
    nm_assert(nm_c_list_contains_entry(&_shutdown_waitobj_lst_head, handle, lst));

    if (handle->watched_obj)
        g_object_weak_unref(handle->watched_obj, _shutdown_waitobj_cb, handle);
    _shutdown_waitobj_unregister(handle);
}

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

/**
 * nm_utils_file_is_in_path:
 * @abs_filename: the absolute filename to test
 * @abs_path: the absolute path, to check whether filename is in.
 *
 * This tests, whether @abs_filename is a file which lies inside @abs_path.
 * Basically, this checks whether @abs_filename is the same as @abs_path +
 * basename(@abs_filename). It allows simple normalizations, like coalescing
 * multiple "//".
 *
 * However, beware that this function is purely filename based. That means,
 * it will reject files that reference the same file (i.e. inode) via
 * symlinks or bind mounts. Maybe one would like to check for file (inode)
 * identity, but that is not really possible based on the file name alone.
 *
 * This means, that nm_utils_file_is_in_path("/var/run/some-file", "/var/run")
 * will succeed, but nm_utils_file_is_in_path("/run/some-file", "/var/run")
 * will not (although, it's well known that they reference the same path).
 *
 * Also, note that @abs_filename must not have trailing slashes itself.
 * So, this will reject nm_utils_file_is_in_path("/usr/lib/", "/usr") as
 * invalid, because the function searches for file names (and "lib/" is
 * clearly a directory).
 *
 * Returns: if @abs_filename is a file inside @abs_path, returns the
 *   trailing part of @abs_filename which is the filename. Otherwise,
 *   %NULL.
 */
const char *
nm_utils_file_is_in_path(const char *abs_filename, const char *abs_path)
{
    const char *path;

    g_return_val_if_fail(abs_filename && abs_filename[0] == '/', NULL);
    g_return_val_if_fail(abs_path && abs_path[0] == '/', NULL);

    path = nm_sd_utils_path_startswith(abs_filename, abs_path);
    if (!path)
        return NULL;

    nm_assert(path[0] != '/');
    nm_assert(path > abs_filename);
    nm_assert(path <= &abs_filename[strlen(abs_filename)]);

    /* we require a non-empty remainder with no slashes. That is, only a filename.
     *
     * Note this will reject "/var/run/" as not being in "/var",
     * while "/var/run" would pass. The function searches for files
     * only, so a trailing slash (indicating a directory) is not allowed).
     * This is despite that the function cannot determine whether "/var/run"
     * is itself a file or a directory. "*/
    return path[0] && !strchr(path, '/') ? path : NULL;
}

/* The returned qdisc array is valid as long as s_tc is not modified */
GPtrArray *
nm_utils_qdiscs_from_tc_setting(NMPlatform *platform, NMSettingTCConfig *s_tc, int ip_ifindex)
{
    GPtrArray *qdiscs;
    guint      nqdiscs;
    guint      i;

    nqdiscs = nm_setting_tc_config_get_num_qdiscs(s_tc);
    qdiscs  = g_ptr_array_new_full(nqdiscs, (GDestroyNotify) nmp_object_unref);

    for (i = 0; i < nqdiscs; i++) {
        NMTCQdisc *      s_qdisc = nm_setting_tc_config_get_qdisc(s_tc, i);
        NMPObject *      q       = nmp_object_new(NMP_OBJECT_TYPE_QDISC, NULL);
        NMPlatformQdisc *qdisc   = NMP_OBJECT_CAST_QDISC(q);

        qdisc->ifindex = ip_ifindex;
        qdisc->kind    = nm_tc_qdisc_get_kind(s_qdisc);

        qdisc->addr_family = AF_UNSPEC;
        qdisc->handle      = nm_tc_qdisc_get_handle(s_qdisc);
        qdisc->parent      = nm_tc_qdisc_get_parent(s_qdisc);
        qdisc->info        = 0;

#define GET_ATTR(name, dst, variant_type, type, dflt)                                  \
    G_STMT_START                                                                       \
    {                                                                                  \
        GVariant *_variant = nm_tc_qdisc_get_attribute(s_qdisc, "" name "");           \
                                                                                       \
        if (_variant && g_variant_is_of_type(_variant, G_VARIANT_TYPE_##variant_type)) \
            (dst) = g_variant_get_##type(_variant);                                    \
        else                                                                           \
            (dst) = (dflt);                                                            \
    }                                                                                  \
    G_STMT_END

        if (strcmp(qdisc->kind, "fq_codel") == 0) {
            GET_ATTR("limit", qdisc->fq_codel.limit, UINT32, uint32, 0);
            GET_ATTR("flows", qdisc->fq_codel.flows, UINT32, uint32, 0);
            GET_ATTR("target", qdisc->fq_codel.target, UINT32, uint32, 0);
            GET_ATTR("interval", qdisc->fq_codel.interval, UINT32, uint32, 0);
            GET_ATTR("quantum", qdisc->fq_codel.quantum, UINT32, uint32, 0);
            GET_ATTR("ce_threshold",
                     qdisc->fq_codel.ce_threshold,
                     UINT32,
                     uint32,
                     NM_PLATFORM_FQ_CODEL_CE_THRESHOLD_DISABLED);
            GET_ATTR("memory_limit",
                     qdisc->fq_codel.memory_limit,
                     UINT32,
                     uint32,
                     NM_PLATFORM_FQ_CODEL_MEMORY_LIMIT_UNSET);
            GET_ATTR("ecn", qdisc->fq_codel.ecn, BOOLEAN, boolean, FALSE);
        } else if (nm_streq(qdisc->kind, "sfq")) {
            GET_ATTR("limit", qdisc->sfq.limit, UINT32, uint32, 0);
            GET_ATTR("flows", qdisc->sfq.flows, UINT32, uint32, 0);
            GET_ATTR("divisor", qdisc->sfq.divisor, UINT32, uint32, 0);
            GET_ATTR("perturb", qdisc->sfq.perturb_period, INT32, int32, 0);
            GET_ATTR("quantum", qdisc->sfq.quantum, UINT32, uint32, 0);
            GET_ATTR("depth", qdisc->sfq.depth, UINT32, uint32, 0);
        } else if (nm_streq(qdisc->kind, "tbf")) {
            GET_ATTR("rate", qdisc->tbf.rate, UINT64, uint64, 0);
            GET_ATTR("burst", qdisc->tbf.burst, UINT32, uint32, 0);
            GET_ATTR("limit", qdisc->tbf.limit, UINT32, uint32, 0);
            GET_ATTR("latency", qdisc->tbf.latency, UINT32, uint32, 0);
        }
#undef GET_ATTR

        g_ptr_array_add(qdiscs, q);
    }

    return qdiscs;
}

/* The returned tfilter array is valid as long as s_tc is not modified */
GPtrArray *
nm_utils_tfilters_from_tc_setting(NMPlatform *platform, NMSettingTCConfig *s_tc, int ip_ifindex)
{
    GPtrArray *tfilters;
    guint      ntfilters;
    guint      i;

    ntfilters = nm_setting_tc_config_get_num_tfilters(s_tc);
    tfilters  = g_ptr_array_new_full(ntfilters, (GDestroyNotify) nmp_object_unref);

    for (i = 0; i < ntfilters; i++) {
        NMTCTfilter *      s_tfilter = nm_setting_tc_config_get_tfilter(s_tc, i);
        NMTCAction *       action;
        NMPObject *        t       = nmp_object_new(NMP_OBJECT_TYPE_TFILTER, NULL);
        NMPlatformTfilter *tfilter = NMP_OBJECT_CAST_TFILTER(t);

        tfilter->ifindex     = ip_ifindex;
        tfilter->kind        = nm_tc_tfilter_get_kind(s_tfilter);
        tfilter->addr_family = AF_UNSPEC;
        tfilter->handle      = nm_tc_tfilter_get_handle(s_tfilter);
        tfilter->parent      = nm_tc_tfilter_get_parent(s_tfilter);
        tfilter->info        = TC_H_MAKE(0, htons(ETH_P_ALL));

        action = nm_tc_tfilter_get_action(s_tfilter);
        if (action) {
            GVariant *var;

            tfilter->action.kind = nm_tc_action_get_kind(action);

            if (strcmp(tfilter->action.kind, "simple") == 0) {
                var = nm_tc_action_get_attribute(action, "sdata");
                if (var && g_variant_is_of_type(var, G_VARIANT_TYPE_BYTESTRING)) {
                    g_strlcpy(tfilter->action.simple.sdata,
                              g_variant_get_bytestring(var),
                              sizeof(tfilter->action.simple.sdata));
                }
            } else if (strcmp(tfilter->action.kind, "mirred") == 0) {
                if (nm_tc_action_get_attribute(action, "egress"))
                    tfilter->action.mirred.egress = TRUE;

                if (nm_tc_action_get_attribute(action, "ingress"))
                    tfilter->action.mirred.ingress = TRUE;

                if (nm_tc_action_get_attribute(action, "mirror"))
                    tfilter->action.mirred.mirror = TRUE;

                if (nm_tc_action_get_attribute(action, "redirect"))
                    tfilter->action.mirred.redirect = TRUE;

                var = nm_tc_action_get_attribute(action, "dev");
                if (var && g_variant_is_of_type(var, G_VARIANT_TYPE_STRING)) {
                    int ifindex;

                    ifindex =
                        nm_platform_link_get_ifindex(platform, g_variant_get_string(var, NULL));
                    if (ifindex > 0)
                        tfilter->action.mirred.ifindex = ifindex;
                }
            }
        }

        g_ptr_array_add(tfilters, t);
    }

    return tfilters;
}

void
nm_utils_ip_route_attribute_to_platform(int                addr_family,
                                        NMIPRoute *        s_route,
                                        NMPlatformIPRoute *r,
                                        guint32            route_table)
{
    GVariant *          variant;
    guint32             table;
    NMIPAddr            addr;
    NMPlatformIP4Route *r4 = (NMPlatformIP4Route *) r;
    NMPlatformIP6Route *r6 = (NMPlatformIP6Route *) r;
    gboolean            onlink;

    nm_assert(s_route);
    nm_assert_addr_family(addr_family);
    nm_assert(r);

#define GET_ATTR(name, dst, variant_type, type, dflt)                                  \
    G_STMT_START                                                                       \
    {                                                                                  \
        GVariant *_variant = nm_ip_route_get_attribute(s_route, "" name "");           \
                                                                                       \
        if (_variant && g_variant_is_of_type(_variant, G_VARIANT_TYPE_##variant_type)) \
            (dst) = g_variant_get_##type(_variant);                                    \
        else                                                                           \
            (dst) = (dflt);                                                            \
    }                                                                                  \
    G_STMT_END

    if ((variant = nm_ip_route_get_attribute(s_route, NM_IP_ROUTE_ATTRIBUTE_TYPE))
        && g_variant_is_of_type(variant, G_VARIANT_TYPE_STRING)) {
        int type;

        type = nm_net_aux_rtnl_rtntype_a2n(g_variant_get_string(variant, NULL));
        nm_assert(NM_IN_SET(type, RTN_UNICAST, RTN_LOCAL));

        r->type_coerced = nm_platform_route_type_coerce(type);
    } else
        r->type_coerced = nm_platform_route_type_coerce(RTN_UNICAST);

    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_TABLE, table, UINT32, uint32, 0);

    if (!table && r->type_coerced == nm_platform_route_type_coerce(RTN_LOCAL))
        r->table_coerced = nm_platform_route_table_coerce(RT_TABLE_LOCAL);
    else
        r->table_coerced = nm_platform_route_table_coerce(table ?: (route_table ?: RT_TABLE_MAIN));

    if (NM_IS_IPv4(addr_family)) {
        guint8 scope;

        GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_TOS, r4->tos, BYTE, byte, 0);
        GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_SCOPE, scope, BYTE, byte, RT_SCOPE_NOWHERE);
        r4->scope_inv = nm_platform_route_scope_inv(scope);
    }

    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_ONLINK, onlink, BOOLEAN, boolean, FALSE);

    r->r_rtm_flags = ((onlink) ? (unsigned) RTNH_F_ONLINK : 0u);

    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_WINDOW, r->window, UINT32, uint32, 0);
    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_CWND, r->cwnd, UINT32, uint32, 0);
    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_INITCWND, r->initcwnd, UINT32, uint32, 0);
    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_INITRWND, r->initrwnd, UINT32, uint32, 0);
    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_MTU, r->mtu, UINT32, uint32, 0);
    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_LOCK_WINDOW, r->lock_window, BOOLEAN, boolean, FALSE);
    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_LOCK_CWND, r->lock_cwnd, BOOLEAN, boolean, FALSE);
    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_LOCK_INITCWND, r->lock_initcwnd, BOOLEAN, boolean, FALSE);
    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_LOCK_INITRWND, r->lock_initrwnd, BOOLEAN, boolean, FALSE);
    GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_LOCK_MTU, r->lock_mtu, BOOLEAN, boolean, FALSE);

    if ((variant = nm_ip_route_get_attribute(s_route, NM_IP_ROUTE_ATTRIBUTE_SRC))
        && g_variant_is_of_type(variant, G_VARIANT_TYPE_STRING)) {
        if (inet_pton(addr_family, g_variant_get_string(variant, NULL), &addr) == 1) {
            if (NM_IS_IPv4(addr_family))
                r4->pref_src = addr.addr4;
            else
                r6->pref_src = addr.addr6;
        }
    }

    if (!NM_IS_IPv4(addr_family)
        && (variant = nm_ip_route_get_attribute(s_route, NM_IP_ROUTE_ATTRIBUTE_FROM))
        && g_variant_is_of_type(variant, G_VARIANT_TYPE_STRING)) {
        int prefix;

        if (nm_utils_parse_inaddr_prefix_bin(addr_family,
                                             g_variant_get_string(variant, NULL),
                                             NULL,
                                             &addr,
                                             &prefix)) {
            if (prefix < 0)
                prefix = 128;
            r6->src      = addr.addr6;
            r6->src_plen = prefix;
        }
    }
#undef GET_ATTR
}

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

static int
_addresses_sort_cmp_4(gconstpointer a, gconstpointer b, gpointer user_data)
{
    return nm_platform_ip4_address_pretty_sort_cmp(
        NMP_OBJECT_CAST_IP4_ADDRESS(*((const NMPObject **) a)),
        NMP_OBJECT_CAST_IP4_ADDRESS(*((const NMPObject **) b)));
}

static int
_addresses_sort_cmp_6(gconstpointer a, gconstpointer b, gpointer user_data)
{
    return nm_platform_ip6_address_pretty_sort_cmp(
        NMP_OBJECT_CAST_IP6_ADDRESS(*((const NMPObject **) a)),
        NMP_OBJECT_CAST_IP6_ADDRESS(*((const NMPObject **) b)),
        (((NMSettingIP6ConfigPrivacy) GPOINTER_TO_INT(user_data))
         == NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR));
}

void
nm_utils_ip_addresses_to_dbus(int                          addr_family,
                              const NMDedupMultiHeadEntry *head_entry,
                              const NMPObject *            best_default_route,
                              NMSettingIP6ConfigPrivacy    ipv6_privacy,
                              GVariant **                  out_address_data,
                              GVariant **                  out_addresses)
{
    const int       IS_IPv4 = NM_IS_IPv4(addr_family);
    GVariantBuilder builder_data;
    GVariantBuilder builder_legacy;
    char            addr_str[NM_UTILS_INET_ADDRSTRLEN];
    gs_free const NMPObject **addresses = NULL;
    guint                     naddr;
    guint                     i;

    nm_assert_addr_family(addr_family);

    if (out_address_data)
        g_variant_builder_init(&builder_data, G_VARIANT_TYPE("aa{sv}"));
    if (out_addresses) {
        if (IS_IPv4)
            g_variant_builder_init(&builder_legacy, G_VARIANT_TYPE("aau"));
        else
            g_variant_builder_init(&builder_legacy, G_VARIANT_TYPE("a(ayuay)"));
    }

    if (!head_entry)
        goto out;

    addresses =
        (const NMPObject **) nm_dedup_multi_objs_to_array_head(head_entry, NULL, NULL, &naddr);

    nm_assert(addresses && naddr);

    g_qsort_with_data(addresses,
                      naddr,
                      sizeof(addresses[0]),
                      IS_IPv4 ? _addresses_sort_cmp_4 : _addresses_sort_cmp_6,
                      GINT_TO_POINTER(ipv6_privacy));

    for (i = 0; i < naddr; i++) {
        const NMPlatformIPXAddress *address = NMP_OBJECT_CAST_IPX_ADDRESS(addresses[i]);

        if (out_address_data) {
            GVariantBuilder addr_builder;
            gconstpointer   p;

            g_variant_builder_init(&addr_builder, G_VARIANT_TYPE("a{sv}"));

            g_variant_builder_add(
                &addr_builder,
                "{sv}",
                "address",
                g_variant_new_string(
                    nm_utils_inet_ntop(addr_family, address->ax.address_ptr, addr_str)));

            g_variant_builder_add(&addr_builder,
                                  "{sv}",
                                  "prefix",
                                  g_variant_new_uint32(address->ax.plen));

            p = NULL;
            if (IS_IPv4) {
                if (address->a4.peer_address != address->a4.address)
                    p = &address->a4.peer_address;
            } else {
                if (!IN6_IS_ADDR_UNSPECIFIED(&address->a6.peer_address)
                    && !IN6_ARE_ADDR_EQUAL(&address->a6.peer_address, &address->a6.address))
                    p = &address->a6.peer_address;
            }
            if (p) {
                g_variant_builder_add(
                    &addr_builder,
                    "{sv}",
                    "peer",
                    g_variant_new_string(nm_utils_inet_ntop(addr_family, p, addr_str)));
            }

            if (IS_IPv4) {
                if (*address->a4.label) {
                    g_variant_builder_add(&addr_builder,
                                          "{sv}",
                                          NM_IP_ADDRESS_ATTRIBUTE_LABEL,
                                          g_variant_new_string(address->a4.label));
                }
            }

            g_variant_builder_add(&builder_data, "a{sv}", &addr_builder);
        }

        if (out_addresses) {
            if (IS_IPv4) {
                const guint32 dbus_addr[3] = {
                    address->a4.address,
                    address->a4.plen,
                    (i == 0 && best_default_route)
                        ? NMP_OBJECT_CAST_IP4_ROUTE(best_default_route)->gateway
                        : (guint32) 0,
                };

                g_variant_builder_add(&builder_legacy, "@au", nm_g_variant_new_au(dbus_addr, 3));
            } else {
                g_variant_builder_add(
                    &builder_legacy,
                    "(@ayu@ay)",
                    nm_g_variant_new_ay_in6addr(&address->a6.address),
                    address->a6.plen,
                    nm_g_variant_new_ay_in6addr(
                        (i == 0 && best_default_route)
                            ? &NMP_OBJECT_CAST_IP6_ROUTE(best_default_route)->gateway
                            : &in6addr_any));
            }
        }
    }

out:
    NM_SET_OUT(out_address_data, g_variant_builder_end(&builder_data));
    NM_SET_OUT(out_addresses, g_variant_builder_end(&builder_legacy));
}

void
nm_utils_ip_routes_to_dbus(int                          addr_family,
                           const NMDedupMultiHeadEntry *head_entry,
                           GVariant **                  out_route_data,
                           GVariant **                  out_routes)
{
    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
    NMDedupMultiIter iter;
    const NMPObject *obj;
    GVariantBuilder  builder_data;
    GVariantBuilder  builder_legacy;
    char             addr_str[NM_UTILS_INET_ADDRSTRLEN];

    nm_assert_addr_family(addr_family);

    if (out_route_data)
        g_variant_builder_init(&builder_data, G_VARIANT_TYPE("aa{sv}"));
    if (out_routes) {
        if (IS_IPv4)
            g_variant_builder_init(&builder_legacy, G_VARIANT_TYPE("aau"));
        else
            g_variant_builder_init(&builder_legacy, G_VARIANT_TYPE("a(ayuayu)"));
    }

    nm_dedup_multi_iter_init(&iter, head_entry);
    while (nm_platform_dedup_multi_iter_next_obj(&iter, &obj, NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4))) {
        const NMPlatformIPXRoute *r = NMP_OBJECT_CAST_IPX_ROUTE(obj);
        struct in6_addr           n;

        nm_assert(r);
        nm_assert(r->rx.plen <= 8u * nm_utils_addr_family_to_size(addr_family));
        nm_assert(!IS_IPv4
                  || r->r4.network
                         == nm_utils_ip4_address_clear_host_address(r->r4.network, r->r4.plen));
        nm_assert(
            IS_IPv4
            || (memcmp(&r->r6.network,
                       nm_utils_ip6_address_clear_host_address(&n, &r->r6.network, r->r6.plen),
                       sizeof(n))
                == 0));

        if (r->rx.type_coerced != nm_platform_route_type_coerce(RTN_UNICAST))
            continue;

        if (out_route_data) {
            GVariantBuilder route_builder;
            gconstpointer   gateway;

            g_variant_builder_init(&route_builder, G_VARIANT_TYPE("a{sv}"));

            g_variant_builder_add(
                &route_builder,
                "{sv}",
                "dest",
                g_variant_new_string(nm_utils_inet_ntop(addr_family, r->rx.network_ptr, addr_str)));

            g_variant_builder_add(&route_builder,
                                  "{sv}",
                                  "prefix",
                                  g_variant_new_uint32(r->rx.plen));

            gateway = nm_platform_ip_route_get_gateway(addr_family, &r->rx);
            if (!nm_ip_addr_is_null(addr_family, gateway)) {
                g_variant_builder_add(
                    &route_builder,
                    "{sv}",
                    "next-hop",
                    g_variant_new_string(nm_utils_inet_ntop(addr_family, gateway, addr_str)));
            }

            g_variant_builder_add(&route_builder,
                                  "{sv}",
                                  "metric",
                                  g_variant_new_uint32(r->rx.metric));

            if (!nm_platform_route_table_is_main(r->rx.table_coerced)) {
                g_variant_builder_add(
                    &route_builder,
                    "{sv}",
                    "table",
                    g_variant_new_uint32(
                        nm_platform_route_table_uncoerce(r->rx.table_coerced, TRUE)));
            }

            g_variant_builder_add(&builder_data, "a{sv}", &route_builder);
        }

        if (out_routes) {
            /* legacy versions of nm_ip[46]_route_set_prefix() in libnm-util assert that the
             * plen is positive. Skip the default routes not to break older clients. */
            if (!nm_platform_route_table_is_main(r->rx.table_coerced)
                || NM_PLATFORM_IP_ROUTE_IS_DEFAULT(r))
                continue;

            if (IS_IPv4) {
                const guint32 dbus_route[4] = {
                    r->r4.network,
                    r->r4.plen,
                    r->r4.gateway,
                    r->r4.metric,
                };

                g_variant_builder_add(&builder_legacy, "@au", nm_g_variant_new_au(dbus_route, 4));
            } else {
                g_variant_builder_add(&builder_legacy,
                                      "(@ayu@ayu)",
                                      nm_g_variant_new_ay_in6addr(&r->r6.network),
                                      (guint32) r->r6.plen,
                                      nm_g_variant_new_ay_in6addr(&r->r6.gateway),
                                      (guint32) r->r6.metric);
            }
        }
    }

    NM_SET_OUT(out_route_data, g_variant_builder_end(&builder_data));
    NM_SET_OUT(out_routes, g_variant_builder_end(&builder_legacy));
}

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

/* Singleton NMPlatform subclass instance and cached class object */
NM_DEFINE_SINGLETON_INSTANCE(NMPlatform);

NM_DEFINE_SINGLETON_REGISTER(NMPlatform);

/**
 * nm_platform_setup:
 * @instance: the #NMPlatform instance
 *
 * Failing to set up #NMPlatform singleton results in a fatal error,
 * as well as trying to initialize it multiple times without freeing
 * it.
 *
 * NetworkManager will typically use only one platform object during
 * its run. Test programs might want to switch platform implementations,
 * though.
 */
void
nm_platform_setup(NMPlatform *instance)
{
    g_return_if_fail(NM_IS_PLATFORM(instance));
    g_return_if_fail(!singleton_instance);

    singleton_instance = instance;

    nm_singleton_instance_register();

    nm_log_dbg(LOGD_CORE,
               "setup %s singleton (" NM_HASH_OBFUSCATE_PTR_FMT ")",
               "NMPlatform",
               NM_HASH_OBFUSCATE_PTR(instance));
}

/**
 * nm_platform_get:
 * @self: platform instance
 *
 * Retrieve #NMPlatform singleton. Use this whenever you want to connect to
 * #NMPlatform signals. It is an error to call it before nm_platform_setup().
 *
 * Returns: (transfer none): The #NMPlatform singleton reference.
 */
NMPlatform *
nm_platform_get()
{
    g_assert(singleton_instance);

    return singleton_instance;
}

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

void
nm_linux_platform_setup(void)
{
    nm_platform_setup(nm_linux_platform_new(FALSE, FALSE));
}
