summary refs log tree commit diff
path: root/src/platform/nmp-object.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/platform/nmp-object.c')
-rw-r--r--src/platform/nmp-object.c2461
1 files changed, 1419 insertions, 1042 deletions
diff --git a/src/platform/nmp-object.c b/src/platform/nmp-object.c
index ecec8f0f..a8600705 100644
--- a/src/platform/nmp-object.c
+++ b/src/platform/nmp-object.c
@@ -51,6 +51,11 @@
 
 /*****************************************************************************/
 
+typedef struct {
+	NMDedupMultiIdxType parent;
+	NMPCacheIdType cache_id_type;
+} DedupMultiIdxType;
+
 struct _NMPCache {
 	/* the cache contains only one hash table for all object types, and similarly
 	 * it contains only one NMMultiIndex.
@@ -66,23 +71,238 @@ struct _NMPCache {
 	 * This effectively merges the udev-device cache into the NMPCache.
 	 */
 
-	GHashTable *idx_main;
-	NMMultiIndex *idx_multi;
+	NMDedupMultiIndex *multi_idx;
+
+	/* an idx_type entry for each NMP_CACHE_ID_TYPE. Note that NONE (zero)
+	 * is skipped, so the index is shifted by one: idx_type[cache_id_type - 1].
+	 *
+	 * Don't bother, use _idx_type_get() instead! */
+	DedupMultiIdxType idx_types[NMP_CACHE_ID_TYPE_MAX];
 
 	gboolean use_udev;
 };
 
 /*****************************************************************************/
 
-static inline guint
-_id_hash_ip6_addr (const struct in6_addr *addr)
+static const NMDedupMultiIdxTypeClass _dedup_multi_idx_type_class;
+
+static void
+_idx_obj_id_hash_update (const NMDedupMultiIdxType *idx_type,
+                         const NMDedupMultiObj *obj,
+                         NMHashState *h)
 {
-	guint hash = (guint) 0x897da53981a13ULL;
-	int i;
+	const NMPObject *o = (NMPObject *) obj;
+
+	nm_assert (idx_type && idx_type->klass == &_dedup_multi_idx_type_class);
+	nm_assert (NMP_OBJECT_GET_TYPE (o) != NMP_OBJECT_TYPE_UNKNOWN);
+
+	nmp_object_id_hash_update (o, h);
+}
+
+static gboolean
+_idx_obj_id_equal (const NMDedupMultiIdxType *idx_type,
+                   const NMDedupMultiObj *obj_a,
+                   const NMDedupMultiObj *obj_b)
+{
+	const NMPObject *o_a = (NMPObject *) obj_a;
+	const NMPObject *o_b = (NMPObject *) obj_b;
+
+	nm_assert (idx_type && idx_type->klass == &_dedup_multi_idx_type_class);
+	nm_assert (NMP_OBJECT_GET_TYPE (o_a) != NMP_OBJECT_TYPE_UNKNOWN);
+	nm_assert (NMP_OBJECT_GET_TYPE (o_b) != NMP_OBJECT_TYPE_UNKNOWN);
 
-	for (i = 0; i < sizeof (*addr); i++)
-		hash = (hash * 33) + ((const guint8 *) addr)[i];
-	return hash;
+	return nmp_object_id_equal (o_a, o_b);
+}
+
+static guint
+_idx_obj_part (const DedupMultiIdxType *idx_type,
+               const NMPObject *obj_a,
+               const NMPObject *obj_b,
+               NMHashState *h)
+{
+	NMPObjectType obj_type;
+
+	/* the hash/equals functions are strongly related. So, keep them
+	 * side-by-side and do it all in _idx_obj_part(). */
+
+	nm_assert (idx_type);
+	nm_assert (idx_type->parent.klass == &_dedup_multi_idx_type_class);
+	nm_assert (obj_a);
+	nm_assert (NMP_OBJECT_GET_TYPE (obj_a) != NMP_OBJECT_TYPE_UNKNOWN);
+	nm_assert (!obj_b || (NMP_OBJECT_GET_TYPE (obj_b) != NMP_OBJECT_TYPE_UNKNOWN));
+	nm_assert (!h || !obj_b);
+
+	switch (idx_type->cache_id_type) {
+
+	case NMP_CACHE_ID_TYPE_OBJECT_TYPE:
+		if (obj_b)
+			return NMP_OBJECT_GET_TYPE (obj_a) == NMP_OBJECT_GET_TYPE (obj_b);
+		if (h) {
+			nm_hash_update_vals (h,
+			                     idx_type->cache_id_type,
+			                     NMP_OBJECT_GET_TYPE (obj_a));
+		}
+		return 1;
+
+	case NMP_CACHE_ID_TYPE_LINK_BY_IFNAME:
+		if (NMP_OBJECT_GET_TYPE (obj_a) != NMP_OBJECT_TYPE_LINK) {
+			/* first check, whether obj_a is suitable for this idx_type.
+			 * If not, return 0 (which is correct for partitionable(), hash() and equal()
+			 * functions. */
+			if (h)
+				nm_hash_update_val (h, obj_a);
+			return 0;
+		}
+		if (obj_b) {
+			/* we are in equal() mode. Compare obj_b with obj_a. */
+			return    NMP_OBJECT_GET_TYPE (obj_b) == NMP_OBJECT_TYPE_LINK
+			       && nm_streq (obj_a->link.name, obj_b->link.name);
+		}
+		if (h) {
+			nm_hash_update_val (h, idx_type->cache_id_type);
+			nm_hash_update_strarr (h, obj_a->link.name);
+		}
+		/* just return 1, to indicate that obj_a is partitionable by this idx_type. */
+		return 1;
+
+	case NMP_CACHE_ID_TYPE_DEFAULT_ROUTES:
+		if (   !NM_IN_SET (NMP_OBJECT_GET_TYPE (obj_a), NMP_OBJECT_TYPE_IP4_ROUTE,
+		                                                NMP_OBJECT_TYPE_IP6_ROUTE)
+		    || !NM_PLATFORM_IP_ROUTE_IS_DEFAULT (&obj_a->ip_route)
+		    || !nmp_object_is_visible (obj_a)) {
+			if (h)
+				nm_hash_update_val (h, obj_a);
+			return 0;
+		}
+		if (obj_b) {
+			return    NMP_OBJECT_GET_TYPE (obj_a) == NMP_OBJECT_GET_TYPE (obj_b)
+			       && NM_PLATFORM_IP_ROUTE_IS_DEFAULT (&obj_b->ip_route)
+			       && nmp_object_is_visible (obj_b);
+		}
+		if (h) {
+			nm_hash_update_vals (h,
+			                     idx_type->cache_id_type,
+			                     NMP_OBJECT_GET_TYPE (obj_a));
+		}
+		return 1;
+
+	case NMP_CACHE_ID_TYPE_ADDRROUTE_BY_IFINDEX:
+		if (   !NM_IN_SET (NMP_OBJECT_GET_TYPE (obj_a), NMP_OBJECT_TYPE_IP4_ADDRESS,
+		                                                NMP_OBJECT_TYPE_IP6_ADDRESS,
+		                                                NMP_OBJECT_TYPE_IP4_ROUTE,
+		                                                NMP_OBJECT_TYPE_IP6_ROUTE)
+		    || !nmp_object_is_visible (obj_a)) {
+			if (h)
+				nm_hash_update_val (h, obj_a);
+			return 0;
+		}
+		nm_assert (obj_a->object.ifindex > 0);
+		if (obj_b) {
+			return    NMP_OBJECT_GET_TYPE (obj_a) == NMP_OBJECT_GET_TYPE (obj_b)
+			       && obj_a->object.ifindex == obj_b->object.ifindex
+			       && nmp_object_is_visible (obj_b);
+		}
+		if (h) {
+			nm_hash_update_vals (h,
+			                     idx_type->cache_id_type,
+			                     obj_a->object.ifindex);
+		}
+		return 1;
+
+	case NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID:
+		obj_type = NMP_OBJECT_GET_TYPE (obj_a);
+		if (   !NM_IN_SET (obj_type, NMP_OBJECT_TYPE_IP4_ROUTE,
+		                             NMP_OBJECT_TYPE_IP6_ROUTE)
+		    || obj_a->object.ifindex <= 0) {
+			if (h)
+				nm_hash_update_val (h, obj_a);
+			return 0;
+		}
+		if (obj_b) {
+			return    obj_type == NMP_OBJECT_GET_TYPE (obj_b)
+			       && obj_b->object.ifindex > 0
+			       && (obj_type == NMP_OBJECT_TYPE_IP4_ROUTE
+			           ? (nm_platform_ip4_route_cmp (&obj_a->ip4_route, &obj_b->ip4_route, NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID) == 0)
+			           : (nm_platform_ip6_route_cmp (&obj_a->ip6_route, &obj_b->ip6_route, NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID) == 0));
+		}
+		if (h) {
+			nm_hash_update_val (h, idx_type->cache_id_type);
+			if (obj_type == NMP_OBJECT_TYPE_IP4_ROUTE)
+				nm_platform_ip4_route_hash_update (&obj_a->ip4_route, NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID, h);
+			else
+				nm_platform_ip6_route_hash_update (&obj_a->ip6_route, NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID, h);
+		}
+		return 1;
+
+	case NMP_CACHE_ID_TYPE_NONE:
+	case __NMP_CACHE_ID_TYPE_MAX:
+		break;
+	}
+	nm_assert_not_reached ();
+	return 0;
+}
+
+static gboolean
+_idx_obj_partitionable (const NMDedupMultiIdxType *idx_type,
+                        const NMDedupMultiObj *obj)
+{
+	return _idx_obj_part ((DedupMultiIdxType *) idx_type,
+	                      (NMPObject *) obj,
+	                      NULL,
+	                      NULL) != 0;
+}
+
+static void
+_idx_obj_partition_hash_update (const NMDedupMultiIdxType *idx_type,
+                                const NMDedupMultiObj *obj,
+                                NMHashState *h)
+{
+	_idx_obj_part ((DedupMultiIdxType *) idx_type,
+	               (NMPObject *) obj,
+	               NULL,
+	               h);
+}
+
+static gboolean
+_idx_obj_partition_equal (const NMDedupMultiIdxType *idx_type,
+                          const NMDedupMultiObj *obj_a,
+                          const NMDedupMultiObj *obj_b)
+{
+	return _idx_obj_part ((DedupMultiIdxType *) idx_type,
+	                      (NMPObject *) obj_a,
+	                      (NMPObject *) obj_b,
+	                      NULL);
+}
+
+static const NMDedupMultiIdxTypeClass _dedup_multi_idx_type_class = {
+	.idx_obj_id_hash_update = _idx_obj_id_hash_update,
+	.idx_obj_id_equal = _idx_obj_id_equal,
+	.idx_obj_partitionable = _idx_obj_partitionable,
+	.idx_obj_partition_hash_update = _idx_obj_partition_hash_update,
+	.idx_obj_partition_equal = _idx_obj_partition_equal,
+};
+
+static void
+_dedup_multi_idx_type_init (DedupMultiIdxType *idx_type, NMPCacheIdType cache_id_type)
+{
+	nm_dedup_multi_idx_type_init ((NMDedupMultiIdxType *) idx_type,
+	                              &_dedup_multi_idx_type_class);
+	idx_type->cache_id_type = cache_id_type;
+}
+
+/*****************************************************************************/
+
+static void
+_vlan_xgress_qos_mappings_hash_update (guint n_map,
+                                       const NMVlanQosMapping *map,
+                                       NMHashState *h)
+{
+	/* ensure no padding. */
+	G_STATIC_ASSERT (sizeof (NMVlanQosMapping) == 2 * sizeof (guint32));
+
+	nm_hash_update_val (h, n_map);
+	if (n_map)
+		nm_hash_update (h, map, n_map * sizeof (*map));
 }
 
 static int
@@ -150,12 +370,18 @@ _link_get_driver (struct udev_device *udevice, const char *kind, int ifindex)
 }
 
 void
-_nmp_object_fixup_link_udev_fields (NMPObject *obj, gboolean use_udev)
+_nmp_object_fixup_link_udev_fields (NMPObject **obj_new, NMPObject *obj_orig, gboolean use_udev)
 {
 	const char *driver = NULL;
 	gboolean initialized = FALSE;
+	NMPObject *obj;
 
-	nm_assert (NMP_OBJECT_GET_TYPE (obj) == NMP_OBJECT_TYPE_LINK);
+	nm_assert (obj_orig || *obj_new);
+	nm_assert (obj_new);
+	nm_assert (!obj_orig || NMP_OBJECT_GET_TYPE (obj_orig) == NMP_OBJECT_TYPE_LINK);
+	nm_assert (!*obj_new || NMP_OBJECT_GET_TYPE (*obj_new) == NMP_OBJECT_TYPE_LINK);
+
+	obj = *obj_new ?: obj_orig;
 
 	/* The link contains internal fields that are combined by
 	 * properties from netlink and udev. Update those properties */
@@ -179,17 +405,34 @@ _nmp_object_fixup_link_udev_fields (NMPObject *obj, gboolean use_udev)
 		}
 	}
 
+	if (   nm_streq0 (obj->link.driver, driver)
+	    && obj->link.initialized == initialized)
+		return;
+
+	if (!*obj_new)
+		obj = *obj_new = nmp_object_clone (obj, FALSE);
+
 	obj->link.driver = driver;
 	obj->link.initialized = initialized;
 }
 
 static void
-_nmp_object_fixup_link_master_connected (NMPObject *obj, const NMPCache *cache)
+_nmp_object_fixup_link_master_connected (NMPObject **obj_new, NMPObject *obj_orig, const NMPCache *cache)
 {
-	nm_assert (NMP_OBJECT_GET_TYPE (obj) == NMP_OBJECT_TYPE_LINK);
+	NMPObject *obj;
+
+	nm_assert (obj_orig || *obj_new);
+	nm_assert (obj_new);
+	nm_assert (!obj_orig || NMP_OBJECT_GET_TYPE (obj_orig) == NMP_OBJECT_TYPE_LINK);
+	nm_assert (!*obj_new || NMP_OBJECT_GET_TYPE (*obj_new) == NMP_OBJECT_TYPE_LINK);
+
+	obj = *obj_new ?: obj_orig;
 
-	if (nmp_cache_link_connected_needs_toggle (cache, obj, NULL, NULL))
+	if (nmp_cache_link_connected_needs_toggle (cache, obj, NULL, NULL)) {
+		if (!*obj_new)
+			obj = *obj_new = nmp_object_clone (obj, FALSE);
 		obj->link.connected = !obj->link.connected;
+	}
 }
 
 /*****************************************************************************/
@@ -204,33 +447,6 @@ nmp_class_from_type (NMPObjectType obj_type)
 
 /*****************************************************************************/
 
-NMPObject *
-nmp_object_ref (NMPObject *obj)
-{
-	g_return_val_if_fail (NMP_OBJECT_IS_VALID (obj), NULL);
-	g_return_val_if_fail (obj->_ref_count != NMP_REF_COUNT_STACKINIT, NULL);
-	obj->_ref_count++;
-
-	return obj;
-}
-
-void
-nmp_object_unref (NMPObject *obj)
-{
-	if (obj) {
-		g_return_if_fail (obj->_ref_count > 0);
-		g_return_if_fail (obj->_ref_count != NMP_REF_COUNT_STACKINIT);
-		if (--obj->_ref_count <= 0) {
-			const NMPClass *klass = obj->_class;
-
-			nm_assert (!obj->is_cached);
-			if (klass->cmd_obj_dispose)
-				klass->cmd_obj_dispose (obj);
-			g_slice_free1 (klass->sizeof_data + G_STRUCT_OFFSET (NMPObject, object), obj);
-		}
-	}
-}
-
 static void
 _vt_cmd_obj_dispose_link (NMPObject *obj)
 {
@@ -259,7 +475,7 @@ _nmp_object_new_from_class (const NMPClass *klass)
 
 	obj = g_slice_alloc0 (klass->sizeof_data + G_STRUCT_OFFSET (NMPObject, object));
 	obj->_class = klass;
-	obj->_ref_count = 1;
+	obj->parent._ref_count = 1;
 	return obj;
 }
 
@@ -287,14 +503,29 @@ nmp_object_new_link (int ifindex)
 
 /*****************************************************************************/
 
-static const NMPObject *
+static void
 _nmp_object_stackinit_from_class (NMPObject *obj, const NMPClass *klass)
 {
+	nm_assert (obj);
+	nm_assert (klass);
+
+	memset (obj, 0, sizeof (NMPObject));
+	obj->_class = klass;
+	obj->parent._ref_count = NM_OBJ_REF_COUNT_STACKINIT;
+}
+
+static NMPObject *
+_nmp_object_stackinit_from_type (NMPObject *obj, NMPObjectType obj_type)
+{
+	const NMPClass *klass;
+
+	nm_assert (obj);
+	klass = nmp_class_from_type (obj_type);
 	nm_assert (klass);
 
 	memset (obj, 0, sizeof (NMPObject));
 	obj->_class = klass;
-	obj->_ref_count = NMP_REF_COUNT_STACKINIT;
+	obj->parent._ref_count = NM_OBJ_REF_COUNT_STACKINIT;
 	return obj;
 }
 
@@ -318,31 +549,24 @@ nmp_object_stackinit_id  (NMPObject *obj, const NMPObject *src)
 	nm_assert (obj);
 
 	klass = NMP_OBJECT_GET_CLASS (src);
-	if (!klass->cmd_obj_stackinit_id)
-		nmp_object_stackinit (obj, klass->obj_type, NULL);
-	else
-		klass->cmd_obj_stackinit_id (obj, src);
+	_nmp_object_stackinit_from_class (obj, klass);
+	if (klass->cmd_plobj_id_copy)
+		klass->cmd_plobj_id_copy (&obj->object, &src->object);
 	return obj;
 }
 
 const NMPObject *
 nmp_object_stackinit_id_link (NMPObject *obj, int ifindex)
 {
-	nmp_object_stackinit (obj, NMP_OBJECT_TYPE_LINK, NULL);
+	_nmp_object_stackinit_from_type (obj, NMP_OBJECT_TYPE_LINK);
 	obj->link.ifindex = ifindex;
 	return obj;
 }
 
-static void
-_vt_cmd_obj_stackinit_id_link (NMPObject *obj, const NMPObject *src)
-{
-	nmp_object_stackinit_id_link (obj, src->link.ifindex);
-}
-
 const NMPObject *
 nmp_object_stackinit_id_ip4_address (NMPObject *obj, int ifindex, guint32 address, guint8 plen, guint32 peer_address)
 {
-	nmp_object_stackinit (obj, NMP_OBJECT_TYPE_IP4_ADDRESS, NULL);
+	_nmp_object_stackinit_from_type (obj, NMP_OBJECT_TYPE_IP4_ADDRESS);
 	obj->ip4_address.ifindex = ifindex;
 	obj->ip4_address.address = address;
 	obj->ip4_address.plen = plen;
@@ -350,64 +574,16 @@ nmp_object_stackinit_id_ip4_address (NMPObject *obj, int ifindex, guint32 addres
 	return obj;
 }
 
-static void
-_vt_cmd_obj_stackinit_id_ip4_address (NMPObject *obj, const NMPObject *src)
-{
-	nmp_object_stackinit_id_ip4_address (obj, src->ip_address.ifindex, src->ip4_address.address, src->ip_address.plen, src->ip4_address.peer_address);
-}
-
 const NMPObject *
-nmp_object_stackinit_id_ip6_address (NMPObject *obj, int ifindex, const struct in6_addr *address, guint8 plen)
+nmp_object_stackinit_id_ip6_address (NMPObject *obj, int ifindex, const struct in6_addr *address)
 {
-	nmp_object_stackinit (obj, NMP_OBJECT_TYPE_IP6_ADDRESS, NULL);
+	_nmp_object_stackinit_from_type (obj, NMP_OBJECT_TYPE_IP6_ADDRESS);
 	obj->ip4_address.ifindex = ifindex;
 	if (address)
 		obj->ip6_address.address = *address;
-	obj->ip6_address.plen = plen;
 	return obj;
 }
 
-static void
-_vt_cmd_obj_stackinit_id_ip6_address (NMPObject *obj, const NMPObject *src)
-{
-	nmp_object_stackinit_id_ip6_address (obj, src->ip_address.ifindex, &src->ip6_address.address, src->ip_address.plen);
-}
-
-const NMPObject *
-nmp_object_stackinit_id_ip4_route (NMPObject *obj, int ifindex, guint32 network, guint8 plen, guint32 metric)
-{
-	nmp_object_stackinit (obj, NMP_OBJECT_TYPE_IP4_ROUTE, NULL);
-	obj->ip4_route.ifindex = ifindex;
-	obj->ip4_route.network = network;
-	obj->ip4_route.plen = plen;
-	obj->ip4_route.metric = metric;
-	return obj;
-}
-
-static void
-_vt_cmd_obj_stackinit_id_ip4_route (NMPObject *obj, const NMPObject *src)
-{
-	nmp_object_stackinit_id_ip4_route (obj, src->ip_route.ifindex, src->ip4_route.network, src->ip_route.plen, src->ip_route.metric);
-}
-
-const NMPObject *
-nmp_object_stackinit_id_ip6_route (NMPObject *obj, int ifindex, const struct in6_addr *network, guint8 plen, guint32 metric)
-{
-	nmp_object_stackinit (obj, NMP_OBJECT_TYPE_IP6_ROUTE, NULL);
-	obj->ip6_route.ifindex = ifindex;
-	if (network)
-		obj->ip6_route.network = *network;
-	obj->ip6_route.plen = plen;
-	obj->ip6_route.metric = metric;
-	return obj;
-}
-
-static void
-_vt_cmd_obj_stackinit_id_ip6_route (NMPObject *obj, const NMPObject *src)
-{
-	nmp_object_stackinit_id_ip6_route (obj, src->ip_route.ifindex, &src->ip6_route.network, src->ip_route.plen, src->ip_route.metric);
-}
-
 /*****************************************************************************/
 
 const char *
@@ -435,9 +611,8 @@ nmp_object_to_string (const NMPObject *obj, NMPObjectToStringMode to_string_mode
 		return klass->cmd_plobj_to_string_id (&obj->object, buf, buf_size);
 	case NMP_OBJECT_TO_STRING_ALL:
 		g_snprintf (buf, buf_size,
-		            "[%s,%p,%d,%ccache,%calive,%cvisible; %s]",
-		            klass->obj_type_name, obj, obj->_ref_count,
-		            obj->is_cached ? '+' : '-',
+		            "[%s,%p,%u,%calive,%cvisible; %s]",
+		            klass->obj_type_name, obj, obj->parent._ref_count,
 		            nmp_object_is_alive (obj) ? '+' : '-',
 		            nmp_object_is_visible (obj) ? '+' : '-',
 		            NMP_OBJECT_GET_CLASS (obj)->cmd_plobj_to_string (&obj->object, buf2, sizeof (buf2)));
@@ -462,9 +637,8 @@ _vt_cmd_obj_to_string_link (const NMPObject *obj, NMPObjectToStringMode to_strin
 		return klass->cmd_plobj_to_string_id (&obj->object, buf, buf_size);
 	case NMP_OBJECT_TO_STRING_ALL:
 		g_snprintf (buf, buf_size,
-		            "[%s,%p,%d,%ccache,%calive,%cvisible,%cin-nl,%p; %s]",
-		            klass->obj_type_name, obj, obj->_ref_count,
-		            obj->is_cached ? '+' : '-',
+		            "[%s,%p,%u,%calive,%cvisible,%cin-nl,%p; %s]",
+		            klass->obj_type_name, obj, obj->parent._ref_count,
 		            nmp_object_is_alive (obj) ? '+' : '-',
 		            nmp_object_is_visible (obj) ? '+' : '-',
 		            obj->_link.netlink.is_in_netlink ? '+' : '-',
@@ -503,9 +677,8 @@ _vt_cmd_obj_to_string_lnk_vlan (const NMPObject *obj, NMPObjectToStringMode to_s
 	case NMP_OBJECT_TO_STRING_ALL:
 
 		g_snprintf (buf, buf_size,
-		            "[%s,%p,%d,%ccache,%calive,%cvisible; %s]",
-		            klass->obj_type_name, obj, obj->_ref_count,
-		            obj->is_cached ? '+' : '-',
+		            "[%s,%p,%u,%calive,%cvisible; %s]",
+		            klass->obj_type_name, obj, obj->parent._ref_count,
 		            nmp_object_is_alive (obj) ? '+' : '-',
 		            nmp_object_is_visible (obj) ? '+' : '-',
 		            nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_PUBLIC, buf2, sizeof (buf2)));
@@ -562,10 +735,53 @@ _vt_cmd_plobj_to_string_id_##type (const NMPlatformObject *_obj, char *buf, gsiz
 _vt_cmd_plobj_to_string_id (link,        NMPlatformLink,       "%d",            obj->ifindex);
 _vt_cmd_plobj_to_string_id (ip4_address, NMPlatformIP4Address, "%d: %s/%d%s%s", obj->ifindex, nm_utils_inet4_ntop ( obj->address, buf1), obj->plen,
                                                                obj->peer_address != obj->address ? "," : "",
-                                                               obj->peer_address != obj->address ? nm_utils_inet4_ntop (obj->peer_address & nm_utils_ip4_prefix_to_netmask (obj->plen), buf2) : "");
+                                                               obj->peer_address != obj->address ? nm_utils_inet4_ntop (nm_utils_ip4_address_clear_host_address (obj->peer_address, obj->plen), buf2) : "");
 _vt_cmd_plobj_to_string_id (ip6_address, NMPlatformIP6Address, "%d: %s",        obj->ifindex, nm_utils_inet6_ntop (&obj->address, buf1));
-_vt_cmd_plobj_to_string_id (ip4_route,   NMPlatformIP4Route,   "%d: %s/%d %d",  obj->ifindex, nm_utils_inet4_ntop ( obj->network, buf1), obj->plen, obj->metric);
-_vt_cmd_plobj_to_string_id (ip6_route,   NMPlatformIP6Route,   "%d: %s/%d %d",  obj->ifindex, nm_utils_inet6_ntop (&obj->network, buf1), obj->plen, obj->metric);
+
+void
+nmp_object_hash_update (const NMPObject *obj, NMHashState *h)
+{
+	const NMPClass *klass;
+
+	g_return_if_fail (NMP_OBJECT_IS_VALID (obj));
+
+	klass = NMP_OBJECT_GET_CLASS (obj);
+
+	nm_hash_update_val (h, klass->obj_type);
+	if (klass->cmd_obj_hash_update)
+		klass->cmd_obj_hash_update (obj, h);
+	else if (klass->cmd_plobj_hash_update)
+		klass->cmd_plobj_hash_update (&obj->object, h);
+	else
+		nm_hash_update_val (h, obj);
+}
+
+static void
+_vt_cmd_obj_hash_update_link (const NMPObject *obj, NMHashState *h)
+{
+	nm_assert (NMP_OBJECT_GET_TYPE (obj) == NMP_OBJECT_TYPE_LINK);
+
+	nm_platform_link_hash_update (&obj->link, h);
+	nm_hash_update_vals (h,
+	                     obj->_link.netlink.is_in_netlink,
+	                     obj->_link.udev.device);
+	if (obj->_link.netlink.lnk)
+		nmp_object_hash_update (obj->_link.netlink.lnk, h);
+}
+
+static void
+_vt_cmd_obj_hash_update_lnk_vlan (const NMPObject *obj, NMHashState *h)
+{
+	nm_assert (NMP_OBJECT_GET_TYPE (obj) == NMP_OBJECT_TYPE_LNK_VLAN);
+
+	nm_platform_lnk_vlan_hash_update (&obj->lnk_vlan, h);
+	_vlan_xgress_qos_mappings_hash_update (obj->_lnk_vlan.n_ingress_qos_map,
+	                                       obj->_lnk_vlan.ingress_qos_map,
+	                                       h);
+	_vlan_xgress_qos_mappings_hash_update (obj->_lnk_vlan.n_egress_qos_map,
+	                                       obj->_lnk_vlan.egress_qos_map,
+	                                       h);
+}
 
 int
 nmp_object_cmp (const NMPObject *obj1, const NMPObject *obj2)
@@ -585,8 +801,10 @@ nmp_object_cmp (const NMPObject *obj1, const NMPObject *obj2)
 	klass1 = NMP_OBJECT_GET_CLASS (obj1);
 	klass2 = NMP_OBJECT_GET_CLASS (obj2);
 
-	if (klass1 != klass2)
+	if (klass1 != klass2) {
+		nm_assert (klass1->obj_type != klass2->obj_type);
 		return klass1->obj_type < klass2->obj_type ? -1 : 1;
+	}
 
 	if (klass1->cmd_obj_cmp)
 		return klass1->cmd_obj_cmp (obj1, obj2);
@@ -731,16 +949,12 @@ _vt_cmd_plobj_id_copy (ip6_address, NMPlatformIP6Address, {
 	dst->address = src->address;
 });
 _vt_cmd_plobj_id_copy (ip4_route, NMPlatformIP4Route, {
-	dst->ifindex = src->ifindex;
-	dst->plen = src->plen;
-	dst->metric = src->metric;
-	dst->network = src->network;
+	*dst = *src;
+	nm_assert (nm_platform_ip4_route_cmp (dst, src, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) == 0);
 });
 _vt_cmd_plobj_id_copy (ip6_route, NMPlatformIP6Route, {
-	dst->ifindex = src->ifindex;
-	dst->plen = src->plen;
-	dst->metric = src->metric;
-	dst->network = src->network;
+	*dst = *src;
+	nm_assert (nm_platform_ip6_route_cmp (dst, src, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) == 0);
 });
 
 /* Uses internally nmp_object_copy(), hence it also violates the const
@@ -761,127 +975,157 @@ nmp_object_clone (const NMPObject *obj, gboolean id_only)
 	return dst;
 }
 
-gboolean
-nmp_object_id_equal (const NMPObject *obj1, const NMPObject *obj2)
+int
+nmp_object_id_cmp (const NMPObject *obj1, const NMPObject *obj2)
 {
-	const NMPClass *klass;
+	const NMPClass *klass, *klass2;
 
-	if (obj1 == obj2)
-		return TRUE;
-	if (!obj1 || !obj2)
-		return FALSE;
+	NM_CMP_SELF (obj1, obj2);
 
 	g_return_val_if_fail (NMP_OBJECT_IS_VALID (obj1), FALSE);
 	g_return_val_if_fail (NMP_OBJECT_IS_VALID (obj2), FALSE);
 
 	klass = NMP_OBJECT_GET_CLASS (obj1);
-	return    klass == NMP_OBJECT_GET_CLASS (obj2)
-	       && klass->cmd_plobj_id_equal
-	       && klass->cmd_plobj_id_equal (&obj1->object, &obj2->object);
+	nm_assert (!klass->cmd_plobj_id_hash_update == !klass->cmd_plobj_id_cmp);
+
+	klass2 = NMP_OBJECT_GET_CLASS (obj2);
+	nm_assert (klass);
+	if (klass != klass2) {
+		nm_assert (klass2);
+		NM_CMP_DIRECT (klass->obj_type, klass2->obj_type);
+		/* resort to pointer comparison */
+		if (klass < klass2)
+			return -1;
+		return 1;
+	}
+
+	if (!klass->cmd_plobj_id_cmp) {
+		/* the klass doesn't implement ID cmp(). That means, different objects
+		 * never compare equal, but the cmp() according to their pointer value. */
+		return (obj1 < obj2) ? -1 : 1;
+	}
+
+	return klass->cmd_plobj_id_cmp (&obj1->object, &obj2->object);
 }
 
-#define _vt_cmd_plobj_id_equal(type, plat_type, cmd) \
-static gboolean \
-_vt_cmd_plobj_id_equal_##type (const NMPlatformObject *_obj1, const NMPlatformObject *_obj2) \
+#define _vt_cmd_plobj_id_cmp(type, plat_type, cmd) \
+static int \
+_vt_cmd_plobj_id_cmp_##type (const NMPlatformObject *_obj1, const NMPlatformObject *_obj2) \
 { \
 	const plat_type *const obj1 = (const plat_type *) _obj1; \
 	const plat_type *const obj2 = (const plat_type *) _obj2; \
-	return (cmd); \
-}
-_vt_cmd_plobj_id_equal (link, NMPlatformLink,
-                           obj1->ifindex == obj2->ifindex);
-_vt_cmd_plobj_id_equal (ip4_address, NMPlatformIP4Address,
-                           obj1->ifindex == obj2->ifindex
-                        && obj1->plen == obj2->plen
-                        && obj1->address == obj2->address
-                        /* for IPv4 addresses, you can add the same local address with differing peer-adddress
-                         * (IFA_ADDRESS), provided that their net-part differs. */
-                        && ((obj1->peer_address ^ obj2->peer_address) & nm_utils_ip4_prefix_to_netmask (obj1->plen)) == 0);
-_vt_cmd_plobj_id_equal (ip6_address, NMPlatformIP6Address,
-                           obj1->ifindex == obj2->ifindex
-                        /* for IPv6 addresses, the prefix length is not part of the primary identifier. */
-                        && IN6_ARE_ADDR_EQUAL (&obj1->address, &obj2->address));
-_vt_cmd_plobj_id_equal (ip4_route, NMPlatformIP4Route,
-                           obj1->ifindex == obj2->ifindex
-                        && obj1->plen == obj2->plen
-                        && obj1->metric == obj2->metric
-                        && nm_utils_ip4_address_clear_host_address (obj1->network, obj1->plen) == nm_utils_ip4_address_clear_host_address (obj2->network, obj2->plen));
-_vt_cmd_plobj_id_equal (ip6_route, NMPlatformIP6Route,
-                           obj1->ifindex == obj2->ifindex
-                        && obj1->plen == obj2->plen
-                        && obj1->metric == obj2->metric
-                        && ({
-                                struct in6_addr n1, n2;
-
-                                IN6_ARE_ADDR_EQUAL(nm_utils_ip6_address_clear_host_address (&n1, &obj1->network, obj1->plen),
-                                                   nm_utils_ip6_address_clear_host_address (&n2, &obj2->network, obj2->plen));
-                            }));
+	\
+	NM_CMP_SELF (obj1, obj2); \
+	{ cmd; } \
+	return 0; \
+}
+_vt_cmd_plobj_id_cmp (link, NMPlatformLink,
+                      NM_CMP_FIELD (obj1, obj2, ifindex);
+)
+_vt_cmd_plobj_id_cmp (ip4_address, NMPlatformIP4Address,
+                      NM_CMP_FIELD (obj1, obj2, ifindex);
+                      NM_CMP_FIELD (obj1, obj2, plen);
+                      NM_CMP_FIELD (obj1, obj2, address);
+                      /* for IPv4 addresses, you can add the same local address with differing peer-adddress
+                       * (IFA_ADDRESS), provided that their net-part differs. */
+                      NM_CMP_DIRECT_IN4ADDR_SAME_PREFIX (obj1->peer_address, obj2->peer_address, obj1->plen);
+)
+_vt_cmd_plobj_id_cmp (ip6_address, NMPlatformIP6Address,
+                      NM_CMP_FIELD (obj1, obj2, ifindex);
+                      /* for IPv6 addresses, the prefix length is not part of the primary identifier. */
+                      NM_CMP_FIELD_IN6ADDR (obj1, obj2, address);
+)
 
-guint
-nmp_object_id_hash (const NMPObject *obj)
+static int
+_vt_cmd_plobj_id_cmp_ip4_route (const NMPlatformObject *obj1, const NMPlatformObject *obj2)
 {
-	const NMPClass *klass;
+	return nm_platform_ip4_route_cmp ((NMPlatformIP4Route *) obj1, (NMPlatformIP4Route *) obj2, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID);
+}
 
-	if (!obj)
-		return 0;
+static int
+_vt_cmd_plobj_id_cmp_ip6_route (const NMPlatformObject *obj1, const NMPlatformObject *obj2)
+{
+	return nm_platform_ip6_route_cmp ((NMPlatformIP6Route *) obj1, (NMPlatformIP6Route *) obj2, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID);
+}
+
+void
+nmp_object_id_hash_update (const NMPObject *obj, NMHashState *h)
+{
+	const NMPClass *klass;
 
-	g_return_val_if_fail (NMP_OBJECT_IS_VALID (obj), 0);
+	g_return_if_fail (NMP_OBJECT_IS_VALID (obj));
 
 	klass = NMP_OBJECT_GET_CLASS (obj);
 
-	if (klass->cmd_plobj_id_hash)
-		return klass->cmd_plobj_id_hash (&obj->object);
+	nm_assert (!klass->cmd_plobj_id_hash_update == !klass->cmd_plobj_id_cmp);
+
+	if (!klass->cmd_plobj_id_hash_update) {
+		/* The klass doesn't implement ID compare. It means, to use pointer
+		 * equality. */
+		nm_hash_update_val (h, obj);
+		return;
+	}
 
-	/* unhashable objects implement pointer equality. */
-	return g_direct_hash (obj);
+	nm_hash_update_val (h, klass->obj_type);
+	klass->cmd_plobj_id_hash_update (&obj->object, h);
 }
 
-#define _vt_cmd_plobj_id_hash(type, plat_type, cmd) \
-static guint \
-_vt_cmd_plobj_id_hash_##type (const NMPlatformObject *_obj) \
+guint
+nmp_object_id_hash (const NMPObject *obj)
+{
+	NMHashState h;
+
+	if (!obj)
+		return 0;
+
+	nm_hash_init (&h, 914932607u);
+	nmp_object_id_hash_update (obj, &h);
+	return nm_hash_complete (&h);
+}
+
+#define _vt_cmd_plobj_id_hash_update(type, plat_type, cmd) \
+static void \
+_vt_cmd_plobj_id_hash_update_##type (const NMPlatformObject *_obj, NMHashState *h) \
 { \
 	const plat_type *const obj = (const plat_type *) _obj; \
-	guint hash; \
 	{ cmd; } \
-	return hash; \
 }
-_vt_cmd_plobj_id_hash (link, NMPlatformLink, {
-	hash = (guint) 3982791431u;
-	hash = hash      + ((guint) obj->ifindex);
+_vt_cmd_plobj_id_hash_update (link, NMPlatformLink, {
+	nm_hash_update_val (h, obj->ifindex);
 })
-_vt_cmd_plobj_id_hash (ip4_address, NMPlatformIP4Address, {
-	hash = (guint) 3591309853u;
-	hash = hash      + ((guint) obj->ifindex);
-	hash = hash * 33 + ((guint) obj->plen);
-	hash = hash * 33 + ((guint) obj->address);
-
-	/* for IPv4 we must also consider the net-part of the peer-address (IFA_ADDRESS) */
-	hash = hash * 33 + ((guint) (obj->peer_address & nm_utils_ip4_prefix_to_netmask (obj->plen)));
+_vt_cmd_plobj_id_hash_update (ip4_address, NMPlatformIP4Address, {
+	nm_hash_update_vals (h,
+	                     obj->ifindex,
+	                     obj->plen,
+	                     obj->address,
+	                     /* for IPv4 we must also consider the net-part of the peer-address (IFA_ADDRESS) */
+	                     nm_utils_ip4_address_clear_host_address (obj->peer_address, obj->plen));
 })
-_vt_cmd_plobj_id_hash (ip6_address, NMPlatformIP6Address, {
-	hash = (guint) 2907861637u;
-	hash = hash      + ((guint) obj->ifindex);
-	/* for IPv6 addresses, the prefix length is not part of the primary identifier. */
-	hash = hash * 33 + _id_hash_ip6_addr (&obj->address);
+_vt_cmd_plobj_id_hash_update (ip6_address, NMPlatformIP6Address, {
+	nm_hash_update_vals (h,
+	                     obj->ifindex,
+	                     /* for IPv6 addresses, the prefix length is not part of the primary identifier. */
+	                     obj->address);
 })
-_vt_cmd_plobj_id_hash (ip4_route, NMPlatformIP4Route, {
-	hash = (guint) 2569857221u;
-	hash = hash      + ((guint) obj->ifindex);
-	hash = hash * 33 + ((guint) obj->plen);
-	hash = hash * 33 + ((guint) obj->metric);
-	hash = hash * 33 + ((guint) nm_utils_ip4_address_clear_host_address (obj->network, obj->plen));
+_vt_cmd_plobj_id_hash_update (ip4_route, NMPlatformIP4Route, {
+	nm_platform_ip4_route_hash_update (obj, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID, h);
 })
-_vt_cmd_plobj_id_hash (ip6_route, NMPlatformIP6Route, {
-	hash = (guint) 3999787007u;
-	hash = hash      + ((guint) obj->ifindex);
-	hash = hash * 33 + ((guint) obj->plen);
-	hash = hash * 33 + ((guint) obj->metric);
-	hash = hash * 33 + ({
-	                        struct in6_addr n1;
-	                        _id_hash_ip6_addr (nm_utils_ip6_address_clear_host_address (&n1, &obj->network, obj->plen));
-	                    });
+_vt_cmd_plobj_id_hash_update (ip6_route, NMPlatformIP6Route, {
+	nm_platform_ip6_route_hash_update (obj, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID, h);
 })
 
+static inline void
+_vt_cmd_plobj_hash_update_ip4_route (const NMPlatformObject *obj, NMHashState *h)
+{
+	return nm_platform_ip4_route_hash_update ((const NMPlatformIP4Route *) obj, NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL, h);
+}
+
+static inline void
+_vt_cmd_plobj_hash_update_ip6_route (const NMPlatformObject *obj, NMHashState *h)
+{
+	return nm_platform_ip6_route_hash_update ((const NMPlatformIP6Route *) obj, NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL, h);
+}
+
 gboolean
 nmp_object_is_alive (const NMPObject *obj)
 {
@@ -958,365 +1202,92 @@ _vt_cmd_obj_is_visible_link (const NMPObject *obj)
 
 /*****************************************************************************/
 
-_NM_UTILS_LOOKUP_DEFINE (static, _nmp_cache_id_size_by_type, NMPCacheIdType, guint,
-	NM_UTILS_LOOKUP_DEFAULT (({ nm_assert_not_reached (); (guint) 0; })),
-	NM_UTILS_LOOKUP_ITEM (NMP_CACHE_ID_TYPE_OBJECT_TYPE,                            nm_offsetofend (NMPCacheId, object_type)),
-	NM_UTILS_LOOKUP_ITEM (NMP_CACHE_ID_TYPE_OBJECT_TYPE_VISIBLE_ONLY,               nm_offsetofend (NMPCacheId, object_type)),
-	NM_UTILS_LOOKUP_ITEM (NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_NO_DEFAULT,              nm_offsetofend (NMPCacheId, object_type)),
-	NM_UTILS_LOOKUP_ITEM (NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_ONLY_DEFAULT,            nm_offsetofend (NMPCacheId, object_type)),
-	NM_UTILS_LOOKUP_ITEM (NMP_CACHE_ID_TYPE_ADDRROUTE_VISIBLE_BY_IFINDEX,           nm_offsetofend (NMPCacheId, object_type_by_ifindex)),
-	NM_UTILS_LOOKUP_ITEM (NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_BY_IFINDEX_NO_DEFAULT,   nm_offsetofend (NMPCacheId, object_type_by_ifindex)),
-	NM_UTILS_LOOKUP_ITEM (NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_BY_IFINDEX_ONLY_DEFAULT, nm_offsetofend (NMPCacheId, object_type_by_ifindex)),
-	NM_UTILS_LOOKUP_ITEM (NMP_CACHE_ID_TYPE_LINK_BY_IFNAME,                         nm_offsetofend (NMPCacheId, link_by_ifname)),
-	NM_UTILS_LOOKUP_ITEM (NMP_CACHE_ID_TYPE_ROUTES_BY_DESTINATION_IP4,              nm_offsetofend (NMPCacheId, routes_by_destination_ip4)),
-	NM_UTILS_LOOKUP_ITEM (NMP_CACHE_ID_TYPE_ROUTES_BY_DESTINATION_IP6,              nm_offsetofend (NMPCacheId, routes_by_destination_ip6)),
-	NM_UTILS_LOOKUP_ITEM_IGNORE (NMP_CACHE_ID_TYPE_NONE),
-	NM_UTILS_LOOKUP_ITEM_IGNORE (__NMP_CACHE_ID_TYPE_MAX),
-);
-
-gboolean
-nmp_cache_id_equal (const NMPCacheId *a, const NMPCacheId *b)
-{
-	if (a->_id_type != b->_id_type)
-		return FALSE;
-	return memcmp (a, b, _nmp_cache_id_size_by_type (a->_id_type)) == 0;
-}
-
-guint
-nmp_cache_id_hash (const NMPCacheId *id)
-{
-	guint hash = 5381;
-	guint i, n;
-
-	n = _nmp_cache_id_size_by_type (id->_id_type);
-	for (i = 0; i < n; i++)
-		hash = ((hash << 5) + hash) + ((char *) id)[i]; /* hash * 33 + c */
-	return hash;
-}
-
-NMPCacheId *
-nmp_cache_id_clone (const NMPCacheId *id)
-{
-	NMPCacheId *id2;
-	guint n;
-
-	n = _nmp_cache_id_size_by_type (id->_id_type);
-	id2 = g_slice_alloc (n);
-	memcpy (id2, id, n);
-	return id2;
-}
+static const guint8 _supported_cache_ids_link[] = {
+	NMP_CACHE_ID_TYPE_OBJECT_TYPE,
+	NMP_CACHE_ID_TYPE_LINK_BY_IFNAME,
+	0,
+};
 
-void
-nmp_cache_id_destroy (NMPCacheId *id)
-{
-	guint n;
+static const guint8 _supported_cache_ids_ipx_address[] = {
+	NMP_CACHE_ID_TYPE_OBJECT_TYPE,
+	NMP_CACHE_ID_TYPE_ADDRROUTE_BY_IFINDEX,
+	0,
+};
 
-	n = _nmp_cache_id_size_by_type (id->_id_type);
-	g_slice_free1 (n, id);
-}
+static const guint8 _supported_cache_ids_ipx_route[] = {
+	NMP_CACHE_ID_TYPE_OBJECT_TYPE,
+	NMP_CACHE_ID_TYPE_ADDRROUTE_BY_IFINDEX,
+	NMP_CACHE_ID_TYPE_DEFAULT_ROUTES,
+	NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID,
+	0,
+};
 
 /*****************************************************************************/
 
 static void
-_nmp_cache_id_init (NMPCacheId *id, NMPCacheIdType id_type)
-{
-	/* there is no need to set the entire @id to zero when
-	 * initializing the ID.
-	 *
-	 * First, depending on the @id_type only part of the
-	 * @id is actually used (_nmp_cache_id_size_by_type).
-	 *
-	 * Second, the nmp_cache_id_init_*() *MUST* anyway make sure
-	 * that all relevant fields are set. Since it happens that
-	 * all structs have the packed attribute, there are no holes
-	 * due to alignment, and it becomes simple for nmp_cache_id_init_*()
-	 * to ensure that all fields are set. */
-
-#if NM_MORE_ASSERTS
-	nm_assert (id);
-	{
-		guint i;
-
-		/* initialized with some bogus canary to hopefully detect when we miss
-		 * to initialize a field of the cache-id. */
-		for (i = 0; i < sizeof (*id); i++) {
-			((char *) id)[i] = GPOINTER_TO_UINT (id) ^ i;
-		}
-	}
-#endif
-
-	id->_id_type = id_type;
-}
-
-NMPCacheId *
-nmp_cache_id_init_object_type (NMPCacheId *id, NMPObjectType obj_type, gboolean visible_only)
+_vt_dedup_obj_destroy (NMDedupMultiObj *obj)
 {
-	_nmp_cache_id_init (id, visible_only
-	                        ? NMP_CACHE_ID_TYPE_OBJECT_TYPE_VISIBLE_ONLY
-	                        : NMP_CACHE_ID_TYPE_OBJECT_TYPE);
-	id->object_type.obj_type = obj_type;
-	return id;
-}
-
-NMPCacheId *
-nmp_cache_id_init_addrroute_visible_by_ifindex (NMPCacheId *id,
-                                                NMPObjectType obj_type,
-                                                int ifindex)
-{
-	g_return_val_if_fail (NM_IN_SET (obj_type,
-	                                 NMP_OBJECT_TYPE_IP4_ADDRESS, NMP_OBJECT_TYPE_IP4_ROUTE,
-	                                 NMP_OBJECT_TYPE_IP6_ADDRESS, NMP_OBJECT_TYPE_IP6_ROUTE), NULL);
-
-	if (ifindex <= 0)
-		return nmp_cache_id_init_object_type (id, obj_type, TRUE);
-
-	_nmp_cache_id_init (id, NMP_CACHE_ID_TYPE_ADDRROUTE_VISIBLE_BY_IFINDEX);
-	id->object_type_by_ifindex.obj_type = obj_type;
-	memcpy (&id->object_type_by_ifindex._misaligned_ifindex, &ifindex, sizeof (int));
-	return id;
-}
-
-NMPCacheId *
-nmp_cache_id_init_routes_visible (NMPCacheId *id,
-                                  NMPObjectType obj_type,
-                                  gboolean with_default,
-                                  gboolean with_non_default,
-                                  int ifindex)
-{
-	g_return_val_if_fail (NM_IN_SET (obj_type, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE), NULL);
-
-	if (with_default) {
-		if (with_non_default) {
-			if (ifindex <= 0)
-				return nmp_cache_id_init_object_type (id, obj_type, TRUE);
-			return nmp_cache_id_init_addrroute_visible_by_ifindex (id, obj_type, ifindex);
-		}
-		_nmp_cache_id_init (id, NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_BY_IFINDEX_ONLY_DEFAULT);
-	} else if (with_non_default)
-		_nmp_cache_id_init (id, NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_BY_IFINDEX_NO_DEFAULT);
-	else
-		g_return_val_if_reached (NULL);
-
-	id->object_type_by_ifindex.obj_type = obj_type;
-	memcpy (&id->object_type_by_ifindex._misaligned_ifindex, &ifindex, sizeof (int));
-	return id;
-}
-
-NMPCacheId *
-nmp_cache_id_init_link_by_ifname (NMPCacheId *id,
-                                  const char *ifname)
-{
-	gsize l;
-
-	if (   !ifname
-	    || (l = strlen (ifname)) > sizeof (id->link_by_ifname.ifname_short))
-		g_return_val_if_reached (id);
-
-	_nmp_cache_id_init (id, NMP_CACHE_ID_TYPE_LINK_BY_IFNAME);
+	NMPObject *o = (NMPObject *) obj;
+	const NMPClass *klass;
 
-	memset (id->link_by_ifname.ifname_short, 0, sizeof (id->link_by_ifname.ifname_short));
-	/* the trailing NUL is dropped!! */
-	memcpy (id->link_by_ifname.ifname_short, ifname, l);
+	nm_assert (o->parent._ref_count == 0);
+	nm_assert (!o->parent._multi_idx);
 
-	return id;
+	klass = o->_class;
+	if (klass->cmd_obj_dispose)
+		klass->cmd_obj_dispose (o);
+	g_slice_free1 (klass->sizeof_data + G_STRUCT_OFFSET (NMPObject, object), o);
 }
 
-NMPCacheId *
-nmp_cache_id_init_routes_by_destination_ip4 (NMPCacheId *id,
-                                             guint32 network,
-                                             guint8 plen,
-                                             guint32 metric)
+static const NMDedupMultiObj *
+_vt_dedup_obj_clone (const NMDedupMultiObj *obj)
 {
-	_nmp_cache_id_init (id, NMP_CACHE_ID_TYPE_ROUTES_BY_DESTINATION_IP4);
-	id->routes_by_destination_ip4.plen = plen;
-	memcpy (&id->routes_by_destination_ip4._misaligned_metric,  &metric, sizeof (guint32));
-	memcpy (&id->routes_by_destination_ip4._misaligned_network, &network, sizeof (guint32));
-	return id;
+	return (const NMDedupMultiObj *) nmp_object_clone ((const NMPObject *) obj, FALSE);
 }
 
-NMPCacheId *
-nmp_cache_id_init_routes_by_destination_ip6 (NMPCacheId *id,
-                                             const struct in6_addr *network,
-                                             guint8 plen,
-                                             guint32 metric)
-{
-	_nmp_cache_id_init (id, NMP_CACHE_ID_TYPE_ROUTES_BY_DESTINATION_IP6);
-	id->routes_by_destination_ip4.plen = plen;
-	memcpy (&id->routes_by_destination_ip6._misaligned_metric,  &metric, sizeof (guint32));
-	memcpy (&id->routes_by_destination_ip6._misaligned_network, network ?: &nm_ip_addr_zero.addr6, sizeof (struct in6_addr));
-	return id;
-}
+#define DEDUP_MULTI_OBJ_CLASS_INIT() \
+	{ \
+		.obj_clone                      = _vt_dedup_obj_clone, \
+		.obj_destroy                    = _vt_dedup_obj_destroy, \
+		.obj_full_hash_update           = (void (*)(const NMDedupMultiObj *obj, NMHashState *h)) nmp_object_hash_update, \
+		.obj_full_equal                 = (gboolean (*)(const NMDedupMultiObj *obj_a, const NMDedupMultiObj *obj_b)) nmp_object_equal, \
+	}
 
 /*****************************************************************************/
 
-static gboolean
-_nmp_object_init_cache_id (const NMPObject *obj, NMPCacheIdType id_type, NMPCacheId *id, const NMPCacheId **out_id)
+static NMDedupMultiIdxType *
+_idx_type_get (const NMPCache *cache, NMPCacheIdType cache_id_type)
 {
-	const NMPClass *klass = NMP_OBJECT_GET_CLASS (obj);
+	nm_assert (cache);
+	nm_assert (cache_id_type > NMP_CACHE_ID_TYPE_NONE);
+	nm_assert (cache_id_type <= NMP_CACHE_ID_TYPE_MAX);
+	nm_assert ((int) cache_id_type - 1 >= 0);
+	nm_assert ((int) cache_id_type - 1 < G_N_ELEMENTS (cache->idx_types));
 
-	switch (id_type) {
-	case NMP_CACHE_ID_TYPE_OBJECT_TYPE:
-		*out_id = nmp_cache_id_init_object_type (id, klass->obj_type, FALSE);
-		return TRUE;
-	case NMP_CACHE_ID_TYPE_OBJECT_TYPE_VISIBLE_ONLY:
-		if (nmp_object_is_visible (obj))
-			*out_id = nmp_cache_id_init_object_type (id, klass->obj_type, TRUE);
-		else
-			*out_id = NULL;
-		return TRUE;
-	default:
-		return    klass->cmd_obj_init_cache_id
-		       && klass->cmd_obj_init_cache_id (obj, id_type, id, out_id);
-	}
+	return (NMDedupMultiIdxType *) &cache->idx_types[cache_id_type - 1];
 }
 
-static const guint8 _supported_cache_ids_link[] = {
-	NMP_CACHE_ID_TYPE_OBJECT_TYPE,
-	NMP_CACHE_ID_TYPE_OBJECT_TYPE_VISIBLE_ONLY,
-	NMP_CACHE_ID_TYPE_LINK_BY_IFNAME,
-	0,
-};
-
-static gboolean
-_vt_cmd_obj_init_cache_id_link (const NMPObject *obj, NMPCacheIdType id_type, NMPCacheId *id, const NMPCacheId **out_id)
+gboolean
+nmp_cache_use_udev_get (const NMPCache *cache)
 {
-	switch (id_type) {
-	case NMP_CACHE_ID_TYPE_LINK_BY_IFNAME:
-		if (obj->link.name[0]) {
-			*out_id = nmp_cache_id_init_link_by_ifname (id, obj->link.name);
-			return TRUE;
-		}
-		break;
-	default:
-		return FALSE;
-	}
-	*out_id = NULL;
-	return TRUE;
-}
-
-static const guint8 _supported_cache_ids_ipx_address[] = {
-	NMP_CACHE_ID_TYPE_OBJECT_TYPE,
-	NMP_CACHE_ID_TYPE_OBJECT_TYPE_VISIBLE_ONLY,
-	NMP_CACHE_ID_TYPE_ADDRROUTE_VISIBLE_BY_IFINDEX,
-	0,
-};
-
-static gboolean
-_vt_cmd_obj_init_cache_id_ipx_address (const NMPObject *obj, NMPCacheIdType id_type, NMPCacheId *id, const NMPCacheId **out_id)
-{
-	switch (id_type) {
-	case NMP_CACHE_ID_TYPE_ADDRROUTE_VISIBLE_BY_IFINDEX:
-		if (nmp_object_is_visible (obj)) {
-			nm_assert (obj->object.ifindex > 0);
-			*out_id = nmp_cache_id_init_addrroute_visible_by_ifindex (id, NMP_OBJECT_GET_TYPE (obj), obj->object.ifindex);
-			return TRUE;
-		}
-		break;
-	default:
-		return FALSE;
-	}
-	*out_id = NULL;
-	return TRUE;
-}
-
-static const guint8 _supported_cache_ids_ip4_route[] = {
-	NMP_CACHE_ID_TYPE_OBJECT_TYPE,
-	NMP_CACHE_ID_TYPE_OBJECT_TYPE_VISIBLE_ONLY,
-	NMP_CACHE_ID_TYPE_ADDRROUTE_VISIBLE_BY_IFINDEX,
-	NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_NO_DEFAULT,
-	NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_ONLY_DEFAULT,
-	NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_BY_IFINDEX_NO_DEFAULT,
-	NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_BY_IFINDEX_ONLY_DEFAULT,
-	NMP_CACHE_ID_TYPE_ROUTES_BY_DESTINATION_IP4,
-	0,
-};
-
-static const guint8 _supported_cache_ids_ip6_route[] = {
-	NMP_CACHE_ID_TYPE_OBJECT_TYPE,
-	NMP_CACHE_ID_TYPE_OBJECT_TYPE_VISIBLE_ONLY,
-	NMP_CACHE_ID_TYPE_ADDRROUTE_VISIBLE_BY_IFINDEX,
-	NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_NO_DEFAULT,
-	NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_ONLY_DEFAULT,
-	NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_BY_IFINDEX_NO_DEFAULT,
-	NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_BY_IFINDEX_ONLY_DEFAULT,
-	NMP_CACHE_ID_TYPE_ROUTES_BY_DESTINATION_IP6,
-	0,
-};
+	g_return_val_if_fail (cache, TRUE);
 
-static gboolean
-_vt_cmd_obj_init_cache_id_ipx_route (const NMPObject *obj, NMPCacheIdType id_type, NMPCacheId *id, const NMPCacheId **out_id)
-{
-	switch (id_type) {
-	case NMP_CACHE_ID_TYPE_ADDRROUTE_VISIBLE_BY_IFINDEX:
-		if (nmp_object_is_visible (obj)) {
-			nm_assert (obj->object.ifindex > 0);
-			*out_id = nmp_cache_id_init_addrroute_visible_by_ifindex (id, NMP_OBJECT_GET_TYPE (obj), obj->object.ifindex);
-			return TRUE;
-		}
-		break;
-	case NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_NO_DEFAULT:
-		if (   nmp_object_is_visible  (obj)
-		    && !NM_PLATFORM_IP_ROUTE_IS_DEFAULT (&obj->ip_route)) {
-			nm_assert (obj->object.ifindex > 0);
-			*out_id = nmp_cache_id_init_routes_visible (id, NMP_OBJECT_GET_TYPE (obj), FALSE, TRUE, 0);
-			return TRUE;
-		}
-		break;
-	case NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_ONLY_DEFAULT:
-		if (   nmp_object_is_visible  (obj)
-		    && NM_PLATFORM_IP_ROUTE_IS_DEFAULT (&obj->ip_route)) {
-			nm_assert (obj->object.ifindex > 0);
-			*out_id = nmp_cache_id_init_routes_visible (id, NMP_OBJECT_GET_TYPE (obj), TRUE, FALSE, 0);
-			return TRUE;
-		}
-		break;
-	case NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_BY_IFINDEX_NO_DEFAULT:
-		if (   nmp_object_is_visible (obj)
-		    && !NM_PLATFORM_IP_ROUTE_IS_DEFAULT (&obj->ip_route)) {
-			nm_assert (obj->object.ifindex > 0);
-			*out_id = nmp_cache_id_init_routes_visible (id, NMP_OBJECT_GET_TYPE (obj), FALSE, TRUE, obj->object.ifindex);
-			return TRUE;
-		}
-		break;
-	case NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_BY_IFINDEX_ONLY_DEFAULT:
-		if (   nmp_object_is_visible (obj)
-		    && NM_PLATFORM_IP_ROUTE_IS_DEFAULT (&obj->ip_route)) {
-			nm_assert (obj->object.ifindex > 0);
-			*out_id = nmp_cache_id_init_routes_visible (id, NMP_OBJECT_GET_TYPE (obj), TRUE, FALSE, obj->object.ifindex);
-			return TRUE;
-		}
-		break;
-	case NMP_CACHE_ID_TYPE_ROUTES_BY_DESTINATION_IP4:
-		if (NMP_OBJECT_GET_CLASS (obj)->obj_type == NMP_OBJECT_TYPE_IP4_ROUTE) {
-			*out_id = nmp_cache_id_init_routes_by_destination_ip4 (id, obj->ip4_route.network, obj->ip_route.plen, obj->ip_route.metric);
-			return TRUE;
-		}
-		return FALSE;
-	case NMP_CACHE_ID_TYPE_ROUTES_BY_DESTINATION_IP6:
-		if (NMP_OBJECT_GET_CLASS (obj)->obj_type == NMP_OBJECT_TYPE_IP6_ROUTE) {
-			*out_id = nmp_cache_id_init_routes_by_destination_ip6 (id, &obj->ip6_route.network, obj->ip_route.plen, obj->ip_route.metric);
-			return TRUE;
-		}
-		return FALSE;
-	default:
-		return FALSE;
-	}
-	*out_id = NULL;
-	return TRUE;
+	return cache->use_udev;
 }
 
 /*****************************************************************************/
 
 gboolean
-nmp_cache_use_udev_get (const NMPCache *cache)
+nmp_cache_link_connected_for_slave (int ifindex_master, const NMPObject *slave)
 {
-	g_return_val_if_fail (cache, TRUE);
+	nm_assert (NMP_OBJECT_GET_TYPE (slave) == NMP_OBJECT_TYPE_LINK);
 
-	return cache->use_udev;
+	return    ifindex_master > 0
+	       && slave->link.master == ifindex_master
+	       && slave->link.connected
+	       && nmp_object_is_visible (slave);
 }
 
-/*****************************************************************************/
-
 /**
  * nmp_cache_link_connected_needs_toggle:
  * @cache: the platform cache
@@ -1341,9 +1312,7 @@ nmp_cache_use_udev_get (const NMPCache *cache)
 gboolean
 nmp_cache_link_connected_needs_toggle (const NMPCache *cache, const NMPObject *master, const NMPObject *potential_slave, const NMPObject *ignore_slave)
 {
-	const NMPlatformLink *const *links;
 	gboolean is_lower_up = FALSE;
-	guint len, i;
 
 	if (   !master
 	    || NMP_OBJECT_GET_TYPE (master) != NMP_OBJECT_TYPE_LINK
@@ -1361,27 +1330,23 @@ nmp_cache_link_connected_needs_toggle (const NMPCache *cache, const NMPObject *m
 		potential_slave = NULL;
 
 	if (   potential_slave
-	    && nmp_object_is_visible (potential_slave)
-	    && potential_slave->link.ifindex > 0
-	    && potential_slave->link.master == master->link.ifindex
-	    && potential_slave->link.connected) {
+	    && nmp_cache_link_connected_for_slave (master->link.ifindex, potential_slave))
 		is_lower_up = TRUE;
-	} else {
-		NMPCacheId cache_id;
-
-		links = (const NMPlatformLink *const *) nmp_cache_lookup_multi (cache, nmp_cache_id_init_object_type (&cache_id, NMP_OBJECT_TYPE_LINK, FALSE), &len);
-		for (i = 0; i < len; i++) {
-			const NMPlatformLink *link = links[i];
+	else {
+		NMPLookup lookup;
+		NMDedupMultiIter iter;
+		const NMPlatformLink *link = NULL;
+
+		nmp_cache_iter_for_each_link (&iter,
+		                              nmp_cache_lookup (cache,
+		                                                nmp_lookup_init_obj_type (&lookup,
+		                                                                          NMP_OBJECT_TYPE_LINK)),
+		                              &link) {
 			const NMPObject *obj = NMP_OBJECT_UP_CAST ((NMPlatformObject *) link);
 
-			nm_assert (NMP_OBJECT_GET_TYPE (NMP_OBJECT_UP_CAST ((NMPlatformObject *) link)) == NMP_OBJECT_TYPE_LINK);
-
 			if (   (!potential_slave || potential_slave->link.ifindex != link->ifindex)
 			    && ignore_slave != obj
-			    && link->ifindex > 0
-			    && link->master == master->link.ifindex
-			    && nmp_object_is_visible (obj)
-			    && link->connected) {
+			    && nmp_cache_link_connected_for_slave (master->link.ifindex, obj)) {
 				is_lower_up = TRUE;
 				break;
 			}
@@ -1424,95 +1389,293 @@ nmp_cache_link_connected_needs_toggle_by_ifindex (const NMPCache *cache, int mas
 
 /*****************************************************************************/
 
-const NMPlatformObject *const *
-nmp_cache_lookup_multi (const NMPCache *cache, const NMPCacheId *cache_id, guint *out_len)
+static const NMDedupMultiEntry *
+_lookup_entry_with_idx_type (const NMPCache *cache,
+                             NMPCacheIdType cache_id_type,
+                             const NMPObject *obj)
 {
-	return (const NMPlatformObject *const *) nm_multi_index_lookup (cache->idx_multi,
-	                                                                (const NMMultiIndexId *) cache_id,
-	                                                                out_len);
+	const NMDedupMultiEntry *entry;
+
+	nm_assert (cache);
+	nm_assert (NMP_OBJECT_IS_VALID (obj));
+
+	entry = nm_dedup_multi_index_lookup_obj (cache->multi_idx,
+	                                         _idx_type_get (cache, cache_id_type),
+	                                         obj);
+	nm_assert (!entry
+	           || (   NMP_OBJECT_IS_VALID (entry->obj)
+	               && NMP_OBJECT_GET_CLASS (entry->obj) == NMP_OBJECT_GET_CLASS (obj)));
+	return entry;
 }
 
-GArray *
-nmp_cache_lookup_multi_to_array (const NMPCache *cache, NMPObjectType obj_type, const NMPCacheId *cache_id)
+static const NMDedupMultiEntry *
+_lookup_entry (const NMPCache *cache, const NMPObject *obj)
 {
-	const NMPClass *klass = nmp_class_from_type (obj_type);
-	guint len, i;
-	const NMPlatformObject *const *objects;
-	GArray *array;
-
-	g_return_val_if_fail (klass, NULL);
+	return _lookup_entry_with_idx_type (cache, NMP_CACHE_ID_TYPE_OBJECT_TYPE, obj);
+}
 
-	objects = nmp_cache_lookup_multi (cache, cache_id, &len);
-	array = g_array_sized_new (FALSE, FALSE, klass->sizeof_public, len);
+const NMDedupMultiEntry *
+nmp_cache_lookup_entry_with_idx_type (const NMPCache *cache,
+                                      NMPCacheIdType cache_id_type,
+                                      const NMPObject *obj)
+{
+	g_return_val_if_fail (cache, NULL);
+	g_return_val_if_fail (obj, NULL);
+	g_return_val_if_fail (cache_id_type > NMP_CACHE_ID_TYPE_NONE && cache_id_type <= NMP_CACHE_ID_TYPE_MAX, NULL);
 
-	for (i = 0; i < len; i++) {
-		nm_assert (NMP_OBJECT_GET_CLASS (NMP_OBJECT_UP_CAST (objects[i])) == klass);
-		g_array_append_vals (array, objects[i], 1);
-	}
-	return array;
+	return _lookup_entry_with_idx_type (cache, cache_id_type, obj);
 }
 
-const NMPObject *
-nmp_cache_lookup_obj (const NMPCache *cache, const NMPObject *obj)
+const NMDedupMultiEntry *
+nmp_cache_lookup_entry (const NMPCache *cache, const NMPObject *obj)
 {
+	g_return_val_if_fail (cache, NULL);
 	g_return_val_if_fail (obj, NULL);
 
-	return g_hash_table_lookup (cache->idx_main, obj);
+	return _lookup_entry (cache, obj);
 }
 
-const NMPObject *
-nmp_cache_lookup_link (const NMPCache *cache, int ifindex)
+const NMDedupMultiEntry *
+nmp_cache_lookup_entry_link (const NMPCache *cache, int ifindex)
 {
 	NMPObject obj_needle;
 
-	return nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&obj_needle, ifindex));
+	g_return_val_if_fail (cache, NULL);
+	g_return_val_if_fail (ifindex > 0, NULL);
+
+	nmp_object_stackinit_id_link (&obj_needle, ifindex);
+	return _lookup_entry (cache, &obj_needle);
 }
 
-/**
- * nmp_cache_find_other_route_for_same_destination:
- * @cache:
- * @route:
- *
- * Look into the cache whether there is a route to the same destination,
- * in terms of network/plen,metric.
- *
- * Returns: (transfer none): the first found route object from the cache
- *   that has the same (network/plen,metric) values as @route, but has different
- *   ID. Or %NULL, if no such route exists.
- */
 const NMPObject *
-nmp_cache_find_other_route_for_same_destination (const NMPCache *cache, const NMPObject *route)
+nmp_cache_lookup_obj (const NMPCache *cache, const NMPObject *obj)
 {
-	NMPCacheId cache_id;
-	const NMPlatformObject *const *list;
+	return nm_dedup_multi_entry_get_obj (nmp_cache_lookup_entry (cache, obj));
+}
 
+const NMPObject *
+nmp_cache_lookup_link (const NMPCache *cache, int ifindex)
+{
+	return nm_dedup_multi_entry_get_obj (nmp_cache_lookup_entry_link (cache, ifindex));
+}
+
+/*****************************************************************************/
+
+const NMDedupMultiHeadEntry *
+nmp_cache_lookup_all (const NMPCache *cache,
+                      NMPCacheIdType cache_id_type,
+                      const NMPObject *select_obj)
+{
 	nm_assert (cache);
+	nm_assert (NMP_OBJECT_IS_VALID (select_obj));
 
-	switch (NMP_OBJECT_GET_TYPE (route)) {
+	return nm_dedup_multi_index_lookup_head (cache->multi_idx,
+	                                         _idx_type_get (cache, cache_id_type),
+	                                         select_obj);
+}
+
+static const NMPLookup *
+_L (const NMPLookup *lookup)
+{
+#if NM_MORE_ASSERTS
+	DedupMultiIdxType idx_type;
+
+	nm_assert (lookup);
+	_dedup_multi_idx_type_init (&idx_type, lookup->cache_id_type);
+	nm_assert (idx_type.parent.klass->idx_obj_partitionable  ((NMDedupMultiIdxType *) &idx_type, (NMDedupMultiObj *) &lookup->selector_obj));
+#endif
+	return lookup;
+}
+
+const NMPLookup *
+nmp_lookup_init_obj_type (NMPLookup *lookup,
+                          NMPObjectType obj_type)
+{
+	NMPObject *o;
+
+	nm_assert (lookup);
+
+	switch (obj_type) {
+	case NMP_OBJECT_TYPE_LINK:
+	case NMP_OBJECT_TYPE_IP4_ADDRESS:
+	case NMP_OBJECT_TYPE_IP6_ADDRESS:
 	case NMP_OBJECT_TYPE_IP4_ROUTE:
-		nmp_cache_id_init_routes_by_destination_ip4 (&cache_id, route->ip4_route.network, route->ip_route.plen, route->ip_route.metric);
-		break;
 	case NMP_OBJECT_TYPE_IP6_ROUTE:
-		nmp_cache_id_init_routes_by_destination_ip6 (&cache_id, &route->ip6_route.network, route->ip_route.plen, route->ip_route.metric);
-		break;
+		o = _nmp_object_stackinit_from_type (&lookup->selector_obj, obj_type);
+		lookup->cache_id_type = NMP_CACHE_ID_TYPE_OBJECT_TYPE;
+		return _L (lookup);
 	default:
+		nm_assert_not_reached ();
+		return NULL;
+	}
+}
+
+const NMPLookup *
+nmp_lookup_init_link_by_ifname (NMPLookup *lookup,
+                                const char *ifname)
+{
+	NMPObject *o;
+
+	nm_assert (lookup);
+	nm_assert (ifname);
+	nm_assert (strlen (ifname) < IFNAMSIZ);
+
+	o = _nmp_object_stackinit_from_type (&lookup->selector_obj, NMP_OBJECT_TYPE_LINK);
+	if (g_strlcpy (o->link.name, ifname, sizeof (o->link.name)) >= sizeof (o->link.name))
 		g_return_val_if_reached (NULL);
+	lookup->cache_id_type = NMP_CACHE_ID_TYPE_LINK_BY_IFNAME;
+	return _L (lookup);
+}
+
+const NMPLookup *
+nmp_lookup_init_addrroute (NMPLookup *lookup,
+                           NMPObjectType obj_type,
+                           int ifindex)
+{
+	NMPObject *o;
+
+	nm_assert (lookup);
+	nm_assert (NM_IN_SET (obj_type, NMP_OBJECT_TYPE_IP4_ADDRESS,
+	                                NMP_OBJECT_TYPE_IP6_ADDRESS,
+	                                NMP_OBJECT_TYPE_IP4_ROUTE,
+	                                NMP_OBJECT_TYPE_IP6_ROUTE));
+
+	if (ifindex <= 0) {
+		return nmp_lookup_init_obj_type (lookup,
+		                                 obj_type);
 	}
 
-	list = nmp_cache_lookup_multi (cache, &cache_id, NULL);
-	if (list) {
-		for (; *list; list++) {
-			const NMPObject *candidate = NMP_OBJECT_UP_CAST (*list);
+	o = _nmp_object_stackinit_from_type (&lookup->selector_obj, obj_type);
+	o->object.ifindex = ifindex;
+	lookup->cache_id_type = NMP_CACHE_ID_TYPE_ADDRROUTE_BY_IFINDEX;
+	return _L (lookup);
+}
+
+const NMPLookup *
+nmp_lookup_init_route_default (NMPLookup *lookup,
+                               NMPObjectType obj_type)
+{
+	NMPObject *o;
 
-			nm_assert (NMP_OBJECT_GET_CLASS (route) == NMP_OBJECT_GET_CLASS (candidate));
+	nm_assert (lookup);
+	nm_assert (NM_IN_SET (obj_type, NMP_OBJECT_TYPE_IP4_ROUTE,
+	                                NMP_OBJECT_TYPE_IP6_ROUTE));
 
-			if (!nmp_object_id_equal (route, candidate))
-				return candidate;
-		}
+	o = _nmp_object_stackinit_from_type (&lookup->selector_obj, obj_type);
+	o->object.ifindex = 1;
+	lookup->cache_id_type = NMP_CACHE_ID_TYPE_DEFAULT_ROUTES;
+	return _L (lookup);
+}
+
+const NMPLookup *
+nmp_lookup_init_route_by_weak_id (NMPLookup *lookup,
+                                  const NMPObject *obj)
+{
+	const NMPlatformIP4Route *r4;
+	const NMPlatformIP6Route *r6;
+
+	nm_assert (lookup);
+
+	switch (NMP_OBJECT_GET_TYPE (obj)) {
+	case NMP_OBJECT_TYPE_IP4_ROUTE:
+		r4 = NMP_OBJECT_CAST_IP4_ROUTE (obj);
+		return nmp_lookup_init_ip4_route_by_weak_id (lookup,
+		                                             r4->network,
+		                                             r4->plen,
+		                                             r4->metric,
+		                                             r4->tos);
+	case NMP_OBJECT_TYPE_IP6_ROUTE:
+		r6 = NMP_OBJECT_CAST_IP6_ROUTE (obj);
+		return nmp_lookup_init_ip6_route_by_weak_id (lookup,
+		                                             &r6->network,
+		                                             r6->plen,
+		                                             r6->metric,
+		                                             &r6->src,
+		                                             r6->src_plen);
+	default:
+		nm_assert_not_reached ();
+		return NULL;
 	}
-	return NULL;
 }
 
+const NMPLookup *
+nmp_lookup_init_ip4_route_by_weak_id (NMPLookup *lookup,
+                                      in_addr_t network,
+                                      guint plen,
+                                      guint32 metric,
+                                      guint8 tos)
+{
+	NMPObject *o;
+
+	nm_assert (lookup);
+
+	o = _nmp_object_stackinit_from_type (&lookup->selector_obj, NMP_OBJECT_TYPE_IP4_ROUTE);
+	o->object.ifindex = 1;
+	o->ip_route.plen = plen;
+	o->ip_route.metric = metric;
+	if (network)
+		o->ip4_route.network = network;
+	o->ip4_route.tos = tos;
+	lookup->cache_id_type = NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID;
+	return _L (lookup);
+}
+
+const NMPLookup *
+nmp_lookup_init_ip6_route_by_weak_id (NMPLookup *lookup,
+                                      const struct in6_addr *network,
+                                      guint plen,
+                                      guint32 metric,
+                                      const struct in6_addr *src,
+                                      guint8 src_plen)
+{
+	NMPObject *o;
+
+	nm_assert (lookup);
+
+	o = _nmp_object_stackinit_from_type (&lookup->selector_obj, NMP_OBJECT_TYPE_IP6_ROUTE);
+	o->object.ifindex = 1;
+	o->ip_route.plen = plen;
+	o->ip_route.metric = metric;
+	if (network)
+		o->ip6_route.network = *network;
+	if (src)
+		o->ip6_route.src = *src;
+	o->ip6_route.src_plen = src_plen;
+	lookup->cache_id_type = NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID;
+	return _L (lookup);
+}
+
+/*****************************************************************************/
+
+GArray *
+nmp_cache_lookup_to_array (const NMDedupMultiHeadEntry *head_entry,
+                           NMPObjectType obj_type,
+                           gboolean visible_only)
+{
+	const NMPClass *klass = nmp_class_from_type (obj_type);
+	NMDedupMultiIter iter;
+	const NMPObject *o;
+	GArray *array;
+
+	g_return_val_if_fail (klass, NULL);
+
+	array = g_array_sized_new (FALSE, FALSE,
+	                           klass->sizeof_public,
+	                           head_entry ? head_entry->len : 0);
+	nmp_cache_iter_for_each (&iter,
+	                         head_entry,
+	                         &o) {
+		nm_assert (NMP_OBJECT_GET_CLASS (o) == klass);
+		if (   visible_only
+		    && !nmp_object_is_visible (o))
+			continue;
+		g_array_append_vals (array, &o->object, 1);
+	}
+	return array;
+}
+
+/*****************************************************************************/
+
 const NMPObject *
 nmp_cache_lookup_link_full (const NMPCache *cache,
                             int ifindex,
@@ -1524,9 +1687,10 @@ nmp_cache_lookup_link_full (const NMPCache *cache,
 {
 	NMPObject obj_needle;
 	const NMPObject *obj;
-	const NMPlatformObject *const *list;
-	guint i, len;
-	NMPCacheId cache_id, *p_cache_id;
+	NMDedupMultiIter iter;
+	const NMDedupMultiHeadEntry *head_entry;
+	const NMPlatformLink *link = NULL;
+	NMPLookup lookup;
 
 	if (ifindex > 0) {
 		obj = nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&obj_needle, ifindex));
@@ -1541,25 +1705,21 @@ nmp_cache_lookup_link_full (const NMPCache *cache,
 	} else if (!ifname && !match_fn)
 		return NULL;
 	else {
-		if (   ifname
-		    && strlen (ifname) <= sizeof (cache_id.link_by_ifname.ifname_short)) {
-			p_cache_id = nmp_cache_id_init_link_by_ifname (&cache_id, ifname);
-			ifname = NULL;
-		} else {
-			p_cache_id = nmp_cache_id_init_object_type (&cache_id, NMP_OBJECT_TYPE_LINK, visible_only);
-			visible_only = FALSE;
-		}
+		if (ifname) {
+			if (strlen (ifname) >= IFNAMSIZ)
+				return NULL;
+			nmp_lookup_init_link_by_ifname (&lookup, ifname);
+		} else
+			nmp_lookup_init_obj_type (&lookup, NMP_OBJECT_TYPE_LINK);
 
-		list = nmp_cache_lookup_multi (cache, p_cache_id, &len);
-		for (i = 0; i < len; i++) {
-			obj = NMP_OBJECT_UP_CAST (list[i]);
+		head_entry = nmp_cache_lookup (cache, &lookup);
+		nmp_cache_iter_for_each_link (&iter, head_entry, &link) {
+			obj = NMP_OBJECT_UP_CAST (link);
 
 			if (visible_only && !nmp_object_is_visible (obj))
 				continue;
 			if (link_type != NM_LINK_TYPE_NONE && obj->link.type != link_type)
 				continue;
-			if (ifname && strcmp (ifname, obj->link.name))
-				continue;
 			if (match_fn && !match_fn (obj, user_data))
 				continue;
 
@@ -1569,455 +1729,710 @@ nmp_cache_lookup_link_full (const NMPCache *cache,
 	}
 }
 
-GHashTable *
-nmp_cache_lookup_all_to_hash (const NMPCache *cache,
-                              NMPCacheId *cache_id,
-                              GHashTable *hash)
-{
-	NMMultiIndexIdIter iter;
-	gpointer plobj;
-
-	nm_multi_index_id_iter_init (&iter, cache->idx_multi, (const NMMultiIndexId *) cache_id);
-
-	if (nm_multi_index_id_iter_next (&iter, &plobj)) {
-		if (!hash)
-			hash = g_hash_table_new_full (NULL, NULL, (GDestroyNotify) nmp_object_unref, NULL);
-
-		do {
-			g_hash_table_add (hash, nmp_object_ref (NMP_OBJECT_UP_CAST (plobj)));
-		} while (nm_multi_index_id_iter_next (&iter, &plobj));
-	}
-
-	return hash;
-}
-
 /*****************************************************************************/
 
 static void
-_nmp_cache_update_cache (NMPCache *cache, NMPObject *obj, gboolean remove)
-{
-	const guint8 *id_type;
-
-	for (id_type = NMP_OBJECT_GET_CLASS (obj)->supported_cache_ids; *id_type; id_type++) {
-		NMPCacheId cache_id_storage;
-		const NMPCacheId *cache_id;
-
-		if (!_nmp_object_init_cache_id (obj, *id_type, &cache_id_storage, &cache_id))
-			continue;
-		if (!cache_id)
-			continue;
-
-		/* We don't put @obj itself into the multi index, but &obj->object. As of now, all
-		 * users expect a pointer to NMPlatformObject, not NMPObject.
-		 * You can use NMP_OBJECT_UP_CAST() to retrieve the original @obj pointer.
-		 *
-		 * If need be, we could determine based on @id_type which pointer we want to store. */
-
-		if (remove) {
-			if (!nm_multi_index_remove (cache->idx_multi, &cache_id->base, &obj->object))
-				g_assert_not_reached ();
+_idxcache_update_other_cache_ids (NMPCache *cache,
+                                  NMPCacheIdType cache_id_type,
+                                  const NMPObject *obj_old,
+                                  const NMPObject *obj_new,
+                                  gboolean is_dump)
+{
+	const NMDedupMultiEntry *entry_new;
+	const NMDedupMultiEntry *entry_old;
+	const NMDedupMultiEntry *entry_order;
+	NMDedupMultiIdxType *idx_type;
+
+	nm_assert (obj_new || obj_old);
+	nm_assert (!obj_new || NMP_OBJECT_GET_TYPE (obj_new) != NMP_OBJECT_TYPE_UNKNOWN);
+	nm_assert (!obj_old || NMP_OBJECT_GET_TYPE (obj_old) != NMP_OBJECT_TYPE_UNKNOWN);
+	nm_assert (!obj_old || !obj_new || NMP_OBJECT_GET_CLASS (obj_new) == NMP_OBJECT_GET_CLASS (obj_old));
+	nm_assert (!obj_old || !obj_new || !nmp_object_equal (obj_new, obj_old));
+	nm_assert (!obj_new || obj_new == nm_dedup_multi_index_obj_find (cache->multi_idx, obj_new));
+	nm_assert (!obj_old || obj_old == nm_dedup_multi_index_obj_find (cache->multi_idx, obj_old));
+
+	idx_type = _idx_type_get (cache, cache_id_type);
+
+	if (obj_old) {
+		entry_old = nm_dedup_multi_index_lookup_obj (cache->multi_idx,
+		                                             idx_type,
+		                                             obj_old);
+		if (!obj_new) {
+			if (entry_old)
+				nm_dedup_multi_index_remove_entry (cache->multi_idx, entry_old);
+			return;
+		}
+	} else
+		entry_old = NULL;
+
+	if (obj_new) {
+		if (   obj_old
+		    && nm_dedup_multi_idx_type_id_equal (idx_type, obj_old, obj_new)
+		    && nm_dedup_multi_idx_type_partition_equal (idx_type, obj_old, obj_new)) {
+			/* optimize. We just looked up the @obj_old entry and @obj_new compares equal
+			 * according to idx_obj_id_equal(). entry_new is the same as entry_old. */
+			entry_new = entry_old;
 		} else {
-			if (!nm_multi_index_add (cache->idx_multi, &cache_id->base, &obj->object))
-				g_assert_not_reached ();
+			entry_new = nm_dedup_multi_index_lookup_obj (cache->multi_idx,
+			                                             idx_type,
+			                                             obj_new);
 		}
-	}
-}
 
-static void
-_nmp_cache_update_add (NMPCache *cache, NMPObject *obj)
-{
-	nm_assert (!obj->is_cached);
-	nmp_object_ref (obj);
-	nm_assert (!nm_multi_index_lookup_first_by_value (cache->idx_multi, &obj->object));
-	if (!nm_g_hash_table_add (cache->idx_main, obj))
-		g_assert_not_reached ();
-	obj->is_cached = TRUE;
-	_nmp_cache_update_cache (cache, obj, FALSE);
-}
+		if (entry_new)
+			entry_order = entry_new;
+		else if (   entry_old
+		         && nm_dedup_multi_idx_type_partition_equal (idx_type, entry_old->obj, obj_new))
+			entry_order = entry_old;
+		else
+			entry_order = NULL;
+		nm_dedup_multi_index_add_full (cache->multi_idx,
+		                               idx_type,
+		                               obj_new,
+		                               is_dump
+		                                 ? NM_DEDUP_MULTI_IDX_MODE_APPEND_FORCE
+		                                 : NM_DEDUP_MULTI_IDX_MODE_APPEND,
+		                               is_dump
+		                                 ? NULL
+		                                 : entry_order,
+		                               entry_new ?: NM_DEDUP_MULTI_ENTRY_MISSING,
+		                               entry_new ? entry_new->head : (entry_order ? entry_order->head : NULL),
+		                               &entry_new,
+		                               NULL);
 
-static void
-_nmp_cache_update_remove (NMPCache *cache, NMPObject *obj)
-{
-	nm_assert (obj->is_cached);
-	_nmp_cache_update_cache (cache, obj, TRUE);
-	obj->is_cached = FALSE;
-	if (!g_hash_table_remove (cache->idx_main, obj))
-		g_assert_not_reached ();
+#if NM_MORE_ASSERTS
+		if (entry_new) {
+			nm_assert (idx_type->klass->idx_obj_partitionable);
+			nm_assert (idx_type->klass->idx_obj_partition_equal);
+			nm_assert (idx_type->klass->idx_obj_partitionable (idx_type, entry_new->obj));
+			nm_assert (idx_type->klass->idx_obj_partition_equal (idx_type, (gpointer) obj_new, entry_new->obj));
+		}
+#endif
+	} else
+		entry_new = NULL;
 
-	/* @obj is possibly a dangling pointer at this point. No problem, multi-index doesn't dereference. */
-	nm_assert (!nm_multi_index_lookup_first_by_value (cache->idx_multi, &obj->object));
+	if (   entry_old
+	    && entry_old != entry_new)
+		nm_dedup_multi_index_remove_entry (cache->multi_idx, entry_old);
 }
 
 static void
-_nmp_cache_update_update (NMPCache *cache, NMPObject *obj, const NMPObject *new)
+_idxcache_update (NMPCache *cache,
+                  const NMDedupMultiEntry *entry_old,
+                  NMPObject *obj_new,
+                  gboolean is_dump,
+                  const NMDedupMultiEntry **out_entry_new)
 {
-	const guint8 *id_type;
+	const NMPClass *klass;
+	const guint8 *i_idx_type;
+	NMDedupMultiIdxType *idx_type_o = _idx_type_get (cache, NMP_CACHE_ID_TYPE_OBJECT_TYPE);
+	const NMDedupMultiEntry *entry_new = NULL;
+	nm_auto_nmpobj const NMPObject *obj_old = NULL;
+
+	/* we update an object in the cache.
+	 *
+	 * Note that @entry_old MUST be what is currently tracked in multi_idx, and it must
+	 * have the same ID as @obj_new. */
 
-	nm_assert (NMP_OBJECT_GET_CLASS (obj) == NMP_OBJECT_GET_CLASS (new));
-	nm_assert (obj->is_cached);
-	nm_assert (!new->is_cached);
+	nm_assert (cache);
+	nm_assert (entry_old || obj_new);
+	nm_assert (!obj_new || nmp_object_is_alive (obj_new));
+	nm_assert (!entry_old || entry_old == nm_dedup_multi_index_lookup_obj (cache->multi_idx, idx_type_o, entry_old->obj));
+	nm_assert (!obj_new || entry_old == nm_dedup_multi_index_lookup_obj (cache->multi_idx, idx_type_o, obj_new));
+	nm_assert (!entry_old || entry_old->head->idx_type == idx_type_o);
+	nm_assert (   !entry_old
+	           || !obj_new
+	           || nm_dedup_multi_idx_type_partition_equal (idx_type_o, entry_old->obj, obj_new));
+	nm_assert (   !entry_old
+	           || !obj_new
+	           || nm_dedup_multi_idx_type_id_equal (idx_type_o, entry_old->obj, obj_new));
+	nm_assert (   !entry_old
+	           || !obj_new
+	           || (   obj_new->parent.klass == ((const NMPObject *) entry_old->obj)->parent.klass
+	               && !obj_new->parent.klass->obj_full_equal ((NMDedupMultiObj *) obj_new, entry_old->obj)));
+
+	/* keep a reference to the pre-existing entry */
+	if (entry_old)
+		obj_old = nmp_object_ref (entry_old->obj);
+
+	/* first update the main index NMP_CACHE_ID_TYPE_OBJECT_TYPE.
+	 * We already know the pre-existing @entry old, so all that
+	 * nm_dedup_multi_index_add_full() effectively does, is update the
+	 * obj reference.
+	 *
+	 * We also get the new boxed object, which we need below. */
+	if (obj_new) {
+		nm_auto_nmpobj NMPObject *obj_old2 = NULL;
+
+		nm_dedup_multi_index_add_full (cache->multi_idx,
+		                               idx_type_o,
+		                               obj_new,
+		                               is_dump
+		                                 ? NM_DEDUP_MULTI_IDX_MODE_APPEND_FORCE
+		                                 : NM_DEDUP_MULTI_IDX_MODE_APPEND,
+		                               NULL,
+		                               entry_old ?: NM_DEDUP_MULTI_ENTRY_MISSING,
+		                               NULL,
+		                               &entry_new,
+		                               (const NMDedupMultiObj **) &obj_old2);
+		nm_assert (entry_new);
+		nm_assert (obj_old == obj_old2);
+		nm_assert (!entry_old || entry_old == entry_new);
+	} else
+		nm_dedup_multi_index_remove_entry (cache->multi_idx, entry_old);
 
-	for (id_type = NMP_OBJECT_GET_CLASS (obj)->supported_cache_ids; *id_type; id_type++) {
-		NMPCacheId cache_id_storage_obj, cache_id_storage_new;
-		const NMPCacheId *cache_id_obj, *cache_id_new;
+	/* now update all other indexes. We know the previously boxed entry, and the
+	 * newly boxed one. */
+	klass = NMP_OBJECT_GET_CLASS (entry_new ? entry_new->obj : obj_old);
+	for (i_idx_type = klass->supported_cache_ids; *i_idx_type; i_idx_type++) {
+		NMPCacheIdType id_type = *i_idx_type;
 
-		if (!_nmp_object_init_cache_id (obj, *id_type, &cache_id_storage_obj, &cache_id_obj))
+		if (id_type == NMP_CACHE_ID_TYPE_OBJECT_TYPE)
 			continue;
-		if (!_nmp_object_init_cache_id (new, *id_type, &cache_id_storage_new, &cache_id_new))
-			g_assert_not_reached ();
-		if (!nm_multi_index_move (cache->idx_multi, (NMMultiIndexId *) cache_id_obj, (NMMultiIndexId *) cache_id_new, &obj->object))
-			g_assert_not_reached ();
+		_idxcache_update_other_cache_ids (cache, id_type,
+		                                  obj_old,
+		                                  entry_new ? entry_new->obj : NULL,
+		                                  is_dump);
 	}
-	nmp_object_copy (obj, new, FALSE);
+
+	NM_SET_OUT (out_entry_new, entry_new);
 }
 
 NMPCacheOpsType
-nmp_cache_remove (NMPCache *cache, const NMPObject *obj, gboolean equals_by_ptr, NMPObject **out_obj, gboolean *out_was_visible, NMPCachePreHook pre_hook, gpointer user_data)
+nmp_cache_remove (NMPCache *cache,
+                  const NMPObject *obj_needle,
+                  gboolean equals_by_ptr,
+                  gboolean only_dirty,
+                  const NMPObject **out_obj_old)
 {
-	NMPObject *old;
+	const NMDedupMultiEntry *entry_old;
+	const NMPObject *obj_old;
 
-	nm_assert (NMP_OBJECT_IS_VALID (obj));
+	entry_old = _lookup_entry (cache, obj_needle);
 
-	old = g_hash_table_lookup (cache->idx_main, obj);
-	if (!old) {
-		if (out_obj)
-			*out_obj = NULL;
-		if (out_was_visible)
-			*out_was_visible = FALSE;
+	if (!entry_old) {
+		NM_SET_OUT (out_obj_old, NULL);
 		return NMP_CACHE_OPS_UNCHANGED;
 	}
 
-	if (out_obj)
-		*out_obj = nmp_object_ref (old);
-	if (out_was_visible)
-		*out_was_visible = nmp_object_is_visible (old);
-	if (equals_by_ptr && old != obj) {
+	obj_old = entry_old->obj;
+
+	NM_SET_OUT (out_obj_old, nmp_object_ref (obj_old));
+
+	if (   equals_by_ptr
+	    && obj_old != obj_needle) {
 		/* We found an identical object, but we only delete it if it's the same pointer as
-		 * @obj. */
+		 * @obj_needle. */
 		return NMP_CACHE_OPS_UNCHANGED;
 	}
-	if (pre_hook)
-		pre_hook (cache, old, NULL, NMP_CACHE_OPS_REMOVED, user_data);
-	_nmp_cache_update_remove (cache, old);
+	if (   only_dirty
+	    && !entry_old->dirty) {
+		/* the entry is not dirty. Skip. */
+		return NMP_CACHE_OPS_UNCHANGED;
+	}
+	_idxcache_update (cache, entry_old, NULL, FALSE, NULL);
 	return NMP_CACHE_OPS_REMOVED;
 }
 
 NMPCacheOpsType
-nmp_cache_remove_netlink (NMPCache *cache, const NMPObject *obj_needle, NMPObject **out_obj, gboolean *out_was_visible, NMPCachePreHook pre_hook, gpointer user_data)
+nmp_cache_remove_netlink (NMPCache *cache,
+                          const NMPObject *obj_needle,
+                          const NMPObject **out_obj_old,
+                          const NMPObject **out_obj_new)
 {
-	if (NMP_OBJECT_GET_TYPE (obj_needle) == NMP_OBJECT_TYPE_LINK) {
-		NMPObject *old;
-		nm_auto_nmpobj NMPObject *obj = NULL;
+	const NMDedupMultiEntry *entry_old;
+	const NMDedupMultiEntry *entry_new = NULL;
+	const NMPObject *obj_old;
+	nm_auto_nmpobj NMPObject *obj_new = NULL;
+
+	entry_old = _lookup_entry (cache, obj_needle);
+
+	if (!entry_old) {
+		NM_SET_OUT (out_obj_old, NULL);
+		NM_SET_OUT (out_obj_new, NULL);
+		return NMP_CACHE_OPS_UNCHANGED;
+	}
+
+	obj_old = entry_old->obj;
 
+	if (NMP_OBJECT_GET_TYPE (obj_needle) == NMP_OBJECT_TYPE_LINK) {
 		/* For nmp_cache_remove_netlink() we have an incomplete @obj_needle instance to be
 		 * removed from netlink. Link objects are alive without being in netlink when they
 		 * have a udev-device. All we want to do in this case is clear the netlink.is_in_netlink
 		 * flag. */
 
-		old = (NMPObject *) nmp_cache_lookup_link (cache, obj_needle->link.ifindex);
-		if (!old) {
-			if (out_obj)
-				*out_obj = NULL;
-			if (out_was_visible)
-				*out_was_visible = FALSE;
-			return NMP_CACHE_OPS_UNCHANGED;
-		}
-
-		if (out_obj)
-			*out_obj = nmp_object_ref (old);
-		if (out_was_visible)
-			*out_was_visible = nmp_object_is_visible (old);
+		NM_SET_OUT (out_obj_old, nmp_object_ref (obj_old));
 
-		if (!old->_link.netlink.is_in_netlink) {
-			nm_assert (old->_link.udev.device);
+		if (!obj_old->_link.netlink.is_in_netlink) {
+			nm_assert (obj_old->_link.udev.device);
+			NM_SET_OUT (out_obj_new, nmp_object_ref (obj_old));
 			return NMP_CACHE_OPS_UNCHANGED;
 		}
 
-		if (!old->_link.udev.device) {
-			/* the update would make @old invalid. Remove it. */
-			if (pre_hook)
-				pre_hook (cache, old, NULL, NMP_CACHE_OPS_REMOVED, user_data);
-			_nmp_cache_update_remove (cache, old);
+		if (!obj_old->_link.udev.device) {
+			/* the update would make @obj_old invalid. Remove it. */
+			_idxcache_update (cache, entry_old, NULL, FALSE, NULL);
+			NM_SET_OUT (out_obj_new, NULL);
 			return NMP_CACHE_OPS_REMOVED;
 		}
 
-		obj = nmp_object_clone (old, FALSE);
-		obj->_link.netlink.is_in_netlink = FALSE;
+		obj_new = nmp_object_clone (obj_old, FALSE);
+		obj_new->_link.netlink.is_in_netlink = FALSE;
 
-		_nmp_object_fixup_link_master_connected (obj, cache);
-		_nmp_object_fixup_link_udev_fields (obj, cache->use_udev);
+		_nmp_object_fixup_link_master_connected (&obj_new, NULL, cache);
+		_nmp_object_fixup_link_udev_fields (&obj_new, NULL, cache->use_udev);
 
-		if (pre_hook)
-			pre_hook (cache, old, obj, NMP_CACHE_OPS_UPDATED, user_data);
-		_nmp_cache_update_update (cache, old, obj);
+		_idxcache_update (cache,
+		                  entry_old,
+		                  obj_new,
+		                  FALSE,
+		                  &entry_new);
+		NM_SET_OUT (out_obj_new, nmp_object_ref (entry_new->obj));
 		return NMP_CACHE_OPS_UPDATED;
-	} else
-		return nmp_cache_remove (cache, obj_needle, FALSE, out_obj, out_was_visible, pre_hook, user_data);
+	}
+
+	NM_SET_OUT (out_obj_old, nmp_object_ref (obj_old));
+	NM_SET_OUT (out_obj_new, NULL);
+	_idxcache_update (cache, entry_old, NULL, FALSE, NULL);
+	return NMP_CACHE_OPS_REMOVED;
 }
 
 /**
  * nmp_cache_update_netlink:
  * @cache: the platform cache
- * @obj: a #NMPObject instance as received from netlink and created via
+ * @obj_hand_over: a #NMPObject instance as received from netlink and created via
  *    nmp_object_from_nl(). Especially for link, it must not have the udev
  *    replated fields set.
  *    This instance will be modified and might be put into the cache. When
  *    calling nmp_cache_update_netlink() you hand @obj over to the cache.
  *    Except, that the cache will increment the ref count as appropriate. You
  *    must still unref the obj to release your part of the ownership.
- * @out_obj: (allow-none): (out): return the object instance that is inside
- *    the cache. If you specify non %NULL, you must always unref the returned
- *    instance. If the return value indicates that the object was removed,
- *    the object is no longer in the cache. Even if the return value indicates
- *    that the object was unchanged, it will still return @out_obj -- if
- *    such an object is in the cache.
- * @out_was_visible: (allow-none): (out): whether the object was visible before
- *    the update operation.
- * @pre_hook: (allow-none): a callback *before* the object gets updated. You cannot
- *    influence the outcome and must not do anything beyong inspecting the changes.
- * @user_data:
+ * @is_dump: whether this update comes during a dump of object of the same kind.
+ *    kernel dumps objects in a certain order, which matters especially for routes.
+ *    Before a dump we mark all objects as dirty, and remove all untouched objects
+ *    afterwards. Hence, during a dump, every update should move the object to the
+ *    end of the list, to obtain the correct order. That means, to use NM_DEDUP_MULTI_IDX_MODE_APPEND_FORCE,
+ *    instead of NM_DEDUP_MULTI_IDX_MODE_APPEND.
+ * @out_obj_old: (allow-none): (out): return the object with same ID as @obj_hand_over,
+ *    that was in the cache before update. If an object is returned, the caller must
+ *    unref it afterwards.
+ * @out_obj_new: (allow-none): (out): return the object from the cache after update.
+ *    The caller must unref this object.
  *
  * Returns: how the cache changed.
+ *
+ * Even if there was no change in the cace (NMP_CACHE_OPS_UNCHANGED), @out_obj_old
+ * and @out_obj_new will be set accordingly.
  **/
 NMPCacheOpsType
-nmp_cache_update_netlink (NMPCache *cache, NMPObject *obj, NMPObject **out_obj, gboolean *out_was_visible, NMPCachePreHook pre_hook, gpointer user_data)
+nmp_cache_update_netlink (NMPCache *cache,
+                          NMPObject *obj_hand_over,
+                          gboolean is_dump,
+                          const NMPObject **out_obj_old,
+                          const NMPObject **out_obj_new)
 {
-	NMPObject *old;
-
-	nm_assert (NMP_OBJECT_IS_VALID (obj));
-	nm_assert (!NMP_OBJECT_IS_STACKINIT (obj));
-	nm_assert (!obj->is_cached);
+	const NMDedupMultiEntry *entry_old;
+	const NMDedupMultiEntry *entry_new;
+	const NMPObject *obj_old;
+	gboolean is_alive;
 
+	nm_assert (cache);
+	nm_assert (NMP_OBJECT_IS_VALID (obj_hand_over));
+	nm_assert (!NMP_OBJECT_IS_STACKINIT (obj_hand_over));
 	/* A link object from netlink must have the udev related fields unset.
 	 * We could implement to handle that, but there is no need to support such
 	 * a use-case */
-	nm_assert (NMP_OBJECT_GET_TYPE (obj) != NMP_OBJECT_TYPE_LINK ||
-	           (   !obj->_link.udev.device
-	            && !obj->link.driver));
+	nm_assert (NMP_OBJECT_GET_TYPE (obj_hand_over) != NMP_OBJECT_TYPE_LINK ||
+	           (   !obj_hand_over->_link.udev.device
+	            && !obj_hand_over->link.driver));
+	nm_assert (nm_dedup_multi_index_obj_find (cache->multi_idx, obj_hand_over) != obj_hand_over);
 
-	old = g_hash_table_lookup (cache->idx_main, obj);
+	entry_old = _lookup_entry (cache, obj_hand_over);
 
-	if (out_obj)
-		*out_obj = NULL;
-	if (out_was_visible)
-		*out_was_visible = FALSE;
+	if (!entry_old) {
 
-	if (!old) {
-		if (!nmp_object_is_alive (obj))
-			return NMP_CACHE_OPS_UNCHANGED;
+		NM_SET_OUT (out_obj_old, NULL);
 
-		if (NMP_OBJECT_GET_TYPE (obj) == NMP_OBJECT_TYPE_LINK) {
-			_nmp_object_fixup_link_master_connected (obj, cache);
-			_nmp_object_fixup_link_udev_fields (obj, cache->use_udev);
+		if (!nmp_object_is_alive (obj_hand_over)) {
+			NM_SET_OUT (out_obj_new, NULL);
+			return NMP_CACHE_OPS_UNCHANGED;
 		}
 
-		if (out_obj)
-			*out_obj = nmp_object_ref (obj);
+		if (NMP_OBJECT_GET_TYPE (obj_hand_over) == NMP_OBJECT_TYPE_LINK) {
+			_nmp_object_fixup_link_master_connected (&obj_hand_over, NULL, cache);
+			_nmp_object_fixup_link_udev_fields (&obj_hand_over, NULL, cache->use_udev);
+		}
 
-		if (pre_hook)
-			pre_hook (cache, NULL, obj, NMP_CACHE_OPS_ADDED, user_data);
-		_nmp_cache_update_add (cache, obj);
+		_idxcache_update (cache,
+		                  entry_old,
+		                  obj_hand_over,
+		                  is_dump,
+		                  &entry_new);
+		NM_SET_OUT (out_obj_new, nmp_object_ref (entry_new->obj));
 		return NMP_CACHE_OPS_ADDED;
-	} else if (old == obj) {
-		/* updating a cached object inplace is not supported because the object contributes to hash-key
-		 * for NMMultiIndex. Modifying an object that is inside NMMultiIndex means that these
-		 * keys change.
-		 * The problem is, that for a given object NMMultiIndex does not support (efficient)
-		 * reverse lookup to get all the NMPCacheIds to which it belongs. If that would be implemented,
-		 * it would be possible to implement inplace-update.
-		 *
-		 * There is an un-optimized reverse lookup via nm_multi_index_iter_init(), but we don't want
-		 * that because we might have a large number of indexes to search.
-		 *
-		 * We could add efficient reverse lookup by adding a reverse index to NMMultiIndex. But that
-		 * also adds some cost to support an (uncommon?) usage pattern.
-		 *
-		 * Instead we just don't support it, instead we expect the user to
-		 * create a new instance from netlink.
-		 *
-		 * TL;DR: a cached object must never be modified.
-		 */
-		g_assert_not_reached ();
-	} else {
-		gboolean is_alive = FALSE;
-
-		nm_assert (old->is_cached);
-
-		if (out_obj)
-			*out_obj = nmp_object_ref (old);
-		if (out_was_visible)
-			*out_was_visible = nmp_object_is_visible (old);
-
-		if (NMP_OBJECT_GET_TYPE (obj) == NMP_OBJECT_TYPE_LINK) {
-			if (!obj->_link.netlink.is_in_netlink) {
-				if (!old->_link.netlink.is_in_netlink) {
-					nm_assert (old->_link.udev.device);
-					return NMP_CACHE_OPS_UNCHANGED;
-				}
-				if (old->_link.udev.device) {
-					/* @obj is not in netlink.
-					 *
-					 * This is similar to nmp_cache_remove_netlink(), but there we preserve the
-					 * preexisting netlink properties. The use case of that is when kernel_get_object()
-					 * cannot load an object (based on the id of a needle).
-					 *
-					 * Here we keep the data provided from @obj. The usecase is when receiving
-					 * a valid @obj instance from netlink with RTM_DELROUTE.
-					 */
-					is_alive = TRUE;
-				}
-			} else
-				is_alive = TRUE;
+	}
 
-			if (is_alive) {
-				_nmp_object_fixup_link_master_connected (obj, cache);
+	obj_old = entry_old->obj;
 
-				/* Merge the netlink parts with what we have from udev. */
-				udev_device_unref (obj->_link.udev.device);
-				obj->_link.udev.device = old->_link.udev.device ? udev_device_ref (old->_link.udev.device) : NULL;
-				_nmp_object_fixup_link_udev_fields (obj, cache->use_udev);
+	if (NMP_OBJECT_GET_TYPE (obj_hand_over) == NMP_OBJECT_TYPE_LINK) {
+		if (!obj_hand_over->_link.netlink.is_in_netlink) {
+			if (!obj_old->_link.netlink.is_in_netlink) {
+				nm_assert (obj_old->_link.udev.device);
+				NM_SET_OUT (out_obj_old, nmp_object_ref (obj_old));
+				NM_SET_OUT (out_obj_new, nmp_object_ref (obj_old));
+				return NMP_CACHE_OPS_UNCHANGED;
 			}
+			if (obj_old->_link.udev.device) {
+				/* @obj_hand_over is not in netlink.
+				 *
+				 * This is similar to nmp_cache_remove_netlink(), but there we preserve the
+				 * preexisting netlink properties. The use case of that is when kernel_get_object()
+				 * cannot load an object (based on the id of a needle).
+				 *
+				 * Here we keep the data provided from @obj_hand_over. The usecase is when receiving
+				 * a valid @obj_hand_over instance from netlink with RTM_DELROUTE.
+				 */
+				is_alive = TRUE;
+			} else
+				is_alive = FALSE;
 		} else
-			is_alive = nmp_object_is_alive (obj);
+			is_alive = TRUE;
 
-		if (!is_alive) {
-			/* the update would make @old invalid. Remove it. */
-			if (pre_hook)
-				pre_hook (cache, old, NULL, NMP_CACHE_OPS_REMOVED, user_data);
-			_nmp_cache_update_remove (cache, old);
-			return NMP_CACHE_OPS_REMOVED;
+		if (is_alive) {
+			_nmp_object_fixup_link_master_connected (&obj_hand_over, NULL, cache);
+
+			/* Merge the netlink parts with what we have from udev. */
+			udev_device_unref (obj_hand_over->_link.udev.device);
+			obj_hand_over->_link.udev.device = obj_old->_link.udev.device ? udev_device_ref (obj_old->_link.udev.device) : NULL;
+			_nmp_object_fixup_link_udev_fields (&obj_hand_over, NULL, cache->use_udev);
+
+			if (obj_hand_over->_link.netlink.lnk) {
+				nm_auto_nmpobj const NMPObject *lnk_old = obj_hand_over->_link.netlink.lnk;
+
+				/* let's dedup/intern the lnk object. */
+				obj_hand_over->_link.netlink.lnk = nm_dedup_multi_index_obj_intern (cache->multi_idx, lnk_old);
+			}
 		}
+	} else
+		is_alive = nmp_object_is_alive (obj_hand_over);
 
-		if (nmp_object_equal (old, obj))
-			return NMP_CACHE_OPS_UNCHANGED;
+	NM_SET_OUT (out_obj_old, nmp_object_ref (obj_old));
 
-		if (pre_hook)
-			pre_hook (cache, old, obj, NMP_CACHE_OPS_UPDATED, user_data);
-		_nmp_cache_update_update (cache, old, obj);
-		return NMP_CACHE_OPS_UPDATED;
+	if (!is_alive) {
+		/* the update would make @obj_old invalid. Remove it. */
+		_idxcache_update (cache, entry_old, NULL, FALSE, NULL);
+		NM_SET_OUT (out_obj_new, NULL);
+		return NMP_CACHE_OPS_REMOVED;
 	}
+
+	if (nmp_object_equal (obj_old, obj_hand_over)) {
+		nm_dedup_multi_entry_set_dirty (entry_old, FALSE);
+		NM_SET_OUT (out_obj_new, nmp_object_ref (obj_old));
+		return NMP_CACHE_OPS_UNCHANGED;
+	}
+
+	_idxcache_update (cache,
+	                  entry_old,
+	                  obj_hand_over,
+	                  is_dump,
+	                  &entry_new);
+	NM_SET_OUT (out_obj_new, nmp_object_ref (entry_new->obj));
+	return NMP_CACHE_OPS_UPDATED;
 }
 
 NMPCacheOpsType
-nmp_cache_update_link_udev (NMPCache *cache, int ifindex, struct udev_device *udevice, NMPObject **out_obj, gboolean *out_was_visible, NMPCachePreHook pre_hook, gpointer user_data)
-{
-	NMPObject *old;
-	nm_auto_nmpobj NMPObject *obj = NULL;
+nmp_cache_update_netlink_route (NMPCache *cache,
+                                NMPObject *obj_hand_over,
+                                gboolean is_dump,
+                                guint16 nlmsgflags,
+                                const NMPObject **out_obj_old,
+                                const NMPObject **out_obj_new,
+                                const NMPObject **out_obj_replace,
+                                gboolean *out_resync_required)
+{
+	NMDedupMultiIter iter;
+	const NMDedupMultiEntry *entry_old;
+	const NMDedupMultiEntry *entry_new;
+	const NMDedupMultiEntry *entry_cur;
+	const NMDedupMultiEntry *entry_replace;
+	const NMDedupMultiHeadEntry *head_entry;
+	gboolean is_alive;
+	NMPCacheOpsType ops_type = NMP_CACHE_OPS_UNCHANGED;
+	gboolean resync_required;
 
-	old = (NMPObject *) nmp_cache_lookup_link (cache, ifindex);
+	nm_assert (cache);
+	nm_assert (NMP_OBJECT_IS_VALID (obj_hand_over));
+	nm_assert (!NMP_OBJECT_IS_STACKINIT (obj_hand_over));
+	/* A link object from netlink must have the udev related fields unset.
+	 * We could implement to handle that, but there is no need to support such
+	 * a use-case */
+	nm_assert (NM_IN_SET (NMP_OBJECT_GET_TYPE (obj_hand_over), NMP_OBJECT_TYPE_IP4_ROUTE,
+	                                                           NMP_OBJECT_TYPE_IP6_ROUTE));
+	nm_assert (nm_dedup_multi_index_obj_find (cache->multi_idx, obj_hand_over) != obj_hand_over);
 
-	if (out_obj)
-		*out_obj = NULL;
-	if (out_was_visible)
-		*out_was_visible = FALSE;
+	entry_old = _lookup_entry (cache, obj_hand_over);
+	entry_new = NULL;
 
-	if (!old) {
-		if (!udevice)
-			return NMP_CACHE_OPS_UNCHANGED;
+	NM_SET_OUT (out_obj_old, nmp_object_ref (nm_dedup_multi_entry_get_obj (entry_old)));
+
+	if (!entry_old) {
 
-		obj = nmp_object_new (NMP_OBJECT_TYPE_LINK, NULL);
-		obj->link.ifindex = ifindex;
-		obj->_link.udev.device = udev_device_ref (udevice);
+		if (!nmp_object_is_alive (obj_hand_over))
+			goto update_done;
+
+		_idxcache_update (cache,
+		                  NULL,
+		                  obj_hand_over,
+		                  is_dump,
+		                  &entry_new);
+		ops_type = NMP_CACHE_OPS_ADDED;
+		goto update_done;
+	}
 
-		_nmp_object_fixup_link_udev_fields (obj, cache->use_udev);
+	is_alive = nmp_object_is_alive (obj_hand_over);
+
+	if (!is_alive) {
+		/* the update would make @entry_old invalid. Remove it. */
+		_idxcache_update (cache, entry_old, NULL, FALSE, NULL);
+		ops_type = NMP_CACHE_OPS_REMOVED;
+		goto update_done;
+	}
+
+	if (nmp_object_equal (entry_old->obj, obj_hand_over)) {
+		nm_dedup_multi_entry_set_dirty (entry_old, FALSE);
+		goto update_done;
+	}
 
-		nm_assert (nmp_object_is_alive (obj));
+	_idxcache_update (cache,
+	                  entry_old,
+	                  obj_hand_over,
+	                  is_dump,
+	                  &entry_new);
+	ops_type = NMP_CACHE_OPS_UPDATED;
 
-		if (out_obj)
-			*out_obj = nmp_object_ref (obj);
+update_done:
+	NM_SET_OUT (out_obj_new, nmp_object_ref (nm_dedup_multi_entry_get_obj (entry_new)));
 
-		if (pre_hook)
-			pre_hook (cache, NULL, obj, NMP_CACHE_OPS_ADDED, user_data);
-		_nmp_cache_update_add (cache, obj);
+	/* a RTM_GETROUTE event may signal that another object was replaced.
+	 * Find out whether that is the case and return it as @obj_replaced.
+	 *
+	 * Also, fixup the order of @entry_new within NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID
+	 * index. For most parts, we don't care about the order of objects (including routes).
+	 * But NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID we must keep in the correct order, to
+	 * properly find @obj_replaced. */
+	resync_required = FALSE;
+	entry_replace = NULL;
+	if (is_dump) {
+		goto out;
+	}
+
+	if (!entry_new) {
+		if (   NM_FLAGS_HAS (nlmsgflags, NLM_F_REPLACE)
+		    && nmp_cache_lookup_all (cache,
+		                             NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID,
+		                             obj_hand_over)) {
+			/* hm. @obj_hand_over was not added, meaning it was not alive.
+			 * However, we track some other objects with the same weak-id.
+			 * It's unclear what that means. To be sure, resync. */
+			resync_required = TRUE;
+		}
+		goto out;
+	}
+
+	entry_cur = _lookup_entry_with_idx_type (cache,
+	                                         NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID,
+	                                         entry_new->obj);
+	if (!entry_cur) {
+		nm_assert_not_reached ();
+		goto out;
+	}
+	nm_assert (entry_cur->obj == entry_new->obj);
+
+	head_entry = entry_cur->head;
+	nm_assert (head_entry == nmp_cache_lookup_all (cache,
+	                                               NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID,
+	                                               entry_cur->obj));
+
+	if (head_entry->len == 1) {
+		/* there is only one object, and we expect it to be @obj_new. */
+		nm_assert (nm_dedup_multi_head_entry_get_idx (head_entry, 0) == entry_cur);
+		goto out;
+	}
+
+	switch (nlmsgflags & (NLM_F_REPLACE | NLM_F_EXCL | NLM_F_CREATE | NLM_F_APPEND)) {
+	case NLM_F_REPLACE:
+		/* ip route change */
+
+		/* get the first element (but skip @obj_new). */
+		nm_dedup_multi_iter_init (&iter, head_entry);
+		if (!nm_dedup_multi_iter_next (&iter))
+			nm_assert_not_reached ();
+		if (iter.current == entry_cur) {
+			if (!nm_dedup_multi_iter_next (&iter))
+				nm_assert_not_reached ();
+		}
+		entry_replace = iter.current;
+
+		nm_assert (   entry_replace
+		           && entry_cur != entry_replace);
+
+		nm_dedup_multi_entry_reorder (entry_cur, entry_replace, FALSE);
+		break;
+	case NLM_F_CREATE | NLM_F_APPEND:
+		/* ip route append */
+		nm_dedup_multi_entry_reorder (entry_cur, NULL, TRUE);
+		break;
+	case NLM_F_CREATE:
+		/* ip route prepend */
+		nm_dedup_multi_entry_reorder (entry_cur, NULL, FALSE);
+		break;
+	default:
+		/* this is an unexecpted case, probably a bug that we need to handle better. */
+		resync_required = TRUE;
+		break;
+	}
+
+out:
+	NM_SET_OUT (out_obj_replace, nmp_object_ref (nm_dedup_multi_entry_get_obj (entry_replace)));
+	NM_SET_OUT (out_resync_required, resync_required);
+	return ops_type;
+}
+
+
+NMPCacheOpsType
+nmp_cache_update_link_udev (NMPCache *cache,
+                            int ifindex,
+                            struct udev_device *udevice,
+                            const NMPObject **out_obj_old,
+                            const NMPObject **out_obj_new)
+{
+	const NMPObject *obj_old;
+	nm_auto_nmpobj NMPObject *obj_new = NULL;
+	const NMDedupMultiEntry *entry_old;
+	const NMDedupMultiEntry *entry_new;
+
+	entry_old = nmp_cache_lookup_entry_link (cache, ifindex);
+
+	if (!entry_old) {
+		if (!udevice) {
+			NM_SET_OUT (out_obj_old, NULL);
+			NM_SET_OUT (out_obj_new, NULL);
+			return NMP_CACHE_OPS_UNCHANGED;
+		}
+
+		obj_new = nmp_object_new (NMP_OBJECT_TYPE_LINK, NULL);
+		obj_new->link.ifindex = ifindex;
+		obj_new->_link.udev.device = udev_device_ref (udevice);
+
+		_nmp_object_fixup_link_udev_fields (&obj_new, NULL, cache->use_udev);
+
+		_idxcache_update (cache,
+		                  NULL,
+		                  obj_new,
+		                  FALSE,
+		                  &entry_new);
+		NM_SET_OUT (out_obj_old, NULL);
+		NM_SET_OUT (out_obj_new, nmp_object_ref (entry_new->obj));
 		return NMP_CACHE_OPS_ADDED;
 	} else {
-		nm_assert (old->is_cached);
-
-		if (out_obj)
-			*out_obj = nmp_object_ref (old);
-		if (out_was_visible)
-			*out_was_visible = nmp_object_is_visible (old);
+		obj_old = entry_old->obj;
+		NM_SET_OUT (out_obj_old, nmp_object_ref (obj_old));
 
-		if (old->_link.udev.device == udevice)
+		if (obj_old->_link.udev.device == udevice) {
+			NM_SET_OUT (out_obj_new, nmp_object_ref (obj_old));
 			return NMP_CACHE_OPS_UNCHANGED;
+		}
 
-		if (!udevice && !old->_link.netlink.is_in_netlink) {
-			/* the update would make @old invalid. Remove it. */
-			if (pre_hook)
-				pre_hook (cache, old, NULL, NMP_CACHE_OPS_REMOVED, user_data);
-			_nmp_cache_update_remove (cache, old);
+		if (!udevice && !obj_old->_link.netlink.is_in_netlink) {
+			/* the update would make @obj_old invalid. Remove it. */
+			_idxcache_update (cache, entry_old, NULL, FALSE, NULL);
+			NM_SET_OUT (out_obj_new, NULL);
 			return NMP_CACHE_OPS_REMOVED;
 		}
 
-		obj = nmp_object_clone (old, FALSE);
-
-		udev_device_unref (obj->_link.udev.device);
-		obj->_link.udev.device = udevice ? udev_device_ref (udevice) : NULL;
+		obj_new = nmp_object_clone (obj_old, FALSE);
 
-		_nmp_object_fixup_link_udev_fields (obj, cache->use_udev);
+		udev_device_unref (obj_new->_link.udev.device);
+		obj_new->_link.udev.device = udevice ? udev_device_ref (udevice) : NULL;
 
-		nm_assert (nmp_object_is_alive (obj));
+		_nmp_object_fixup_link_udev_fields (&obj_new, NULL, cache->use_udev);
 
-		if (pre_hook)
-			pre_hook (cache, old, obj, NMP_CACHE_OPS_UPDATED, user_data);
-		_nmp_cache_update_update (cache, old, obj);
+		_idxcache_update (cache,
+		                  entry_old,
+		                  obj_new,
+		                  FALSE,
+		                  &entry_new);
+		NM_SET_OUT (out_obj_new, nmp_object_ref (entry_new->obj));
 		return NMP_CACHE_OPS_UPDATED;
 	}
 }
 
 NMPCacheOpsType
-nmp_cache_update_link_master_connected (NMPCache *cache, int ifindex, NMPObject **out_obj, gboolean *out_was_visible, NMPCachePreHook pre_hook, gpointer user_data)
+nmp_cache_update_link_master_connected (NMPCache *cache,
+                                        int ifindex,
+                                        const NMPObject **out_obj_old,
+                                        const NMPObject **out_obj_new)
 {
-	NMPObject *old;
-	nm_auto_nmpobj NMPObject *obj = NULL;
+	const NMDedupMultiEntry *entry_old;
+	const NMDedupMultiEntry *entry_new = NULL;
+	const NMPObject *obj_old;
+	nm_auto_nmpobj NMPObject *obj_new = NULL;
+
+	entry_old = nmp_cache_lookup_entry_link (cache, ifindex);
 
-	old = (NMPObject *) nmp_cache_lookup_link (cache, ifindex);
+	if (!entry_old) {
+		NM_SET_OUT (out_obj_old, NULL);
+		NM_SET_OUT (out_obj_new, NULL);
+		return NMP_CACHE_OPS_UNCHANGED;
+	}
 
-	if (!old) {
-		if (out_obj)
-			*out_obj = NULL;
-		if (out_was_visible)
-			*out_was_visible = FALSE;
+	obj_old = entry_old->obj;
 
+	if (!nmp_cache_link_connected_needs_toggle (cache, obj_old, NULL, NULL)) {
+		NM_SET_OUT (out_obj_old, nmp_object_ref (obj_old));
+		NM_SET_OUT (out_obj_new, nmp_object_ref (obj_old));
 		return NMP_CACHE_OPS_UNCHANGED;
 	}
 
-	nm_assert (old->is_cached);
+	obj_new = nmp_object_clone (obj_old, FALSE);
+	obj_new->link.connected = !obj_old->link.connected;
 
-	if (out_obj)
-		*out_obj = nmp_object_ref (old);
-	if (out_was_visible)
-		*out_was_visible = nmp_object_is_visible (old);
+	NM_SET_OUT (out_obj_old, nmp_object_ref (obj_old));
+	_idxcache_update (cache,
+	                  entry_old,
+	                  obj_new,
+	                  FALSE,
+	                  &entry_new);
+	NM_SET_OUT (out_obj_new, nmp_object_ref (entry_new->obj));
+	return NMP_CACHE_OPS_UPDATED;
+}
 
-	if (!nmp_cache_link_connected_needs_toggle (cache, old, NULL, NULL))
-		return NMP_CACHE_OPS_UNCHANGED;
+/*****************************************************************************/
 
-	obj = nmp_object_clone (old, FALSE);
-	obj->link.connected = !old->link.connected;
+void
+nmp_cache_dirty_set_all (NMPCache *cache, NMPObjectType obj_type)
+{
+	NMPObject obj_needle;
 
-	nm_assert (nmp_object_is_alive (obj));
+	nm_assert (cache);
 
-	if (pre_hook)
-		pre_hook (cache, old, obj, NMP_CACHE_OPS_UPDATED, user_data);
-	_nmp_cache_update_update (cache, old, obj);
-	return NMP_CACHE_OPS_UPDATED;
+	nm_dedup_multi_index_dirty_set_head (cache->multi_idx,
+	                                     _idx_type_get (cache, NMP_CACHE_ID_TYPE_OBJECT_TYPE),
+	                                     _nmp_object_stackinit_from_type (&obj_needle, obj_type));
 }
 
 /*****************************************************************************/
 
 NMPCache *
-nmp_cache_new (gboolean use_udev)
-{
-	NMPCache *cache = g_new (NMPCache, 1);
-
-	cache->idx_main = g_hash_table_new_full ((GHashFunc) nmp_object_id_hash,
-	                                         (GEqualFunc) nmp_object_id_equal,
-	                                         (GDestroyNotify) nmp_object_unref,
-	                                         NULL);
-	cache->idx_multi = nm_multi_index_new ((NMMultiIndexFuncHash) nmp_cache_id_hash,
-	                                       (NMMultiIndexFuncEqual) nmp_cache_id_equal,
-	                                       (NMMultiIndexFuncClone) nmp_cache_id_clone,
-	                                       (NMMultiIndexFuncDestroy) nmp_cache_id_destroy);
+nmp_cache_new (NMDedupMultiIndex *multi_idx, gboolean use_udev)
+{
+	NMPCache *cache = g_slice_new0 (NMPCache);
+	guint i;
+
+	for (i = NMP_CACHE_ID_TYPE_NONE + 1; i <= NMP_CACHE_ID_TYPE_MAX; i++)
+		_dedup_multi_idx_type_init ((DedupMultiIdxType *) _idx_type_get (cache, i), i);
+
+	cache->multi_idx = nm_dedup_multi_index_ref (multi_idx);
+
 	cache->use_udev = !!use_udev;
 	return cache;
 }
@@ -2025,23 +2440,14 @@ nmp_cache_new (gboolean use_udev)
 void
 nmp_cache_free (NMPCache *cache)
 {
-	GHashTableIter iter;
-	NMPObject *obj;
+	guint i;
 
-	/* No need to cumbersomely remove the objects properly. They are not hooked up
-	 * in a complicated way, we can just unref them together with cache->idx_main.
-	 *
-	 * But we must clear the @is_cached flag. */
-	g_hash_table_iter_init (&iter, cache->idx_main);
-	while (g_hash_table_iter_next (&iter, (gpointer *) &obj, NULL)) {
-		nm_assert (obj->is_cached);
-		obj->is_cached = FALSE;
-	}
+	for (i = NMP_CACHE_ID_TYPE_NONE + 1; i <= NMP_CACHE_ID_TYPE_MAX; i++)
+		nm_dedup_multi_index_remove_idx (cache->multi_idx, _idx_type_get (cache, i));
 
-	nm_multi_index_free (cache->idx_multi);
-	g_hash_table_unref (cache->idx_main);
+	nm_dedup_multi_index_unref (cache->multi_idx);
 
-	g_free (cache);
+	g_slice_free (NMPCache, cache);
 }
 
 /*****************************************************************************/
@@ -2049,63 +2455,13 @@ nmp_cache_free (NMPCache *cache)
 void
 ASSERT_nmp_cache_is_consistent (const NMPCache *cache)
 {
-#if NM_MORE_ASSERTS
-	NMMultiIndexIter iter_multi;
-	GHashTableIter iter_hash;
-	guint i, len;
-	NMPCacheId cache_id_storage;
-	const NMPCacheId *cache_id, *cache_id2;
-	const NMPlatformObject *const *objects;
-	const NMPObject *obj;
-
-	g_assert (cache);
-
-	g_hash_table_iter_init (&iter_hash, cache->idx_main);
-	while (g_hash_table_iter_next (&iter_hash, (gpointer *) &obj, NULL)) {
-		const guint8 *id_type;
-
-		g_assert (NMP_OBJECT_IS_VALID (obj));
-		g_assert (nmp_object_is_alive (obj));
-
-		for (id_type = NMP_OBJECT_GET_CLASS (obj)->supported_cache_ids; *id_type; id_type++) {
-			if (!_nmp_object_init_cache_id (obj, *id_type, &cache_id_storage, &cache_id))
-				continue;
-			if (!cache_id)
-				continue;
-			g_assert (nm_multi_index_contains (cache->idx_multi, &cache_id->base, &obj->object));
-		}
-	}
-
-	nm_multi_index_iter_init (&iter_multi, cache->idx_multi, NULL);
-	while (nm_multi_index_iter_next (&iter_multi,
-	                                 (const NMMultiIndexId **) &cache_id,
-	                                 (void *const**) &objects,
-	                                 &len)) {
-		g_assert (len > 0 && objects && objects[len] == NULL);
-
-		for (i = 0; i < len; i++) {
-			g_assert (objects[i]);
-			obj = NMP_OBJECT_UP_CAST (objects[i]);
-			g_assert (NMP_OBJECT_IS_VALID (obj));
-
-			/* for now, enforce that all objects for a certain index are of the same type. */
-			g_assert (NMP_OBJECT_GET_CLASS (obj) == NMP_OBJECT_GET_CLASS (NMP_OBJECT_UP_CAST (objects[0])));
-
-			if (!_nmp_object_init_cache_id (obj, cache_id->_id_type, &cache_id_storage, &cache_id2))
-				g_assert_not_reached ();
-			g_assert (cache_id2);
-			g_assert (nmp_cache_id_equal (cache_id, cache_id2));
-			g_assert_cmpint (nmp_cache_id_hash (cache_id), ==, nmp_cache_id_hash (cache_id2));
-
-			g_assert (obj == g_hash_table_lookup (cache->idx_main, obj));
-		}
-	}
-#endif
 }
+
 /*****************************************************************************/
 
 const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
 	[NMP_OBJECT_TYPE_LINK - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_LINK,
 		.sizeof_data                        = sizeof (NMPObjectLink),
 		.sizeof_public                      = sizeof (NMPlatformLink),
@@ -2115,22 +2471,23 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
 		.signal_type_id                     = NM_PLATFORM_SIGNAL_ID_LINK,
 		.signal_type                        = NM_PLATFORM_SIGNAL_LINK_CHANGED,
 		.supported_cache_ids                = _supported_cache_ids_link,
-		.cmd_obj_init_cache_id              = _vt_cmd_obj_init_cache_id_link,
+		.cmd_obj_hash_update                = _vt_cmd_obj_hash_update_link,
 		.cmd_obj_cmp                        = _vt_cmd_obj_cmp_link,
 		.cmd_obj_copy                       = _vt_cmd_obj_copy_link,
-		.cmd_obj_stackinit_id               = _vt_cmd_obj_stackinit_id_link,
 		.cmd_obj_dispose                    = _vt_cmd_obj_dispose_link,
 		.cmd_obj_is_alive                   = _vt_cmd_obj_is_alive_link,
 		.cmd_obj_is_visible                 = _vt_cmd_obj_is_visible_link,
 		.cmd_obj_to_string                  = _vt_cmd_obj_to_string_link,
 		.cmd_plobj_id_copy                  = _vt_cmd_plobj_id_copy_link,
-		.cmd_plobj_id_equal                 = _vt_cmd_plobj_id_equal_link,
-		.cmd_plobj_id_hash                  = _vt_cmd_plobj_id_hash_link,
+		.cmd_plobj_id_cmp                   = _vt_cmd_plobj_id_cmp_link,
+		.cmd_plobj_id_hash_update           = _vt_cmd_plobj_id_hash_update_link,
 		.cmd_plobj_to_string_id             = _vt_cmd_plobj_to_string_id_link,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_link_to_string,
+		.cmd_plobj_hash_update              = (void (*) (const NMPlatformObject *obj, NMHashState *h)) nm_platform_link_hash_update,
 		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_link_cmp,
 	},
 	[NMP_OBJECT_TYPE_IP4_ADDRESS - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_IP4_ADDRESS,
 		.sizeof_data                        = sizeof (NMPObjectIP4Address),
 		.sizeof_public                      = sizeof (NMPlatformIP4Address),
@@ -2140,17 +2497,17 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
 		.signal_type_id                     = NM_PLATFORM_SIGNAL_ID_IP4_ADDRESS,
 		.signal_type                        = NM_PLATFORM_SIGNAL_IP4_ADDRESS_CHANGED,
 		.supported_cache_ids                = _supported_cache_ids_ipx_address,
-		.cmd_obj_init_cache_id              = _vt_cmd_obj_init_cache_id_ipx_address,
-		.cmd_obj_stackinit_id               = _vt_cmd_obj_stackinit_id_ip4_address,
 		.cmd_obj_is_alive                   = _vt_cmd_obj_is_alive_ipx_address,
 		.cmd_plobj_id_copy                  = _vt_cmd_plobj_id_copy_ip4_address,
-		.cmd_plobj_id_equal                 = _vt_cmd_plobj_id_equal_ip4_address,
-		.cmd_plobj_id_hash                  = _vt_cmd_plobj_id_hash_ip4_address,
+		.cmd_plobj_id_cmp                   = _vt_cmd_plobj_id_cmp_ip4_address,
+		.cmd_plobj_id_hash_update           = _vt_cmd_plobj_id_hash_update_ip4_address,
 		.cmd_plobj_to_string_id             = _vt_cmd_plobj_to_string_id_ip4_address,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_ip4_address_to_string,
+		.cmd_plobj_hash_update              = (void (*) (const NMPlatformObject *obj, NMHashState *h)) nm_platform_ip4_address_hash_update,
 		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_ip4_address_cmp,
 	},
 	[NMP_OBJECT_TYPE_IP6_ADDRESS - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_IP6_ADDRESS,
 		.sizeof_data                        = sizeof (NMPObjectIP6Address),
 		.sizeof_public                      = sizeof (NMPlatformIP6Address),
@@ -2160,17 +2517,17 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
 		.signal_type_id                     = NM_PLATFORM_SIGNAL_ID_IP6_ADDRESS,
 		.signal_type                        = NM_PLATFORM_SIGNAL_IP6_ADDRESS_CHANGED,
 		.supported_cache_ids                = _supported_cache_ids_ipx_address,
-		.cmd_obj_init_cache_id              = _vt_cmd_obj_init_cache_id_ipx_address,
-		.cmd_obj_stackinit_id               = _vt_cmd_obj_stackinit_id_ip6_address,
 		.cmd_obj_is_alive                   = _vt_cmd_obj_is_alive_ipx_address,
 		.cmd_plobj_id_copy                  = _vt_cmd_plobj_id_copy_ip6_address,
-		.cmd_plobj_id_equal                 = _vt_cmd_plobj_id_equal_ip6_address,
-		.cmd_plobj_id_hash                  = _vt_cmd_plobj_id_hash_ip6_address,
+		.cmd_plobj_id_cmp                   = _vt_cmd_plobj_id_cmp_ip6_address,
+		.cmd_plobj_id_hash_update           = _vt_cmd_plobj_id_hash_update_ip6_address,
 		.cmd_plobj_to_string_id             = _vt_cmd_plobj_to_string_id_ip6_address,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_ip6_address_to_string,
+		.cmd_plobj_hash_update              = (void (*) (const NMPlatformObject *obj, NMHashState *h)) nm_platform_ip6_address_hash_update,
 		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_ip6_address_cmp
 	},
 	[NMP_OBJECT_TYPE_IP4_ROUTE - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_IP4_ROUTE,
 		.sizeof_data                        = sizeof (NMPObjectIP4Route),
 		.sizeof_public                      = sizeof (NMPlatformIP4Route),
@@ -2179,18 +2536,18 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
 		.rtm_gettype                        = RTM_GETROUTE,
 		.signal_type_id                     = NM_PLATFORM_SIGNAL_ID_IP4_ROUTE,
 		.signal_type                        = NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED,
-		.supported_cache_ids                = _supported_cache_ids_ip4_route,
-		.cmd_obj_init_cache_id              = _vt_cmd_obj_init_cache_id_ipx_route,
-		.cmd_obj_stackinit_id               = _vt_cmd_obj_stackinit_id_ip4_route,
+		.supported_cache_ids                = _supported_cache_ids_ipx_route,
 		.cmd_obj_is_alive                   = _vt_cmd_obj_is_alive_ipx_route,
 		.cmd_plobj_id_copy                  = _vt_cmd_plobj_id_copy_ip4_route,
-		.cmd_plobj_id_equal                 = _vt_cmd_plobj_id_equal_ip4_route,
-		.cmd_plobj_id_hash                  = _vt_cmd_plobj_id_hash_ip4_route,
-		.cmd_plobj_to_string_id             = _vt_cmd_plobj_to_string_id_ip4_route,
+		.cmd_plobj_id_cmp                   = _vt_cmd_plobj_id_cmp_ip4_route,
+		.cmd_plobj_id_hash_update           = _vt_cmd_plobj_id_hash_update_ip4_route,
+		.cmd_plobj_to_string_id             = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_ip4_route_to_string,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_ip4_route_to_string,
-		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_ip4_route_cmp,
+		.cmd_plobj_hash_update              = _vt_cmd_plobj_hash_update_ip4_route,
+		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_ip4_route_cmp_full,
 	},
 	[NMP_OBJECT_TYPE_IP6_ROUTE - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_IP6_ROUTE,
 		.sizeof_data                        = sizeof (NMPObjectIP6Route),
 		.sizeof_public                      = sizeof (NMPlatformIP6Route),
@@ -2199,109 +2556,129 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
 		.rtm_gettype                        = RTM_GETROUTE,
 		.signal_type_id                     = NM_PLATFORM_SIGNAL_ID_IP6_ROUTE,
 		.signal_type                        = NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED,
-		.supported_cache_ids                = _supported_cache_ids_ip6_route,
-		.cmd_obj_init_cache_id              = _vt_cmd_obj_init_cache_id_ipx_route,
-		.cmd_obj_stackinit_id               = _vt_cmd_obj_stackinit_id_ip6_route,
+		.supported_cache_ids                = _supported_cache_ids_ipx_route,
 		.cmd_obj_is_alive                   = _vt_cmd_obj_is_alive_ipx_route,
 		.cmd_plobj_id_copy                  = _vt_cmd_plobj_id_copy_ip6_route,
-		.cmd_plobj_id_equal                 = _vt_cmd_plobj_id_equal_ip6_route,
-		.cmd_plobj_id_hash                  = _vt_cmd_plobj_id_hash_ip6_route,
-		.cmd_plobj_to_string_id             = _vt_cmd_plobj_to_string_id_ip6_route,
+		.cmd_plobj_id_cmp                   = _vt_cmd_plobj_id_cmp_ip6_route,
+		.cmd_plobj_id_hash_update           = _vt_cmd_plobj_id_hash_update_ip6_route,
+		.cmd_plobj_to_string_id             = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_ip6_route_to_string,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_ip6_route_to_string,
-		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_ip6_route_cmp,
+		.cmd_plobj_hash_update              = _vt_cmd_plobj_hash_update_ip6_route,
+		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_ip6_route_cmp_full,
 	},
 	[NMP_OBJECT_TYPE_LNK_GRE - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_LNK_GRE,
 		.sizeof_data                        = sizeof (NMPObjectLnkGre),
 		.sizeof_public                      = sizeof (NMPlatformLnkGre),
 		.obj_type_name                      = "gre",
 		.lnk_link_type                      = NM_LINK_TYPE_GRE,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_lnk_gre_to_string,
+		.cmd_plobj_hash_update              = (void (*) (const NMPlatformObject *obj, NMHashState *h)) nm_platform_lnk_gre_hash_update,
 		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_lnk_gre_cmp,
 	},
 	[NMP_OBJECT_TYPE_LNK_INFINIBAND - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_LNK_INFINIBAND,
 		.sizeof_data                        = sizeof (NMPObjectLnkInfiniband),
 		.sizeof_public                      = sizeof (NMPlatformLnkInfiniband),
 		.obj_type_name                      = "infiniband",
 		.lnk_link_type                      = NM_LINK_TYPE_INFINIBAND,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_lnk_infiniband_to_string,
+		.cmd_plobj_hash_update              = (void (*) (const NMPlatformObject *obj, NMHashState *h)) nm_platform_lnk_infiniband_hash_update,
 		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_lnk_infiniband_cmp,
 	},
 	[NMP_OBJECT_TYPE_LNK_IP6TNL - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_LNK_IP6TNL,
 		.sizeof_data                        = sizeof (NMPObjectLnkIp6Tnl),
 		.sizeof_public                      = sizeof (NMPlatformLnkIp6Tnl),
 		.obj_type_name                      = "ip6tnl",
 		.lnk_link_type                      = NM_LINK_TYPE_IP6TNL,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_lnk_ip6tnl_to_string,
+		.cmd_plobj_hash_update              = (void (*) (const NMPlatformObject *obj, NMHashState *h)) nm_platform_lnk_ip6tnl_hash_update,
 		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_lnk_ip6tnl_cmp,
 	},
 	[NMP_OBJECT_TYPE_LNK_IPIP - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_LNK_IPIP,
 		.sizeof_data                        = sizeof (NMPObjectLnkIpIp),
 		.sizeof_public                      = sizeof (NMPlatformLnkIpIp),
 		.obj_type_name                      = "ipip",
 		.lnk_link_type                      = NM_LINK_TYPE_IPIP,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_lnk_ipip_to_string,
+		.cmd_plobj_hash_update              = (void (*) (const NMPlatformObject *obj, NMHashState *h)) nm_platform_lnk_ipip_hash_update,
 		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_lnk_ipip_cmp,
 	},
 	[NMP_OBJECT_TYPE_LNK_MACSEC - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_LNK_MACSEC,
 		.sizeof_data                        = sizeof (NMPObjectLnkMacsec),
 		.sizeof_public                      = sizeof (NMPlatformLnkMacsec),
 		.obj_type_name                      = "macsec",
 		.lnk_link_type                      = NM_LINK_TYPE_MACSEC,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_lnk_macsec_to_string,
+		.cmd_plobj_hash_update              = (void (*) (const NMPlatformObject *obj, NMHashState *h)) nm_platform_lnk_macsec_hash_update,
 		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_lnk_macsec_cmp,
 	},
 	[NMP_OBJECT_TYPE_LNK_MACVLAN - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_LNK_MACVLAN,
 		.sizeof_data                        = sizeof (NMPObjectLnkMacvlan),
 		.sizeof_public                      = sizeof (NMPlatformLnkMacvlan),
 		.obj_type_name                      = "macvlan",
 		.lnk_link_type                      = NM_LINK_TYPE_MACVLAN,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_lnk_macvlan_to_string,
+		.cmd_plobj_hash_update              = (void (*) (const NMPlatformObject *obj, NMHashState *h)) nm_platform_lnk_macvlan_hash_update,
 		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_lnk_macvlan_cmp,
 	},
 	[NMP_OBJECT_TYPE_LNK_MACVTAP - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_LNK_MACVTAP,
 		.sizeof_data                        = sizeof (NMPObjectLnkMacvtap),
 		.sizeof_public                      = sizeof (NMPlatformLnkMacvtap),
 		.obj_type_name                      = "macvtap",
 		.lnk_link_type                      = NM_LINK_TYPE_MACVTAP,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_lnk_macvlan_to_string,
+		.cmd_plobj_hash_update              = (void (*) (const NMPlatformObject *obj, NMHashState *h)) nm_platform_lnk_macvlan_hash_update,
 		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_lnk_macvlan_cmp,
 	},
 	[NMP_OBJECT_TYPE_LNK_SIT - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_LNK_SIT,
 		.sizeof_data                        = sizeof (NMPObjectLnkSit),
 		.sizeof_public                      = sizeof (NMPlatformLnkSit),
 		.obj_type_name                      = "sit",
 		.lnk_link_type                      = NM_LINK_TYPE_SIT,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_lnk_sit_to_string,
+		.cmd_plobj_hash_update              = (void (*) (const NMPlatformObject *obj, NMHashState *h)) nm_platform_lnk_sit_hash_update,
 		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_lnk_sit_cmp,
 	},
 	[NMP_OBJECT_TYPE_LNK_VLAN - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_LNK_VLAN,
 		.sizeof_data                        = sizeof (NMPObjectLnkVlan),
 		.sizeof_public                      = sizeof (NMPlatformLnkVlan),
 		.obj_type_name                      = "vlan",
 		.lnk_link_type                      = NM_LINK_TYPE_VLAN,
+		.cmd_obj_hash_update                = _vt_cmd_obj_hash_update_lnk_vlan,
 		.cmd_obj_cmp                        = _vt_cmd_obj_cmp_lnk_vlan,
 		.cmd_obj_copy                       = _vt_cmd_obj_copy_lnk_vlan,
 		.cmd_obj_dispose                    = _vt_cmd_obj_dispose_lnk_vlan,
 		.cmd_obj_to_string                  = _vt_cmd_obj_to_string_lnk_vlan,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_lnk_vlan_to_string,
+		.cmd_plobj_hash_update              = (void (*) (const NMPlatformObject *obj, NMHashState *h)) nm_platform_lnk_vlan_hash_update,
 		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_lnk_vlan_cmp,
 	},
 	[NMP_OBJECT_TYPE_LNK_VXLAN - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_LNK_VXLAN,
 		.sizeof_data                        = sizeof (NMPObjectLnkVxlan),
 		.sizeof_public                      = sizeof (NMPlatformLnkVxlan),
 		.obj_type_name                      = "vxlan",
 		.lnk_link_type                      = NM_LINK_TYPE_VXLAN,
 		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_lnk_vxlan_to_string,
+		.cmd_plobj_hash_update              = (void (*) (const NMPlatformObject *obj, NMHashState *h)) nm_platform_lnk_vxlan_hash_update,
 		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_lnk_vxlan_cmp,
 	},
 };