summary refs log tree commit diff
path: root/src/core/nm-ip6-config.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/nm-ip6-config.c')
-rw-r--r--src/core/nm-ip6-config.c2750
1 files changed, 2750 insertions, 0 deletions
diff --git a/src/core/nm-ip6-config.c b/src/core/nm-ip6-config.c
new file mode 100644
index 00000000..1f7def34
--- /dev/null
+++ b/src/core/nm-ip6-config.c
@@ -0,0 +1,2750 @@
+/* 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-ip6-config.h"
+
+#include <arpa/inet.h>
+#include <resolv.h>
+#include <linux/rtnetlink.h>
+#include <linux/if.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 "nm-core-internal.h"
+#include "NetworkManagerUtils.h"
+#include "nm-ip4-config.h"
+#include "ndisc/nm-ndisc.h"
+#include "nm-dbus-object.h"
+
+/*****************************************************************************/
+
+static gboolean
+_route_valid(const NMPlatformIP6Route *r)
+{
+    struct in6_addr n;
+
+    return r && r->plen <= 128
+           && (memcmp(&r->network,
+                      nm_utils_ip6_address_clear_host_address(&n, &r->network, r->plen),
+                      sizeof(n))
+               == 0);
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    int                       ifindex;
+    int                       dns_priority;
+    NMSettingIP6ConfigPrivacy privacy;
+    GArray *                  nameservers;
+    GPtrArray *               domains;
+    GPtrArray *               searches;
+    GPtrArray *               dns_options;
+    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_ip6_addresses_;
+        NMDedupMultiIdxType         idx_ip6_addresses;
+    };
+    union {
+        NMIPConfigDedupMultiIdxType idx_ip6_routes_;
+        NMDedupMultiIdxType         idx_ip6_routes;
+    };
+    NMIPConfigFlags config_flags;
+    bool            never_default : 1;
+} NMIP6ConfigPrivate;
+
+struct _NMIP6Config {
+    NMIPConfig         parent;
+    NMIP6ConfigPrivate _priv;
+};
+
+struct _NMIP6ConfigClass {
+    NMIPConfigClass parent;
+};
+
+G_DEFINE_TYPE(NMIP6Config, nm_ip6_config, NM_TYPE_IP_CONFIG)
+
+#define NM_IP6_CONFIG_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMIP6Config, NM_IS_IP6_CONFIG)
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMIP6Config,
+                             PROP_MULTI_IDX,
+                             PROP_IFINDEX,
+                             PROP_ADDRESS_DATA,
+                             PROP_ADDRESSES,
+                             PROP_ROUTE_DATA,
+                             PROP_ROUTES,
+                             PROP_GATEWAY,
+                             PROP_NAMESERVERS,
+                             PROP_DOMAINS,
+                             PROP_SEARCHES,
+                             PROP_DNS_OPTIONS,
+                             PROP_DNS_PRIORITY, );
+
+/*****************************************************************************/
+
+static void
+_add_address(NMIP6Config *self, const NMPObject *obj_new, const NMPlatformIP6Address *new);
+static void _add_route(NMIP6Config *    self,
+                       const NMPObject *obj_new,
+                       const NMPlatformIP6Route *new,
+                       const NMPObject **out_obj_new);
+static const NMDedupMultiEntry *
+_lookup_route(const NMIP6Config *self, const NMPObject *needle, NMPlatformIPRouteCmpType cmp_type);
+
+/*****************************************************************************/
+
+int
+nm_ip6_config_get_ifindex(const NMIP6Config *self)
+{
+    return NM_IP6_CONFIG_GET_PRIVATE(self)->ifindex;
+}
+
+NMDedupMultiIndex *
+nm_ip6_config_get_multi_idx(const NMIP6Config *self)
+{
+    return NM_IP6_CONFIG_GET_PRIVATE(self)->multi_idx;
+}
+
+void
+nm_ip6_config_set_privacy(NMIP6Config *self, NMSettingIP6ConfigPrivacy privacy)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    priv->privacy = privacy;
+}
+
+/*****************************************************************************/
+
+void
+nm_ip6_config_set_never_default(NMIP6Config *self, gboolean never_default)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    priv->never_default = never_default;
+}
+
+gboolean
+nm_ip6_config_get_never_default(const NMIP6Config *self)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    return priv->never_default;
+}
+
+/*****************************************************************************/
+
+const NMDedupMultiHeadEntry *
+nm_ip6_config_lookup_addresses(const NMIP6Config *self)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    return nm_dedup_multi_index_lookup_head(priv->multi_idx, &priv->idx_ip6_addresses, NULL);
+}
+
+void
+nm_ip_config_iter_ip6_address_init(NMDedupMultiIter *ipconf_iter, const NMIP6Config *self)
+{
+    nm_assert(NM_IS_IP6_CONFIG(self));
+
+    nm_dedup_multi_iter_init(ipconf_iter, nm_ip6_config_lookup_addresses(self));
+}
+
+/*****************************************************************************/
+
+const NMDedupMultiHeadEntry *
+nm_ip6_config_lookup_routes(const NMIP6Config *self)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    return nm_dedup_multi_index_lookup_head(priv->multi_idx, &priv->idx_ip6_routes, NULL);
+}
+
+void
+nm_ip_config_iter_ip6_route_init(NMDedupMultiIter *ipconf_iter, const NMIP6Config *self)
+{
+    nm_assert(NM_IS_IP6_CONFIG(self));
+
+    nm_dedup_multi_iter_init(ipconf_iter, nm_ip6_config_lookup_routes(self));
+}
+
+/*****************************************************************************/
+
+const NMPObject *
+nm_ip6_config_best_default_route_get(const NMIP6Config *self)
+{
+    g_return_val_if_fail(NM_IS_IP6_CONFIG(self), NULL);
+
+    return NM_IP6_CONFIG_GET_PRIVATE(self)->best_default_route;
+}
+
+const NMPObject *
+_nm_ip6_config_best_default_route_find(const NMIP6Config *self)
+{
+    NMDedupMultiIter ipconf_iter;
+    const NMPObject *new_best_default_route = NULL;
+
+    nm_ip_config_iter_ip6_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;
+}
+
+/*****************************************************************************/
+
+static void
+_notify_addresses(NMIP6Config *self)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_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(NMIP6Config *self)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    nm_assert(priv->best_default_route == _nm_ip6_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)
+{
+    return nm_platform_ip6_address_pretty_sort_cmp(
+        NMP_OBJECT_CAST_IP6_ADDRESS(c_list_entry(lst_a, NMDedupMultiEntry, lst_entries)->obj),
+        NMP_OBJECT_CAST_IP6_ADDRESS(c_list_entry(lst_b, NMDedupMultiEntry, lst_entries)->obj),
+        (((NMSettingIP6ConfigPrivacy) GPOINTER_TO_INT(user_data))
+         == NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR));
+}
+
+gboolean
+_nmtst_ip6_config_addresses_sort(NMIP6Config *self)
+{
+    NMIP6ConfigPrivate *         priv;
+    const NMDedupMultiHeadEntry *head_entry;
+
+    g_return_val_if_fail(NM_IS_IP6_CONFIG(self), FALSE);
+
+    head_entry = nm_ip6_config_lookup_addresses(self);
+    if (head_entry && head_entry->len > 1) {
+        gboolean changed;
+        gs_free gconstpointer *addresses_old = NULL;
+        guint                  naddr, j;
+        NMDedupMultiIter       iter;
+
+        priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+        addresses_old = nm_dedup_multi_objs_to_array_head(head_entry, NULL, NULL, &naddr);
+        nm_assert(addresses_old);
+        nm_assert(naddr > 0 && naddr == head_entry->len);
+
+        nm_dedup_multi_head_entry_sort(head_entry,
+                                       sort_captured_addresses,
+                                       GINT_TO_POINTER(priv->privacy));
+
+        changed = FALSE;
+        j       = 0;
+        nm_dedup_multi_iter_for_each (&iter, head_entry) {
+            nm_assert(j < naddr);
+            if (iter.current->obj != addresses_old[j++])
+                changed = TRUE;
+        }
+        nm_assert(j == naddr);
+
+        if (changed) {
+            _notify_addresses(self);
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+
+NMIP6Config *
+nm_ip6_config_clone(const NMIP6Config *self)
+{
+    NMIP6Config *copy;
+
+    copy = nm_ip6_config_new(nm_ip6_config_get_multi_idx(self), -1);
+    nm_ip6_config_replace(copy, self, NULL);
+
+    return copy;
+}
+
+NMIP6Config *
+nm_ip6_config_capture(NMDedupMultiIndex *       multi_idx,
+                      NMPlatform *              platform,
+                      int                       ifindex,
+                      NMSettingIP6ConfigPrivacy use_temporary)
+{
+    NMIP6Config *                self;
+    NMIP6ConfigPrivate *         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_ip6_config_new(multi_idx, ifindex);
+    priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    head_entry = nm_platform_lookup_object(platform, NMP_OBJECT_TYPE_IP6_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_ip6_addresses_,
+                                       ifindex,
+                                       plobj,
+                                       NULL,
+                                       FALSE,
+                                       TRUE,
+                                       NULL,
+                                       NULL))
+                nm_assert_not_reached();
+        }
+        head_entry = nm_ip6_config_lookup_addresses(self);
+        nm_assert(head_entry);
+        nm_dedup_multi_head_entry_sort(head_entry,
+                                       sort_captured_addresses,
+                                       GINT_TO_POINTER(use_temporary));
+        _notify_addresses(self);
+    }
+
+    head_entry = nm_platform_lookup_object(platform, NMP_OBJECT_TYPE_IP6_ROUTE, ifindex);
+
+    nmp_cache_iter_for_each (&iter, head_entry, &plobj)
+        _add_route(self, plobj, NULL, NULL);
+
+    return self;
+}
+
+void
+nm_ip6_config_update_routes_metric(NMIP6Config *self, gint64 metric)
+{
+    gs_free NMPlatformIP6Route *routes      = NULL;
+    gboolean                    need_update = FALSE;
+    const NMPlatformIP6Route *  r;
+    NMDedupMultiIter            iter;
+    guint                       num = 0, i = 0;
+
+    nm_ip_config_iter_ip6_route_for_each (&iter, self, &r) {
+        if (r->metric != metric)
+            need_update = TRUE;
+        num++;
+    }
+    if (!need_update)
+        return;
+
+    routes = g_new(NMPlatformIP6Route, num);
+    nm_ip_config_iter_ip6_route_for_each (&iter, self, &r) {
+        routes[i]        = *r;
+        routes[i].metric = metric;
+        i++;
+    }
+
+    g_object_freeze_notify(G_OBJECT(self));
+    nm_ip6_config_reset_routes(self);
+    for (i = 0; i < num; i++)
+        nm_ip6_config_add_route(self, &routes[i], NULL);
+    g_object_thaw_notify(G_OBJECT(self));
+}
+
+void
+nm_ip6_config_add_dependent_routes(NMIP6Config *self,
+                                   guint32      route_table,
+                                   guint32      route_metric,
+                                   gboolean     is_vrf)
+{
+    const NMPlatformIP6Address *my_addr;
+    const NMPlatformIP6Route *  my_route;
+    int                         ifindex;
+    NMDedupMultiIter            iter;
+
+    g_return_if_fail(NM_IS_IP6_CONFIG(self));
+
+    ifindex = nm_ip6_config_get_ifindex(self);
+    g_return_if_fail(ifindex > 0);
+
+    /* For IPv6 addresses received via SLAAC/autoconf, we explicitly add the
+     * device-routes (onlink) to NMIP6Config.
+     *
+     * For manually added IPv6 routes, add the device routes explicitly. */
+
+    /* Pre-generate multicast route */
+    {
+        nm_auto_nmpobj NMPObject *r = NULL;
+        NMPlatformIP6Route *      route;
+
+        r                         = nmp_object_new(NMP_OBJECT_TYPE_IP6_ROUTE, NULL);
+        route                     = NMP_OBJECT_CAST_IP6_ROUTE(r);
+        route->ifindex            = ifindex;
+        route->network.s6_addr[0] = 0xffu;
+        route->plen               = 8;
+        route->table_coerced      = nm_platform_route_table_coerce(RT_TABLE_LOCAL);
+        route->type_coerced       = nm_platform_route_type_coerce(RTN_UNICAST);
+        route->metric             = 256;
+
+        _add_route(self, r, NULL, NULL);
+    }
+
+    nm_ip_config_iter_ip6_address_for_each (&iter, self, &my_addr) {
+        NMPlatformIP6Route *route;
+        gboolean            has_peer;
+        int                 routes_n, routes_i;
+
+        if (my_addr->external)
+            continue;
+
+        {
+            nm_auto_nmpobj NMPObject *r = NULL;
+
+            /* Pre-generate local route added by kernel */
+            r                   = nmp_object_new(NMP_OBJECT_TYPE_IP6_ROUTE, NULL);
+            route               = NMP_OBJECT_CAST_IP6_ROUTE(r);
+            route->ifindex      = ifindex;
+            route->network      = my_addr->address;
+            route->plen         = 128;
+            route->type_coerced = nm_platform_route_type_coerce(RTN_LOCAL);
+            route->metric       = 0;
+            route->table_coerced =
+                nm_platform_route_table_coerce(is_vrf ? route_table : RT_TABLE_LOCAL);
+            _add_route(self, r, NULL, NULL);
+        }
+
+        if (NM_FLAGS_HAS(my_addr->n_ifa_flags, IFA_F_NOPREFIXROUTE))
+            continue;
+        if (my_addr->plen == 0)
+            continue;
+
+        has_peer = !IN6_IS_ADDR_UNSPECIFIED(&my_addr->peer_address);
+
+        /* If we have an IPv6 peer, we add two /128 routes
+         * (unless, both addresses are identical). */
+        routes_n =
+            (has_peer && !IN6_ARE_ADDR_EQUAL(&my_addr->address, &my_addr->peer_address)) ? 2 : 1;
+
+        for (routes_i = 0; routes_i < routes_n; routes_i++) {
+            nm_auto_nmpobj NMPObject *r = NULL;
+
+            r     = nmp_object_new(NMP_OBJECT_TYPE_IP6_ROUTE, NULL);
+            route = NMP_OBJECT_CAST_IP6_ROUTE(r);
+
+            route->ifindex       = ifindex;
+            route->rt_source     = NM_IP_CONFIG_SOURCE_KERNEL;
+            route->table_coerced = nm_platform_route_table_coerce(route_table);
+            route->metric        = route_metric;
+
+            if (has_peer) {
+                if (routes_i == 0)
+                    route->network = my_addr->address;
+                else
+                    route->network = my_addr->peer_address;
+                route->plen = 128;
+            } else {
+                nm_utils_ip6_address_clear_host_address(&route->network,
+                                                        &my_addr->address,
+                                                        my_addr->plen);
+                route->plen = my_addr->plen;
+            }
+
+            nm_platform_ip_route_normalize(AF_INET6, (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);
+        }
+    }
+
+again:
+    nm_ip_config_iter_ip6_route_for_each (&iter, self, &my_route) {
+        NMPlatformIP6Route rt;
+
+        if (!NM_PLATFORM_IP_ROUTE_IS_DEFAULT(my_route)
+            || IN6_IS_ADDR_UNSPECIFIED(&my_route->gateway)
+            || NM_IS_IP_CONFIG_SOURCE_RTPROT(my_route->rt_source)
+            || nm_ip6_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    = 128;
+        rt.gateway = in6addr_any;
+        _add_route(self, NULL, &rt, NULL);
+        /* adding the route might have invalidated the iteration. Start again. */
+        goto again;
+    }
+}
+
+gboolean
+nm_ip6_config_commit(const NMIP6Config *    self,
+                     NMPlatform *           platform,
+                     NMIPRouteTableSyncMode route_table_sync,
+                     GPtrArray **           out_temporary_not_available)
+{
+    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_IP6_CONFIG(self), FALSE);
+
+    ifindex = nm_ip6_config_get_ifindex(self);
+    g_return_val_if_fail(ifindex > 0, FALSE);
+
+    addresses =
+        nm_dedup_multi_objs_to_ptr_array_head(nm_ip6_config_lookup_addresses(self), NULL, NULL);
+
+    routes = nm_dedup_multi_objs_to_ptr_array_head(nm_ip6_config_lookup_routes(self), NULL, NULL);
+
+    routes_prune =
+        nm_platform_ip_route_get_prune_list(platform, AF_INET6, ifindex, route_table_sync);
+
+    nm_platform_ip6_address_sync(platform, ifindex, addresses, FALSE);
+
+    if (!nm_platform_ip_route_sync(platform,
+                                   AF_INET6,
+                                   ifindex,
+                                   routes,
+                                   routes_prune,
+                                   out_temporary_not_available))
+        success = FALSE;
+
+    return success;
+}
+
+void
+nm_ip6_config_merge_setting(NMIP6Config *      self,
+                            NMSettingIPConfig *setting,
+                            guint32            route_table,
+                            guint32            route_metric)
+{
+    guint           naddresses, nroutes, nnameservers, nsearches;
+    const char *    gateway_str;
+    struct in6_addr gateway_bin;
+    int             i, priority;
+
+    if (!setting)
+        return;
+
+    g_return_if_fail(NM_IS_SETTING_IP6_CONFIG(setting));
+
+    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);
+
+    g_object_freeze_notify(G_OBJECT(self));
+
+    /* Gateway */
+    if (!nm_setting_ip_config_get_never_default(setting)
+        && (gateway_str = nm_setting_ip_config_get_gateway(setting))
+        && inet_pton(AF_INET6, gateway_str, &gateway_bin) == 1
+        && !IN6_IS_ADDR_UNSPECIFIED(&gateway_bin)) {
+        const NMPlatformIP6Route 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);
+        NMPlatformIP6Address address;
+
+        memset(&address, 0, sizeof(address));
+        nm_ip_address_get_address_binary(s_addr, &address.address);
+        address.plen = nm_ip_address_get_prefix(s_addr);
+        nm_assert(address.plen <= 128);
+        address.lifetime    = NM_PLATFORM_LIFETIME_PERMANENT;
+        address.preferred   = NM_PLATFORM_LIFETIME_PERMANENT;
+        address.addr_source = NM_IP_CONFIG_SOURCE_USER;
+
+        _add_address(self, NULL, &address);
+    }
+
+    /* Routes */
+    for (i = 0; i < nroutes; i++) {
+        NMIPRoute *        s_route = nm_setting_ip_config_get_route(setting, i);
+        NMPlatformIP6Route route;
+        gint64             m;
+
+        if (nm_ip_route_get_family(s_route) != AF_INET6) {
+            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 <= 128);
+
+        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 = nm_utils_ip6_route_metric_normalize(m);
+        route.rt_source = NM_IP_CONFIG_SOURCE_USER;
+
+        nm_utils_ip6_address_clear_host_address(&route.network, &route.network, route.plen);
+
+        nm_utils_ip_route_attribute_to_platform(AF_INET6,
+                                                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_ip6_config_reset_nameservers(self);
+        nm_ip6_config_reset_domains(self);
+        nm_ip6_config_reset_searches(self);
+    }
+    for (i = 0; i < nnameservers; i++) {
+        struct in6_addr ip;
+
+        if (inet_pton(AF_INET6, nm_setting_ip_config_get_dns(setting, i), &ip) == 1)
+            nm_ip6_config_add_nameserver(self, &ip);
+    }
+    for (i = 0; i < nsearches; i++)
+        nm_ip6_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_ip6_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_ip6_config_set_dns_priority(self, priority);
+
+    nm_ip6_config_set_never_default(self, nm_setting_ip_config_get_never_default(setting));
+
+    g_object_thaw_notify(G_OBJECT(self));
+}
+
+NMSetting *
+nm_ip6_config_create_setting(const NMIP6Config *self, gboolean maybe_ipv6_disabled)
+{
+    const NMIP6ConfigPrivate *  priv;
+    NMSettingIPConfig *         s_ip6;
+    guint                       nnameservers, nsearches, noptions;
+    const char *                method = NULL;
+    char                        sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    int                         i;
+    NMDedupMultiIter            ipconf_iter;
+    const NMPlatformIP6Address *address;
+    const NMPlatformIP6Route *  route;
+
+    s_ip6 = NM_SETTING_IP_CONFIG(nm_setting_ip6_config_new());
+
+    if (!self) {
+        g_object_set(s_ip6, NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP6_CONFIG_METHOD_IGNORE, NULL);
+        return NM_SETTING(s_ip6);
+    }
+
+    priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    nnameservers = nm_ip6_config_get_num_nameservers(self);
+    nsearches    = nm_ip6_config_get_num_searches(self);
+    noptions     = nm_ip6_config_get_num_dns_options(self);
+
+    /* Addresses */
+    nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, self, &address) {
+        NMIPAddress *s_addr;
+
+        /* Ignore link-local address. */
+        if (IN6_IS_ADDR_LINKLOCAL(&address->address)) {
+            if (!method)
+                method = NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL;
+            continue;
+        }
+
+        /* Detect dynamic address */
+        if (address->lifetime != NM_PLATFORM_LIFETIME_PERMANENT) {
+            method = NM_SETTING_IP6_CONFIG_METHOD_AUTO;
+            continue;
+        }
+
+        /* Static address found. */
+        if (!method || strcmp(method, NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL) == 0)
+            method = NM_SETTING_IP6_CONFIG_METHOD_MANUAL;
+
+        s_addr = nm_ip_address_new_binary(AF_INET6, &address->address, address->plen, NULL);
+        nm_setting_ip_config_add_address(s_ip6, s_addr);
+        nm_ip_address_unref(s_addr);
+    }
+
+    /* Gateway */
+    if (priv->best_default_route && nm_setting_ip_config_get_num_addresses(s_ip6) > 0) {
+        g_object_set(
+            s_ip6,
+            NM_SETTING_IP_CONFIG_GATEWAY,
+            _nm_utils_inet6_ntop(&NMP_OBJECT_CAST_IP6_ROUTE(priv->best_default_route)->gateway,
+                                 sbuf),
+            NULL);
+    }
+
+    /* Use 'ignore' if the method wasn't previously set */
+    if (!method) {
+        method = maybe_ipv6_disabled ? NM_SETTING_IP6_CONFIG_METHOD_DISABLED
+                                     : NM_SETTING_IP6_CONFIG_METHOD_IGNORE;
+    }
+
+    g_object_set(s_ip6, NM_SETTING_IP_CONFIG_METHOD, method, NULL);
+
+    /* Routes */
+    nm_ip_config_iter_ip6_route_for_each (&ipconf_iter, self, &route) {
+        NMIPRoute *s_route;
+
+        /* Ignore link-local route. */
+        if (IN6_IS_ADDR_LINKLOCAL(&route->network))
+            continue;
+
+        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_INET6,
+                                         &route->network,
+                                         route->plen,
+                                         &route->gateway,
+                                         route->metric,
+                                         NULL);
+        nm_setting_ip_config_add_route(s_ip6, s_route);
+        nm_ip_route_unref(s_route);
+    }
+
+    /* DNS */
+    for (i = 0; i < nnameservers; i++) {
+        const struct in6_addr *nameserver = nm_ip6_config_get_nameserver(self, i);
+
+        nm_setting_ip_config_add_dns(s_ip6, _nm_utils_inet6_ntop(nameserver, sbuf));
+    }
+    for (i = 0; i < nsearches; i++) {
+        const char *search = nm_ip6_config_get_search(self, i);
+
+        nm_setting_ip_config_add_dns_search(s_ip6, search);
+    }
+    for (i = 0; i < noptions; i++) {
+        const char *option = nm_ip6_config_get_dns_option(self, i);
+
+        nm_setting_ip_config_add_dns_option(s_ip6, option);
+    }
+
+    g_object_set(s_ip6,
+                 NM_SETTING_IP_CONFIG_DNS_PRIORITY,
+                 nm_ip6_config_get_dns_priority(self),
+                 NULL);
+
+    return NM_SETTING(s_ip6);
+}
+
+/*****************************************************************************/
+
+void
+nm_ip6_config_merge(NMIP6Config *        dst,
+                    const NMIP6Config *  src,
+                    NMIPConfigMergeFlags merge_flags,
+                    guint32              default_route_metric_penalty)
+{
+    guint32                     i;
+    NMDedupMultiIter            ipconf_iter;
+    const NMPlatformIP6Address *address = NULL;
+    const NMIP6ConfigPrivate *  src_priv;
+
+    g_return_if_fail(src != NULL);
+    g_return_if_fail(dst != NULL);
+
+    src_priv = NM_IP6_CONFIG_GET_PRIVATE(src);
+
+    g_object_freeze_notify(G_OBJECT(dst));
+
+    /* addresses */
+    nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, src, &address) {
+        if (NM_FLAGS_HAS(merge_flags, NM_IP_CONFIG_MERGE_EXTERNAL) && !address->external) {
+            NMPlatformIP6Address 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_ip6_config_get_num_nameservers(src); i++)
+            nm_ip6_config_add_nameserver(dst, nm_ip6_config_get_nameserver(src, i));
+    }
+
+    /* routes */
+    if (!NM_FLAGS_HAS(merge_flags, NM_IP_CONFIG_MERGE_NO_ROUTES)) {
+        const NMPlatformIP6Route *r_src;
+
+        nm_ip_config_iter_ip6_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) {
+                    NMPlatformIP6Route 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_ip6_config_get_num_domains(src); i++)
+            nm_ip6_config_add_domain(dst, nm_ip6_config_get_domain(src, i));
+    }
+
+    /* dns searches */
+    if (!NM_FLAGS_HAS(merge_flags, NM_IP_CONFIG_MERGE_NO_DNS)) {
+        for (i = 0; i < nm_ip6_config_get_num_searches(src); i++)
+            nm_ip6_config_add_search(dst, nm_ip6_config_get_search(src, i));
+    }
+
+    /* dns options */
+    if (!NM_FLAGS_HAS(merge_flags, NM_IP_CONFIG_MERGE_NO_DNS)) {
+        for (i = 0; i < nm_ip6_config_get_num_dns_options(src); i++)
+            nm_ip6_config_add_dns_option(dst, nm_ip6_config_get_dns_option(src, i));
+    }
+
+    /* DNS priority */
+    if (nm_ip6_config_get_dns_priority(src))
+        nm_ip6_config_set_dns_priority(dst, nm_ip6_config_get_dns_priority(src));
+
+    g_object_thaw_notify(G_OBJECT(dst));
+}
+
+/*****************************************************************************/
+
+static int
+_nameservers_get_index(const NMIP6Config *self, const struct in6_addr *ns)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+    guint                     i;
+
+    for (i = 0; i < priv->nameservers->len; i++) {
+        const struct in6_addr *n = &g_array_index(priv->nameservers, struct in6_addr, i);
+
+        if (IN6_ARE_ADDR_EQUAL(ns, n))
+            return (int) i;
+    }
+    return -1;
+}
+
+static int
+_domains_get_index(const NMIP6Config *self, const char *domain)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_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 NMIP6Config *self, const char *search)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_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 NMIP6Config *self, const char *option)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_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;
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_ip6_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_ip6_config_subtract(NMIP6Config *      dst,
+                       const NMIP6Config *src,
+                       guint32            default_route_metric_penalty)
+{
+    NMIP6ConfigPrivate *        dst_priv;
+    guint                       i;
+    int                         idx;
+    const NMPlatformIP6Address *a;
+    const NMPlatformIP6Route *  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_IP6_CONFIG_GET_PRIVATE(dst);
+
+    g_object_freeze_notify(G_OBJECT(dst));
+
+    /* addresses */
+    changed = FALSE;
+    nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, src, &a) {
+        if (nm_dedup_multi_index_remove_obj(dst_priv->multi_idx,
+                                            &dst_priv->idx_ip6_addresses,
+                                            NMP_OBJECT_UP_CAST(a),
+                                            NULL))
+            changed = TRUE;
+    }
+    if (changed)
+        _notify_addresses(dst);
+
+    /* nameservers */
+    for (i = 0; i < nm_ip6_config_get_num_nameservers(src); i++) {
+        idx = _nameservers_get_index(dst, nm_ip6_config_get_nameserver(src, i));
+        if (idx >= 0)
+            nm_ip6_config_del_nameserver(dst, idx);
+    }
+
+    /* routes */
+    changed               = FALSE;
+    changed_default_route = FALSE;
+    nm_ip_config_iter_ip6_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) {
+            NMPlatformIP6Route *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_IP6_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_ip6_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_ip6_config_best_default_route_find(dst));
+        _notify(dst, PROP_GATEWAY);
+    }
+    if (changed)
+        _notify_routes(dst);
+
+    /* domains */
+    for (i = 0; i < nm_ip6_config_get_num_domains(src); i++) {
+        idx = _domains_get_index(dst, nm_ip6_config_get_domain(src, i));
+        if (idx >= 0)
+            nm_ip6_config_del_domain(dst, idx);
+    }
+
+    /* dns searches */
+    for (i = 0; i < nm_ip6_config_get_num_searches(src); i++) {
+        idx = _searches_get_index(dst, nm_ip6_config_get_search(src, i));
+        if (idx >= 0)
+            nm_ip6_config_del_search(dst, idx);
+    }
+
+    /* dns options */
+    for (i = 0; i < nm_ip6_config_get_num_dns_options(src); i++) {
+        idx = _dns_options_get_index(dst, nm_ip6_config_get_dns_option(src, i));
+        if (idx >= 0)
+            nm_ip6_config_del_dns_option(dst, idx);
+    }
+
+    /* DNS priority */
+    if (nm_ip6_config_get_dns_priority(src) == nm_ip6_config_get_dns_priority(dst))
+        nm_ip6_config_set_dns_priority(dst, 0);
+
+    g_object_thaw_notify(G_OBJECT(dst));
+}
+
+static gboolean
+_nm_ip6_config_intersect_helper(NMIP6Config *      dst,
+                                const NMIP6Config *src,
+                                gboolean           intersect_addresses,
+                                gboolean           intersect_routes,
+                                guint32            default_route_metric_penalty,
+                                gboolean           update_dst)
+{
+    NMIP6ConfigPrivate *        dst_priv;
+    const NMIP6ConfigPrivate *  src_priv;
+    NMDedupMultiIter            ipconf_iter;
+    const NMPlatformIP6Address *a;
+    const NMPlatformIP6Route *  r;
+    gboolean                    changed, result = FALSE;
+    const NMPObject *           new_best_default_route;
+
+    g_return_val_if_fail(src, FALSE);
+    g_return_val_if_fail(dst, FALSE);
+
+    dst_priv = NM_IP6_CONFIG_GET_PRIVATE(dst);
+    src_priv = NM_IP6_CONFIG_GET_PRIVATE(src);
+
+    if (update_dst)
+        g_object_freeze_notify(G_OBJECT(dst));
+
+    /* addresses */
+    if (intersect_addresses) {
+        changed = FALSE;
+        nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, dst, &a) {
+            if (nm_dedup_multi_index_lookup_obj(src_priv->multi_idx,
+                                                &src_priv->idx_ip6_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_ip6_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) {
+            NMPlatformIP6Route *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_IP6_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_ip6_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 */
+
+    if (update_dst)
+        g_object_thaw_notify(G_OBJECT(dst));
+
+    return result;
+}
+
+/**
+ * nm_ip6_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_ip6_config_intersect(NMIP6Config *      dst,
+                        const NMIP6Config *src,
+                        gboolean           intersect_addresses,
+                        gboolean           intersect_routes,
+                        guint32            default_route_metric_penalty)
+{
+    _nm_ip6_config_intersect_helper(dst,
+                                    src,
+                                    intersect_addresses,
+                                    intersect_routes,
+                                    default_route_metric_penalty,
+                                    TRUE);
+}
+
+/**
+ * nm_ip6_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.
+ */
+NMIP6Config *
+nm_ip6_config_intersect_alloc(const NMIP6Config *a,
+                              const NMIP6Config *b,
+                              gboolean           intersect_addresses,
+                              gboolean           intersect_routes,
+                              guint32            default_route_metric_penalty)
+{
+    NMIP6Config *a_copy;
+
+    if (_nm_ip6_config_intersect_helper((NMIP6Config *) a,
+                                        b,
+                                        intersect_addresses,
+                                        intersect_routes,
+                                        default_route_metric_penalty,
+                                        FALSE)) {
+        a_copy = nm_ip6_config_clone(a);
+        _nm_ip6_config_intersect_helper(a_copy,
+                                        b,
+                                        intersect_addresses,
+                                        intersect_routes,
+                                        default_route_metric_penalty,
+                                        TRUE);
+        return a_copy;
+    } else
+        return NULL;
+}
+
+/**
+ * nm_ip6_config_replace:
+ * @dst: config which will be replaced with everything in @src
+ * @src: config to copy over to @dst
+ * @relevant_changes: return whether there are changes to the
+ * destination object that are relevant. This is equal to
+ * nm_ip6_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_ip6_config_replace(NMIP6Config *dst, const NMIP6Config *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;
+    NMIP6ConfigPrivate *         dst_priv;
+    const NMIP6ConfigPrivate *   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(NM_IS_IP6_CONFIG(src), FALSE);
+    g_return_val_if_fail(NM_IS_IP6_CONFIG(dst), FALSE);
+    g_return_val_if_fail(src != dst, FALSE);
+
+#if NM_MORE_ASSERTS
+    config_equal = nm_ip6_config_equal(dst, src);
+#endif
+
+    dst_priv = NM_IP6_CONFIG_GET_PRIVATE(dst);
+    src_priv = NM_IP6_CONFIG_GET_PRIVATE(src);
+
+    g_return_val_if_fail(src_priv->ifindex > 0, FALSE);
+
+    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_ip6_config_lookup_addresses(src);
+    nm_dedup_multi_iter_init(&ipconf_iter_src, head_entry_src);
+    nm_ip_config_iter_ip6_address_init(&ipconf_iter_dst, dst);
+    are_equal = TRUE;
+    while (TRUE) {
+        gboolean                    has;
+        const NMPlatformIP6Address *r_src = NULL;
+        const NMPlatformIP6Address *r_dst = NULL;
+
+        has = nm_platform_dedup_multi_iter_next_ip6_address(&ipconf_iter_src, &r_src);
+        if (has != nm_platform_dedup_multi_iter_next_ip6_address(&ipconf_iter_dst, &r_dst)) {
+            are_equal            = FALSE;
+            has_relevant_changes = TRUE;
+            break;
+        }
+        if (!has)
+            break;
+
+        if (nm_platform_ip6_address_cmp(r_src, r_dst) != 0) {
+            are_equal = FALSE;
+            if (!IN6_ARE_ADDR_EQUAL(&r_src->address, &r_dst->address) || r_src->plen != r_dst->plen
+                || !IN6_ARE_ADDR_EQUAL(nm_platform_ip6_address_get_peer(r_src),
+                                       nm_platform_ip6_address_get_peer(r_dst))) {
+                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_ip6_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_ip6_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_ip6_addresses,
+                                              FALSE);
+        _notify_addresses(dst);
+    }
+
+    /* routes */
+    head_entry_src = nm_ip6_config_lookup_routes(src);
+    nm_dedup_multi_iter_init(&ipconf_iter_src, head_entry_src);
+    nm_ip_config_iter_ip6_route_init(&ipconf_iter_dst, dst);
+    are_equal = TRUE;
+    while (TRUE) {
+        gboolean                  has;
+        const NMPlatformIP6Route *r_src = NULL;
+        const NMPlatformIP6Route *r_dst = NULL;
+
+        has = nm_platform_dedup_multi_iter_next_ip6_route(&ipconf_iter_src, &r_src);
+        if (has != nm_platform_dedup_multi_iter_next_ip6_route(&ipconf_iter_dst, &r_dst)) {
+            are_equal            = FALSE;
+            has_relevant_changes = TRUE;
+            break;
+        }
+        if (!has)
+            break;
+
+        if (nm_platform_ip6_route_cmp_full(r_src, r_dst) != 0) {
+            are_equal = FALSE;
+            if (r_src->plen != r_dst->plen
+                || !nm_utils_ip6_address_same_prefix(&r_src->network, &r_dst->network, r_src->plen)
+                || r_src->metric != r_dst->metric
+                || !IN6_ARE_ADDR_EQUAL(&r_src->gateway, &r_dst->gateway)) {
+                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_ip6_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_ip6_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_ip6_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_ip6_config_get_num_nameservers(src);
+    are_equal = num == nm_ip6_config_get_num_nameservers(dst);
+    if (are_equal) {
+        for (i = 0; i < num; i++) {
+            if (!IN6_ARE_ADDR_EQUAL(nm_ip6_config_get_nameserver(src, i),
+                                    nm_ip6_config_get_nameserver(dst, i))) {
+                are_equal = FALSE;
+                break;
+            }
+        }
+    }
+    if (!are_equal) {
+        nm_ip6_config_reset_nameservers(dst);
+        for (i = 0; i < num; i++)
+            nm_ip6_config_add_nameserver(dst, nm_ip6_config_get_nameserver(src, i));
+        has_relevant_changes = TRUE;
+    }
+
+    /* domains */
+    num       = nm_ip6_config_get_num_domains(src);
+    are_equal = num == nm_ip6_config_get_num_domains(dst);
+    if (are_equal) {
+        for (i = 0; i < num; i++) {
+            if (g_strcmp0(nm_ip6_config_get_domain(src, i), nm_ip6_config_get_domain(dst, i))) {
+                are_equal = FALSE;
+                break;
+            }
+        }
+    }
+    if (!are_equal) {
+        nm_ip6_config_reset_domains(dst);
+        for (i = 0; i < num; i++)
+            nm_ip6_config_add_domain(dst, nm_ip6_config_get_domain(src, i));
+        has_relevant_changes = TRUE;
+    }
+
+    /* dns searches */
+    num       = nm_ip6_config_get_num_searches(src);
+    are_equal = num == nm_ip6_config_get_num_searches(dst);
+    if (are_equal) {
+        for (i = 0; i < num; i++) {
+            if (g_strcmp0(nm_ip6_config_get_search(src, i), nm_ip6_config_get_search(dst, i))) {
+                are_equal = FALSE;
+                break;
+            }
+        }
+    }
+    if (!are_equal) {
+        nm_ip6_config_reset_searches(dst);
+        for (i = 0; i < num; i++)
+            nm_ip6_config_add_search(dst, nm_ip6_config_get_search(src, i));
+        has_relevant_changes = TRUE;
+    }
+
+    /* dns options */
+    num       = nm_ip6_config_get_num_dns_options(src);
+    are_equal = num == nm_ip6_config_get_num_dns_options(dst);
+    if (are_equal) {
+        for (i = 0; i < num; i++) {
+            if (g_strcmp0(nm_ip6_config_get_dns_option(src, i),
+                          nm_ip6_config_get_dns_option(dst, i))) {
+                are_equal = FALSE;
+                break;
+            }
+        }
+    }
+    if (!are_equal) {
+        nm_ip6_config_reset_dns_options(dst);
+        for (i = 0; i < num; i++)
+            nm_ip6_config_add_dns_option(dst, nm_ip6_config_get_dns_option(src, i));
+        has_relevant_changes = TRUE;
+    }
+
+    /* DNS priority */
+    if (src_priv->dns_priority != dst_priv->dns_priority) {
+        nm_ip6_config_set_dns_priority(dst, src_priv->dns_priority);
+        has_minor_changes = TRUE;
+    }
+
+    if (src_priv->privacy != dst_priv->privacy) {
+        nm_ip6_config_set_privacy(dst, src_priv->privacy);
+        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_ip6_config_reset_addresses_ndisc(NMIP6Config *         self,
+                                    const NMNDiscAddress *addresses,
+                                    guint                 addresses_n,
+                                    guint8                plen,
+                                    guint32               ifa_flags)
+{
+    NMIP6ConfigPrivate *priv;
+    guint               i;
+    gboolean            changed = FALSE;
+    gint32              base_time_sec;
+
+    g_return_if_fail(NM_IS_IP6_CONFIG(self));
+
+    priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->ifindex > 0);
+
+    /* the base-timestamp doesn't matter it's only an anchor for the
+     * expiry. However, try to re-use the same base-time for a while
+     * by rounding it to 10000 seconds.
+     *
+     * That is because we deduplicate and NMPlatformIP6Address instances
+     * so using the same timestamps is preferable. */
+    base_time_sec = nm_utils_get_monotonic_timestamp_sec();
+    base_time_sec = (base_time_sec / 10000) * 10000;
+    base_time_sec = NM_MAX(1, base_time_sec);
+
+    nm_dedup_multi_index_dirty_set_idx(priv->multi_idx, &priv->idx_ip6_addresses);
+
+    for (i = 0; i < addresses_n; i++) {
+        const NMNDiscAddress *ndisc_addr = &addresses[i];
+        NMPObject             obj;
+        NMPlatformIP6Address *a;
+
+        nmp_object_stackinit(&obj, NMP_OBJECT_TYPE_IP6_ADDRESS, NULL);
+        a              = NMP_OBJECT_CAST_IP6_ADDRESS(&obj);
+        a->ifindex     = priv->ifindex;
+        a->address     = ndisc_addr->address;
+        a->plen        = plen;
+        a->timestamp   = base_time_sec,
+        a->lifetime    = _nm_ndisc_lifetime_from_expiry(((gint64) base_time_sec) * 1000,
+                                                     ndisc_addr->expiry_msec,
+                                                     TRUE),
+        a->preferred   = _nm_ndisc_lifetime_from_expiry(((gint64) base_time_sec) * 1000,
+                                                      ndisc_addr->expiry_preferred_msec,
+                                                      TRUE),
+        a->addr_source = NM_IP_CONFIG_SOURCE_NDISC;
+        a->n_ifa_flags = ifa_flags;
+
+        if (_nm_ip_config_add_obj(priv->multi_idx,
+                                  &priv->idx_ip6_addresses_,
+                                  priv->ifindex,
+                                  &obj,
+                                  NULL,
+                                  FALSE,
+                                  TRUE,
+                                  NULL,
+                                  NULL))
+            changed = TRUE;
+    }
+
+    if (nm_dedup_multi_index_dirty_remove_idx(priv->multi_idx, &priv->idx_ip6_addresses, FALSE) > 0)
+        changed = TRUE;
+
+    if (changed)
+        _notify_addresses(self);
+}
+
+void
+nm_ip6_config_reset_addresses(NMIP6Config *self)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    if (nm_dedup_multi_index_remove_idx(priv->multi_idx, &priv->idx_ip6_addresses) > 0)
+        _notify_addresses(self);
+}
+
+static void
+_add_address(NMIP6Config *self, const NMPObject *obj_new, const NMPlatformIP6Address *new)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    if (_nm_ip_config_add_obj(priv->multi_idx,
+                              &priv->idx_ip6_addresses_,
+                              priv->ifindex,
+                              obj_new,
+                              (const NMPlatformObject *) new,
+                              TRUE,
+                              FALSE,
+                              NULL,
+                              NULL))
+        _notify_addresses(self);
+}
+
+/**
+ * nm_ip6_config_add_address:
+ * @self: the #NMIP6Config
+ * @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_ip6_config_add_address(NMIP6Config *self, const NMPlatformIP6Address *new)
+{
+    g_return_if_fail(self);
+    g_return_if_fail(new);
+    g_return_if_fail(new->plen <= 128);
+    g_return_if_fail(NM_IP6_CONFIG_GET_PRIVATE(self)->ifindex > 0);
+
+    _add_address(self, NULL, new);
+}
+
+void
+_nmtst_ip6_config_del_address(NMIP6Config *self, guint i)
+{
+    const NMPlatformIP6Address *a;
+
+    a = _nmtst_ip6_config_get_address(self, i);
+    if (!nm_ip6_config_nmpobj_remove(self, NMP_OBJECT_UP_CAST(a)))
+        g_assert_not_reached();
+}
+
+guint
+nm_ip6_config_get_num_addresses(const NMIP6Config *self)
+{
+    const NMDedupMultiHeadEntry *head_entry;
+
+    head_entry = nm_ip6_config_lookup_addresses(self);
+    return head_entry ? head_entry->len : 0;
+}
+
+const NMPlatformIP6Address *
+nm_ip6_config_get_first_address(const NMIP6Config *self)
+{
+    NMDedupMultiIter            iter;
+    const NMPlatformIP6Address *a = NULL;
+
+    nm_ip_config_iter_ip6_address_for_each (&iter, self, &a)
+        return a;
+    return NULL;
+}
+
+const NMPlatformIP6Address *
+_nmtst_ip6_config_get_address(const NMIP6Config *self, guint i)
+{
+    NMDedupMultiIter            iter;
+    const NMPlatformIP6Address *a = NULL;
+    guint                       j;
+
+    j = 0;
+    nm_ip_config_iter_ip6_address_for_each (&iter, self, &a) {
+        if (i == j)
+            return a;
+        j++;
+    }
+    g_return_val_if_reached(NULL);
+}
+
+const NMPlatformIP6Address *
+nm_ip6_config_lookup_address(const NMIP6Config *self, const struct in6_addr *addr)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+    NMPObject                 obj_stack;
+    const NMDedupMultiEntry * entry;
+
+    nmp_object_stackinit_id_ip6_address(&obj_stack, priv->ifindex, addr);
+    entry = nm_dedup_multi_index_lookup_obj(priv->multi_idx, &priv->idx_ip6_addresses, &obj_stack);
+    return entry ? NMP_OBJECT_CAST_IP6_ADDRESS(entry->obj) : NULL;
+}
+
+const NMPlatformIP6Address *
+nm_ip6_config_find_first_address(const NMIP6Config *self, NMPlatformMatchFlags match_flag)
+{
+    const NMPlatformIP6Address *addr;
+    NMDedupMultiIter            iter;
+
+    g_return_val_if_fail(NM_IS_IP6_CONFIG(self), NULL);
+
+    nm_assert(!NM_FLAGS_ANY(
+        match_flag,
+        ~(NM_PLATFORM_MATCH_WITH_ADDRTYPE__ANY | NM_PLATFORM_MATCH_WITH_ADDRSTATE__ANY)));
+
+    nm_assert(NM_FLAGS_ANY(match_flag, NM_PLATFORM_MATCH_WITH_ADDRTYPE__ANY));
+    nm_assert(NM_FLAGS_ANY(match_flag, NM_PLATFORM_MATCH_WITH_ADDRSTATE__ANY));
+
+    nm_ip_config_iter_ip6_address_for_each (&iter, self, &addr) {
+        if (nm_platform_ip6_address_match(addr, match_flag))
+            return addr;
+    }
+    return NULL;
+}
+
+/**
+ * nm_ip6_config_has_dad_pending_addresses
+ * @self: configuration containing the addresses to check
+ * @candidates: configuration with the list of addresses we are
+ *   interested in
+ *
+ * Check whether there are addresses with DAD pending in @self, that
+ * are also contained in @candidates.
+ *
+ * Returns: %TRUE if at least one matching address was found, %FALSE
+ *   otherwise
+ */
+gboolean
+nm_ip6_config_has_any_dad_pending(const NMIP6Config *self, const NMIP6Config *candidates)
+{
+    NMDedupMultiIter            ipconf_iter;
+    const NMPlatformIP6Address *addr, *addr_c;
+
+    nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, self, &addr) {
+        if (NM_FLAGS_HAS(addr->n_ifa_flags, IFA_F_TENTATIVE)
+            && !NM_FLAGS_HAS(addr->n_ifa_flags, IFA_F_DADFAILED)
+            && !NM_FLAGS_HAS(addr->n_ifa_flags, IFA_F_OPTIMISTIC)) {
+            addr_c = nm_ip6_config_lookup_address(candidates, &addr->address);
+            if (addr_c) {
+                if (addr->plen == addr_c->plen)
+                    return TRUE;
+            }
+        }
+    }
+
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+static const NMDedupMultiEntry *
+_lookup_route(const NMIP6Config *self, const NMPObject *needle, NMPlatformIPRouteCmpType cmp_type)
+{
+    const NMIP6ConfigPrivate *priv;
+
+    nm_assert(NM_IS_IP6_CONFIG(self));
+    nm_assert(NMP_OBJECT_GET_TYPE(needle) == NMP_OBJECT_TYPE_IP6_ROUTE);
+
+    priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    return _nm_ip_config_lookup_ip_route(priv->multi_idx, &priv->idx_ip6_routes_, needle, cmp_type);
+}
+
+void
+nm_ip6_config_reset_routes_ndisc(NMIP6Config *         self,
+                                 const NMNDiscGateway *gateways,
+                                 guint                 gateways_n,
+                                 const NMNDiscRoute *  routes,
+                                 guint                 routes_n,
+                                 guint32               route_table,
+                                 guint32               route_metric,
+                                 gboolean              kernel_support_rta_pref)
+{
+    NMIP6ConfigPrivate *priv;
+    guint               i;
+    gboolean            changed = FALSE;
+    const NMPObject *   new_best_default_route;
+
+    g_return_if_fail(NM_IS_IP6_CONFIG(self));
+
+    priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->ifindex > 0);
+
+    nm_dedup_multi_index_dirty_set_idx(priv->multi_idx, &priv->idx_ip6_routes);
+
+    new_best_default_route = NULL;
+    for (i = 0; i < routes_n; i++) {
+        const NMNDiscRoute *ndisc_route = &routes[i];
+        NMPObject           obj;
+        const NMPObject *   obj_new;
+        NMPlatformIP6Route *r;
+
+        nmp_object_stackinit(&obj, NMP_OBJECT_TYPE_IP6_ROUTE, NULL);
+        r                = NMP_OBJECT_CAST_IP6_ROUTE(&obj);
+        r->ifindex       = priv->ifindex;
+        r->network       = ndisc_route->network;
+        r->plen          = ndisc_route->plen;
+        r->gateway       = ndisc_route->gateway;
+        r->rt_source     = NM_IP_CONFIG_SOURCE_NDISC;
+        r->table_coerced = nm_platform_route_table_coerce(route_table);
+        r->metric        = route_metric;
+        r->rt_pref       = ndisc_route->preference;
+        nm_assert((NMIcmpv6RouterPref) r->rt_pref == ndisc_route->preference);
+
+        if (_nm_ip_config_add_obj(priv->multi_idx,
+                                  &priv->idx_ip6_routes_,
+                                  priv->ifindex,
+                                  &obj,
+                                  NULL,
+                                  FALSE,
+                                  TRUE,
+                                  NULL,
+                                  &obj_new))
+            changed = TRUE;
+        new_best_default_route =
+            _nm_ip_config_best_default_route_find_better(new_best_default_route, obj_new);
+    }
+
+    if (gateways_n) {
+        const NMPObject *  obj_new;
+        NMPlatformIP6Route r = {
+            .rt_source     = NM_IP_CONFIG_SOURCE_NDISC,
+            .ifindex       = priv->ifindex,
+            .table_coerced = nm_platform_route_table_coerce(route_table),
+            .metric        = route_metric,
+        };
+        const NMIcmpv6RouterPref first_pref = gateways[0].preference;
+
+        for (i = 0; i < gateways_n; i++) {
+            r.gateway = gateways[i].address;
+            r.rt_pref = gateways[i].preference;
+            nm_assert((NMIcmpv6RouterPref) r.rt_pref == gateways[i].preference);
+            if (_nm_ip_config_add_obj(priv->multi_idx,
+                                      &priv->idx_ip6_routes_,
+                                      priv->ifindex,
+                                      NULL,
+                                      (const NMPlatformObject *) &r,
+                                      FALSE,
+                                      TRUE,
+                                      NULL,
+                                      &obj_new))
+                changed = TRUE;
+            new_best_default_route =
+                _nm_ip_config_best_default_route_find_better(new_best_default_route, obj_new);
+
+            if (first_pref != gateways[i].preference && !kernel_support_rta_pref) {
+                /* We are unable to configure a router preference. Hence, we skip all gateways
+                 * with a different preference from the first gateway. Note, that the gateways
+                 * are sorted in order of highest to lowest preference. */
+                break;
+            }
+        }
+    }
+
+    if (nm_dedup_multi_index_dirty_remove_idx(priv->multi_idx, &priv->idx_ip6_routes, FALSE) > 0)
+        changed = TRUE;
+
+    if (nmp_object_ref_set(&priv->best_default_route, new_best_default_route)) {
+        changed = TRUE;
+        _notify(self, PROP_GATEWAY);
+    }
+
+    if (changed)
+        _notify_routes(self);
+}
+
+void
+nm_ip6_config_reset_routes(NMIP6Config *self)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    if (nm_dedup_multi_index_remove_idx(priv->multi_idx, &priv->idx_ip6_routes) > 0) {
+        if (nm_clear_nmp_object(&priv->best_default_route))
+            _notify(self, PROP_GATEWAY);
+        _notify_routes(self);
+    }
+}
+
+static void
+_add_route(NMIP6Config *    self,
+           const NMPObject *obj_new,
+           const NMPlatformIP6Route *new,
+           const NMPObject **out_obj_new)
+{
+    NMIP6ConfigPrivate * priv               = NM_IP6_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_IP6_ROUTE(obj_new)));
+
+    if (_nm_ip_config_add_obj(priv->multi_idx,
+                              &priv->idx_ip6_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_ip6_config_add_route:
+ * @self: the #NMIP6Config
+ * @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_ip6_config_add_route(NMIP6Config *self,
+                        const NMPlatformIP6Route *new,
+                        const NMPObject **out_obj_new)
+{
+    g_return_if_fail(self);
+    g_return_if_fail(new);
+    g_return_if_fail(new->plen <= 128);
+    g_return_if_fail(NM_IP6_CONFIG_GET_PRIVATE(self)->ifindex > 0);
+
+    _add_route(self, NULL, new, out_obj_new);
+}
+
+void
+_nmtst_ip6_config_del_route(NMIP6Config *self, guint i)
+{
+    const NMPlatformIP6Route *r;
+
+    r = _nmtst_ip6_config_get_route(self, i);
+    if (!nm_ip6_config_nmpobj_remove(self, NMP_OBJECT_UP_CAST(r)))
+        g_assert_not_reached();
+}
+
+guint
+nm_ip6_config_get_num_routes(const NMIP6Config *self)
+{
+    const NMDedupMultiHeadEntry *head_entry;
+
+    head_entry = nm_ip6_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 NMPlatformIP6Route *
+_nmtst_ip6_config_get_route(const NMIP6Config *self, guint i)
+{
+    NMDedupMultiIter          iter;
+    const NMPlatformIP6Route *r = NULL;
+    guint                     j;
+
+    j = 0;
+    nm_ip_config_iter_ip6_route_for_each (&iter, self, &r) {
+        if (i == j)
+            return r;
+        j++;
+    }
+    g_return_val_if_reached(NULL);
+}
+
+const NMPlatformIP6Route *
+nm_ip6_config_get_direct_route_for_host(const NMIP6Config *    self,
+                                        const struct in6_addr *host,
+                                        guint32                route_table)
+{
+    const NMPlatformIP6Route *best_route = NULL;
+    const NMPlatformIP6Route *item;
+    NMDedupMultiIter          ipconf_iter;
+
+    g_return_val_if_fail(host && !IN6_IS_ADDR_UNSPECIFIED(host), NULL);
+
+    nm_ip_config_iter_ip6_route_for_each (&ipconf_iter, self, &item) {
+        if (!IN6_IS_ADDR_UNSPECIFIED(&item->gateway))
+            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_ip6_address_same_prefix(host, &item->network, item->plen))
+            continue;
+
+        if (best_route && best_route->metric <= item->metric)
+            continue;
+
+        best_route = item;
+    }
+    return best_route;
+}
+
+const NMPlatformIP6Address *
+nm_ip6_config_get_subnet_for_host(const NMIP6Config *self, const struct in6_addr *host)
+{
+    NMDedupMultiIter            iter;
+    const NMPlatformIP6Address *item;
+    const NMPlatformIP6Address *subnet = NULL;
+    struct in6_addr             subnet2, host2;
+
+    g_return_val_if_fail(host && !IN6_IS_ADDR_UNSPECIFIED(host), NULL);
+
+    nm_ip_config_iter_ip6_address_for_each (&iter, self, &item) {
+        if (subnet && subnet->plen >= item->plen)
+            continue;
+
+        nm_utils_ip6_address_clear_host_address(&host2, host, item->plen);
+        nm_utils_ip6_address_clear_host_address(&subnet2, &item->address, item->plen);
+
+        if (IN6_ARE_ADDR_EQUAL(&subnet2, &host2))
+            subnet = item;
+    }
+
+    return subnet;
+}
+
+/*****************************************************************************/
+
+void
+nm_ip6_config_reset_nameservers(NMIP6Config *self)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    if (priv->nameservers->len != 0) {
+        g_array_set_size(priv->nameservers, 0);
+        _notify(self, PROP_NAMESERVERS);
+    }
+}
+
+void
+nm_ip6_config_add_nameserver(NMIP6Config *self, const struct in6_addr *new)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+    int                 i;
+
+    g_return_if_fail(new != NULL);
+
+    for (i = 0; i < priv->nameservers->len; i++)
+        if (IN6_ARE_ADDR_EQUAL(new, &g_array_index(priv->nameservers, struct in6_addr, i)))
+            return;
+
+    g_array_append_val(priv->nameservers, *new);
+    _notify(self, PROP_NAMESERVERS);
+}
+
+void
+nm_ip6_config_del_nameserver(NMIP6Config *self, guint i)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    g_return_if_fail(i < priv->nameservers->len);
+
+    g_array_remove_index(priv->nameservers, i);
+    _notify(self, PROP_NAMESERVERS);
+}
+
+guint
+nm_ip6_config_get_num_nameservers(const NMIP6Config *self)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    return priv->nameservers->len;
+}
+
+const struct in6_addr *
+nm_ip6_config_get_nameserver(const NMIP6Config *self, guint i)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    return &g_array_index(priv->nameservers, struct in6_addr, i);
+}
+
+/*****************************************************************************/
+
+void
+nm_ip6_config_reset_domains(NMIP6Config *self)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    if (priv->domains->len != 0) {
+        g_ptr_array_set_size(priv->domains, 0);
+        _notify(self, PROP_DOMAINS);
+    }
+}
+
+void
+nm_ip6_config_add_domain(NMIP6Config *self, const char *domain)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    if (_nm_ip_config_check_and_add_domain(priv->domains, domain))
+        _notify(self, PROP_DOMAINS);
+}
+
+void
+nm_ip6_config_del_domain(NMIP6Config *self, guint i)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_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_ip6_config_get_num_domains(const NMIP6Config *self)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    return priv->domains->len;
+}
+
+const char *
+nm_ip6_config_get_domain(const NMIP6Config *self, guint i)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    return g_ptr_array_index(priv->domains, i);
+}
+
+/*****************************************************************************/
+
+void
+nm_ip6_config_reset_searches(NMIP6Config *self)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    if (priv->searches->len != 0) {
+        g_ptr_array_set_size(priv->searches, 0);
+        _notify(self, PROP_SEARCHES);
+    }
+}
+
+void
+nm_ip6_config_add_search(NMIP6Config *self, const char *search)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    if (_nm_ip_config_check_and_add_domain(priv->searches, search))
+        _notify(self, PROP_SEARCHES);
+}
+
+void
+nm_ip6_config_del_search(NMIP6Config *self, guint i)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_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_ip6_config_get_num_searches(const NMIP6Config *self)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    return priv->searches->len;
+}
+
+const char *
+nm_ip6_config_get_search(const NMIP6Config *self, guint i)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    return g_ptr_array_index(priv->searches, i);
+}
+
+/*****************************************************************************/
+
+void
+nm_ip6_config_reset_dns_options(NMIP6Config *self)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_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_ip6_config_add_dns_option(NMIP6Config *self, const char *new)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_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_ip6_config_del_dns_option(NMIP6Config *self, guint i)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_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_ip6_config_get_num_dns_options(const NMIP6Config *self)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    return priv->dns_options->len;
+}
+
+const char *
+nm_ip6_config_get_dns_option(const NMIP6Config *self, guint i)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    return g_ptr_array_index(priv->dns_options, i);
+}
+
+/*****************************************************************************/
+
+NMIPConfigFlags
+nm_ip6_config_get_config_flags(const NMIP6Config *self)
+{
+    return NM_IP6_CONFIG_GET_PRIVATE(self)->config_flags;
+}
+
+void
+nm_ip6_config_set_config_flags(NMIP6Config *self, NMIPConfigFlags flags, NMIPConfigFlags mask)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_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_ip6_config_set_dns_priority(NMIP6Config *self, int priority)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    if (priority != priv->dns_priority) {
+        priv->dns_priority = priority;
+        _notify(self, PROP_DNS_PRIORITY);
+    }
+}
+
+int
+nm_ip6_config_get_dns_priority(const NMIP6Config *self)
+{
+    const NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    return priv->dns_priority;
+}
+
+/*****************************************************************************/
+
+const NMPObject *
+nm_ip6_config_nmpobj_lookup(const NMIP6Config *self, const NMPObject *needle)
+{
+    const NMIP6ConfigPrivate * priv;
+    const NMDedupMultiIdxType *idx_type;
+
+    g_return_val_if_fail(NM_IS_IP6_CONFIG(self), NULL);
+
+    priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+    switch (NMP_OBJECT_GET_TYPE(needle)) {
+    case NMP_OBJECT_TYPE_IP6_ADDRESS:
+        idx_type = &priv->idx_ip6_addresses;
+        break;
+    case NMP_OBJECT_TYPE_IP6_ROUTE:
+        idx_type = &priv->idx_ip6_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_ip6_config_nmpobj_remove(NMIP6Config *self, const NMPObject *needle)
+{
+    NMIP6ConfigPrivate * priv;
+    NMDedupMultiIdxType *idx_type;
+    nm_auto_nmpobj const NMPObject *obj_old = NULL;
+    guint                           n;
+
+    g_return_val_if_fail(NM_IS_IP6_CONFIG(self), FALSE);
+
+    priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+    switch (NMP_OBJECT_GET_TYPE(needle)) {
+    case NMP_OBJECT_TYPE_IP6_ADDRESS:
+        idx_type = &priv->idx_ip6_addresses;
+        break;
+    case NMP_OBJECT_TYPE_IP6_ROUTE:
+        idx_type = &priv->idx_ip6_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_IP6_ADDRESS:
+        _notify_addresses(self);
+        break;
+    case NMP_OBJECT_TYPE_IP6_ROUTE:
+        if (priv->best_default_route == obj_old) {
+            if (nmp_object_ref_set(&priv->best_default_route,
+                                   _nm_ip6_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));
+}
+
+static void
+hash_in6addr(GChecksum *sum, const struct in6_addr *a)
+{
+    if (a)
+        g_checksum_update(sum, (const guint8 *) a, sizeof(*a));
+    else
+        g_checksum_update(sum, (const guint8 *) &in6addr_any, sizeof(in6addr_any));
+}
+
+void
+nm_ip6_config_hash(const NMIP6Config *self, GChecksum *sum, gboolean dns_only)
+{
+    guint32                     i;
+    const char *                s;
+    NMDedupMultiIter            ipconf_iter;
+    const NMPlatformIP6Address *address;
+    const NMPlatformIP6Route *  route;
+
+    g_return_if_fail(self);
+    g_return_if_fail(sum);
+
+    if (dns_only == FALSE) {
+        nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, self, &address) {
+            hash_in6addr(sum, &address->address);
+            hash_u32(sum, address->plen);
+        }
+
+        nm_ip_config_iter_ip6_route_for_each (&ipconf_iter, self, &route) {
+            hash_in6addr(sum, &route->network);
+            hash_u32(sum, route->plen);
+            hash_in6addr(sum, &route->gateway);
+            hash_u32(sum, route->metric);
+        }
+    }
+
+    for (i = 0; i < nm_ip6_config_get_num_nameservers(self); i++)
+        hash_in6addr(sum, nm_ip6_config_get_nameserver(self, i));
+
+    for (i = 0; i < nm_ip6_config_get_num_domains(self); i++) {
+        s = nm_ip6_config_get_domain(self, i);
+        g_checksum_update(sum, (const guint8 *) s, strlen(s));
+    }
+
+    for (i = 0; i < nm_ip6_config_get_num_searches(self); i++) {
+        s = nm_ip6_config_get_search(self, i);
+        g_checksum_update(sum, (const guint8 *) s, strlen(s));
+    }
+
+    for (i = 0; i < nm_ip6_config_get_num_dns_options(self); i++) {
+        s = nm_ip6_config_get_dns_option(self, i);
+        g_checksum_update(sum, (const guint8 *) s, strlen(s));
+    }
+}
+
+/**
+ * nm_ip6_config_equal:
+ * @a: first config to compare
+ * @b: second config to compare
+ *
+ * Compares two #NMIP6Configs 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_ip6_config_equal(const NMIP6Config *a, const NMIP6Config *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_ip6_config_hash(a, a_checksum, FALSE);
+    if (b)
+        nm_ip6_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
+nameservers_to_gvalue(GArray *array, GValue *value)
+{
+    GVariantBuilder builder;
+    guint           i = 0;
+
+    g_variant_builder_init(&builder, G_VARIANT_TYPE("aay"));
+
+    while (array && (i < array->len)) {
+        struct in6_addr *addr;
+
+        addr = &g_array_index(array, struct in6_addr, i++);
+        g_variant_builder_add(&builder,
+                              "@ay",
+                              g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, addr, 16, 1));
+    }
+
+    g_value_take_variant(value, g_variant_builder_end(&builder));
+}
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMIP6Config *       self = NM_IP6_CONFIG(object);
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    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_INET6,
+                                          nm_ip6_config_lookup_addresses(self),
+                                          priv->best_default_route,
+                                          priv->privacy,
+                                          &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_INET6,
+                                       nm_ip6_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_inet6_ntop_dup(
+                                    &NMP_OBJECT_CAST_IP6_ROUTE(priv->best_default_route)->gateway));
+        } else
+            g_value_set_string(value, NULL);
+        break;
+    case PROP_NAMESERVERS:
+        nameservers_to_gvalue(priv->nameservers, value);
+        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;
+    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)
+{
+    NMIP6Config *       self = NM_IP6_CONFIG(object);
+    NMIP6ConfigPrivate *priv = NM_IP6_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_ip6_config_init(NMIP6Config *self)
+{
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    nm_ip_config_dedup_multi_idx_type_init((NMIPConfigDedupMultiIdxType *) &priv->idx_ip6_addresses,
+                                           NMP_OBJECT_TYPE_IP6_ADDRESS);
+    nm_ip_config_dedup_multi_idx_type_init((NMIPConfigDedupMultiIdxType *) &priv->idx_ip6_routes,
+                                           NMP_OBJECT_TYPE_IP6_ROUTE);
+
+    priv->nameservers = g_array_new(FALSE, TRUE, sizeof(struct in6_addr));
+    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);
+}
+
+NMIP6Config *
+nm_ip6_config_new(NMDedupMultiIndex *multi_idx, int ifindex)
+{
+    g_return_val_if_fail(ifindex >= -1, NULL);
+    return g_object_new(NM_TYPE_IP6_CONFIG,
+                        NM_IP6_CONFIG_MULTI_IDX,
+                        multi_idx,
+                        NM_IP6_CONFIG_IFINDEX,
+                        ifindex,
+                        NULL);
+}
+
+NMIP6Config *
+nm_ip6_config_new_cloned(const NMIP6Config *src)
+{
+    NMIP6Config *new;
+
+    g_return_val_if_fail(NM_IS_IP6_CONFIG(src), NULL);
+
+    new = nm_ip6_config_new(nm_ip6_config_get_multi_idx(src), nm_ip6_config_get_ifindex(src));
+    nm_ip6_config_replace(new, src, NULL);
+    return new;
+}
+
+static void
+finalize(GObject *object)
+{
+    NMIP6Config *       self = NM_IP6_CONFIG(object);
+    NMIP6ConfigPrivate *priv = NM_IP6_CONFIG_GET_PRIVATE(self);
+
+    nm_clear_nmp_object(&priv->best_default_route);
+
+    nm_dedup_multi_index_remove_idx(priv->multi_idx, &priv->idx_ip6_addresses);
+    nm_dedup_multi_index_remove_idx(priv->multi_idx, &priv->idx_ip6_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_OBJECT_CLASS(nm_ip6_config_parent_class)->finalize(object);
+
+    nm_dedup_multi_index_unref(priv->multi_idx);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_ip6_config = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_IP6_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",
+                                                             "a(ayuay)",
+                                                             NM_IP6_CONFIG_ADDRESSES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("AddressData",
+                                                             "aa{sv}",
+                                                             NM_IP6_CONFIG_ADDRESS_DATA),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Gateway", "s", NM_IP6_CONFIG_GATEWAY),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Routes",
+                                                             "a(ayuayu)",
+                                                             NM_IP6_CONFIG_ROUTES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("RouteData",
+                                                             "aa{sv}",
+                                                             NM_IP6_CONFIG_ROUTE_DATA),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Nameservers",
+                                                             "aay",
+                                                             NM_IP6_CONFIG_NAMESERVERS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Domains",
+                                                             "as",
+                                                             NM_IP6_CONFIG_DOMAINS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Searches",
+                                                             "as",
+                                                             NM_IP6_CONFIG_SEARCHES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("DnsOptions",
+                                                             "as",
+                                                             NM_IP6_CONFIG_DNS_OPTIONS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("DnsPriority",
+                                                             "i",
+                                                             NM_IP6_CONFIG_DNS_PRIORITY), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_ip6_config_class_init(NMIP6ConfigClass *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     = FALSE;
+    ip_config_class->addr_family = AF_INET6;
+
+    dbus_object_class->export_path     = NM_DBUS_EXPORT_PATH_NUMBERED(NM_DBUS_PATH "/IP6Config");
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_ip6_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_IP6_CONFIG_MULTI_IDX,
+                             "",
+                             "",
+                             G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_IFINDEX] =
+        g_param_spec_int(NM_IP6_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_IP6_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_IP6_CONFIG_ADDRESSES,
+                             "",
+                             "",
+                             G_VARIANT_TYPE("a(ayuay)"),
+                             NULL,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_ROUTE_DATA] =
+        g_param_spec_variant(NM_IP6_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_IP6_CONFIG_ROUTES,
+                                                       "",
+                                                       "",
+                                                       G_VARIANT_TYPE("a(ayuayu)"),
+                                                       NULL,
+                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_GATEWAY] = g_param_spec_string(NM_IP6_CONFIG_GATEWAY,
+                                                       "",
+                                                       "",
+                                                       NULL,
+                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_NAMESERVERS] =
+        g_param_spec_variant(NM_IP6_CONFIG_NAMESERVERS,
+                             "",
+                             "",
+                             G_VARIANT_TYPE("aay"),
+                             NULL,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_DOMAINS]  = g_param_spec_boxed(NM_IP6_CONFIG_DOMAINS,
+                                                      "",
+                                                      "",
+                                                      G_TYPE_STRV,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_SEARCHES] = g_param_spec_boxed(NM_IP6_CONFIG_SEARCHES,
+                                                       "",
+                                                       "",
+                                                       G_TYPE_STRV,
+                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_DNS_OPTIONS] =
+        g_param_spec_boxed(NM_IP6_CONFIG_DNS_OPTIONS,
+                           "",
+                           "",
+                           G_TYPE_STRV,
+                           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_DNS_PRIORITY] = g_param_spec_int(NM_IP6_CONFIG_DNS_PRIORITY,
+                                                         "",
+                                                         "",
+                                                         G_MININT32,
+                                                         G_MAXINT32,
+                                                         0,
+                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}