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

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

#include "nm-ip4-config.h"

#include <arpa/inet.h>
#include <resolv.h>
#include <linux/rtnetlink.h>

#include "nm-glib-aux/nm-dedup-multi.h"

#include "nm-utils.h"
#include "platform/nmp-object.h"
#include "platform/nm-platform.h"
#include "nm-platform/nm-platform-utils.h"
#include "NetworkManagerUtils.h"
#include "nm-core-internal.h"
#include "nm-dbus-object.h"

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

/* internal guint32 are assigned to gobject properties of type uint. Ensure, that uint is large enough */
G_STATIC_ASSERT(sizeof(uint) >= sizeof(guint32));
G_STATIC_ASSERT(G_MAXUINT >= 0xFFFFFFFF);

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

static gboolean
_route_valid(const NMPlatformIP4Route *r)
{
    return r && r->plen <= 32
           && r->network == nm_utils_ip4_address_clear_host_address(r->network, r->plen);
}

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

static void
_idx_obj_id_hash_update(const NMDedupMultiIdxType *idx_type,
                        const NMDedupMultiObj *    obj,
                        NMHashState *              h)
{
    nmp_object_id_hash_update((NMPObject *) obj, h);
}

static gboolean
_idx_obj_id_equal(const NMDedupMultiIdxType *idx_type,
                  const NMDedupMultiObj *    obj_a,
                  const NMDedupMultiObj *    obj_b)
{
    return nmp_object_id_equal((NMPObject *) obj_a, (NMPObject *) obj_b);
}

void
nm_ip_config_dedup_multi_idx_type_init(NMIPConfigDedupMultiIdxType *idx_type,
                                       NMPObjectType                obj_type)
{
    static const NMDedupMultiIdxTypeClass idx_type_class = {
        .idx_obj_id_hash_update = _idx_obj_id_hash_update,
        .idx_obj_id_equal       = _idx_obj_id_equal,
    };

    nm_dedup_multi_idx_type_init((NMDedupMultiIdxType *) idx_type, &idx_type_class);
    idx_type->obj_type = obj_type;
}

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

gboolean
_nm_ip_config_add_obj(NMDedupMultiIndex *          multi_idx,
                      NMIPConfigDedupMultiIdxType *idx_type,
                      int                          ifindex,
                      const NMPObject *            obj_new,
                      const NMPlatformObject *     pl_new,
                      gboolean                     merge,
                      gboolean                     append_force,
                      const NMPObject **           out_obj_old /* returns a reference! */,
                      const NMPObject **           out_obj_new /* does not return a reference */)
{
    NMPObject                obj_new_stackinit;
    const NMDedupMultiEntry *entry_old;
    const NMDedupMultiEntry *entry_new;

    nm_assert(multi_idx);
    nm_assert(idx_type);
    nm_assert(NM_IN_SET(idx_type->obj_type,
                        NMP_OBJECT_TYPE_IP4_ADDRESS,
                        NMP_OBJECT_TYPE_IP4_ROUTE,
                        NMP_OBJECT_TYPE_IP6_ADDRESS,
                        NMP_OBJECT_TYPE_IP6_ROUTE));
    nm_assert(ifindex > 0);

    /* we go through extra lengths to accept a full obj_new object. That one,
     * can be reused by increasing the ref-count. */
    if (!obj_new) {
        nm_assert(pl_new);
        obj_new = nmp_object_stackinit(&obj_new_stackinit, idx_type->obj_type, pl_new);
        NMP_OBJECT_CAST_OBJ_WITH_IFINDEX(&obj_new_stackinit)->ifindex = ifindex;
    } else {
        nm_assert(!pl_new);
        nm_assert(NMP_OBJECT_GET_TYPE(obj_new) == idx_type->obj_type);
        if (NMP_OBJECT_CAST_OBJ_WITH_IFINDEX(obj_new)->ifindex != ifindex) {
            obj_new = nmp_object_stackinit_obj(&obj_new_stackinit, obj_new);
            NMP_OBJECT_CAST_OBJ_WITH_IFINDEX(&obj_new_stackinit)->ifindex = ifindex;
        }
    }
    nm_assert(NMP_OBJECT_GET_TYPE(obj_new) == idx_type->obj_type);
    nm_assert(nmp_object_is_alive(obj_new));

    entry_old = nm_dedup_multi_index_lookup_obj(multi_idx, &idx_type->parent, obj_new);

    if (entry_old) {
        gboolean         modified = FALSE;
        const NMPObject *obj_old  = entry_old->obj;

        if (nmp_object_equal(obj_new, obj_old)) {
            nm_dedup_multi_entry_set_dirty(entry_old, FALSE);
            goto append_force_and_out;
        }

        /* if @merge, we merge the new object with the existing one.
         * Otherwise, we replace it entirely. */
        if (merge) {
            switch (idx_type->obj_type) {
            case NMP_OBJECT_TYPE_IP4_ADDRESS:
            case NMP_OBJECT_TYPE_IP6_ADDRESS:
                /* for addresses that we read from the kernel, we keep the timestamps as defined
                 * by the previous source (item_old). The reason is, that the other source configured the lifetimes
                 * with "what should be" and the kernel values are "what turned out after configuring it".
                 *
                 * For other sources, the longer lifetime wins. */
                if ((obj_new->ip_address.addr_source == NM_IP_CONFIG_SOURCE_KERNEL
                     && obj_old->ip_address.addr_source != NM_IP_CONFIG_SOURCE_KERNEL)
                    || nm_platform_ip_address_cmp_expiry(NMP_OBJECT_CAST_IP_ADDRESS(obj_old),
                                                         NMP_OBJECT_CAST_IP_ADDRESS(obj_new))
                           > 0) {
                    obj_new = nmp_object_stackinit_obj(&obj_new_stackinit, obj_new);
                    obj_new_stackinit.ip_address.timestamp =
                        NMP_OBJECT_CAST_IP_ADDRESS(obj_old)->timestamp;
                    obj_new_stackinit.ip_address.lifetime =
                        NMP_OBJECT_CAST_IP_ADDRESS(obj_old)->lifetime;
                    obj_new_stackinit.ip_address.preferred =
                        NMP_OBJECT_CAST_IP_ADDRESS(obj_old)->preferred;
                    modified = TRUE;
                }

                /* keep the maximum addr_source. */
                if (obj_new->ip_address.addr_source < obj_old->ip_address.addr_source) {
                    obj_new = nmp_object_stackinit_obj(&obj_new_stackinit, obj_new);
                    obj_new_stackinit.ip_address.addr_source = obj_old->ip_address.addr_source;
                    modified                                 = TRUE;
                }
                break;
            case NMP_OBJECT_TYPE_IP4_ROUTE:
            case NMP_OBJECT_TYPE_IP6_ROUTE:
                /* keep the maximum rt_source. */
                if (obj_new->ip_route.rt_source < obj_old->ip_route.rt_source) {
                    obj_new = nmp_object_stackinit_obj(&obj_new_stackinit, obj_new);
                    obj_new_stackinit.ip_route.rt_source = obj_old->ip_route.rt_source;
                    modified                             = TRUE;
                }
                break;
            default:
                nm_assert_not_reached();
                break;
            }

            if (modified && nmp_object_equal(obj_new, obj_old)) {
                nm_dedup_multi_entry_set_dirty(entry_old, FALSE);
                goto append_force_and_out;
            }
        }
    }

    if (!nm_dedup_multi_index_add_full(multi_idx,
                                       &idx_type->parent,
                                       obj_new,
                                       append_force ? NM_DEDUP_MULTI_IDX_MODE_APPEND_FORCE
                                                    : NM_DEDUP_MULTI_IDX_MODE_APPEND,
                                       NULL,
                                       entry_old ?: NM_DEDUP_MULTI_ENTRY_MISSING,
                                       NULL,
                                       &entry_new,
                                       out_obj_old)) {
        nm_assert_not_reached();
        NM_SET_OUT(out_obj_new, NULL);
        return FALSE;
    }

    NM_SET_OUT(out_obj_new, entry_new->obj);
    return TRUE;

append_force_and_out:
    NM_SET_OUT(out_obj_old, nmp_object_ref(entry_old->obj));
    NM_SET_OUT(out_obj_new, entry_old->obj);
    if (append_force) {
        if (nm_dedup_multi_entry_reorder(entry_old, NULL, TRUE))
            return TRUE;
    }
    return FALSE;
}

/**
 * _nm_ip_config_lookup_ip_route:
 * @multi_idx:
 * @idx_type:
 * @needle:
 * @cmp_type: after lookup, filter the result by comparing with @cmp_type. Only
 *   return the result, if it compares equal to @needle according to this @cmp_type.
 *   Note that the index uses %NM_PLATFORM_IP_ROUTE_CMP_TYPE_DST type, so passing
 *   that compare-type means not to filter any further.
 *
 * Returns: the found entry or %NULL.
 */
const NMDedupMultiEntry *
_nm_ip_config_lookup_ip_route(const NMDedupMultiIndex *          multi_idx,
                              const NMIPConfigDedupMultiIdxType *idx_type,
                              const NMPObject *                  needle,
                              NMPlatformIPRouteCmpType           cmp_type)
{
    const NMDedupMultiEntry *entry;

    nm_assert(multi_idx);
    nm_assert(idx_type);
    nm_assert(NM_IN_SET(idx_type->obj_type, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE));
    nm_assert(NMP_OBJECT_GET_TYPE(needle) == idx_type->obj_type);

    entry = nm_dedup_multi_index_lookup_obj(multi_idx, &idx_type->parent, needle);
    if (!entry)
        return NULL;

    if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) {
        nm_assert((NMP_OBJECT_GET_TYPE(needle) == NMP_OBJECT_TYPE_IP4_ROUTE
                   && nm_platform_ip4_route_cmp(NMP_OBJECT_CAST_IP4_ROUTE(entry->obj),
                                                NMP_OBJECT_CAST_IP4_ROUTE(needle),
                                                cmp_type)
                          == 0)
                  || (NMP_OBJECT_GET_TYPE(needle) == NMP_OBJECT_TYPE_IP6_ROUTE
                      && nm_platform_ip6_route_cmp(NMP_OBJECT_CAST_IP6_ROUTE(entry->obj),
                                                   NMP_OBJECT_CAST_IP6_ROUTE(needle),
                                                   cmp_type)
                             == 0));
    } else {
        if (NMP_OBJECT_GET_TYPE(needle) == NMP_OBJECT_TYPE_IP4_ROUTE) {
            if (nm_platform_ip4_route_cmp(NMP_OBJECT_CAST_IP4_ROUTE(entry->obj),
                                          NMP_OBJECT_CAST_IP4_ROUTE(needle),
                                          cmp_type)
                != 0)
                return NULL;
        } else {
            if (nm_platform_ip6_route_cmp(NMP_OBJECT_CAST_IP6_ROUTE(entry->obj),
                                          NMP_OBJECT_CAST_IP6_ROUTE(needle),
                                          cmp_type)
                != 0)
                return NULL;
        }
    }
    return entry;
}

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

NM_GOBJECT_PROPERTIES_DEFINE(NMIP4Config,
                             PROP_MULTI_IDX,
                             PROP_IFINDEX,
                             PROP_ADDRESS_DATA,
                             PROP_ADDRESSES,
                             PROP_ROUTE_DATA,
                             PROP_ROUTES,
                             PROP_GATEWAY,
                             PROP_NAMESERVER_DATA,
                             PROP_NAMESERVERS,
                             PROP_DOMAINS,
                             PROP_SEARCHES,
                             PROP_DNS_OPTIONS,
                             PROP_WINS_SERVER_DATA,
                             PROP_WINS_SERVERS,
                             PROP_DNS_PRIORITY, );

typedef struct {
    bool                     metered : 1;
    bool                     never_default : 1;
    guint32                  mtu;
    int                      ifindex;
    NMIPConfigSource         mtu_source;
    int                      dns_priority;
    NMSettingConnectionMdns  mdns;
    NMSettingConnectionLlmnr llmnr;
    GArray *                 nameservers;
    GPtrArray *              domains;
    GPtrArray *              searches;
    GPtrArray *              dns_options;
    GArray *                 nis;
    char *                   nis_domain;
    GArray *                 wins;
    GVariant *               address_data_variant;
    GVariant *               addresses_variant;
    GVariant *               route_data_variant;
    GVariant *               routes_variant;
    NMDedupMultiIndex *      multi_idx;
    const NMPObject *        best_default_route;
    union {
        NMIPConfigDedupMultiIdxType idx_ip4_addresses_;
        NMDedupMultiIdxType         idx_ip4_addresses;
    };
    union {
        NMIPConfigDedupMultiIdxType idx_ip4_routes_;
        NMDedupMultiIdxType         idx_ip4_routes;
    };
    NMIPConfigFlags config_flags;
} NMIP4ConfigPrivate;

struct _NMIP4Config {
    NMIPConfig         parent;
    NMIP4ConfigPrivate _priv;
};

struct _NMIP4ConfigClass {
    NMIPConfigClass parent;
};

G_DEFINE_TYPE(NMIP4Config, nm_ip4_config, NM_TYPE_IP_CONFIG)

#define NM_IP4_CONFIG_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMIP4Config, NM_IS_IP4_CONFIG)

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

static void
_add_address(NMIP4Config *self, const NMPObject *obj_new, const NMPlatformIP4Address *new);
static void _add_route(NMIP4Config *    self,
                       const NMPObject *obj_new,
                       const NMPlatformIP4Route *new,
                       const NMPObject **out_obj_new);
static const NMDedupMultiEntry *
_lookup_route(const NMIP4Config *self, const NMPObject *needle, NMPlatformIPRouteCmpType cmp_type);

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

int
nm_ip4_config_get_ifindex(const NMIP4Config *self)
{
    return NM_IP4_CONFIG_GET_PRIVATE(self)->ifindex;
}

NMDedupMultiIndex *
nm_ip4_config_get_multi_idx(const NMIP4Config *self)
{
    return NM_IP4_CONFIG_GET_PRIVATE(self)->multi_idx;
}

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

const NMDedupMultiHeadEntry *
nm_ip4_config_lookup_addresses(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return nm_dedup_multi_index_lookup_head(priv->multi_idx, &priv->idx_ip4_addresses, NULL);
}

void
nm_ip_config_iter_ip4_address_init(NMDedupMultiIter *ipconf_iter, const NMIP4Config *self)
{
    nm_assert(NM_IS_IP4_CONFIG(self));

    nm_dedup_multi_iter_init(ipconf_iter, nm_ip4_config_lookup_addresses(self));
}

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

const NMDedupMultiHeadEntry *
nm_ip4_config_lookup_routes(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return nm_dedup_multi_index_lookup_head(priv->multi_idx, &priv->idx_ip4_routes, NULL);
}

void
nm_ip_config_iter_ip4_route_init(NMDedupMultiIter *ipconf_iter, const NMIP4Config *self)
{
    nm_assert(NM_IS_IP4_CONFIG(self));

    nm_dedup_multi_iter_init(ipconf_iter, nm_ip4_config_lookup_routes(self));
}

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

const NMPObject *
_nm_ip_config_best_default_route_find_better(const NMPObject *obj_cur, const NMPObject *obj_cmp)
{
    nm_assert(!obj_cur
              || NM_IN_SET(NMP_OBJECT_GET_TYPE(obj_cur),
                           NMP_OBJECT_TYPE_IP4_ROUTE,
                           NMP_OBJECT_TYPE_IP6_ROUTE));
    nm_assert(!obj_cmp
              || (!obj_cur
                  && NM_IN_SET(NMP_OBJECT_GET_TYPE(obj_cmp),
                               NMP_OBJECT_TYPE_IP4_ROUTE,
                               NMP_OBJECT_TYPE_IP6_ROUTE))
              || NMP_OBJECT_GET_TYPE(obj_cur) == NMP_OBJECT_GET_TYPE(obj_cmp));
    nm_assert(!obj_cur || nmp_object_ip_route_is_best_defaut_route(obj_cur));

    /* assumes that @obj_cur is already the best default route (or NULL). It checks whether
     * @obj_cmp is also a default route and returns the best of both. */
    if (obj_cmp && nmp_object_ip_route_is_best_defaut_route(obj_cmp)) {
        guint32 metric_cur, metric_cmp;

        if (!obj_cur)
            return obj_cmp;

        metric_cur = NMP_OBJECT_CAST_IP_ROUTE(obj_cur)->metric;
        metric_cmp = NMP_OBJECT_CAST_IP_ROUTE(obj_cmp)->metric;

        if (metric_cmp < metric_cur)
            return obj_cmp;

        if (metric_cmp == metric_cur) {
            int c;

            /* Routes have the same metric. We still want to deterministically
             * prefer one or the other. It's important to consistently choose one
             * or the other, so that the order doesn't matter how routes are added
             * (and merged). */
            c = nmp_object_cmp(obj_cur, obj_cmp);
            if (c != 0)
                return c < 0 ? obj_cur : obj_cmp;

            /* as last resort, compare pointers. */
            if (obj_cmp < obj_cur)
                return obj_cmp;
        }
    }
    return obj_cur;
}

gboolean
_nm_ip_config_best_default_route_merge(const NMPObject **best_default_route,
                                       const NMPObject * new_candidate)
{
    new_candidate =
        _nm_ip_config_best_default_route_find_better(*best_default_route, new_candidate);
    return nmp_object_ref_set(best_default_route, new_candidate);
}

const NMPObject *
nm_ip4_config_best_default_route_get(const NMIP4Config *self)
{
    g_return_val_if_fail(NM_IS_IP4_CONFIG(self), NULL);

    return NM_IP4_CONFIG_GET_PRIVATE(self)->best_default_route;
}

const NMPObject *
_nm_ip4_config_best_default_route_find(const NMIP4Config *self)
{
    NMDedupMultiIter ipconf_iter;
    const NMPObject *new_best_default_route = NULL;

    nm_ip_config_iter_ip4_route_for_each (&ipconf_iter, self, NULL) {
        new_best_default_route =
            _nm_ip_config_best_default_route_find_better(new_best_default_route,
                                                         ipconf_iter.current->obj);
    }
    return new_best_default_route;
}

in_addr_t
nmtst_ip4_config_get_gateway(NMIP4Config *config)
{
    const NMPObject *rt;

    g_assert(NM_IS_IP4_CONFIG(config));

    rt = nm_ip4_config_best_default_route_get(config);
    if (!rt)
        return 0;
    return NMP_OBJECT_CAST_IP4_ROUTE(rt)->gateway;
}

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

static void
_notify_addresses(NMIP4Config *self)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    nm_clear_g_variant(&priv->address_data_variant);
    nm_clear_g_variant(&priv->addresses_variant);
    nm_gobject_notify_together(self, PROP_ADDRESS_DATA, PROP_ADDRESSES);
}

static void
_notify_routes(NMIP4Config *self)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    nm_assert(priv->best_default_route == _nm_ip4_config_best_default_route_find(self));
    nm_clear_g_variant(&priv->route_data_variant);
    nm_clear_g_variant(&priv->routes_variant);
    nm_gobject_notify_together(self, PROP_ROUTE_DATA, PROP_ROUTES);
}

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

static int
sort_captured_addresses(const CList *lst_a, const CList *lst_b, gconstpointer user_data)
{
    const NMPlatformIP4Address *addr_a =
        NMP_OBJECT_CAST_IP4_ADDRESS(c_list_entry(lst_a, NMDedupMultiEntry, lst_entries)->obj);
    const NMPlatformIP4Address *addr_b =
        NMP_OBJECT_CAST_IP4_ADDRESS(c_list_entry(lst_b, NMDedupMultiEntry, lst_entries)->obj);

    nm_assert(addr_a);
    nm_assert(addr_b);

    /* Primary addresses first */
    return NM_FLAGS_HAS(addr_a->n_ifa_flags, IFA_F_SECONDARY)
           - NM_FLAGS_HAS(addr_b->n_ifa_flags, IFA_F_SECONDARY);
}

NMIP4Config *
nm_ip4_config_clone(const NMIP4Config *self)
{
    NMIP4Config *copy;

    copy = nm_ip4_config_new(nm_ip4_config_get_multi_idx(self), -1);
    nm_ip4_config_replace(copy, self, NULL);

    return copy;
}

NMIP4Config *
nm_ip4_config_capture(NMDedupMultiIndex *multi_idx, NMPlatform *platform, int ifindex)
{
    NMIP4Config *                self;
    NMIP4ConfigPrivate *         priv;
    const NMDedupMultiHeadEntry *head_entry;
    NMDedupMultiIter             iter;
    const NMPObject *            plobj = NULL;

    nm_assert(ifindex > 0);

    /* Slaves have no IP configuration */
    if (nm_platform_link_get_master(platform, ifindex) > 0)
        return NULL;

    self = nm_ip4_config_new(multi_idx, ifindex);
    priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    head_entry = nm_platform_lookup_object(platform, NMP_OBJECT_TYPE_IP4_ADDRESS, ifindex);
    if (head_entry) {
        nmp_cache_iter_for_each (&iter, head_entry, &plobj) {
            if (!_nm_ip_config_add_obj(priv->multi_idx,
                                       &priv->idx_ip4_addresses_,
                                       ifindex,
                                       plobj,
                                       NULL,
                                       FALSE,
                                       TRUE,
                                       NULL,
                                       NULL))
                nm_assert_not_reached();
        }
        head_entry = nm_ip4_config_lookup_addresses(self);
        nm_assert(head_entry);
        nm_dedup_multi_head_entry_sort(head_entry, sort_captured_addresses, NULL);
        _notify_addresses(self);
    }

    head_entry = nm_platform_lookup_object(platform, NMP_OBJECT_TYPE_IP4_ROUTE, ifindex);

    /* Extract gateway from default route */
    nmp_cache_iter_for_each (&iter, head_entry, &plobj)
        _add_route(self, plobj, NULL, NULL);

    return self;
}

void
nm_ip4_config_update_routes_metric(NMIP4Config *self, gint64 metric)
{
    gs_free NMPlatformIP4Route *routes      = NULL;
    gboolean                    need_update = FALSE;
    const NMPlatformIP4Route *  r;
    NMDedupMultiIter            iter;
    guint                       num = 0, i = 0;

    nm_ip_config_iter_ip4_route_for_each (&iter, self, &r) {
        if (r->metric != metric)
            need_update = TRUE;
        num++;
    }
    if (!need_update)
        return;

    routes = g_new(NMPlatformIP4Route, num);
    nm_ip_config_iter_ip4_route_for_each (&iter, self, &r) {
        routes[i]        = *r;
        routes[i].metric = metric;
        i++;
    }

    g_object_freeze_notify(G_OBJECT(self));
    nm_ip4_config_reset_routes(self);
    for (i = 0; i < num; i++)
        nm_ip4_config_add_route(self, &routes[i], NULL);
    g_object_thaw_notify(G_OBJECT(self));
}

void
nm_ip4_config_add_dependent_routes(NMIP4Config *self,
                                   guint32      route_table,
                                   guint32      route_metric,
                                   gboolean     is_vrf,
                                   GPtrArray ** out_ip4_dev_route_blacklist)
{
    GPtrArray *                 ip4_dev_route_blacklist = NULL;
    const NMPlatformIP4Address *my_addr;
    const NMPlatformIP4Route *  my_route;
    int                         ifindex;
    NMDedupMultiIter            iter;

    g_return_if_fail(NM_IS_IP4_CONFIG(self));

    ifindex = nm_ip4_config_get_ifindex(self);
    g_return_if_fail(ifindex > 0);

    /* For IPv6 slaac, we explicitly add the device-routes (onlink) to NMIP6Config.
     * As we don't do that for IPv4 (and manual IPv6 addresses), add them explicitly. */

    nm_ip_config_iter_ip4_address_for_each (&iter, self, &my_addr) {
        nm_auto_nmpobj NMPObject *r = NULL;
        NMPlatformIP4Route *      route;
        in_addr_t                 network;

        if (my_addr->plen == 0)
            continue;

        nm_assert(my_addr->plen <= 32);

        /* The destination network depends on the peer-address. */
        network = nm_utils_ip4_address_clear_host_address(my_addr->peer_address, my_addr->plen);

        if (my_addr->external)
            continue;

        /* Pre-generate local route added by kernel */
        r                   = nmp_object_new(NMP_OBJECT_TYPE_IP4_ROUTE, NULL);
        route               = NMP_OBJECT_CAST_IP4_ROUTE(r);
        route->ifindex      = ifindex;
        route->rt_source    = NM_IP_CONFIG_SOURCE_KERNEL;
        route->network      = my_addr->address;
        route->plen         = 32;
        route->pref_src     = my_addr->address;
        route->type_coerced = nm_platform_route_type_coerce(RTN_LOCAL);
        route->scope_inv    = nm_platform_route_scope_inv(RT_SCOPE_HOST);
        route->table_coerced =
            nm_platform_route_table_coerce(is_vrf ? route_table : RT_TABLE_LOCAL);
        _add_route(self, r, NULL, NULL);
        nm_clear_pointer(&r, nmp_object_unref);

        if (nm_utils_ip4_address_is_zeronet(network)) {
            /* Kernel doesn't add device-routes for destinations that
             * start with 0.x.y.z. Skip them. */
            continue;
        }

        if (my_addr->plen == 32 && my_addr->address == my_addr->peer_address) {
            /* Kernel doesn't add device-routes for /32 addresses unless
             * they have a peer. */
            continue;
        }

        r     = nmp_object_new(NMP_OBJECT_TYPE_IP4_ROUTE, NULL);
        route = NMP_OBJECT_CAST_IP4_ROUTE(r);

        route->ifindex       = ifindex;
        route->rt_source     = NM_IP_CONFIG_SOURCE_KERNEL;
        route->network       = network;
        route->plen          = my_addr->plen;
        route->pref_src      = my_addr->address;
        route->table_coerced = nm_platform_route_table_coerce(route_table);
        route->metric        = route_metric;
        route->scope_inv     = nm_platform_route_scope_inv(NM_RT_SCOPE_LINK);

        nm_platform_ip_route_normalize(AF_INET, (NMPlatformIPRoute *) route);

        if (_lookup_route(self, r, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID)) {
            /* we already track this route. Don't add it again. */
        } else
            _add_route(self, r, NULL, NULL);

        if (out_ip4_dev_route_blacklist
            && (route_table != RT_TABLE_MAIN
                || route_metric != NM_PLATFORM_ROUTE_METRIC_IP4_DEVICE_ROUTE)) {
            nm_auto_nmpobj NMPObject *r_dev = NULL;

            r_dev                = nmp_object_clone(r, FALSE);
            route                = NMP_OBJECT_CAST_IP4_ROUTE(r_dev);
            route->table_coerced = nm_platform_route_table_coerce(RT_TABLE_MAIN);
            route->metric        = NM_PLATFORM_ROUTE_METRIC_IP4_DEVICE_ROUTE;

            nm_platform_ip_route_normalize(AF_INET, (NMPlatformIPRoute *) route);

            if (_lookup_route(self, r_dev, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID)) {
                /* we track such a route explicitly. Don't blacklist it. */
            } else {
                if (!ip4_dev_route_blacklist)
                    ip4_dev_route_blacklist =
                        g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);

                g_ptr_array_add(ip4_dev_route_blacklist, g_steal_pointer(&r_dev));
            }
        }
    }

again:
    nm_ip_config_iter_ip4_route_for_each (&iter, self, &my_route) {
        NMPlatformIP4Route rt;

        if (!NM_PLATFORM_IP_ROUTE_IS_DEFAULT(my_route) || my_route->gateway == 0
            || NM_IS_IP_CONFIG_SOURCE_RTPROT(my_route->rt_source)
            || nm_ip4_config_get_direct_route_for_host(
                self,
                my_route->gateway,
                nm_platform_route_table_uncoerce(my_route->table_coerced, TRUE)))
            continue;

        rt         = *my_route;
        rt.network = my_route->gateway;
        rt.plen    = 32;
        rt.gateway = 0;
        _add_route(self, NULL, &rt, NULL);
        /* adding the route might have invalidated the iteration. Start again. */
        goto again;
    }

    NM_SET_OUT(out_ip4_dev_route_blacklist, ip4_dev_route_blacklist);
}

gboolean
nm_ip4_config_commit(const NMIP4Config *    self,
                     NMPlatform *           platform,
                     NMIPRouteTableSyncMode route_table_sync)
{
    gs_unref_ptrarray GPtrArray *addresses    = NULL;
    gs_unref_ptrarray GPtrArray *routes       = NULL;
    gs_unref_ptrarray GPtrArray *routes_prune = NULL;
    int                          ifindex;
    gboolean                     success = TRUE;

    g_return_val_if_fail(NM_IS_IP4_CONFIG(self), FALSE);

    ifindex = nm_ip4_config_get_ifindex(self);
    g_return_val_if_fail(ifindex > 0, FALSE);

    addresses =
        nm_dedup_multi_objs_to_ptr_array_head(nm_ip4_config_lookup_addresses(self), NULL, NULL);

    routes = nm_dedup_multi_objs_to_ptr_array_head(nm_ip4_config_lookup_routes(self), NULL, NULL);

    routes_prune =
        nm_platform_ip_route_get_prune_list(platform, AF_INET, ifindex, route_table_sync);

    nm_platform_ip4_address_sync(platform, ifindex, addresses);

    if (!nm_platform_ip_route_sync(platform, AF_INET, ifindex, routes, routes_prune, NULL))
        success = FALSE;

    return success;
}

void
nm_ip4_config_merge_setting(NMIP4Config *            self,
                            NMSettingIPConfig *      setting,
                            NMSettingConnectionMdns  mdns,
                            NMSettingConnectionLlmnr llmnr,
                            guint32                  route_table,
                            guint32                  route_metric)
{
    guint       naddresses, nroutes, nnameservers, nsearches;
    int         i, priority;
    const char *gateway_str;
    guint32     gateway_bin;

    if (!setting)
        return;

    g_return_if_fail(NM_IS_SETTING_IP4_CONFIG(setting));

    g_object_freeze_notify(G_OBJECT(self));

    naddresses   = nm_setting_ip_config_get_num_addresses(setting);
    nroutes      = nm_setting_ip_config_get_num_routes(setting);
    nnameservers = nm_setting_ip_config_get_num_dns(setting);
    nsearches    = nm_setting_ip_config_get_num_dns_searches(setting);

    /* Gateway */
    if (!nm_setting_ip_config_get_never_default(setting)
        && (gateway_str = nm_setting_ip_config_get_gateway(setting))
        && inet_pton(AF_INET, gateway_str, &gateway_bin) == 1 && gateway_bin) {
        const NMPlatformIP4Route r = {
            .rt_source     = NM_IP_CONFIG_SOURCE_USER,
            .gateway       = gateway_bin,
            .table_coerced = nm_platform_route_table_coerce(route_table),
            .metric        = route_metric,
        };

        _add_route(self, NULL, &r, NULL);
    }

    /* Addresses */
    for (i = 0; i < naddresses; i++) {
        NMIPAddress *        s_addr = nm_setting_ip_config_get_address(setting, i);
        GVariant *           label;
        NMPlatformIP4Address address;

        memset(&address, 0, sizeof(address));
        nm_ip_address_get_address_binary(s_addr, &address.address);
        address.peer_address = address.address;
        address.plen         = nm_ip_address_get_prefix(s_addr);
        nm_assert(address.plen <= 32);
        address.lifetime    = NM_PLATFORM_LIFETIME_PERMANENT;
        address.preferred   = NM_PLATFORM_LIFETIME_PERMANENT;
        address.addr_source = NM_IP_CONFIG_SOURCE_USER;

        label = nm_ip_address_get_attribute(s_addr, NM_IP_ADDRESS_ATTRIBUTE_LABEL);
        if (label)
            g_strlcpy(address.label, g_variant_get_string(label, NULL), sizeof(address.label));

        _add_address(self, NULL, &address);
    }

    /* Routes */
    for (i = 0; i < nroutes; i++) {
        NMIPRoute *        s_route = nm_setting_ip_config_get_route(setting, i);
        NMPlatformIP4Route route;
        gint64             m;

        if (nm_ip_route_get_family(s_route) != AF_INET) {
            nm_assert_not_reached();
            continue;
        }

        memset(&route, 0, sizeof(route));
        nm_ip_route_get_dest_binary(s_route, &route.network);

        route.plen = nm_ip_route_get_prefix(s_route);
        nm_assert(route.plen <= 32);

        nm_ip_route_get_next_hop_binary(s_route, &route.gateway);
        m = nm_ip_route_get_metric(s_route);
        if (m < 0)
            route.metric = route_metric;
        else
            route.metric = m;
        route.rt_source = NM_IP_CONFIG_SOURCE_USER;

        route.network = nm_utils_ip4_address_clear_host_address(route.network, route.plen);

        nm_utils_ip_route_attribute_to_platform(AF_INET,
                                                s_route,
                                                NM_PLATFORM_IP_ROUTE_CAST(&route),
                                                route_table);
        _add_route(self, NULL, &route, NULL);
    }

    /* DNS */
    if (nm_setting_ip_config_get_ignore_auto_dns(setting)) {
        nm_ip4_config_reset_nameservers(self);
        nm_ip4_config_reset_domains(self);
        nm_ip4_config_reset_searches(self);
    }
    for (i = 0; i < nnameservers; i++) {
        guint32 ip;

        if (inet_pton(AF_INET, nm_setting_ip_config_get_dns(setting, i), &ip) == 1)
            nm_ip4_config_add_nameserver(self, ip);
    }
    for (i = 0; i < nsearches; i++)
        nm_ip4_config_add_search(self, nm_setting_ip_config_get_dns_search(setting, i));

    i = 0;
    while ((i = nm_setting_ip_config_next_valid_dns_option(setting, i)) >= 0) {
        nm_ip4_config_add_dns_option(self, nm_setting_ip_config_get_dns_option(setting, i));
        i++;
    }

    priority = nm_setting_ip_config_get_dns_priority(setting);
    if (priority)
        nm_ip4_config_set_dns_priority(self, priority);

    nm_ip4_config_mdns_set(self, mdns);
    nm_ip4_config_llmnr_set(self, llmnr);

    nm_ip4_config_set_never_default(self, nm_setting_ip_config_get_never_default(setting));

    g_object_thaw_notify(G_OBJECT(self));
}

NMSetting *
nm_ip4_config_create_setting(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *  priv;
    NMSettingIPConfig *         s_ip4;
    guint                       nnameservers, nsearches, noptions;
    const char *                method = NULL;
    int                         i;
    NMDedupMultiIter            ipconf_iter;
    const NMPlatformIP4Address *address;
    const NMPlatformIP4Route *  route;
    char                        sbuf[NM_UTILS_INET_ADDRSTRLEN];

    s_ip4 = NM_SETTING_IP_CONFIG(nm_setting_ip4_config_new());

    if (!self) {
        g_object_set(s_ip4,
                     NM_SETTING_IP_CONFIG_METHOD,
                     NM_SETTING_IP4_CONFIG_METHOD_DISABLED,
                     NULL);
        return NM_SETTING(s_ip4);
    }

    priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    nnameservers = nm_ip4_config_get_num_nameservers(self);
    nsearches    = nm_ip4_config_get_num_searches(self);
    noptions     = nm_ip4_config_get_num_dns_options(self);

    /* Addresses */
    nm_ip_config_iter_ip4_address_for_each (&ipconf_iter, self, &address) {
        NMIPAddress *s_addr;

        /* Detect dynamic address */
        if (address->lifetime != NM_PLATFORM_LIFETIME_PERMANENT) {
            method = NM_SETTING_IP4_CONFIG_METHOD_AUTO;
            continue;
        }

        /* Static address found. */
        if (!method)
            method = NM_SETTING_IP4_CONFIG_METHOD_MANUAL;

        s_addr = nm_ip_address_new_binary(AF_INET, &address->address, address->plen, NULL);
        if (*address->label)
            nm_ip_address_set_attribute(s_addr,
                                        NM_IP_ADDRESS_ATTRIBUTE_LABEL,
                                        g_variant_new_string(address->label));

        nm_setting_ip_config_add_address(s_ip4, s_addr);
        nm_ip_address_unref(s_addr);
    }

    /* Gateway */
    if (priv->best_default_route && nm_setting_ip_config_get_num_addresses(s_ip4) > 0) {
        g_object_set(
            s_ip4,
            NM_SETTING_IP_CONFIG_GATEWAY,
            _nm_utils_inet4_ntop(NMP_OBJECT_CAST_IP4_ROUTE(priv->best_default_route)->gateway,
                                 sbuf),
            NULL);
    }

    /* Use 'disabled' if the method wasn't previously set */
    if (!method)
        method = NM_SETTING_IP4_CONFIG_METHOD_DISABLED;

    g_object_set(s_ip4, NM_SETTING_IP_CONFIG_METHOD, method, NULL);

    /* Routes */
    nm_ip_config_iter_ip4_route_for_each (&ipconf_iter, self, &route) {
        NMIPRoute *s_route;

        if (NM_PLATFORM_IP_ROUTE_IS_DEFAULT(route))
            continue;

        /* Ignore routes provided by external sources */
        if (route->rt_source
            != nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER))
            continue;

        s_route = nm_ip_route_new_binary(AF_INET,
                                         &route->network,
                                         route->plen,
                                         &route->gateway,
                                         route->metric,
                                         NULL);
        nm_setting_ip_config_add_route(s_ip4, s_route);
        nm_ip_route_unref(s_route);
    }

    /* DNS */
    for (i = 0; i < nnameservers; i++) {
        guint32 nameserver = nm_ip4_config_get_nameserver(self, i);

        nm_setting_ip_config_add_dns(s_ip4, _nm_utils_inet4_ntop(nameserver, sbuf));
    }
    for (i = 0; i < nsearches; i++) {
        const char *search = nm_ip4_config_get_search(self, i);

        nm_setting_ip_config_add_dns_search(s_ip4, search);
    }

    for (i = 0; i < noptions; i++) {
        const char *option = nm_ip4_config_get_dns_option(self, i);

        nm_setting_ip_config_add_dns_option(s_ip4, option);
    }

    g_object_set(s_ip4,
                 NM_SETTING_IP_CONFIG_DNS_PRIORITY,
                 nm_ip4_config_get_dns_priority(self),
                 NULL);

    return NM_SETTING(s_ip4);
}

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

void
nm_ip4_config_merge(NMIP4Config *        dst,
                    const NMIP4Config *  src,
                    NMIPConfigMergeFlags merge_flags,
                    guint32              default_route_metric_penalty)
{
    NMIP4ConfigPrivate *        dst_priv;
    const NMIP4ConfigPrivate *  src_priv;
    guint32                     i;
    NMDedupMultiIter            ipconf_iter;
    const NMPlatformIP4Address *address = NULL;

    g_return_if_fail(src != NULL);
    g_return_if_fail(dst != NULL);

    dst_priv = NM_IP4_CONFIG_GET_PRIVATE(dst);
    src_priv = NM_IP4_CONFIG_GET_PRIVATE(src);

    g_object_freeze_notify(G_OBJECT(dst));

    /* addresses */
    nm_ip_config_iter_ip4_address_for_each (&ipconf_iter, src, &address) {
        if (NM_FLAGS_HAS(merge_flags, NM_IP_CONFIG_MERGE_EXTERNAL) && !address->external) {
            NMPlatformIP4Address a;

            a          = *address;
            a.external = TRUE;
            _add_address(dst, NULL, &a);
        } else
            _add_address(dst, NMP_OBJECT_UP_CAST(address), NULL);
    }

    /* nameservers */
    if (!NM_FLAGS_HAS(merge_flags, NM_IP_CONFIG_MERGE_NO_DNS)) {
        for (i = 0; i < nm_ip4_config_get_num_nameservers(src); i++)
            nm_ip4_config_add_nameserver(dst, nm_ip4_config_get_nameserver(src, i));
    }

    /* routes */
    if (!NM_FLAGS_HAS(merge_flags, NM_IP_CONFIG_MERGE_NO_ROUTES)) {
        const NMPlatformIP4Route *r_src;

        nm_ip_config_iter_ip4_route_for_each (&ipconf_iter, src, &r_src) {
            if (NM_PLATFORM_IP_ROUTE_IS_DEFAULT(r_src)) {
                if (NM_FLAGS_HAS(merge_flags, NM_IP_CONFIG_MERGE_NO_DEFAULT_ROUTES)
                    && !NM_FLAGS_HAS(src_priv->config_flags,
                                     NM_IP_CONFIG_FLAGS_IGNORE_MERGE_NO_DEFAULT_ROUTES))
                    continue;
                if (default_route_metric_penalty) {
                    NMPlatformIP4Route r = *r_src;

                    r.metric =
                        nm_utils_ip_route_metric_penalize(r.metric, default_route_metric_penalty);
                    _add_route(dst, NULL, &r, NULL);
                    continue;
                }
            }
            _add_route(dst, ipconf_iter.current->obj, NULL, NULL);
        }
    }

    /* domains */
    if (!NM_FLAGS_HAS(merge_flags, NM_IP_CONFIG_MERGE_NO_DNS)) {
        for (i = 0; i < nm_ip4_config_get_num_domains(src); i++)
            nm_ip4_config_add_domain(dst, nm_ip4_config_get_domain(src, i));
    }

    /* dns searches */
    if (!NM_FLAGS_HAS(merge_flags, NM_IP_CONFIG_MERGE_NO_DNS)) {
        for (i = 0; i < nm_ip4_config_get_num_searches(src); i++)
            nm_ip4_config_add_search(dst, nm_ip4_config_get_search(src, i));
    }

    /* dns options */
    if (!NM_FLAGS_HAS(merge_flags, NM_IP_CONFIG_MERGE_NO_DNS)) {
        for (i = 0; i < nm_ip4_config_get_num_dns_options(src); i++)
            nm_ip4_config_add_dns_option(dst, nm_ip4_config_get_dns_option(src, i));
    }

    /* MTU */
    if (src_priv->mtu_source > dst_priv->mtu_source
        || (src_priv->mtu_source == dst_priv->mtu_source
            && ((!dst_priv->mtu && src_priv->mtu)
                || (dst_priv->mtu && src_priv->mtu < dst_priv->mtu))))
        nm_ip4_config_set_mtu(dst, src_priv->mtu, src_priv->mtu_source);

    /* NIS */
    if (!NM_FLAGS_HAS(merge_flags, NM_IP_CONFIG_MERGE_NO_DNS)) {
        for (i = 0; i < nm_ip4_config_get_num_nis_servers(src); i++)
            nm_ip4_config_add_nis_server(dst, nm_ip4_config_get_nis_server(src, i));

        if (nm_ip4_config_get_nis_domain(src))
            nm_ip4_config_set_nis_domain(dst, nm_ip4_config_get_nis_domain(src));
    }

    /* WINS */
    if (!NM_FLAGS_HAS(merge_flags, NM_IP_CONFIG_MERGE_NO_DNS)) {
        for (i = 0; i < nm_ip4_config_get_num_wins(src); i++)
            nm_ip4_config_add_wins(dst, nm_ip4_config_get_wins(src, i));
    }

    /* metered flag */
    nm_ip4_config_set_metered(dst,
                              nm_ip4_config_get_metered(dst) || nm_ip4_config_get_metered(src));

    /* never default */
    nm_ip4_config_set_never_default(dst,
                                    nm_ip4_config_get_never_default(dst)
                                        || nm_ip4_config_get_never_default(src));

    /* DNS priority */
    if (nm_ip4_config_get_dns_priority(src))
        nm_ip4_config_set_dns_priority(dst, nm_ip4_config_get_dns_priority(src));

    /* mdns */
    nm_ip4_config_mdns_set(dst, NM_MAX(nm_ip4_config_mdns_get(src), nm_ip4_config_mdns_get(dst)));
    /* LLMNR */
    nm_ip4_config_llmnr_set(dst,
                            NM_MAX(nm_ip4_config_llmnr_get(src), nm_ip4_config_llmnr_get(dst)));

    g_object_thaw_notify(G_OBJECT(dst));
}

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

static int
_nameservers_get_index(const NMIP4Config *self, guint32 ns)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);
    guint                     i;

    for (i = 0; i < priv->nameservers->len; i++) {
        guint32 n = g_array_index(priv->nameservers, guint32, i);

        if (ns == n)
            return (int) i;
    }
    return -1;
}

static int
_domains_get_index(const NMIP4Config *self, const char *domain)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);
    guint                     i;

    for (i = 0; i < priv->domains->len; i++) {
        const char *d = g_ptr_array_index(priv->domains, i);

        if (g_strcmp0(domain, d) == 0)
            return (int) i;
    }
    return -1;
}

static int
_searches_get_index(const NMIP4Config *self, const char *search)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);
    guint                     i;

    for (i = 0; i < priv->searches->len; i++) {
        const char *s = g_ptr_array_index(priv->searches, i);

        if (g_strcmp0(search, s) == 0)
            return (int) i;
    }
    return -1;
}

static int
_dns_options_get_index(const NMIP4Config *self, const char *option)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);
    guint                     i;

    for (i = 0; i < priv->dns_options->len; i++) {
        const char *s = g_ptr_array_index(priv->dns_options, i);

        if (g_strcmp0(option, s) == 0)
            return (int) i;
    }
    return -1;
}

static int
_nis_servers_get_index(const NMIP4Config *self, guint32 nis_server)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);
    guint                     i;

    for (i = 0; i < priv->nis->len; i++) {
        guint32 n = g_array_index(priv->nis, guint32, i);

        if (n == nis_server)
            return (int) i;
    }
    return -1;
}

static int
_wins_get_index(const NMIP4Config *self, guint32 wins_server)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);
    guint                     i;

    for (i = 0; i < priv->wins->len; i++) {
        guint32 n = g_array_index(priv->wins, guint32, i);

        if (n == wins_server)
            return (int) i;
    }
    return -1;
}

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

/**
 * nm_ip4_config_subtract:
 * @dst: config from which to remove everything in @src
 * @src: config to remove from @dst
 * @default_route_metric_penalty: pretend that on source we applied
 *   a route penalty on the default-route. It means, for default routes
 *   we don't remove routes that match exactly, but those with a lower
 *   metric (with the penalty removed).
 *
 * Removes everything in @src from @dst.
 */
void
nm_ip4_config_subtract(NMIP4Config *      dst,
                       const NMIP4Config *src,
                       guint32            default_route_metric_penalty)
{
    NMIP4ConfigPrivate *        dst_priv;
    guint                       i;
    int                         idx;
    const NMPlatformIP4Address *a;
    const NMPlatformIP4Route *  r;
    NMDedupMultiIter            ipconf_iter;
    gboolean                    changed;
    gboolean                    changed_default_route;

    g_return_if_fail(src != NULL);
    g_return_if_fail(dst != NULL);

    dst_priv = NM_IP4_CONFIG_GET_PRIVATE(dst);

    g_object_freeze_notify(G_OBJECT(dst));

    /* addresses */
    changed = FALSE;
    nm_ip_config_iter_ip4_address_for_each (&ipconf_iter, src, &a) {
        if (nm_dedup_multi_index_remove_obj(dst_priv->multi_idx,
                                            &dst_priv->idx_ip4_addresses,
                                            NMP_OBJECT_UP_CAST(a),
                                            NULL))
            changed = TRUE;
    }
    if (changed)
        _notify_addresses(dst);

    /* nameservers */
    for (i = 0; i < nm_ip4_config_get_num_nameservers(src); i++) {
        idx = _nameservers_get_index(dst, nm_ip4_config_get_nameserver(src, i));
        if (idx >= 0)
            nm_ip4_config_del_nameserver(dst, idx);
    }

    /* routes */
    changed               = FALSE;
    changed_default_route = FALSE;
    nm_ip_config_iter_ip4_route_for_each (&ipconf_iter, src, &r) {
        const NMPObject *    o_src = NMP_OBJECT_UP_CAST(r);
        NMPObject            o_lookup_copy;
        const NMPObject *    o_lookup;
        nm_auto_nmpobj const NMPObject *obj_old = NULL;

        if (NM_PLATFORM_IP_ROUTE_IS_DEFAULT(r) && default_route_metric_penalty) {
            NMPlatformIP4Route *rr;

            /* the default route was penalized when merging it to the combined ip-config.
             * When subtracting the routes, we must re-do that process when comparing
             * the routes. */
            o_lookup = nmp_object_stackinit_obj(&o_lookup_copy, o_src);
            rr       = NMP_OBJECT_CAST_IP4_ROUTE(&o_lookup_copy);
            rr->metric =
                nm_utils_ip_route_metric_penalize(rr->metric, default_route_metric_penalty);
        } else
            o_lookup = o_src;

        if (nm_dedup_multi_index_remove_obj(dst_priv->multi_idx,
                                            &dst_priv->idx_ip4_routes,
                                            o_lookup,
                                            (gconstpointer *) &obj_old)) {
            if (dst_priv->best_default_route == obj_old) {
                nm_clear_nmp_object(&dst_priv->best_default_route);
                changed_default_route = TRUE;
            }
            changed = TRUE;
        }
    }
    if (changed_default_route) {
        nmp_object_ref_set(&dst_priv->best_default_route,
                           _nm_ip4_config_best_default_route_find(dst));
        _notify(dst, PROP_GATEWAY);
    }
    if (changed)
        _notify_routes(dst);

    /* domains */
    for (i = 0; i < nm_ip4_config_get_num_domains(src); i++) {
        idx = _domains_get_index(dst, nm_ip4_config_get_domain(src, i));
        if (idx >= 0)
            nm_ip4_config_del_domain(dst, idx);
    }

    /* dns searches */
    for (i = 0; i < nm_ip4_config_get_num_searches(src); i++) {
        idx = _searches_get_index(dst, nm_ip4_config_get_search(src, i));
        if (idx >= 0)
            nm_ip4_config_del_search(dst, idx);
    }

    /* dns options */
    for (i = 0; i < nm_ip4_config_get_num_dns_options(src); i++) {
        idx = _dns_options_get_index(dst, nm_ip4_config_get_dns_option(src, i));
        if (idx >= 0)
            nm_ip4_config_del_dns_option(dst, idx);
    }

    /* MTU */
    if (nm_ip4_config_get_mtu(src) == nm_ip4_config_get_mtu(dst)
        && nm_ip4_config_get_mtu_source(src) == nm_ip4_config_get_mtu_source(dst))
        nm_ip4_config_set_mtu(dst, 0, NM_IP_CONFIG_SOURCE_UNKNOWN);

    /* NIS */
    for (i = 0; i < nm_ip4_config_get_num_nis_servers(src); i++) {
        idx = _nis_servers_get_index(dst, nm_ip4_config_get_nis_server(src, i));
        if (idx >= 0)
            nm_ip4_config_del_nis_server(dst, idx);
    }

    if (g_strcmp0(nm_ip4_config_get_nis_domain(src), nm_ip4_config_get_nis_domain(dst)) == 0)
        nm_ip4_config_set_nis_domain(dst, NULL);

    /* WINS */
    for (i = 0; i < nm_ip4_config_get_num_wins(src); i++) {
        idx = _wins_get_index(dst, nm_ip4_config_get_wins(src, i));
        if (idx >= 0)
            nm_ip4_config_del_wins(dst, idx);
    }

    /* DNS priority */
    if (nm_ip4_config_get_dns_priority(src) == nm_ip4_config_get_dns_priority(dst))
        nm_ip4_config_set_dns_priority(dst, 0);

    /* mdns */
    if (nm_ip4_config_mdns_get(src) == nm_ip4_config_mdns_get(dst))
        nm_ip4_config_mdns_set(dst, NM_SETTING_CONNECTION_MDNS_DEFAULT);

    /* LLMNR */
    if (nm_ip4_config_llmnr_get(src) == nm_ip4_config_llmnr_get(dst))
        nm_ip4_config_llmnr_set(dst, NM_SETTING_CONNECTION_LLMNR_DEFAULT);

    g_object_thaw_notify(G_OBJECT(dst));
}

static gboolean
_nm_ip4_config_intersect_helper(NMIP4Config *      dst,
                                const NMIP4Config *src,
                                gboolean           intersect_addresses,
                                gboolean           intersect_routes,
                                guint32            default_route_metric_penalty,
                                gboolean           update_dst)
{
    NMIP4ConfigPrivate *        dst_priv;
    const NMIP4ConfigPrivate *  src_priv;
    NMDedupMultiIter            ipconf_iter;
    const NMPlatformIP4Address *a;
    const NMPlatformIP4Route *  r;
    const NMPObject *           new_best_default_route;
    gboolean                    changed, result = FALSE;

    g_return_val_if_fail(src, FALSE);
    g_return_val_if_fail(dst, FALSE);

    dst_priv = NM_IP4_CONFIG_GET_PRIVATE(dst);
    src_priv = NM_IP4_CONFIG_GET_PRIVATE(src);

    if (update_dst)
        g_object_freeze_notify(G_OBJECT(dst));

    /* addresses */
    if (intersect_addresses) {
        changed = FALSE;
        nm_ip_config_iter_ip4_address_for_each (&ipconf_iter, dst, &a) {
            if (nm_dedup_multi_index_lookup_obj(src_priv->multi_idx,
                                                &src_priv->idx_ip4_addresses,
                                                NMP_OBJECT_UP_CAST(a)))
                continue;

            if (!update_dst)
                return TRUE;

            if (nm_dedup_multi_index_remove_entry(dst_priv->multi_idx, ipconf_iter.current) != 1)
                nm_assert_not_reached();
            changed = TRUE;
        }
        if (changed) {
            _notify_addresses(dst);
            result = TRUE;
        }
    }

    /* ignore nameservers */

    /* routes */
    if (!intersect_routes)
        goto skip_routes;

    changed                = FALSE;
    new_best_default_route = NULL;
    nm_ip_config_iter_ip4_route_for_each (&ipconf_iter, dst, &r) {
        const NMPObject *o_dst = NMP_OBJECT_UP_CAST(r);
        const NMPObject *o_lookup;
        NMPObject        o_lookup_copy;

        if (NM_PLATFORM_IP_ROUTE_IS_DEFAULT(r) && default_route_metric_penalty) {
            NMPlatformIP4Route *rr;

            /* the default route was penalized when merging it to the combined ip-config.
             * When intersecting the routes, we must re-do that process when comparing
             * the routes. */
            o_lookup = nmp_object_stackinit_obj(&o_lookup_copy, o_dst);
            rr       = NMP_OBJECT_CAST_IP4_ROUTE(&o_lookup_copy);
            rr->metric =
                nm_utils_ip_route_metric_penalize(rr->metric, default_route_metric_penalty);
        } else
            o_lookup = o_dst;

        if (nm_dedup_multi_index_lookup_obj(src_priv->multi_idx,
                                            &src_priv->idx_ip4_routes,
                                            o_lookup)) {
            new_best_default_route =
                _nm_ip_config_best_default_route_find_better(new_best_default_route, o_dst);
            continue;
        }

        if (!update_dst)
            return TRUE;

        if (nm_dedup_multi_index_remove_entry(dst_priv->multi_idx, ipconf_iter.current) != 1)
            nm_assert_not_reached();
        changed = TRUE;
    }
    if (nmp_object_ref_set(&dst_priv->best_default_route, new_best_default_route)) {
        nm_assert(changed);
        _notify(dst, PROP_GATEWAY);
    }

    if (changed) {
        _notify_routes(dst);
        result = TRUE;
    }

skip_routes:
    /* ignore domains */
    /* ignore dns searches */
    /* ignore dns options */
    /* ignore NIS */
    /* ignore WINS */
    /* ignore mdns */
    /* ignore LLMNR */

    if (update_dst)
        g_object_thaw_notify(G_OBJECT(dst));
    return result;
}

/**
 * nm_ip4_config_intersect:
 * @dst: a configuration to be updated
 * @src: another configuration
 * @intersect_addresses: whether addresses should be intersected
 * @intersect_routes: whether routes should be intersected
 * @default_route_metric_penalty: the default route metric penalty
 *
 * Computes the intersection between @src and @dst and updates @dst in place
 * with the result.
 */
void
nm_ip4_config_intersect(NMIP4Config *      dst,
                        const NMIP4Config *src,
                        gboolean           intersect_addresses,
                        gboolean           intersect_routes,
                        guint32            default_route_metric_penalty)
{
    _nm_ip4_config_intersect_helper(dst,
                                    src,
                                    intersect_addresses,
                                    intersect_routes,
                                    default_route_metric_penalty,
                                    TRUE);
}

/**
 * nm_ip4_config_intersect_alloc:
 * @a: a configuration
 * @b: another configuration
 * @intersect_addresses: whether addresses should be intersected
 * @intersect_routes: whether routes should be intersected
 * @default_route_metric_penalty: the default route metric penalty
 *
 * Computes the intersection between @a and @b and returns the result in a newly
 * allocated configuration.  As a special case, if @a and @b are identical (with
 * respect to the only properties considered - addresses and routes) the
 * functions returns NULL so that one of existing configuration can be reused
 * without allocation.
 *
 * Returns: the intersection between @a and @b, or %NULL if the result is equal
 * to @a and @b.
 */
NMIP4Config *
nm_ip4_config_intersect_alloc(const NMIP4Config *a,
                              const NMIP4Config *b,
                              gboolean           intersect_addresses,
                              gboolean           intersect_routes,
                              guint32            default_route_metric_penalty)
{
    NMIP4Config *a_copy;

    if (_nm_ip4_config_intersect_helper((NMIP4Config *) a,
                                        b,
                                        intersect_addresses,
                                        intersect_routes,
                                        default_route_metric_penalty,
                                        FALSE)) {
        a_copy = nm_ip4_config_clone(a);
        _nm_ip4_config_intersect_helper(a_copy,
                                        b,
                                        intersect_addresses,
                                        intersect_routes,
                                        default_route_metric_penalty,
                                        TRUE);
        return a_copy;
    } else
        return NULL;
}

/**
 * nm_ip4_config_replace:
 * @dst: config to replace with @src content
 * @src: source config to copy
 * @relevant_changes: return whether there are changes to the
 * destination object that are relevant. This is equal to
 * nm_ip4_config_equal() showing any difference.
 *
 * Replaces everything in @dst with @src so that the two configurations
 * contain the same content -- with the exception of the dbus path.
 *
 * Returns: whether the @dst instance changed in any way (including minor changes,
 * that are not signaled by the output parameter @relevant_changes).
 */
gboolean
nm_ip4_config_replace(NMIP4Config *dst, const NMIP4Config *src, gboolean *relevant_changes)
{
#if NM_MORE_ASSERTS
    gboolean config_equal;
#endif
    gboolean                     has_minor_changes = FALSE, has_relevant_changes = FALSE, are_equal;
    guint                        i, num;
    NMIP4ConfigPrivate *         dst_priv;
    const NMIP4ConfigPrivate *   src_priv;
    NMDedupMultiIter             ipconf_iter_src, ipconf_iter_dst;
    const NMDedupMultiHeadEntry *head_entry_src;
    const NMPObject *            new_best_default_route;

    g_return_val_if_fail(src != NULL, FALSE);
    g_return_val_if_fail(dst != NULL, FALSE);
    g_return_val_if_fail(src != dst, FALSE);

#if NM_MORE_ASSERTS
    config_equal = nm_ip4_config_equal(dst, src);
#endif

    dst_priv = NM_IP4_CONFIG_GET_PRIVATE(dst);
    src_priv = NM_IP4_CONFIG_GET_PRIVATE(src);

    g_object_freeze_notify(G_OBJECT(dst));

    /* ifindex */
    if (src_priv->ifindex != dst_priv->ifindex) {
        dst_priv->ifindex = src_priv->ifindex;
        has_minor_changes = TRUE;
    }

    /* addresses */
    head_entry_src = nm_ip4_config_lookup_addresses(src);
    nm_dedup_multi_iter_init(&ipconf_iter_src, head_entry_src);
    nm_ip_config_iter_ip4_address_init(&ipconf_iter_dst, dst);
    are_equal = TRUE;
    while (TRUE) {
        gboolean                    has;
        const NMPlatformIP4Address *r_src = NULL;
        const NMPlatformIP4Address *r_dst = NULL;

        has = nm_platform_dedup_multi_iter_next_ip4_address(&ipconf_iter_src, &r_src);
        if (has != nm_platform_dedup_multi_iter_next_ip4_address(&ipconf_iter_dst, &r_dst)) {
            are_equal            = FALSE;
            has_relevant_changes = TRUE;
            break;
        }
        if (!has)
            break;

        if (nm_platform_ip4_address_cmp(r_src, r_dst) != 0) {
            are_equal = FALSE;
            if (r_src->address != r_dst->address || r_src->plen != r_dst->plen
                || r_src->peer_address != r_dst->peer_address) {
                has_relevant_changes = TRUE;
                break;
            }
        }
    }
    if (!are_equal) {
        has_minor_changes = TRUE;
        nm_dedup_multi_index_dirty_set_idx(dst_priv->multi_idx, &dst_priv->idx_ip4_addresses);
        nm_dedup_multi_iter_for_each (&ipconf_iter_src, head_entry_src) {
            _nm_ip_config_add_obj(dst_priv->multi_idx,
                                  &dst_priv->idx_ip4_addresses_,
                                  dst_priv->ifindex,
                                  ipconf_iter_src.current->obj,
                                  NULL,
                                  FALSE,
                                  TRUE,
                                  NULL,
                                  NULL);
        }
        nm_dedup_multi_index_dirty_remove_idx(dst_priv->multi_idx,
                                              &dst_priv->idx_ip4_addresses,
                                              FALSE);
        _notify_addresses(dst);
    }

    /* routes */
    head_entry_src = nm_ip4_config_lookup_routes(src);
    nm_dedup_multi_iter_init(&ipconf_iter_src, head_entry_src);
    nm_ip_config_iter_ip4_route_init(&ipconf_iter_dst, dst);
    are_equal = TRUE;
    while (TRUE) {
        gboolean                  has;
        const NMPlatformIP4Route *r_src = NULL;
        const NMPlatformIP4Route *r_dst = NULL;

        has = nm_platform_dedup_multi_iter_next_ip4_route(&ipconf_iter_src, &r_src);
        if (has != nm_platform_dedup_multi_iter_next_ip4_route(&ipconf_iter_dst, &r_dst)) {
            are_equal            = FALSE;
            has_relevant_changes = TRUE;
            break;
        }
        if (!has)
            break;

        if (nm_platform_ip4_route_cmp_full(r_src, r_dst) != 0) {
            are_equal = FALSE;
            if (r_src->plen != r_dst->plen
                || !nm_utils_ip4_address_same_prefix(r_src->network, r_dst->network, r_src->plen)
                || r_src->gateway != r_dst->gateway || r_src->metric != r_dst->metric) {
                has_relevant_changes = TRUE;
                break;
            }
        }
    }
    if (!are_equal) {
        has_minor_changes      = TRUE;
        new_best_default_route = NULL;
        nm_dedup_multi_index_dirty_set_idx(dst_priv->multi_idx, &dst_priv->idx_ip4_routes);
        nm_dedup_multi_iter_for_each (&ipconf_iter_src, head_entry_src) {
            const NMPObject *o = ipconf_iter_src.current->obj;
            const NMPObject *obj_new;

            _nm_ip_config_add_obj(dst_priv->multi_idx,
                                  &dst_priv->idx_ip4_routes_,
                                  dst_priv->ifindex,
                                  o,
                                  NULL,
                                  FALSE,
                                  TRUE,
                                  NULL,
                                  &obj_new);
            new_best_default_route =
                _nm_ip_config_best_default_route_find_better(new_best_default_route, obj_new);
        }
        nm_dedup_multi_index_dirty_remove_idx(dst_priv->multi_idx,
                                              &dst_priv->idx_ip4_routes,
                                              FALSE);
        if (nmp_object_ref_set(&dst_priv->best_default_route, new_best_default_route))
            _notify(dst, PROP_GATEWAY);
        _notify_routes(dst);
    }

    /* nameservers */
    num       = nm_ip4_config_get_num_nameservers(src);
    are_equal = num == nm_ip4_config_get_num_nameservers(dst);
    if (are_equal) {
        for (i = 0; i < num; i++) {
            if (nm_ip4_config_get_nameserver(src, i) != nm_ip4_config_get_nameserver(dst, i)) {
                are_equal = FALSE;
                break;
            }
        }
    }
    if (!are_equal) {
        nm_ip4_config_reset_nameservers(dst);
        for (i = 0; i < num; i++)
            nm_ip4_config_add_nameserver(dst, nm_ip4_config_get_nameserver(src, i));
        has_relevant_changes = TRUE;
    }

    /* domains */
    num       = nm_ip4_config_get_num_domains(src);
    are_equal = num == nm_ip4_config_get_num_domains(dst);
    if (are_equal) {
        for (i = 0; i < num; i++) {
            if (g_strcmp0(nm_ip4_config_get_domain(src, i), nm_ip4_config_get_domain(dst, i))) {
                are_equal = FALSE;
                break;
            }
        }
    }
    if (!are_equal) {
        nm_ip4_config_reset_domains(dst);
        for (i = 0; i < num; i++)
            nm_ip4_config_add_domain(dst, nm_ip4_config_get_domain(src, i));
        has_relevant_changes = TRUE;
    }

    /* dns searches */
    num       = nm_ip4_config_get_num_searches(src);
    are_equal = num == nm_ip4_config_get_num_searches(dst);
    if (are_equal) {
        for (i = 0; i < num; i++) {
            if (g_strcmp0(nm_ip4_config_get_search(src, i), nm_ip4_config_get_search(dst, i))) {
                are_equal = FALSE;
                break;
            }
        }
    }
    if (!are_equal) {
        nm_ip4_config_reset_searches(dst);
        for (i = 0; i < num; i++)
            nm_ip4_config_add_search(dst, nm_ip4_config_get_search(src, i));
        has_relevant_changes = TRUE;
    }

    /* dns options */
    num       = nm_ip4_config_get_num_dns_options(src);
    are_equal = num == nm_ip4_config_get_num_dns_options(dst);
    if (are_equal) {
        for (i = 0; i < num; i++) {
            if (g_strcmp0(nm_ip4_config_get_dns_option(src, i),
                          nm_ip4_config_get_dns_option(dst, i))) {
                are_equal = FALSE;
                break;
            }
        }
    }
    if (!are_equal) {
        nm_ip4_config_reset_dns_options(dst);
        for (i = 0; i < num; i++)
            nm_ip4_config_add_dns_option(dst, nm_ip4_config_get_dns_option(src, i));
        has_relevant_changes = TRUE;
    }

    if (src_priv->mdns != dst_priv->mdns) {
        dst_priv->mdns       = src_priv->mdns;
        has_relevant_changes = TRUE;
    }

    if (src_priv->llmnr != dst_priv->llmnr) {
        dst_priv->llmnr      = src_priv->llmnr;
        has_relevant_changes = TRUE;
    }

    /* DNS priority */
    if (src_priv->dns_priority != dst_priv->dns_priority) {
        nm_ip4_config_set_dns_priority(dst, src_priv->dns_priority);
        has_minor_changes = TRUE;
    }

    /* nis */
    num       = nm_ip4_config_get_num_nis_servers(src);
    are_equal = num == nm_ip4_config_get_num_nis_servers(dst);
    if (are_equal) {
        for (i = 0; i < num; i++) {
            if (nm_ip4_config_get_nis_server(src, i) != nm_ip4_config_get_nis_server(dst, i)) {
                are_equal = FALSE;
                break;
            }
        }
    }
    if (!are_equal) {
        nm_ip4_config_reset_nis_servers(dst);
        for (i = 0; i < num; i++)
            nm_ip4_config_add_nis_server(dst, nm_ip4_config_get_nis_server(src, i));
        has_relevant_changes = TRUE;
    }

    /* nis_domain */
    if (g_strcmp0(src_priv->nis_domain, dst_priv->nis_domain)) {
        nm_ip4_config_set_nis_domain(dst, src_priv->nis_domain);
        has_relevant_changes = TRUE;
    }

    /* wins */
    num       = nm_ip4_config_get_num_wins(src);
    are_equal = num == nm_ip4_config_get_num_wins(dst);
    if (are_equal) {
        for (i = 0; i < num; i++) {
            if (nm_ip4_config_get_wins(src, i) != nm_ip4_config_get_wins(dst, i)) {
                are_equal = FALSE;
                break;
            }
        }
    }
    if (!are_equal) {
        nm_ip4_config_reset_wins(dst);
        for (i = 0; i < num; i++)
            nm_ip4_config_add_wins(dst, nm_ip4_config_get_wins(src, i));
        has_relevant_changes = TRUE;
    }

    /* mtu */
    if (src_priv->mtu != dst_priv->mtu || src_priv->mtu_source != dst_priv->mtu_source) {
        nm_ip4_config_set_mtu(dst, src_priv->mtu, src_priv->mtu_source);
        has_minor_changes = TRUE;
    }

    /* metered */
    if (src_priv->metered != dst_priv->metered) {
        dst_priv->metered = src_priv->metered;
        has_minor_changes = TRUE;
    }

    /* never default */
    if (src_priv->never_default != dst_priv->never_default) {
        dst_priv->never_default = src_priv->never_default;
        has_minor_changes       = TRUE;
    }

#if NM_MORE_ASSERTS
    /* config_equal does not compare *all* the fields, therefore, we might have has_minor_changes
     * regardless of config_equal. But config_equal must correspond to has_relevant_changes. */
    nm_assert(config_equal == !has_relevant_changes);
#endif

    g_object_thaw_notify(G_OBJECT(dst));

    if (relevant_changes)
        *relevant_changes = has_relevant_changes;

    return has_relevant_changes || has_minor_changes;
}

void
nm_ip_config_dump(const NMIPConfig *self, const char *detail, NMLogLevel level, NMLogDomain domain)
{
    NMDedupMultiIter            ipconf_iter;
    const NMPlatformIP4Address *addr4;
    const NMPlatformIP6Address *addr6;
    const NMPlatformIP4Route *  route4;
    const NMPlatformIP6Route *  route6;
    const NMIP4Config *         ip4;
    const NMIP6Config *         ip6;
    int                         addr_family      = AF_UNSPEC;
    char                        addr_family_char = '?';
    const char *                path;
    gconstpointer               ptr;
    guint                       i;

    if (self) {
        addr_family      = nm_ip_config_get_addr_family(self);
        addr_family_char = nm_utils_addr_family_to_char(addr_family);
    }

    nm_log(level, domain, NULL, NULL, "---- NMIP%cConfig %p (%s)", addr_family_char, self, detail);

    if (!self)
        return;

    path = nm_dbus_object_get_path(NM_DBUS_OBJECT(self));
    if (path)
        nm_log(level, domain, NULL, NULL, " path     : %s", path);

    if (addr_family == AF_INET) {
        ip4 = NM_IP4_CONFIG(self);
        nm_ip_config_iter_ip4_address_for_each (&ipconf_iter, ip4, &addr4) {
            nm_log(level,
                   domain,
                   NULL,
                   NULL,
                   " address  : %s",
                   nm_platform_ip4_address_to_string(addr4, NULL, 0));
        }
        nm_ip_config_iter_ip4_route_for_each (&ipconf_iter, ip4, &route4) {
            nm_log(level,
                   domain,
                   NULL,
                   NULL,
                   " route    : %s",
                   nm_platform_ip4_route_to_string(route4, NULL, 0));
        }
    } else {
        ip6 = NM_IP6_CONFIG(self);
        nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, ip6, &addr6) {
            nm_log(level,
                   domain,
                   NULL,
                   NULL,
                   " address  : %s",
                   nm_platform_ip6_address_to_string(addr6, NULL, 0));
        }
        nm_ip_config_iter_ip6_route_for_each (&ipconf_iter, ip6, &route6) {
            nm_log(level,
                   domain,
                   NULL,
                   NULL,
                   " route    : %s",
                   nm_platform_ip6_route_to_string(route6, NULL, 0));
        }
    }

    for (i = 0; i < nm_ip_config_get_num_nameservers(self); i++) {
        char buf[NM_UTILS_INET_ADDRSTRLEN];

        ptr = nm_ip_config_get_nameserver(self, i);
        nm_log(level,
               domain,
               NULL,
               NULL,
               " dns      : %s",
               nm_utils_inet_ntop(addr_family, ptr, buf));
    }

    for (i = 0; i < nm_ip_config_get_num_domains(self); i++)
        nm_log(level, domain, NULL, NULL, " domain   : %s", nm_ip_config_get_domain(self, i));

    for (i = 0; i < nm_ip_config_get_num_searches(self); i++)
        nm_log(level, domain, NULL, NULL, " search   : %s", nm_ip_config_get_search(self, i));

    for (i = 0; i < nm_ip_config_get_num_dns_options(self); i++)
        nm_log(level, domain, NULL, NULL, "dns-option: %s", nm_ip_config_get_dns_option(self, i));

    nm_log(level, domain, NULL, NULL, " dns-prio : %d", nm_ip_config_get_dns_priority(self));

    if (addr_family == AF_INET) {
        ip4 = NM_IP4_CONFIG(self);
        nm_log(level,
               domain,
               NULL,
               NULL,
               " mtu      : %" G_GUINT32_FORMAT " (source: %d)",
               nm_ip4_config_get_mtu(ip4),
               (int) nm_ip4_config_get_mtu_source(ip4));
        nm_log(level, domain, NULL, NULL, " metered  : %d", (int) nm_ip4_config_get_metered(ip4));
    }
}

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

void
nm_ip4_config_reset_addresses(NMIP4Config *self)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    if (nm_dedup_multi_index_remove_idx(priv->multi_idx, &priv->idx_ip4_addresses) > 0)
        _notify_addresses(self);
}

static void
_add_address(NMIP4Config *self, const NMPObject *obj_new, const NMPlatformIP4Address *new)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    if (_nm_ip_config_add_obj(priv->multi_idx,
                              &priv->idx_ip4_addresses_,
                              priv->ifindex,
                              obj_new,
                              (const NMPlatformObject *) new,
                              TRUE,
                              FALSE,
                              NULL,
                              NULL))
        _notify_addresses(self);
}

/**
 * nm_ip4_config_add_address:
 * @self: the #NMIP4Config
 * @new: the new address to add to @self
 *
 * Adds the new address to @self.  If an address with the same basic properties
 * (address, prefix) already exists in @self, it is overwritten with the
 * lifetime and preferred of @new.  The source is also overwritten by the source
 * from @new if that source is higher priority.
 */
void
nm_ip4_config_add_address(NMIP4Config *self, const NMPlatformIP4Address *new)
{
    g_return_if_fail(self);
    g_return_if_fail(new);
    g_return_if_fail(new->plen <= 32);
    g_return_if_fail(NM_IP4_CONFIG_GET_PRIVATE(self)->ifindex > 0);

    _add_address(self, NULL, new);
}

void
_nmtst_ip4_config_del_address(NMIP4Config *self, guint i)
{
    const NMPlatformIP4Address *a;

    a = _nmtst_ip4_config_get_address(self, i);
    if (!nm_ip4_config_nmpobj_remove(self, NMP_OBJECT_UP_CAST(a)))
        g_assert_not_reached();
}

guint
nm_ip4_config_get_num_addresses(const NMIP4Config *self)
{
    const NMDedupMultiHeadEntry *head_entry;

    head_entry = nm_ip4_config_lookup_addresses(self);
    return head_entry ? head_entry->len : 0;
}

const NMPlatformIP4Address *
nm_ip4_config_get_first_address(const NMIP4Config *self)
{
    NMDedupMultiIter            iter;
    const NMPlatformIP4Address *a = NULL;

    nm_ip_config_iter_ip4_address_for_each (&iter, self, &a)
        return a;
    return NULL;
}

const NMPlatformIP4Address *
_nmtst_ip4_config_get_address(const NMIP4Config *self, guint i)
{
    NMDedupMultiIter            iter = {};
    const NMPlatformIP4Address *a    = NULL;
    guint                       j;

    j = 0;
    nm_ip_config_iter_ip4_address_for_each (&iter, self, &a) {
        if (i == j)
            return a;
        j++;
    }
    g_return_val_if_reached(NULL);
}

gboolean
nm_ip4_config_address_exists(const NMIP4Config *self, const NMPlatformIP4Address *needle)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);
    NMPObject                 obj_stack;

    nmp_object_stackinit_id_ip4_address(&obj_stack,
                                        priv->ifindex,
                                        needle->address,
                                        needle->plen,
                                        needle->peer_address);
    return !!nm_dedup_multi_index_lookup_obj(priv->multi_idx, &priv->idx_ip4_addresses, &obj_stack);
}

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

static const NMDedupMultiEntry *
_lookup_route(const NMIP4Config *self, const NMPObject *needle, NMPlatformIPRouteCmpType cmp_type)
{
    const NMIP4ConfigPrivate *priv;

    nm_assert(NM_IS_IP4_CONFIG(self));
    nm_assert(NMP_OBJECT_GET_TYPE(needle) == NMP_OBJECT_TYPE_IP4_ROUTE);

    priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return _nm_ip_config_lookup_ip_route(priv->multi_idx, &priv->idx_ip4_routes_, needle, cmp_type);
}

void
nm_ip4_config_reset_routes(NMIP4Config *self)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    if (nm_dedup_multi_index_remove_idx(priv->multi_idx, &priv->idx_ip4_routes) > 0) {
        if (nm_clear_nmp_object(&priv->best_default_route))
            _notify(self, PROP_GATEWAY);
        _notify_routes(self);
    }
}

static void
_add_route(NMIP4Config *    self,
           const NMPObject *obj_new,
           const NMPlatformIP4Route *new,
           const NMPObject **out_obj_new)
{
    NMIP4ConfigPrivate * priv               = NM_IP4_CONFIG_GET_PRIVATE(self);
    nm_auto_nmpobj const NMPObject *obj_old = NULL;
    const NMPObject *               obj_new_2;

    nm_assert((!new) != (!obj_new));
    nm_assert(!new || _route_valid(new));
    nm_assert(!obj_new || _route_valid(NMP_OBJECT_CAST_IP4_ROUTE(obj_new)));

    if (_nm_ip_config_add_obj(priv->multi_idx,
                              &priv->idx_ip4_routes_,
                              priv->ifindex,
                              obj_new,
                              (const NMPlatformObject *) new,
                              TRUE,
                              FALSE,
                              &obj_old,
                              &obj_new_2)) {
        gboolean changed_default_route = FALSE;

        if (priv->best_default_route == obj_old && obj_old != obj_new_2) {
            changed_default_route = TRUE;
            nm_clear_nmp_object(&priv->best_default_route);
        }
        NM_SET_OUT(out_obj_new, nmp_object_ref(obj_new_2));
        if (_nm_ip_config_best_default_route_merge(&priv->best_default_route, obj_new_2))
            changed_default_route = TRUE;

        if (changed_default_route)
            _notify(self, PROP_GATEWAY);
        _notify_routes(self);
    } else
        NM_SET_OUT(out_obj_new, nmp_object_ref(obj_new_2));
}

/**
 * nm_ip4_config_add_route:
 * @self: the #NMIP4Config
 * @new: the new route to add to @self
 * @out_obj_new: (allow-none) (out): the added route object. Must be unrefed
 *   by caller.
 *
 * Adds the new route to @self.  If a route with the same basic properties
 * (network, prefix) already exists in @self, it is overwritten including the
 * gateway and metric of @new.  The source is also overwritten by the source
 * from @new if that source is higher priority.
 */
void
nm_ip4_config_add_route(NMIP4Config *self,
                        const NMPlatformIP4Route *new,
                        const NMPObject **out_obj_new)
{
    g_return_if_fail(self);
    g_return_if_fail(new);
    g_return_if_fail(new->plen <= 32);
    g_return_if_fail(NM_IP4_CONFIG_GET_PRIVATE(self)->ifindex > 0);

    _add_route(self, NULL, new, out_obj_new);
}

void
_nmtst_ip4_config_del_route(NMIP4Config *self, guint i)
{
    const NMPlatformIP4Route *r;

    r = _nmtst_ip4_config_get_route(self, i);
    if (!nm_ip4_config_nmpobj_remove(self, NMP_OBJECT_UP_CAST(r)))
        g_assert_not_reached();
}

guint
nm_ip4_config_get_num_routes(const NMIP4Config *self)
{
    const NMDedupMultiHeadEntry *head_entry;

    head_entry = nm_ip4_config_lookup_routes(self);
    nm_assert(!head_entry || head_entry->len == c_list_length(&head_entry->lst_entries_head));
    return head_entry ? head_entry->len : 0;
}

const NMPlatformIP4Route *
_nmtst_ip4_config_get_route(const NMIP4Config *self, guint i)
{
    NMDedupMultiIter          iter;
    const NMPlatformIP4Route *r = NULL;
    guint                     j;

    j = 0;
    nm_ip_config_iter_ip4_route_for_each (&iter, self, &r) {
        if (i == j)
            return r;
        j++;
    }
    g_return_val_if_reached(NULL);
}

const NMPlatformIP4Route *
nm_ip4_config_get_direct_route_for_host(const NMIP4Config *self,
                                        in_addr_t          host,
                                        guint32            route_table)
{
    const NMPlatformIP4Route *best_route = NULL;
    const NMPlatformIP4Route *item;
    NMDedupMultiIter          ipconf_iter;

    g_return_val_if_fail(host, NULL);

    nm_ip_config_iter_ip4_route_for_each (&ipconf_iter, self, &item) {
        if (item->gateway != 0)
            continue;

        if (best_route && best_route->plen > item->plen)
            continue;

        if (nm_platform_route_table_uncoerce(item->table_coerced, TRUE) != route_table)
            continue;

        if (nm_utils_ip4_address_clear_host_address(host, item->plen)
            != nm_utils_ip4_address_clear_host_address(item->network, item->plen))
            continue;

        if (best_route && best_route->metric <= item->metric)
            continue;

        best_route = item;
    }
    return best_route;
}

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

void
nm_ip4_config_reset_nameservers(NMIP4Config *self)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    if (priv->nameservers->len != 0) {
        g_array_set_size(priv->nameservers, 0);
        nm_gobject_notify_together(self, PROP_NAMESERVER_DATA, PROP_NAMESERVERS);
    }
}

void
nm_ip4_config_add_nameserver(NMIP4Config *self, guint32 new)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);
    int                 i;

    g_return_if_fail(new != 0);

    for (i = 0; i < priv->nameservers->len; i++)
        if (new == g_array_index(priv->nameservers, guint32, i))
            return;

    g_array_append_val(priv->nameservers, new);
    nm_gobject_notify_together(self, PROP_NAMESERVER_DATA, PROP_NAMESERVERS);
}

void
nm_ip4_config_del_nameserver(NMIP4Config *self, guint i)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    g_return_if_fail(i < priv->nameservers->len);

    g_array_remove_index(priv->nameservers, i);
    nm_gobject_notify_together(self, PROP_NAMESERVER_DATA, PROP_NAMESERVERS);
}

guint
nm_ip4_config_get_num_nameservers(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return priv->nameservers->len;
}

guint32
nm_ip4_config_get_nameserver(const NMIP4Config *self, guint i)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return g_array_index(priv->nameservers, guint32, i);
}

const in_addr_t *
_nm_ip4_config_get_nameserver(const NMIP4Config *self, guint i)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return &g_array_index(priv->nameservers, guint32, i);
}

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

gboolean
_nm_ip_config_check_and_add_domain(GPtrArray *array, const char *domain)
{
    char * copy = NULL;
    size_t len;

    g_return_val_if_fail(domain, FALSE);
    g_return_val_if_fail(domain[0] != '\0', FALSE);

    if (domain[0] == '.' || strstr(domain, ".."))
        return FALSE;

    len = strlen(domain);
    if (domain[len - 1] == '.')
        domain = copy = g_strndup(domain, len - 1);

    if (nm_utils_strv_find_first((char **) array->pdata, array->len, domain) >= 0) {
        g_free(copy);
        return FALSE;
    }

    g_ptr_array_add(array, copy ?: g_strdup(domain));
    return TRUE;
}

void
nm_ip4_config_reset_domains(NMIP4Config *self)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    if (priv->domains->len != 0) {
        g_ptr_array_set_size(priv->domains, 0);
        _notify(self, PROP_DOMAINS);
    }
}

void
nm_ip4_config_add_domain(NMIP4Config *self, const char *domain)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    if (_nm_ip_config_check_and_add_domain(priv->domains, domain))
        _notify(self, PROP_DOMAINS);
}

void
nm_ip4_config_del_domain(NMIP4Config *self, guint i)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    g_return_if_fail(i < priv->domains->len);

    g_ptr_array_remove_index(priv->domains, i);
    _notify(self, PROP_DOMAINS);
}

guint
nm_ip4_config_get_num_domains(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return priv->domains->len;
}

const char *
nm_ip4_config_get_domain(const NMIP4Config *self, guint i)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return g_ptr_array_index(priv->domains, i);
}

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

void
nm_ip4_config_reset_searches(NMIP4Config *self)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    if (priv->searches->len != 0) {
        g_ptr_array_set_size(priv->searches, 0);
        _notify(self, PROP_SEARCHES);
    }
}

void
nm_ip4_config_add_search(NMIP4Config *self, const char *search)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    if (_nm_ip_config_check_and_add_domain(priv->searches, search))
        _notify(self, PROP_SEARCHES);
}

void
nm_ip4_config_del_search(NMIP4Config *self, guint i)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    g_return_if_fail(i < priv->searches->len);

    g_ptr_array_remove_index(priv->searches, i);
    _notify(self, PROP_SEARCHES);
}

guint
nm_ip4_config_get_num_searches(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return priv->searches->len;
}

const char *
nm_ip4_config_get_search(const NMIP4Config *self, guint i)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return g_ptr_array_index(priv->searches, i);
}

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

void
nm_ip4_config_reset_dns_options(NMIP4Config *self)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    if (priv->dns_options->len != 0) {
        g_ptr_array_set_size(priv->dns_options, 0);
        _notify(self, PROP_DNS_OPTIONS);
    }
}

void
nm_ip4_config_add_dns_option(NMIP4Config *self, const char *new)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);
    int                 i;

    g_return_if_fail(new != NULL);
    g_return_if_fail(new[0] != '\0');

    for (i = 0; i < priv->dns_options->len; i++)
        if (!g_strcmp0(g_ptr_array_index(priv->dns_options, i), new))
            return;

    g_ptr_array_add(priv->dns_options, g_strdup(new));
    _notify(self, PROP_DNS_OPTIONS);
}

void
nm_ip4_config_del_dns_option(NMIP4Config *self, guint i)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    g_return_if_fail(i < priv->dns_options->len);

    g_ptr_array_remove_index(priv->dns_options, i);
    _notify(self, PROP_DNS_OPTIONS);
}

guint
nm_ip4_config_get_num_dns_options(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return priv->dns_options->len;
}

const char *
nm_ip4_config_get_dns_option(const NMIP4Config *self, guint i)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return g_ptr_array_index(priv->dns_options, i);
}

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

NMSettingConnectionMdns
nm_ip4_config_mdns_get(const NMIP4Config *self)
{
    return NM_IP4_CONFIG_GET_PRIVATE(self)->mdns;
}

void
nm_ip4_config_mdns_set(NMIP4Config *self, NMSettingConnectionMdns mdns)
{
    NM_IP4_CONFIG_GET_PRIVATE(self)->mdns = mdns;
}

NMSettingConnectionLlmnr
nm_ip4_config_llmnr_get(const NMIP4Config *self)
{
    return NM_IP4_CONFIG_GET_PRIVATE(self)->llmnr;
}

void
nm_ip4_config_llmnr_set(NMIP4Config *self, NMSettingConnectionLlmnr llmnr)
{
    NM_IP4_CONFIG_GET_PRIVATE(self)->llmnr = llmnr;
}

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

NMIPConfigFlags
nm_ip4_config_get_config_flags(const NMIP4Config *self)
{
    return NM_IP4_CONFIG_GET_PRIVATE(self)->config_flags;
}

void
nm_ip4_config_set_config_flags(NMIP4Config *self, NMIPConfigFlags flags, NMIPConfigFlags mask)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    if (mask == 0) {
        /* for convenience, accept 0 mask to set any flags. */
        mask = flags;
    }

    nm_assert(!NM_FLAGS_ANY(flags, ~mask));
    priv->config_flags = (flags & mask) | (priv->config_flags & ~mask);
}

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

void
nm_ip4_config_set_dns_priority(NMIP4Config *self, int priority)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    if (priority != priv->dns_priority) {
        priv->dns_priority = priority;
        _notify(self, PROP_DNS_PRIORITY);
    }
}

int
nm_ip4_config_get_dns_priority(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return priv->dns_priority;
}

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

void
nm_ip4_config_reset_nis_servers(NMIP4Config *self)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    g_array_set_size(priv->nis, 0);
}

void
nm_ip4_config_add_nis_server(NMIP4Config *self, guint32 nis)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);
    int                 i;

    for (i = 0; i < priv->nis->len; i++)
        if (nis == g_array_index(priv->nis, guint32, i))
            return;

    g_array_append_val(priv->nis, nis);
}

void
nm_ip4_config_del_nis_server(NMIP4Config *self, guint i)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    g_return_if_fail(i < priv->nis->len);

    g_array_remove_index(priv->nis, i);
}

guint
nm_ip4_config_get_num_nis_servers(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return priv->nis->len;
}

guint32
nm_ip4_config_get_nis_server(const NMIP4Config *self, guint i)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return g_array_index(priv->nis, guint32, i);
}

void
nm_ip4_config_set_nis_domain(NMIP4Config *self, const char *domain)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    g_free(priv->nis_domain);
    priv->nis_domain = g_strdup(domain);
}

const char *
nm_ip4_config_get_nis_domain(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return priv->nis_domain;
}

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

void
nm_ip4_config_reset_wins(NMIP4Config *self)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    if (priv->wins->len != 0) {
        g_array_set_size(priv->wins, 0);
        nm_gobject_notify_together(self, PROP_WINS_SERVER_DATA, PROP_WINS_SERVERS);
    }
}

void
nm_ip4_config_add_wins(NMIP4Config *self, guint32 wins)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);
    int                 i;

    g_return_if_fail(wins != 0);

    for (i = 0; i < priv->wins->len; i++)
        if (wins == g_array_index(priv->wins, guint32, i))
            return;

    g_array_append_val(priv->wins, wins);
    nm_gobject_notify_together(self, PROP_WINS_SERVER_DATA, PROP_WINS_SERVERS);
}

void
nm_ip4_config_del_wins(NMIP4Config *self, guint i)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    g_return_if_fail(i < priv->wins->len);

    g_array_remove_index(priv->wins, i);
    nm_gobject_notify_together(self, PROP_WINS_SERVER_DATA, PROP_WINS_SERVERS);
}

guint
nm_ip4_config_get_num_wins(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return priv->wins->len;
}

guint32
nm_ip4_config_get_wins(const NMIP4Config *self, guint i)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return g_array_index(priv->wins, guint32, i);
}

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

void
nm_ip4_config_set_mtu(NMIP4Config *self, guint32 mtu, NMIPConfigSource source)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    if (!mtu)
        source = NM_IP_CONFIG_SOURCE_UNKNOWN;

    priv->mtu        = mtu;
    priv->mtu_source = source;
}

guint32
nm_ip4_config_get_mtu(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return priv->mtu;
}

NMIPConfigSource
nm_ip4_config_get_mtu_source(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return priv->mtu_source;
}

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

void
nm_ip4_config_set_metered(NMIP4Config *self, gboolean metered)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    priv->metered = metered;
}

gboolean
nm_ip4_config_get_metered(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return priv->metered;
}

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

void
nm_ip4_config_set_never_default(NMIP4Config *self, gboolean never_default)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    priv->never_default = never_default;
}

gboolean
nm_ip4_config_get_never_default(const NMIP4Config *self)
{
    const NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    return priv->never_default;
}

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

const NMPObject *
nm_ip4_config_nmpobj_lookup(const NMIP4Config *self, const NMPObject *needle)
{
    const NMIP4ConfigPrivate * priv;
    const NMDedupMultiIdxType *idx_type;

    g_return_val_if_fail(NM_IS_IP4_CONFIG(self), NULL);

    priv = NM_IP4_CONFIG_GET_PRIVATE(self);
    switch (NMP_OBJECT_GET_TYPE(needle)) {
    case NMP_OBJECT_TYPE_IP4_ADDRESS:
        idx_type = &priv->idx_ip4_addresses;
        break;
    case NMP_OBJECT_TYPE_IP4_ROUTE:
        idx_type = &priv->idx_ip4_routes;
        break;
    default:
        g_return_val_if_reached(NULL);
    }

    return nm_dedup_multi_entry_get_obj(
        nm_dedup_multi_index_lookup_obj(priv->multi_idx, idx_type, needle));
}

gboolean
nm_ip4_config_nmpobj_remove(NMIP4Config *self, const NMPObject *needle)
{
    NMIP4ConfigPrivate * priv;
    NMDedupMultiIdxType *idx_type;
    nm_auto_nmpobj const NMPObject *obj_old = NULL;
    guint                           n;

    g_return_val_if_fail(NM_IS_IP4_CONFIG(self), FALSE);

    priv = NM_IP4_CONFIG_GET_PRIVATE(self);
    switch (NMP_OBJECT_GET_TYPE(needle)) {
    case NMP_OBJECT_TYPE_IP4_ADDRESS:
        idx_type = &priv->idx_ip4_addresses;
        break;
    case NMP_OBJECT_TYPE_IP4_ROUTE:
        idx_type = &priv->idx_ip4_routes;
        break;
    default:
        g_return_val_if_reached(FALSE);
    }

    n = nm_dedup_multi_index_remove_obj(priv->multi_idx,
                                        idx_type,
                                        needle,
                                        (gconstpointer *) &obj_old);
    if (n != 1) {
        nm_assert(n == 0);
        return FALSE;
    }

    nm_assert(NMP_OBJECT_GET_TYPE(obj_old) == NMP_OBJECT_GET_TYPE(needle));

    switch (NMP_OBJECT_GET_TYPE(obj_old)) {
    case NMP_OBJECT_TYPE_IP4_ADDRESS:
        _notify_addresses(self);
        break;
    case NMP_OBJECT_TYPE_IP4_ROUTE:
        if (priv->best_default_route == obj_old) {
            if (nmp_object_ref_set(&priv->best_default_route,
                                   _nm_ip4_config_best_default_route_find(self)))
                _notify(self, PROP_GATEWAY);
        }
        _notify_routes(self);
        break;
    default:
        nm_assert_not_reached();
    }
    return TRUE;
}

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

static void
hash_u32(GChecksum *sum, guint32 n)
{
    g_checksum_update(sum, (const guint8 *) &n, sizeof(n));
}

void
nm_ip4_config_hash(const NMIP4Config *self, GChecksum *sum, gboolean dns_only)
{
    guint                       i;
    const char *                s;
    NMDedupMultiIter            ipconf_iter;
    const NMPlatformIP4Address *address;
    const NMPlatformIP4Route *  route;
    int                         val;

    g_return_if_fail(self);
    g_return_if_fail(sum);

    if (!dns_only) {
        nm_ip_config_iter_ip4_address_for_each (&ipconf_iter, self, &address) {
            hash_u32(sum, address->address);
            hash_u32(sum, address->plen);
            hash_u32(sum, address->peer_address & _nm_utils_ip4_prefix_to_netmask(address->plen));
        }

        nm_ip_config_iter_ip4_route_for_each (&ipconf_iter, self, &route) {
            hash_u32(sum, route->network);
            hash_u32(sum, route->plen);
            hash_u32(sum, route->gateway);
            hash_u32(sum, route->metric);
        }

        for (i = 0; i < nm_ip4_config_get_num_nis_servers(self); i++)
            hash_u32(sum, nm_ip4_config_get_nis_server(self, i));

        s = nm_ip4_config_get_nis_domain(self);
        if (s)
            g_checksum_update(sum, (const guint8 *) s, strlen(s));
    }

    for (i = 0; i < nm_ip4_config_get_num_nameservers(self); i++)
        hash_u32(sum, nm_ip4_config_get_nameserver(self, i));

    for (i = 0; i < nm_ip4_config_get_num_wins(self); i++)
        hash_u32(sum, nm_ip4_config_get_wins(self, i));

    for (i = 0; i < nm_ip4_config_get_num_domains(self); i++) {
        s = nm_ip4_config_get_domain(self, i);
        g_checksum_update(sum, (const guint8 *) s, strlen(s));
    }

    for (i = 0; i < nm_ip4_config_get_num_searches(self); i++) {
        s = nm_ip4_config_get_search(self, i);
        g_checksum_update(sum, (const guint8 *) s, strlen(s));
    }

    for (i = 0; i < nm_ip4_config_get_num_dns_options(self); i++) {
        s = nm_ip4_config_get_dns_option(self, i);
        g_checksum_update(sum, (const guint8 *) s, strlen(s));
    }

    val = nm_ip4_config_mdns_get(self);
    if (val != NM_SETTING_CONNECTION_MDNS_DEFAULT)
        g_checksum_update(sum, (const guint8 *) &val, sizeof(val));

    val = nm_ip4_config_llmnr_get(self);
    if (val != NM_SETTING_CONNECTION_LLMNR_DEFAULT)
        g_checksum_update(sum, (const guint8 *) &val, sizeof(val));

    /* FIXME(ip-config-checksum): the DNS priority should be considered relevant
     * and added into the checksum as well, but this can't be done right now
     * because in the DNS manager we rely on the fact that an empty
     * configuration (i.e. just created) has a zero checksum. This is needed to
     * avoid rewriting resolv.conf when there is no change.
     *
     * The DNS priority initial value depends on the connection type (VPN or
     * not), so it's a bit difficult to add it to checksum maintaining the
     * assumption of checksum(empty)=0
     */
}

/**
 * nm_ip4_config_equal:
 * @a: first config to compare
 * @b: second config to compare
 *
 * Compares two #NMIP4Configs for basic equality.  This means that all
 * attributes must exist in the same order in both configs (addresses, routes,
 * domains, DNS servers, etc) but some attributes (address lifetimes, and address
 * and route sources) are ignored.
 *
 * Returns: %TRUE if the configurations are basically equal to each other,
 * %FALSE if not
 */
gboolean
nm_ip4_config_equal(const NMIP4Config *a, const NMIP4Config *b)
{
    nm_auto_free_checksum GChecksum *a_checksum = g_checksum_new(G_CHECKSUM_SHA1);
    nm_auto_free_checksum GChecksum *b_checksum = g_checksum_new(G_CHECKSUM_SHA1);
    guint8                           a_data[NM_UTILS_CHECKSUM_LENGTH_SHA1];
    guint8                           b_data[NM_UTILS_CHECKSUM_LENGTH_SHA1];

    if (a)
        nm_ip4_config_hash(a, a_checksum, FALSE);
    if (b)
        nm_ip4_config_hash(b, b_checksum, FALSE);

    nm_utils_checksum_get_digest(a_checksum, a_data);
    nm_utils_checksum_get_digest(b_checksum, b_data);
    return !memcmp(a_data, b_data, sizeof(a_data));
}

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

static void
get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    NMIP4Config *       self = NM_IP4_CONFIG(object);
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);
    char                addr_str[NM_UTILS_INET_ADDRSTRLEN];
    GVariantBuilder     builder_data;
    guint               i;

    switch (prop_id) {
    case PROP_IFINDEX:
        g_value_set_int(value, priv->ifindex);
        break;
    case PROP_ADDRESS_DATA:
    case PROP_ADDRESSES:
        nm_assert(!!priv->address_data_variant == !!priv->addresses_variant);

        if (!priv->address_data_variant) {
            nm_utils_ip_addresses_to_dbus(AF_INET,
                                          nm_ip4_config_lookup_addresses(self),
                                          priv->best_default_route,
                                          NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN,
                                          &priv->address_data_variant,
                                          &priv->addresses_variant);
            g_variant_ref_sink(priv->address_data_variant);
            g_variant_ref_sink(priv->addresses_variant);
        }

        g_value_set_variant(value,
                            prop_id == PROP_ADDRESS_DATA ? priv->address_data_variant
                                                         : priv->addresses_variant);
        break;
    case PROP_ROUTE_DATA:
    case PROP_ROUTES:
        nm_assert(!!priv->route_data_variant == !!priv->routes_variant);

        if (!priv->route_data_variant) {
            nm_utils_ip_routes_to_dbus(AF_INET,
                                       nm_ip4_config_lookup_routes(self),
                                       &priv->route_data_variant,
                                       &priv->routes_variant);
            g_variant_ref_sink(priv->route_data_variant);
            g_variant_ref_sink(priv->routes_variant);
        }

        g_value_set_variant(value,
                            prop_id == PROP_ROUTE_DATA ? priv->route_data_variant
                                                       : priv->routes_variant);
        break;
    case PROP_GATEWAY:
        if (priv->best_default_route) {
            g_value_take_string(value,
                                nm_utils_inet4_ntop_dup(
                                    NMP_OBJECT_CAST_IP4_ROUTE(priv->best_default_route)->gateway));
        } else
            g_value_set_string(value, NULL);
        break;
    case PROP_NAMESERVER_DATA:
        g_variant_builder_init(&builder_data, G_VARIANT_TYPE("aa{sv}"));

        for (i = 0; i < priv->nameservers->len; i++) {
            GVariantBuilder nested_builder;

            _nm_utils_inet4_ntop(g_array_index(priv->nameservers, in_addr_t, i), addr_str);

            g_variant_builder_init(&nested_builder, G_VARIANT_TYPE("a{sv}"));
            g_variant_builder_add(&nested_builder,
                                  "{sv}",
                                  "address",
                                  g_variant_new_string(addr_str));
            g_variant_builder_add(&builder_data, "a{sv}", &nested_builder);
        }

        g_value_take_variant(value, g_variant_builder_end(&builder_data));
        break;
    case PROP_NAMESERVERS:
        g_value_take_variant(value,
                             g_variant_new_fixed_array(G_VARIANT_TYPE_UINT32,
                                                       priv->nameservers->data,
                                                       priv->nameservers->len,
                                                       sizeof(guint32)));
        break;
    case PROP_DOMAINS:
        nm_utils_g_value_set_strv(value, priv->domains);
        break;
    case PROP_SEARCHES:
        nm_utils_g_value_set_strv(value, priv->searches);
        break;
    case PROP_DNS_OPTIONS:
        nm_utils_g_value_set_strv(value, priv->dns_options);
        break;
    case PROP_DNS_PRIORITY:
        g_value_set_int(value, priv->dns_priority);
        break;
    case PROP_WINS_SERVER_DATA:
        g_variant_builder_init(&builder_data, G_VARIANT_TYPE("as"));
        for (i = 0; i < priv->wins->len; i++) {
            g_variant_builder_add(
                &builder_data,
                "s",
                _nm_utils_inet4_ntop(g_array_index(priv->wins, in_addr_t, i), addr_str));
        }
        g_value_take_variant(value, g_variant_builder_end(&builder_data));
        break;
    case PROP_WINS_SERVERS:
        g_value_take_variant(value,
                             g_variant_new_fixed_array(G_VARIANT_TYPE_UINT32,
                                                       priv->wins->data,
                                                       priv->wins->len,
                                                       sizeof(guint32)));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
    NMIP4Config *       self = NM_IP4_CONFIG(object);
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    switch (prop_id) {
    case PROP_MULTI_IDX:
        /* construct-only */
        priv->multi_idx = g_value_get_pointer(value);
        if (!priv->multi_idx)
            g_return_if_reached();
        nm_dedup_multi_index_ref(priv->multi_idx);
        break;
    case PROP_IFINDEX:
        /* construct-only */
        priv->ifindex = g_value_get_int(value);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

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

static void
nm_ip4_config_init(NMIP4Config *self)
{
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    nm_ip_config_dedup_multi_idx_type_init((NMIPConfigDedupMultiIdxType *) &priv->idx_ip4_addresses,
                                           NMP_OBJECT_TYPE_IP4_ADDRESS);
    nm_ip_config_dedup_multi_idx_type_init((NMIPConfigDedupMultiIdxType *) &priv->idx_ip4_routes,
                                           NMP_OBJECT_TYPE_IP4_ROUTE);

    priv->mdns        = NM_SETTING_CONNECTION_MDNS_DEFAULT;
    priv->llmnr       = NM_SETTING_CONNECTION_LLMNR_DEFAULT;
    priv->nameservers = g_array_new(FALSE, FALSE, sizeof(guint32));
    priv->domains     = g_ptr_array_new_with_free_func(g_free);
    priv->searches    = g_ptr_array_new_with_free_func(g_free);
    priv->dns_options = g_ptr_array_new_with_free_func(g_free);
    priv->nis         = g_array_new(FALSE, TRUE, sizeof(guint32));
    priv->wins        = g_array_new(FALSE, TRUE, sizeof(guint32));
}

NMIP4Config *
nm_ip4_config_new(NMDedupMultiIndex *multi_idx, int ifindex)
{
    g_return_val_if_fail(ifindex >= -1, NULL);
    return g_object_new(NM_TYPE_IP4_CONFIG,
                        NM_IP4_CONFIG_MULTI_IDX,
                        multi_idx,
                        NM_IP4_CONFIG_IFINDEX,
                        ifindex,
                        NULL);
}

static void
finalize(GObject *object)
{
    NMIP4Config *       self = NM_IP4_CONFIG(object);
    NMIP4ConfigPrivate *priv = NM_IP4_CONFIG_GET_PRIVATE(self);

    nm_clear_nmp_object(&priv->best_default_route);

    nm_dedup_multi_index_remove_idx(priv->multi_idx, &priv->idx_ip4_addresses);
    nm_dedup_multi_index_remove_idx(priv->multi_idx, &priv->idx_ip4_routes);

    nm_clear_g_variant(&priv->address_data_variant);
    nm_clear_g_variant(&priv->addresses_variant);
    nm_clear_g_variant(&priv->route_data_variant);
    nm_clear_g_variant(&priv->routes_variant);

    g_array_unref(priv->nameservers);
    g_ptr_array_unref(priv->domains);
    g_ptr_array_unref(priv->searches);
    g_ptr_array_unref(priv->dns_options);
    g_array_unref(priv->nis);
    g_free(priv->nis_domain);
    g_array_unref(priv->wins);

    G_OBJECT_CLASS(nm_ip4_config_parent_class)->finalize(object);

    nm_dedup_multi_index_unref(priv->multi_idx);
}

static const NMDBusInterfaceInfoExtended interface_info_ip4_config = {
    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
        NM_DBUS_INTERFACE_IP4_CONFIG,
        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Addresses",
                                                             "aau",
                                                             NM_IP4_CONFIG_ADDRESSES),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("AddressData",
                                                             "aa{sv}",
                                                             NM_IP4_CONFIG_ADDRESS_DATA),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Gateway", "s", NM_IP4_CONFIG_GATEWAY),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Routes", "aau", NM_IP4_CONFIG_ROUTES),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("RouteData",
                                                             "aa{sv}",
                                                             NM_IP4_CONFIG_ROUTE_DATA),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("NameserverData",
                                                           "aa{sv}",
                                                           NM_IP4_CONFIG_NAMESERVER_DATA),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Nameservers",
                                                             "au",
                                                             NM_IP4_CONFIG_NAMESERVERS),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Domains",
                                                             "as",
                                                             NM_IP4_CONFIG_DOMAINS),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Searches",
                                                             "as",
                                                             NM_IP4_CONFIG_SEARCHES),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("DnsOptions",
                                                             "as",
                                                             NM_IP4_CONFIG_DNS_OPTIONS),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("DnsPriority",
                                                             "i",
                                                             NM_IP4_CONFIG_DNS_PRIORITY),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("WinsServerData",
                                                           "as",
                                                           NM_IP4_CONFIG_WINS_SERVER_DATA),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("WinsServers",
                                                             "au",
                                                             NM_IP4_CONFIG_WINS_SERVERS), ), ),
    .legacy_property_changed = TRUE,
};

static void
nm_ip4_config_class_init(NMIP4ConfigClass *klass)
{
    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
    NMIPConfigClass *  ip_config_class   = NM_IP_CONFIG_CLASS(klass);

    ip_config_class->is_ipv4     = TRUE;
    ip_config_class->addr_family = AF_INET;

    dbus_object_class->export_path     = NM_DBUS_EXPORT_PATH_NUMBERED(NM_DBUS_PATH "/IP4Config");
    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_ip4_config);

    object_class->get_property = get_property;
    object_class->set_property = set_property;
    object_class->finalize     = finalize;

    obj_properties[PROP_MULTI_IDX] =
        g_param_spec_pointer(NM_IP4_CONFIG_MULTI_IDX,
                             "",
                             "",
                             G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_IFINDEX] =
        g_param_spec_int(NM_IP4_CONFIG_IFINDEX,
                         "",
                         "",
                         -1,
                         G_MAXINT,
                         -1,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_ADDRESS_DATA] =
        g_param_spec_variant(NM_IP4_CONFIG_ADDRESS_DATA,
                             "",
                             "",
                             G_VARIANT_TYPE("aa{sv}"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_ADDRESSES] =
        g_param_spec_variant(NM_IP4_CONFIG_ADDRESSES,
                             "",
                             "",
                             G_VARIANT_TYPE("aau"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_ROUTE_DATA] =
        g_param_spec_variant(NM_IP4_CONFIG_ROUTE_DATA,
                             "",
                             "",
                             G_VARIANT_TYPE("aa{sv}"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_ROUTES]  = g_param_spec_variant(NM_IP4_CONFIG_ROUTES,
                                                       "",
                                                       "",
                                                       G_VARIANT_TYPE("aau"),
                                                       NULL,
                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_GATEWAY] = g_param_spec_string(NM_IP4_CONFIG_GATEWAY,
                                                       "",
                                                       "",
                                                       NULL,
                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_NAMESERVER_DATA] =
        g_param_spec_variant(NM_IP4_CONFIG_NAMESERVER_DATA,
                             "",
                             "",
                             G_VARIANT_TYPE("aa{sv}"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_NAMESERVERS] =
        g_param_spec_variant(NM_IP4_CONFIG_NAMESERVERS,
                             "",
                             "",
                             G_VARIANT_TYPE("au"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_DOMAINS]  = g_param_spec_boxed(NM_IP4_CONFIG_DOMAINS,
                                                      "",
                                                      "",
                                                      G_TYPE_STRV,
                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_SEARCHES] = g_param_spec_boxed(NM_IP4_CONFIG_SEARCHES,
                                                       "",
                                                       "",
                                                       G_TYPE_STRV,
                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_DNS_OPTIONS] =
        g_param_spec_boxed(NM_IP4_CONFIG_DNS_OPTIONS,
                           "",
                           "",
                           G_TYPE_STRV,
                           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_DNS_PRIORITY] = g_param_spec_int(NM_IP4_CONFIG_DNS_PRIORITY,
                                                         "",
                                                         "",
                                                         G_MININT32,
                                                         G_MAXINT32,
                                                         0,
                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_WINS_SERVER_DATA] =
        g_param_spec_variant(NM_IP4_CONFIG_WINS_SERVER_DATA,
                             "",
                             "",
                             G_VARIANT_TYPE("as"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    obj_properties[PROP_WINS_SERVERS] =
        g_param_spec_variant(NM_IP4_CONFIG_WINS_SERVERS,
                             "",
                             "",
                             G_VARIANT_TYPE("au"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
}
