summary refs log tree commit diff
path: root/src/platform
diff options
context:
space:
mode:
Diffstat (limited to 'src/platform')
-rw-r--r--src/platform/nm-fake-platform.c1437
-rw-r--r--src/platform/nm-linux-platform.c2379
-rw-r--r--src/platform/nm-linux-platform.h6
-rw-r--r--src/platform/nm-platform-private.h42
-rw-r--r--src/platform/nm-platform.c2926
-rw-r--r--src/platform/nm-platform.h469
-rw-r--r--src/platform/nmp-netns.c16
-rw-r--r--src/platform/nmp-netns.h3
-rw-r--r--src/platform/nmp-object.c2461
-rw-r--r--src/platform/nmp-object.h559
-rw-r--r--src/platform/tests/monitor.c2
-rw-r--r--src/platform/tests/test-address.c87
-rw-r--r--src/platform/tests/test-cleanup.c32
-rw-r--r--src/platform/tests/test-common.c445
-rw-r--r--src/platform/tests/test-common.h174
-rw-r--r--src/platform/tests/test-general.c2
-rw-r--r--src/platform/tests/test-link.c31
-rw-r--r--src/platform/tests/test-nmp-object.c494
-rw-r--r--src/platform/tests/test-route.c541
-rw-r--r--src/platform/wifi/wifi-utils-nl80211.c38
-rw-r--r--src/platform/wifi/wifi-utils-wext.c5
21 files changed, 7625 insertions, 4524 deletions
diff --git a/src/platform/nm-fake-platform.c b/src/platform/nm-fake-platform.c
index 38706f37..c199c5ed 100644
--- a/src/platform/nm-fake-platform.c
+++ b/src/platform/nm-fake-platform.c
@@ -32,6 +32,7 @@
 
 #include "nm-core-utils.h"
 #include "nm-platform-utils.h"
+#include "nm-platform-private.h"
 #include "nmp-object.h"
 
 #include "nm-test-utils-core.h"
@@ -39,20 +40,14 @@
 /*****************************************************************************/
 
 typedef struct {
-	NMPlatformLink link;
-
+	const NMPObject *obj;
 	char *udi;
-	NMPObject *lnk;
 	struct in6_addr ip6_lladdr;
 } NMFakePlatformLink;
 
 typedef struct {
 	GHashTable *options;
 	GArray *links;
-	GArray *ip4_addresses;
-	GArray *ip6_addresses;
-	GArray *ip4_routes;
-	GArray *ip6_routes;
 } NMFakePlatformPrivate;
 
 struct _NMFakePlatform {
@@ -96,7 +91,22 @@ G_DEFINE_TYPE (NMFakePlatform, nm_fake_platform, NM_TYPE_PLATFORM)
 
 /*****************************************************************************/
 
-static void link_changed (NMPlatform *platform, NMFakePlatformLink *device, gboolean raise_signal);
+static void link_changed (NMPlatform *platform,
+                          NMFakePlatformLink *device,
+                          NMPCacheOpsType cache_op,
+                          const NMPObject *obj_old);
+
+static gboolean ipx_address_delete (NMPlatform *platform,
+                                    int addr_family,
+                                    int ifindex,
+                                    gconstpointer addr,
+                                    const guint8 *plen,
+                                    gconstpointer peer_addr);
+
+static gboolean ipx_route_delete (NMPlatform *platform,
+                                  int addr_family,
+                                  int ifindex,
+                                  const NMPObject *obj);
 
 static gboolean ip6_address_add (NMPlatform *platform,
                                  int ifindex,
@@ -110,14 +120,6 @@ static gboolean ip6_address_delete (NMPlatform *platform, int ifindex, struct in
 
 /*****************************************************************************/
 
-static gboolean
-_ip4_address_equal_peer_net (in_addr_t peer1, in_addr_t peer2, guint8 plen)
-{
-	return ((peer1 ^ peer2) & nm_utils_ip4_prefix_to_netmask (plen)) == 0;
-}
-
-/*****************************************************************************/
-
 #define ASSERT_SYSCTL_ARGS(pathid, dirfd, path) \
 	G_STMT_START { \
 		const char *const _pathid = (pathid); \
@@ -158,288 +160,338 @@ sysctl_get (NMPlatform *platform, const char *pathid, int dirfd, const char *pat
 	return g_strdup (g_hash_table_lookup (priv->options, path));
 }
 
-static const char *
-type_to_type_name (NMLinkType type)
-{
-	switch (type) {
-	case NM_LINK_TYPE_UNKNOWN:
-		return "unknown";
-	case NM_LINK_TYPE_LOOPBACK:
-		return "loopback";
-	case NM_LINK_TYPE_ETHERNET:
-		return "ethernet";
-	case NM_LINK_TYPE_DUMMY:
-		return "dummy";
-	case NM_LINK_TYPE_BRIDGE:
-		return "bridge";
-	case NM_LINK_TYPE_BOND:
-		return "bond";
-	case NM_LINK_TYPE_TEAM:
-		return "team";
-	case NM_LINK_TYPE_VLAN:
-		return "vlan";
-	case NM_LINK_TYPE_NONE:
-	default:
-		return NULL;
-	}
-}
-
-static void
-link_init (NMFakePlatformLink *device, int ifindex, int type, const char *name)
-{
-	gs_free char *ip6_lladdr = NULL;
-
-	g_assert (!name || strlen (name) < sizeof(device->link.name));
-
-	memset (device, 0, sizeof (*device));
-
-	ip6_lladdr = ifindex > 0 ? g_strdup_printf ("fe80::fa1e:%0x:%0x", ifindex / 256, ifindex % 256) : NULL;
-
-	device->link.ifindex = name ? ifindex : 0;
-	device->link.type = type;
-	device->link.kind = type_to_type_name (type);
-	device->link.driver = type_to_type_name (type);
-	device->udi = g_strdup_printf ("fake:%d", ifindex);
-	device->link.initialized = TRUE;
-	device->ip6_lladdr = *nmtst_inet6_from_string (ip6_lladdr);
-	if (name)
-		strcpy (device->link.name, name);
-	switch (device->link.type) {
-	case NM_LINK_TYPE_DUMMY:
-		device->link.n_ifi_flags = NM_FLAGS_SET (device->link.n_ifi_flags, IFF_NOARP);
-		break;
-	default:
-		device->link.n_ifi_flags = NM_FLAGS_UNSET (device->link.n_ifi_flags, IFF_NOARP);
-		break;
-	}
-}
-
 static NMFakePlatformLink *
 link_get (NMPlatform *platform, int ifindex)
 {
 	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
 	NMFakePlatformLink *device;
+	int idx;
+
+	if (ifindex <= 0)
+		g_return_val_if_reached (NULL);
 
-	if (ifindex >= priv->links->len)
+	idx = ifindex - 1;
+	if (idx >= priv->links->len)
 		goto not_found;
-	device = &g_array_index (priv->links, NMFakePlatformLink, ifindex);
-	if (!device->link.ifindex)
+
+	device = &g_array_index (priv->links, NMFakePlatformLink, idx);
+	if (!device->obj)
 		goto not_found;
 
+	g_assert (ifindex == NMP_OBJECT_CAST_LINK (device->obj)->ifindex);
+	g_assert (device->obj == nm_platform_link_get_obj (platform, ifindex, FALSE));
+
 	return device;
 not_found:
 	_LOGD ("link not found: %d", ifindex);
 	return NULL;
 }
 
-static GArray *
-link_get_all (NMPlatform *platform)
+static void
+link_add_prepare (NMPlatform *platform,
+                  NMFakePlatformLink *device,
+                  NMPObject *obj_tmp)
 {
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	GArray *links = g_array_sized_new (TRUE, TRUE, sizeof (NMPlatformLink), priv->links->len);
-	int i;
+	gboolean connected;
 
-	for (i = 0; i < priv->links->len; i++)
-		if (g_array_index (priv->links, NMFakePlatformLink, i).link.ifindex)
-			g_array_append_val (links, g_array_index (priv->links, NMFakePlatformLink, i).link);
+	/* we must clear the driver, because platform cache want's to set it */
+	g_assert (obj_tmp->link.driver == g_intern_string (obj_tmp->link.driver));
+	obj_tmp->link.driver = NULL;
 
-	return links;
-}
-
-static const NMPlatformLink *
-_nm_platform_link_get (NMPlatform *platform, int ifindex)
-{
-	NMFakePlatformLink *device = link_get (platform, ifindex);
+	if (NM_IN_SET (obj_tmp->link.type, NM_LINK_TYPE_BRIDGE,
+	                                   NM_LINK_TYPE_BOND)) {
+		connected = FALSE;
+		if (NM_FLAGS_HAS (obj_tmp->link.n_ifi_flags, IFF_UP)) {
+			NMPLookup lookup;
+			NMDedupMultiIter iter;
+			const NMPObject *slave_candidate = NULL;
+
+			nmp_cache_iter_for_each (&iter,
+			                         nmp_cache_lookup (nm_platform_get_cache (platform),
+			                                           nmp_lookup_init_obj_type (&lookup,
+			                                                                     NMP_OBJECT_TYPE_LINK)),
+			                         &slave_candidate) {
+				if (nmp_cache_link_connected_for_slave (obj_tmp->link.ifindex, slave_candidate)) {
+					connected = TRUE;
+					break;
+				}
+			}
+		}
+	} else
+		connected = NM_FLAGS_HAS (obj_tmp->link.n_ifi_flags, IFF_UP);
 
-	return device ? &device->link : NULL;
+	obj_tmp->link.n_ifi_flags = NM_FLAGS_ASSIGN (obj_tmp->link.n_ifi_flags, IFF_LOWER_UP, connected);
+	obj_tmp->link.connected = connected;
 }
 
-static const NMPlatformLink *
-_nm_platform_link_get_by_ifname (NMPlatform *platform, const char *ifname)
+static NMFakePlatformLink *
+link_add_pre (NMPlatform *platform,
+              const char *name,
+              NMLinkType type,
+              const void *address,
+              size_t address_len)
 {
 	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	guint i;
+	NMFakePlatformLink *device;
+	int ifindex;
+	NMPObject *o;
+	NMPlatformLink *link;
+	gs_free char *ip6_lladdr = NULL;
 
-	for (i = 0; i < priv->links->len; i++) {
-		NMFakePlatformLink *device = &g_array_index (priv->links, NMFakePlatformLink, i);
+	g_assert (!name || strlen (name) < IFNAMSIZ);
 
-		if (!strcmp (device->link.name, ifname))
-			return &device->link;
-	}
-	return NULL;
-}
+	g_array_set_size (priv->links, priv->links->len + 1);
+	device = &g_array_index (priv->links, NMFakePlatformLink, priv->links->len - 1);
+	ifindex = priv->links->len;
 
-static const NMPlatformLink *
-_nm_platform_link_get_by_address (NMPlatform *platform,
-                                  gconstpointer address,
-                                  size_t length)
-{
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	guint i;
+	memset (device, 0, sizeof (*device));
 
-	if (   length == 0
-	    || length > NM_UTILS_HWADDR_LEN_MAX
-	    || !address)
-		g_return_val_if_reached (NULL);
+	o = nmp_object_new_link (ifindex);
+	link = NMP_OBJECT_CAST_LINK (o);
 
-	for (i = 0; i < priv->links->len; i++) {
-		NMFakePlatformLink *device = &g_array_index (priv->links, NMFakePlatformLink, i);
+	ip6_lladdr = ifindex > 0 ? g_strdup_printf ("fe80::fa1e:%0x:%0x", ifindex / 256, ifindex % 256) : NULL;
 
-		if (   device->link.addr.len == length
-		    && memcmp (device->link.addr.data, address, length) == 0) {
-			return &device->link;
-		}
+	link->ifindex = name ? ifindex : 0;
+	link->type = type;
+	link->kind = g_intern_string (nm_link_type_to_string (type));
+	link->initialized = TRUE;
+	if (name)
+		strcpy (link->name, name);
+	switch (link->type) {
+	case NM_LINK_TYPE_DUMMY:
+		link->n_ifi_flags = NM_FLAGS_SET (link->n_ifi_flags, IFF_NOARP);
+		break;
+	default:
+		link->n_ifi_flags = NM_FLAGS_UNSET (link->n_ifi_flags, IFF_NOARP);
+		break;
 	}
-	return NULL;
-}
 
-static const NMPObject *
-link_get_lnk (NMPlatform *platform,
-              int ifindex,
-              NMLinkType link_type,
-              const NMPlatformLink **out_link)
-{
-	NMFakePlatformLink *device = link_get (platform, ifindex);
+	o->_link.netlink.is_in_netlink = TRUE;
 
-	if (!device)
-		return NULL;
-
-	NM_SET_OUT (out_link, &device->link);
-
-	if (!device->lnk)
-		return NULL;
-
-	if (link_type == NM_LINK_TYPE_NONE)
-		return device->lnk;
+	if (address) {
+		g_assert (address_len > 0 && address_len <= sizeof (link->addr.data));
+		memcpy (link->addr.data, address, address_len);
+		link->addr.len = address_len;
+	} else
+		g_assert (address_len == 0);
 
-	if (   link_type != device->link.type
-	    || link_type != NMP_OBJECT_GET_CLASS (device->lnk)->lnk_link_type)
-		return NULL;
+	device->obj = o;
+	device->udi = g_strdup_printf ("fake:%d", ifindex);
+	device->ip6_lladdr = *nmtst_inet6_from_string (ip6_lladdr);
 
-	return device->lnk;
+	return device;
 }
 
 static gboolean
 link_add (NMPlatform *platform,
           const char *name,
           NMLinkType type,
+          const char *veth_peer,
           const void *address,
           size_t address_len,
           const NMPlatformLink **out_link)
 {
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	NMFakePlatformLink device;
-	NMFakePlatformLink *new_device;
+	NMFakePlatformLink *device;
+	NMFakePlatformLink *device_veth = NULL;
+	nm_auto_nmpobj const NMPObject *obj_old = NULL;
+	nm_auto_nmpobj const NMPObject *obj_new = NULL;
+	nm_auto_nmpobj const NMPObject *obj_old_veth = NULL;
+	nm_auto_nmpobj const NMPObject *obj_new_veth = NULL;
+	NMPCacheOpsType cache_op;
+	NMPCacheOpsType cache_op_veth = NMP_CACHE_OPS_UNCHANGED;
+
+	device = link_add_pre (platform, name, type, address, address_len);
+
+	if (veth_peer) {
+		g_assert (type == NM_LINK_TYPE_VETH);
+		device_veth = link_add_pre (platform, veth_peer, type, NULL, 0);
+	} else
+		g_assert (type != NM_LINK_TYPE_VETH);
+
+	link_add_prepare (platform, device, (NMPObject *) device->obj);
+	cache_op = nmp_cache_update_netlink (nm_platform_get_cache (platform),
+	                                     (NMPObject *) device->obj,
+	                                     FALSE,
+	                                     &obj_old, &obj_new);
+	g_assert (cache_op == NMP_CACHE_OPS_ADDED);
+	nmp_object_unref (device->obj);
+	device->obj = nmp_object_ref (obj_new);
+	if (veth_peer) {
+		link_add_prepare (platform, device_veth, (NMPObject *) device_veth->obj);
+		cache_op_veth = nmp_cache_update_netlink (nm_platform_get_cache (platform),
+		                                          (NMPObject *) device_veth->obj,
+		                                          FALSE,
+		                                          &obj_old_veth, &obj_new_veth);
+		g_assert (cache_op == NMP_CACHE_OPS_ADDED);
+		nmp_object_unref (device->obj);
+		device->obj = nmp_object_ref (obj_new);
+	}
 
-	link_init (&device, priv->links->len, type, name);
+	if (out_link)
+		*out_link = NMP_OBJECT_CAST_LINK (device->obj);
 
-	if (address) {
-		g_return_val_if_fail (address_len > 0 && address_len <= sizeof (device.link.addr.data), FALSE);
-		memcpy (device.link.addr.data, address, address_len);
-		device.link.addr.len = address_len;
-	}
+	link_changed (platform, device, cache_op, NULL);
+	if (veth_peer)
+		link_changed (platform, device_veth, cache_op_veth, NULL);
+
+	return TRUE;
+}
+
+static NMFakePlatformLink *
+link_add_one (NMPlatform *platform,
+              const char *name,
+              NMLinkType link_type,
+              void (*prepare_fcn) (NMPlatform *platform, NMFakePlatformLink *device, gconstpointer user_data),
+              gconstpointer user_data,
+              const NMPlatformLink **out_link)
+{
+	NMFakePlatformLink *device;
+	nm_auto_nmpobj const NMPObject *obj_old = NULL;
+	nm_auto_nmpobj const NMPObject *obj_new = NULL;
+	NMPCacheOpsType cache_op;
+	int ifindex;
 
-	g_array_append_val (priv->links, device);
-	new_device = &g_array_index (priv->links, NMFakePlatformLink, priv->links->len - 1);
+	device = link_add_pre (platform, name, NM_LINK_TYPE_VLAN, NULL, 0);
 
-	if (device.link.ifindex) {
-		g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_LINK_CHANGED, (int) NMP_OBJECT_TYPE_LINK, device.link.ifindex, &device, (int) NM_PLATFORM_SIGNAL_ADDED);
+	ifindex = NMP_OBJECT_CAST_LINK (device->obj)->ifindex;
 
-		link_changed (platform, &g_array_index (priv->links, NMFakePlatformLink, priv->links->len - 1), FALSE);
-	}
+	if (prepare_fcn)
+		prepare_fcn (platform, device, user_data);
 
-	if (out_link)
-		*out_link = &new_device->link;
-	return TRUE;
+	link_add_prepare (platform, device, (NMPObject *) device->obj);
+	cache_op = nmp_cache_update_netlink (nm_platform_get_cache (platform),
+	                                     (NMPObject *) device->obj,
+	                                     FALSE,
+	                                     &obj_old, &obj_new);
+	g_assert (cache_op == NMP_CACHE_OPS_ADDED);
+	nmp_object_unref (device->obj);
+	device->obj = nmp_object_ref (obj_new);
+
+	link_changed (platform, device, cache_op, obj_old);
+
+	device = link_get (platform, ifindex);
+	if (!device)
+		g_assert_not_reached ();
+
+	NM_SET_OUT (out_link, NMP_OBJECT_CAST_LINK (device->obj));
+	return device;
 }
 
 static gboolean
 link_delete (NMPlatform *platform, int ifindex)
 {
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
 	NMFakePlatformLink *device = link_get (platform, ifindex);
-	NMPlatformLink deleted_device;
-	int i;
+	nm_auto_nmpobj const NMPObject *obj_old = NULL;
+	nm_auto_nmpobj const NMPObject *obj_old2 = NULL;
+	NMPCacheOpsType cache_op;
 
-	if (!device || !device->link.ifindex)
+	if (!device)
 		return FALSE;
 
-	memcpy (&deleted_device, &device->link, sizeof (deleted_device));
-	memset (&device->link, 0, sizeof (device->link));
-	g_clear_pointer (&device->lnk, nmp_object_unref);
+	obj_old = g_steal_pointer (&device->obj);
 	g_clear_pointer (&device->udi, g_free);
 
+	cache_op = nmp_cache_remove (nm_platform_get_cache (platform),
+	                             obj_old,
+	                             FALSE,
+	                             FALSE,
+	                             &obj_old2);
+	g_assert (cache_op == NMP_CACHE_OPS_REMOVED);
+	g_assert (obj_old2);
+	g_assert (obj_old == obj_old2);
+
 	/* Remove addresses and routes which belong to the deleted interface */
-	for (i = 0; i < priv->ip4_addresses->len; i++) {
-		NMPlatformIP4Address *address = &g_array_index (priv->ip4_addresses, NMPlatformIP4Address, i);
+	ipx_address_delete (platform, AF_INET, ifindex, NULL, NULL, NULL);
+	ipx_address_delete (platform, AF_INET6, ifindex, NULL, NULL, NULL);
+	ipx_route_delete (platform, AF_INET, ifindex, NULL);
+	ipx_route_delete (platform, AF_INET6, ifindex, NULL);
+
+	nm_platform_cache_update_emit_signal (platform,
+	                                      cache_op,
+	                                      obj_old2,
+	                                      NULL);
+	return TRUE;
+}
 
-		if (address->ifindex == ifindex)
-			memset (address, 0, sizeof (*address));
-	}
-	for (i = 0; i < priv->ip6_addresses->len; i++) {
-		NMPlatformIP6Address *address = &g_array_index (priv->ip6_addresses, NMPlatformIP6Address, i);
+static void
+link_set_obj (NMPlatform *platform,
+              NMFakePlatformLink *device,
+              NMPObject *obj_tmp)
+{
+	nm_auto_nmpobj const NMPObject *obj_new = NULL;
+	nm_auto_nmpobj const NMPObject *obj_old = NULL;
+	nm_auto_nmpobj NMPObject *obj_tmp_tmp = NULL;
+	NMPCacheOpsType cache_op;
 
-		if (address->ifindex == ifindex)
-			memset (address, 0, sizeof (*address));
-	}
-	for (i = 0; i < priv->ip4_routes->len; i++) {
-		NMPlatformIP4Route *route = &g_array_index (priv->ip4_routes, NMPlatformIP4Route, i);
+	g_assert (device);
+	g_assert (NMP_OBJECT_GET_TYPE (device->obj) == NMP_OBJECT_TYPE_LINK);
 
-		if (route->ifindex == ifindex)
-			memset (route, 0, sizeof (*route));
+	if (!obj_tmp) {
+		obj_tmp_tmp = nmp_object_clone (device->obj, FALSE);
+		obj_tmp = obj_tmp_tmp;
 	}
-	for (i = 0; i < priv->ip6_routes->len; i++) {
-		NMPlatformIP6Route *route = &g_array_index (priv->ip6_routes, NMPlatformIP6Route, i);
 
-		if (route->ifindex == ifindex)
-			memset (route, 0, sizeof (*route));
-	}
+	g_assert (NMP_OBJECT_GET_TYPE (obj_tmp) == NMP_OBJECT_TYPE_LINK);
 
-	g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_LINK_CHANGED, (int) NMP_OBJECT_TYPE_LINK, ifindex, &deleted_device, (int) NM_PLATFORM_SIGNAL_REMOVED);
+	link_add_prepare (platform, device, obj_tmp);
+	cache_op = nmp_cache_update_netlink (nm_platform_get_cache (platform),
+	                                     obj_tmp,
+	                                     FALSE,
+	                                     &obj_old, &obj_new);
+	g_assert (NM_IN_SET (cache_op, NMP_CACHE_OPS_UNCHANGED,
+	                               NMP_CACHE_OPS_UPDATED));
+	g_assert (obj_old == device->obj);
+	g_assert (obj_new);
 
-	return TRUE;
-}
+	nmp_object_unref (device->obj);
+	device->obj = nmp_object_ref (obj_new);
 
-static const char *
-link_get_type_name (NMPlatform *platform, int ifindex)
-{
-	return type_to_type_name (nm_platform_link_get_type (platform, ifindex));
+	link_changed (platform, device, cache_op, obj_old);
 }
 
 static void
-link_changed (NMPlatform *platform, NMFakePlatformLink *device, gboolean raise_signal)
+link_set_flags (NMPlatform *platform,
+                NMFakePlatformLink *device,
+                guint n_ifi_flags)
 {
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	int i;
+	nm_auto_nmpobj NMPObject *obj_tmp = NULL;
 
-	if (raise_signal)
-		g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_LINK_CHANGED, (int) NMP_OBJECT_TYPE_LINK, device->link.ifindex, &device->link, (int) NM_PLATFORM_SIGNAL_CHANGED);
+	g_assert (device);
+	g_assert (NMP_OBJECT_GET_TYPE (device->obj) == NMP_OBJECT_TYPE_LINK);
 
-	if (device->link.ifindex && !IN6_IS_ADDR_UNSPECIFIED (&device->ip6_lladdr)) {
-		if (device->link.connected)
-			ip6_address_add (platform, device->link.ifindex, in6addr_any, 64, device->ip6_lladdr, NM_PLATFORM_LIFETIME_PERMANENT, NM_PLATFORM_LIFETIME_PERMANENT, 0);
-		else
-			ip6_address_delete (platform, device->link.ifindex, device->ip6_lladdr, 64);
-	}
+	obj_tmp = nmp_object_clone (device->obj, FALSE);
+	obj_tmp->link.n_ifi_flags = n_ifi_flags;
+	link_set_obj (platform, device, obj_tmp);
+}
 
-	if (device->link.master) {
-		gboolean connected = FALSE;
+static void
+link_changed (NMPlatform *platform,
+              NMFakePlatformLink *device,
+              NMPCacheOpsType cache_op,
+              const NMPObject *obj_old)
+{
+	g_assert (device->obj);
 
-		NMFakePlatformLink *master = link_get (platform, device->link.master);
+	g_assert (!nmp_cache_link_connected_needs_toggle (nm_platform_get_cache (platform),
+	                                                  device->obj, NULL, NULL));
 
-		g_return_if_fail (master && master != device);
+	nm_platform_cache_update_emit_signal (platform,
+	                                      cache_op,
+	                                      obj_old,
+	                                      device->obj);
 
-		for (i = 0; i < priv->links->len; i++) {
-			NMFakePlatformLink *slave = &g_array_index (priv->links, NMFakePlatformLink, i);
+	if (!IN6_IS_ADDR_UNSPECIFIED (&device->ip6_lladdr)) {
+		if (device->obj->link.connected)
+			ip6_address_add (platform, device->obj->link.ifindex, in6addr_any, 64, device->ip6_lladdr, NM_PLATFORM_LIFETIME_PERMANENT, NM_PLATFORM_LIFETIME_PERMANENT, 0);
+		else
+			ip6_address_delete (platform, device->obj->link.ifindex, device->ip6_lladdr, 64);
+	}
 
-			if (slave && slave->link.master == master->link.ifindex && slave->link.connected)
-				connected = TRUE;
-		}
+	if (device->obj->link.master) {
+		NMFakePlatformLink *master;
 
-		if (master->link.connected != connected) {
-			master->link.connected = connected;
-			link_changed (platform, master, TRUE);
-		}
+		master = link_get (platform, device->obj->link.master);
+		link_set_obj (platform, master, NULL);
 	}
 }
 
@@ -447,7 +499,6 @@ static gboolean
 link_set_up (NMPlatform *platform, int ifindex, gboolean *out_no_firmware)
 {
 	NMFakePlatformLink *device = link_get (platform, ifindex);
-	gboolean up, connected;
 
 	if (out_no_firmware)
 		*out_no_firmware = FALSE;
@@ -457,29 +508,9 @@ link_set_up (NMPlatform *platform, int ifindex, gboolean *out_no_firmware)
 		return FALSE;
 	}
 
-	up = TRUE;
-	connected = TRUE;
-	switch (device->link.type) {
-	case NM_LINK_TYPE_DUMMY:
-	case NM_LINK_TYPE_VLAN:
-		break;
-	case NM_LINK_TYPE_BRIDGE:
-	case NM_LINK_TYPE_BOND:
-	case NM_LINK_TYPE_TEAM:
-		connected = FALSE;
-		break;
-	default:
-		connected = FALSE;
-		g_error ("Unexpected device type: %d", device->link.type);
-	}
-
-	if (   NM_FLAGS_HAS (device->link.n_ifi_flags, IFF_UP) != !!up
-	    || device->link.connected != connected) {
-		device->link.n_ifi_flags = NM_FLAGS_ASSIGN (device->link.n_ifi_flags, IFF_UP, up);
-		device->link.connected = connected;
-		link_changed (platform, device, TRUE);
-	}
-
+	link_set_flags (platform,
+	                device,
+	                NM_FLAGS_ASSIGN (device->obj->link.n_ifi_flags, IFF_UP, TRUE));
 	return TRUE;
 }
 
@@ -493,13 +524,9 @@ link_set_down (NMPlatform *platform, int ifindex)
 		return FALSE;
 	}
 
-	if (NM_FLAGS_HAS (device->link.n_ifi_flags, IFF_UP) || device->link.connected) {
-		device->link.n_ifi_flags = NM_FLAGS_UNSET (device->link.n_ifi_flags, IFF_UP);
-		device->link.connected = FALSE;
-
-		link_changed (platform, device, TRUE);
-	}
-
+	link_set_flags (platform,
+	                device,
+	                NM_FLAGS_UNSET (device->obj->link.n_ifi_flags, IFF_UP));
 	return TRUE;
 }
 
@@ -513,10 +540,9 @@ link_set_arp (NMPlatform *platform, int ifindex)
 		return FALSE;
 	}
 
-	device->link.n_ifi_flags = NM_FLAGS_UNSET (device->link.n_ifi_flags, IFF_NOARP);
-
-	link_changed (platform, device, TRUE);
-
+	link_set_flags (platform,
+	                device,
+	                NM_FLAGS_UNSET (device->obj->link.n_ifi_flags, IFF_NOARP));
 	return TRUE;
 }
 
@@ -530,10 +556,9 @@ link_set_noarp (NMPlatform *platform, int ifindex)
 		return FALSE;
 	}
 
-	device->link.n_ifi_flags = NM_FLAGS_SET (device->link.n_ifi_flags, IFF_NOARP);
-
-	link_changed (platform, device, TRUE);
-
+	link_set_flags (platform,
+	                device,
+	                NM_FLAGS_SET (device->obj->link.n_ifi_flags, IFF_NOARP));
 	return TRUE;
 }
 
@@ -541,36 +566,40 @@ static NMPlatformError
 link_set_address (NMPlatform *platform, int ifindex, gconstpointer addr, size_t len)
 {
 	NMFakePlatformLink *device = link_get (platform, ifindex);
+	nm_auto_nmpobj NMPObject *obj_tmp = NULL;
 
-	if (   !device
-	    || len == 0
+	if (   len == 0
 	    || len > NM_UTILS_HWADDR_LEN_MAX
 	    || !addr)
 		g_return_val_if_reached (NM_PLATFORM_ERROR_BUG);
 
-	if (   device->link.addr.len != len
-	    || (   len > 0
-	        && memcmp (device->link.addr.data, addr, len) != 0)) {
-		memcpy (device->link.addr.data, addr, len);
-		device->link.addr.len = len;
-		link_changed (platform, link_get (platform, ifindex), TRUE);
-	}
+	if (!device)
+		return NM_PLATFORM_ERROR_EXISTS;
 
+	obj_tmp = nmp_object_clone (device->obj, FALSE);
+	obj_tmp->link.addr.len = len;
+	memset (obj_tmp->link.addr.data, 0, sizeof (obj_tmp->link.addr.data));
+	memcpy (obj_tmp->link.addr.data, addr, len);
+
+	link_set_obj (platform, device, obj_tmp);
 	return NM_PLATFORM_ERROR_SUCCESS;
 }
 
-static gboolean
+static NMPlatformError
 link_set_mtu (NMPlatform *platform, int ifindex, guint32 mtu)
 {
 	NMFakePlatformLink *device = link_get (platform, ifindex);
+	nm_auto_nmpobj NMPObject *obj_tmp = NULL;
 
-	if (device) {
-		device->link.mtu = mtu;
-		link_changed (platform, device, TRUE);
-	} else
+	if (!device) {
 		_LOGE ("failure changing link: netlink error (No such device)");
+		return NM_PLATFORM_ERROR_EXISTS;
+	}
 
-	return !!device;
+	obj_tmp = nmp_object_clone (device->obj, FALSE);
+	obj_tmp->link.mtu = mtu;
+	link_set_obj (platform, device, obj_tmp);
+	return NM_PLATFORM_ERROR_SUCCESS;
 }
 
 static gboolean
@@ -614,7 +643,7 @@ link_supports_carrier_detect (NMPlatform *platform, int ifindex)
 	if (!device)
 		return FALSE;
 
-	switch (device->link.type) {
+	switch (device->obj->link.type) {
 	case NM_LINK_TYPE_DUMMY:
 		return FALSE;
 	default:
@@ -630,7 +659,7 @@ link_supports_vlans (NMPlatform *platform, int ifindex)
 	if (!device)
 		return FALSE;
 
-	switch (device->link.type) {
+	switch (device->obj->link.type) {
 	case NM_LINK_TYPE_LOOPBACK:
 		return FALSE;
 	default:
@@ -646,7 +675,7 @@ link_supports_sriov (NMPlatform *platform, int ifindex)
 	if (!device)
 		return FALSE;
 
-	switch (device->link.type) {
+	switch (device->obj->link.type) {
 	case NM_LINK_TYPE_LOOPBACK:
 		return FALSE;
 	default:
@@ -663,15 +692,14 @@ link_enslave (NMPlatform *platform, int master, int slave)
 	g_return_val_if_fail (device, FALSE);
 	g_return_val_if_fail (master_device, FALSE);
 
-	if (device->link.master != master) {
-		device->link.master = master;
-
-		if (NM_IN_SET (master_device->link.type, NM_LINK_TYPE_BOND, NM_LINK_TYPE_TEAM)) {
-			device->link.n_ifi_flags = NM_FLAGS_SET (device->link.n_ifi_flags, IFF_UP);
-			device->link.connected = TRUE;
-		}
+	if (device->obj->link.master != master) {
+		nm_auto_nmpobj NMPObject *obj_tmp = NULL;
 
-		link_changed (platform, device, TRUE);
+		obj_tmp = nmp_object_clone (device->obj, FALSE);
+		obj_tmp->link.master = master;
+		if (NM_IN_SET (master_device->obj->link.type, NM_LINK_TYPE_BOND, NM_LINK_TYPE_TEAM))
+			obj_tmp->link.n_ifi_flags = NM_FLAGS_SET (device->obj->link.n_ifi_flags, IFF_UP);
+		link_set_obj (platform, device, obj_tmp);
 	}
 
 	return TRUE;
@@ -682,40 +710,56 @@ link_release (NMPlatform *platform, int master_idx, int slave_idx)
 {
 	NMFakePlatformLink *master = link_get (platform, master_idx);
 	NMFakePlatformLink *slave = link_get (platform, slave_idx);
+	nm_auto_nmpobj NMPObject *obj_tmp = NULL;
 
 	g_return_val_if_fail (master, FALSE);
 	g_return_val_if_fail (slave, FALSE);
 
-	if (slave->link.master != master->link.ifindex)
+	if (slave->obj->link.master != master->obj->link.ifindex)
 		return FALSE;
 
-	slave->link.master = 0;
-
-	link_changed (platform, slave, TRUE);
-	link_changed (platform, master, TRUE);
-
+	obj_tmp = nmp_object_clone (slave->obj, FALSE);
+	obj_tmp->link.master = 0;
+	link_set_obj (platform, slave, obj_tmp);
 	return TRUE;
 }
 
-static gboolean
-vlan_add (NMPlatform *platform, const char *name, int parent, int vlan_id, guint32 vlan_flags, const NMPlatformLink **out_link)
+struct vlan_add_data {
+	guint32 vlan_flags;
+	int parent;
+	int vlan_id;
+};
+
+static void
+_vlan_add_prepare (NMPlatform *platform,
+                   NMFakePlatformLink *device,
+                   gconstpointer user_data)
 {
-	NMFakePlatformLink *device;
+	const struct vlan_add_data *d = user_data;
+	NMPObject *obj_tmp;
+	NMPObject *lnk;
 
-	if (!link_add (platform, name, NM_LINK_TYPE_VLAN, NULL, 0, out_link))
-		return FALSE;
+	obj_tmp = (NMPObject *) device->obj;
 
-	device = link_get (platform, nm_platform_link_get_ifindex (platform, name));
+	lnk = nmp_object_new (NMP_OBJECT_TYPE_LNK_VLAN, NULL);
+	lnk->lnk_vlan.id = d->vlan_id;
+	lnk->lnk_vlan.flags = d->vlan_flags;
 
-	g_return_val_if_fail (device, FALSE);
-	g_return_val_if_fail (!device->lnk, FALSE);
+	obj_tmp->link.parent = d->parent;
+	obj_tmp->_link.netlink.lnk = lnk;
+}
 
-	device->lnk = nmp_object_new (NMP_OBJECT_TYPE_LNK_VLAN, NULL);
-	device->lnk->lnk_vlan.id = vlan_id;
-	device->link.parent = parent;
+static gboolean
+vlan_add (NMPlatform *platform, const char *name, int parent, int vlan_id, guint32 vlan_flags, const NMPlatformLink **out_link)
+{
+	const struct vlan_add_data d = {
+		.parent = parent,
+		.vlan_id = vlan_id,
+		.vlan_flags = vlan_flags,
+	};
 
-	if (out_link)
-		*out_link = &device->link;
+	link_add_one (platform, name, NM_LINK_TYPE_VLAN,
+	              _vlan_add_prepare, &d, out_link);
 	return TRUE;
 }
 
@@ -734,53 +778,76 @@ link_vlan_change (NMPlatform *platform,
 	return FALSE;
 }
 
+static void
+_vxlan_add_prepare (NMPlatform *platform,
+                    NMFakePlatformLink *device,
+                    gconstpointer user_data)
+{
+	const NMPlatformLnkVxlan *props = user_data;
+	NMPObject *obj_tmp;
+	NMPObject *lnk;
+
+	obj_tmp = (NMPObject *) device->obj;
+
+	lnk = nmp_object_new (NMP_OBJECT_TYPE_LNK_VXLAN, NULL);
+	lnk->lnk_vxlan = *props;
+
+	obj_tmp->link.parent = props->parent_ifindex;
+	obj_tmp->_link.netlink.lnk = lnk;
+}
+
 static gboolean
 link_vxlan_add (NMPlatform *platform,
                 const char *name,
                 const NMPlatformLnkVxlan *props,
                 const NMPlatformLink **out_link)
 {
-	NMFakePlatformLink *device;
+	link_add_one (platform, name, NM_LINK_TYPE_VXLAN,
+	              _vxlan_add_prepare, props, out_link);
+	return TRUE;
+}
 
-	if (!link_add (platform, name, NM_LINK_TYPE_VXLAN, NULL, 0, out_link))
-		return FALSE;
+struct infiniband_add_data {
+	int parent;
+	int p_key;
+};
 
-	device = link_get (platform, nm_platform_link_get_ifindex (platform, name));
+static void
+_infiniband_add_prepare (NMPlatform *platform,
+                         NMFakePlatformLink *device,
+                         gconstpointer user_data)
+{
+	const struct infiniband_add_data *d = user_data;
+	NMPObject *obj_tmp;
+	NMPObject *lnk;
 
-	g_return_val_if_fail (device, FALSE);
-	g_return_val_if_fail (!device->lnk, FALSE);
+	obj_tmp = (NMPObject *) device->obj;
 
-	device->lnk = nmp_object_new (NMP_OBJECT_TYPE_LNK_VXLAN, NULL);
-	device->lnk->lnk_vxlan = *props;
-	device->link.parent = props->parent_ifindex;
+	lnk = nmp_object_new (NMP_OBJECT_TYPE_LNK_INFINIBAND, NULL);
+	lnk->lnk_infiniband.p_key = d->p_key;
+	lnk->lnk_infiniband.mode = "datagram";
 
-	if (out_link)
-		*out_link = &device->link;
-	return TRUE;
+	obj_tmp->link.parent = d->parent;
+	obj_tmp->_link.netlink.lnk = lnk;
 }
 
 static gboolean
 infiniband_partition_add (NMPlatform *platform, int parent, int p_key, const NMPlatformLink **out_link)
 {
-	NMFakePlatformLink *device, *parent_device;
+	NMFakePlatformLink *parent_device;
 	char name[IFNAMSIZ];
+	const struct infiniband_add_data d = {
+		.parent = parent,
+		.p_key = p_key,
+	};
 
 	parent_device = link_get (platform, parent);
 	g_return_val_if_fail (parent_device != NULL, FALSE);
 
-	nm_utils_new_infiniband_name (name, parent_device->link.name, p_key);
-
-	if (!link_add (platform, name, NM_LINK_TYPE_INFINIBAND, NULL, 0, out_link))
-		return FALSE;
-
-	device = link_get (platform, nm_platform_link_get_ifindex (platform, name));
-	g_return_val_if_fail (device, FALSE);
-	g_return_val_if_fail (!device->lnk, FALSE);
+	nm_utils_new_infiniband_name (name, parent_device->obj->link.name, p_key);
 
-	device->lnk = nmp_object_new (NMP_OBJECT_TYPE_LNK_VLAN, NULL);
-	device->lnk->lnk_infiniband.p_key = p_key;
-	device->lnk->lnk_infiniband.mode = "datagram";
-	device->link.parent = parent;
+	link_add_one (platform, name, NM_LINK_TYPE_INFINIBAND,
+	              _infiniband_add_prepare, &d, out_link);
 	return TRUE;
 }
 
@@ -793,7 +860,7 @@ infiniband_partition_delete (NMPlatform *platform, int parent, int p_key)
 	parent_device = link_get (platform, parent);
 	g_return_val_if_fail (parent_device != NULL, FALSE);
 
-	nm_utils_new_infiniband_name (name, parent_device->link.name, p_key);
+	nm_utils_new_infiniband_name (name, parent_device->obj->link.name, p_key);
 	return link_delete (platform, nm_platform_link_get_ifindex (platform, name));
 }
 
@@ -804,7 +871,7 @@ wifi_get_capabilities (NMPlatform *platform, int ifindex, NMDeviceWifiCapabiliti
 
 	g_return_val_if_fail (device, FALSE);
 
-	if (device->link.type != NM_LINK_TYPE_WIFI)
+	if (device->obj->link.type != NM_LINK_TYPE_WIFI)
 		return FALSE;
 
 	if (caps) {
@@ -894,59 +961,25 @@ mesh_set_ssid (NMPlatform *platform, int ifindex, const guint8 *ssid, gsize len)
 
 /*****************************************************************************/
 
-static GArray *
-ip4_address_get_all (NMPlatform *platform, int ifindex)
-{
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	GArray *addresses;
-	NMPlatformIP4Address *address;
-	int count = 0, i;
-
-	/* Count addresses */
-	for (i = 0; i < priv->ip4_addresses->len; i++) {
-		address = &g_array_index (priv->ip4_addresses, NMPlatformIP4Address, i);
-		if (address && address->ifindex == ifindex)
-			count++;
-	}
-
-	addresses = g_array_sized_new (TRUE, TRUE, sizeof (NMPlatformIP4Address), count);
-
-	/* Fill addresses */
-	for (i = 0; i < priv->ip4_addresses->len; i++) {
-		address = &g_array_index (priv->ip4_addresses, NMPlatformIP4Address, i);
-		if (address && address->ifindex == ifindex)
-			g_array_append_val (addresses, *address);
-	}
-
-	return addresses;
-}
-
-static GArray *
-ip6_address_get_all (NMPlatform *platform, int ifindex)
+static gboolean
+ipx_address_add (NMPlatform *platform, int addr_family, const NMPlatformObject *address)
 {
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	GArray *addresses;
-	NMPlatformIP6Address *address;
-	int count = 0, i;
-
-	/* Count addresses */
-	for (i = 0; i < priv->ip6_addresses->len; i++) {
-		address = &g_array_index (priv->ip6_addresses, NMPlatformIP6Address, i);
-		if (address && address->ifindex == ifindex)
-			count++;
-	}
+	nm_auto_nmpobj NMPObject *obj = NULL;
+	NMPCacheOpsType cache_op;
+	nm_auto_nmpobj const NMPObject *obj_old = NULL;
+	nm_auto_nmpobj const NMPObject *obj_new = NULL;
+	NMPCache *cache = nm_platform_get_cache (platform);
 
-	addresses = g_array_sized_new (TRUE, TRUE, sizeof (NMPlatformIP6Address), count);
+	g_assert (NM_IN_SET (addr_family, AF_INET, AF_INET6));
 
-	/* Fill addresses */
-	count = 0;
-	for (i = 0; i < priv->ip6_addresses->len; i++) {
-		address = &g_array_index (priv->ip6_addresses, NMPlatformIP6Address, i);
-		if (address && address->ifindex == ifindex)
-			g_array_append_val (addresses, *address);
-	}
+	obj = nmp_object_new (addr_family == AF_INET
+	                        ? NMP_OBJECT_TYPE_IP4_ADDRESS
+	                        : NMP_OBJECT_TYPE_IP6_ADDRESS,
+	                      address);
 
-	return addresses;
+	cache_op = nmp_cache_update_netlink (cache, obj, FALSE, &obj_old, &obj_new);
+	nm_platform_cache_update_emit_signal (platform, cache_op, obj_old, obj_new);
+	return TRUE;
 }
 
 static gboolean
@@ -960,9 +993,7 @@ ip4_address_add (NMPlatform *platform,
                  guint32 flags,
                  const char *label)
 {
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
 	NMPlatformIP4Address address;
-	int i;
 
 	memset (&address, 0, sizeof (address));
 	address.addr_source = NM_IP_CONFIG_SOURCE_KERNEL;
@@ -977,28 +1008,7 @@ ip4_address_add (NMPlatform *platform,
 	if (label)
 		g_strlcpy (address.label, label, sizeof (address.label));
 
-	for (i = 0; i < priv->ip4_addresses->len; i++) {
-		NMPlatformIP4Address *item = &g_array_index (priv->ip4_addresses, NMPlatformIP4Address, i);
-		gboolean changed;
-
-		if (   item->ifindex != address.ifindex
-		    || item->address != address.address
-		    || item->plen != address.plen
-		    || !_ip4_address_equal_peer_net (item->peer_address, address.peer_address, address.plen))
-			continue;
-
-		changed = !nm_platform_ip4_address_cmp (item, &address);
-
-		memcpy (item, &address, sizeof (address));
-		if (changed)
-			g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_IP4_ADDRESS_CHANGED, (int) NMP_OBJECT_TYPE_IP4_ADDRESS, ifindex, &address, (int) NM_PLATFORM_SIGNAL_CHANGED);
-		return TRUE;
-	}
-
-	g_array_append_val (priv->ip4_addresses, address);
-	g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_IP4_ADDRESS_CHANGED, (int) NMP_OBJECT_TYPE_IP4_ADDRESS, ifindex, &address, (int) NM_PLATFORM_SIGNAL_ADDED);
-
-	return TRUE;
+	return ipx_address_add (platform, AF_INET, (const NMPlatformObject *) &address);
 }
 
 static gboolean
@@ -1011,9 +1021,7 @@ ip6_address_add (NMPlatform *platform,
                  guint32 preferred,
                  guint32 flags)
 {
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
 	NMPlatformIP6Address address;
-	int i;
 
 	memset (&address, 0, sizeof (address));
 	address.addr_source = NM_IP_CONFIG_SOURCE_KERNEL;
@@ -1026,380 +1034,321 @@ ip6_address_add (NMPlatform *platform,
 	address.preferred = preferred;
 	address.n_ifa_flags = flags;
 
-	for (i = 0; i < priv->ip6_addresses->len; i++) {
-		NMPlatformIP6Address *item = &g_array_index (priv->ip6_addresses, NMPlatformIP6Address, i);
-		gboolean changed;
-
-		if (   item->ifindex != address.ifindex
-		    || !IN6_ARE_ADDR_EQUAL (&item->address, &address.address))
-			continue;
+	return ipx_address_add (platform, AF_INET6, (const NMPlatformObject *) &address);
+}
 
-		changed = !nm_platform_ip6_address_cmp (item, &address);
+static gboolean
+ipx_address_delete (NMPlatform *platform,
+                    int addr_family,
+                    int ifindex,
+                    gconstpointer addr,
+                    const guint8 *plen,
+                    gconstpointer peer_addr)
+{
+	gs_unref_ptrarray GPtrArray *objs = g_ptr_array_new_with_free_func ((GDestroyNotify) nmp_object_unref);
+	NMDedupMultiIter iter;
+	const NMPObject *o = NULL;
+	guint i;
+	guint32 peer_addr_i;
+
+	g_assert (NM_IN_SET (addr_family, AF_INET, AF_INET6));
+
+	peer_addr_i = peer_addr ? *((guint32 *) peer_addr) : 0;
+
+	nmp_cache_iter_for_each (&iter,
+	                         nm_platform_lookup_addrroute (platform,
+	                                                       addr_family == AF_INET
+	                                                         ? NMP_OBJECT_TYPE_IP4_ADDRESS
+	                                                         : NMP_OBJECT_TYPE_IP6_ADDRESS,
+	                                                       0),
+	                         &o) {
+		const NMPObject *obj_old = NULL;
+
+		if (addr_family == AF_INET) {
+			const NMPlatformIP4Address *address = NMP_OBJECT_CAST_IP4_ADDRESS (o);
+
+			if (   address->ifindex != ifindex
+			    || (addr && address->address != *((guint32 *) addr))
+			    || (plen && address->plen != *plen)
+			    || (   peer_addr
+			        && (((peer_addr_i ^ address->peer_address) & _nm_utils_ip4_prefix_to_netmask (address->plen)) != 0)))
+				continue;
+		} else {
+			const NMPlatformIP6Address *address = NMP_OBJECT_CAST_IP6_ADDRESS (o);
+
+			g_assert (!peer_addr);
+			if (   address->ifindex != ifindex
+			    || (addr && !IN6_ARE_ADDR_EQUAL (&address->address, addr))
+			    || (plen && address->plen != *plen))
+				continue;
+		}
 
-		memcpy (item, &address, sizeof (address));
-		if (changed)
-			g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_IP6_ADDRESS_CHANGED, (int) NMP_OBJECT_TYPE_IP6_ADDRESS, ifindex, &address, (int) NM_PLATFORM_SIGNAL_CHANGED);
-		return TRUE;
+		if (nmp_cache_remove (nm_platform_get_cache (platform),
+		                      o,
+		                      TRUE,
+		                      FALSE,
+		                      &obj_old) != NMP_CACHE_OPS_REMOVED)
+			g_assert_not_reached ();
+		g_assert (obj_old);
+		g_ptr_array_add (objs, (gpointer) obj_old);
 	}
 
-	g_array_append_val (priv->ip6_addresses, address);
-	g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_IP6_ADDRESS_CHANGED, (int) NMP_OBJECT_TYPE_IP6_ADDRESS, ifindex, &address, (int) NM_PLATFORM_SIGNAL_ADDED);
-
+	for (i = 0; i < objs->len; i++) {
+		nm_platform_cache_update_emit_signal (platform,
+		                                      NMP_CACHE_OPS_REMOVED,
+		                                      objs->pdata[i],
+		                                      NULL);
+	}
 	return TRUE;
 }
 
 static gboolean
 ip4_address_delete (NMPlatform *platform, int ifindex, in_addr_t addr, guint8 plen, in_addr_t peer_address)
 {
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	int i;
-
-	for (i = 0; i < priv->ip4_addresses->len; i++) {
-		NMPlatformIP4Address *address = &g_array_index (priv->ip4_addresses, NMPlatformIP4Address, i);
-
-		if (   address->ifindex == ifindex
-		    && address->plen == plen
-		    && address->address == addr
-		    && ((peer_address ^ address->peer_address) & nm_utils_ip4_prefix_to_netmask (plen)) == 0) {
-			NMPlatformIP4Address deleted_address;
-
-			memcpy (&deleted_address, address, sizeof (deleted_address));
-			memset (address, 0, sizeof (*address));
-			g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_IP4_ADDRESS_CHANGED, (int) NMP_OBJECT_TYPE_IP4_ADDRESS, ifindex, &deleted_address, (int) NM_PLATFORM_SIGNAL_REMOVED);
-			return TRUE;
-		}
-	}
-
-	return TRUE;
+	return ipx_address_delete (platform, AF_INET, ifindex, &addr, &plen, &peer_address);
 }
 
 static gboolean
 ip6_address_delete (NMPlatform *platform, int ifindex, struct in6_addr addr, guint8 plen)
 {
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	int i;
-
-	for (i = 0; i < priv->ip6_addresses->len; i++) {
-		NMPlatformIP6Address *address = &g_array_index (priv->ip6_addresses, NMPlatformIP6Address, i);
-
-		if (   address->ifindex == ifindex
-		    && address->plen == plen
-		    && IN6_ARE_ADDR_EQUAL (&address->address, &addr)) {
-			NMPlatformIP6Address deleted_address;
-
-			memcpy (&deleted_address, address, sizeof (deleted_address));
-			memset (address, 0, sizeof (*address));
-			g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_IP6_ADDRESS_CHANGED, (int) NMP_OBJECT_TYPE_IP6_ADDRESS, ifindex, &deleted_address, (int) NM_PLATFORM_SIGNAL_REMOVED);
-			return TRUE;
-		}
-	}
-
-	return TRUE;
-}
-
-static const NMPlatformIP4Address *
-ip4_address_get (NMPlatform *platform, int ifindex, in_addr_t addr, guint8 plen, in_addr_t peer_address)
-{
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	int i;
-
-	for (i = 0; i < priv->ip4_addresses->len; i++) {
-		NMPlatformIP4Address *address = &g_array_index (priv->ip4_addresses, NMPlatformIP4Address, i);
-
-		if (   address->ifindex == ifindex
-		    && address->plen == plen
-		    && address->address == addr
-		    && _ip4_address_equal_peer_net (address->peer_address, peer_address, plen))
-			return address;
-	}
-
-	return NULL;
-}
-
-static const NMPlatformIP6Address *
-ip6_address_get (NMPlatform *platform, int ifindex, struct in6_addr addr, guint8 plen)
-{
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	int i;
-
-	for (i = 0; i < priv->ip6_addresses->len; i++) {
-		NMPlatformIP6Address *address = &g_array_index (priv->ip6_addresses, NMPlatformIP6Address, i);
-
-		if (   address->ifindex == ifindex
-		    && address->plen == plen
-		    && IN6_ARE_ADDR_EQUAL (&address->address, &addr))
-			return address;
-	}
-
-	return NULL;
+	return ipx_address_delete (platform, AF_INET6, ifindex, &addr, &plen, NULL);
 }
 
 /*****************************************************************************/
 
-static GArray *
-ip4_route_get_all (NMPlatform *platform, int ifindex, NMPlatformGetRouteFlags flags)
+static gboolean
+ipx_route_delete (NMPlatform *platform,
+                  int addr_family,
+                  int ifindex,
+                  const NMPObject *obj)
 {
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	GArray *routes;
-	NMPlatformIP4Route *route;
+	gs_unref_ptrarray GPtrArray *objs = g_ptr_array_new_with_free_func ((GDestroyNotify) nmp_object_unref);
+	NMDedupMultiIter iter;
+	const NMPObject *o = NULL;
 	guint i;
-
-	routes = g_array_new (TRUE, TRUE, sizeof (NMPlatformIP4Route));
-
-	if (!NM_FLAGS_ANY (flags, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT | NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT))
-		flags |= NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT | NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT;
-
-	/* Fill routes */
-	for (i = 0; i < priv->ip4_routes->len; i++) {
-		route = &g_array_index (priv->ip4_routes, NMPlatformIP4Route, i);
-		if (route && (!ifindex || route->ifindex == ifindex)) {
-			if (NM_PLATFORM_IP_ROUTE_IS_DEFAULT (route)) {
-				if (NM_FLAGS_HAS (flags, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT))
-					g_array_append_val (routes, *route);
-			} else {
-				if (NM_FLAGS_HAS (flags, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT))
-					g_array_append_val (routes, *route);
-			}
-		}
+	NMPObjectType obj_type;
+
+	if (addr_family == AF_UNSPEC) {
+		g_assert (NM_IN_SET (NMP_OBJECT_GET_TYPE (obj), NMP_OBJECT_TYPE_IP4_ROUTE,
+		                                                NMP_OBJECT_TYPE_IP6_ROUTE));
+		g_assert (ifindex == -1);
+		ifindex = obj->object.ifindex;
+		obj_type = NMP_OBJECT_GET_TYPE (obj);
+	} else {
+		g_assert (NM_IN_SET (addr_family, AF_INET, AF_INET6));
+		g_assert (!obj);
+		g_assert (ifindex > 0);
+		obj_type =   addr_family == AF_INET
+	               ? NMP_OBJECT_TYPE_IP4_ROUTE
+	               : NMP_OBJECT_TYPE_IP6_ROUTE;
 	}
 
-	return routes;
-}
-
-static GArray *
-ip6_route_get_all (NMPlatform *platform, int ifindex, NMPlatformGetRouteFlags flags)
-{
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	GArray *routes;
-	NMPlatformIP6Route *route;
-	guint i;
-
-	routes = g_array_new (TRUE, TRUE, sizeof (NMPlatformIP6Route));
-
-	if (!NM_FLAGS_ANY (flags, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT | NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT))
-		flags |= NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT | NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT;
-
-	/* Fill routes */
-	for (i = 0; i < priv->ip6_routes->len; i++) {
-		route = &g_array_index (priv->ip6_routes, NMPlatformIP6Route, i);
-		if (route && (!ifindex || route->ifindex == ifindex)) {
-			if (NM_PLATFORM_IP_ROUTE_IS_DEFAULT (route)) {
-				if (NM_FLAGS_HAS (flags, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT))
-					g_array_append_val (routes, *route);
+	nmp_cache_iter_for_each (&iter,
+	                         nm_platform_lookup_addrroute (platform,
+	                                                       obj_type,
+	                                                       ifindex),
+	                         &o) {
+		const NMPObject *obj_old = NULL;
+
+		if (obj) {
+			if (obj_type == NMP_OBJECT_TYPE_IP4_ROUTE) {
+				const NMPlatformIP4Route *route = NMP_OBJECT_CAST_IP4_ROUTE (o);
+				const NMPlatformIP4Route *r = NMP_OBJECT_CAST_IP4_ROUTE (obj);
+
+				if (   route->network != r->network
+				    || route->plen != r->plen
+				    || route->metric != r->metric)
+					continue;
 			} else {
-				if (NM_FLAGS_HAS (flags, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT))
-					g_array_append_val (routes, *route);
+				const NMPlatformIP6Route *route = NMP_OBJECT_CAST_IP6_ROUTE (o);
+				const NMPlatformIP6Route *r = NMP_OBJECT_CAST_IP6_ROUTE (obj);
+
+				if (   !IN6_ARE_ADDR_EQUAL (&route->network, &r->network)
+				    || route->plen != r->plen
+				    || route->metric != r->metric)
+					continue;
 			}
 		}
-	}
-
-	return routes;
-}
 
-static gboolean
-ip4_route_delete (NMPlatform *platform, int ifindex, in_addr_t network, guint8 plen, guint32 metric)
-{
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	int i;
-
-	for (i = 0; i < priv->ip4_routes->len; i++) {
-		NMPlatformIP4Route *route = &g_array_index (priv->ip4_routes, NMPlatformIP4Route, i);
-		NMPlatformIP4Route deleted_route;
-
-		if (   route->ifindex != ifindex
-		    || route->network != network
-		    || route->plen != plen
-		    || route->metric != metric)
-			continue;
-
-		memcpy (&deleted_route, route, sizeof (deleted_route));
-		g_array_remove_index (priv->ip4_routes, i);
-		g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED, (int) NMP_OBJECT_TYPE_IP4_ROUTE, ifindex, &deleted_route, (int) NM_PLATFORM_SIGNAL_REMOVED);
+		if (nmp_cache_remove (nm_platform_get_cache (platform),
+		                      o,
+		                      TRUE,
+		                      FALSE,
+		                      &obj_old) != NMP_CACHE_OPS_REMOVED)
+			g_assert_not_reached ();
+		g_assert (obj_old);
+		g_ptr_array_add (objs, (gpointer) obj_old);
 	}
 
+	for (i = 0; i < objs->len; i++) {
+		nm_platform_cache_update_emit_signal (platform,
+		                                      NMP_CACHE_OPS_REMOVED,
+		                                      objs->pdata[i],
+		                                      NULL);
+	}
 	return TRUE;
 }
 
 static gboolean
-ip6_route_delete (NMPlatform *platform, int ifindex, struct in6_addr network, guint8 plen, guint32 metric)
+ip_route_delete (NMPlatform *platform, const NMPObject *obj)
 {
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	int i;
-
-	metric = nm_utils_ip6_route_metric_normalize (metric);
-
-	for (i = 0; i < priv->ip6_routes->len; i++) {
-		NMPlatformIP6Route *route = &g_array_index (priv->ip6_routes, NMPlatformIP6Route, i);
-		NMPlatformIP6Route deleted_route;
-
-		if (   route->ifindex != ifindex
-		    || !IN6_ARE_ADDR_EQUAL (&route->network, &network)
-		    || route->plen != plen
-		    || route->metric != metric)
-			continue;
-
-		memcpy (&deleted_route, route, sizeof (deleted_route));
-		g_array_remove_index (priv->ip6_routes, i);
-		g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED, (int) NMP_OBJECT_TYPE_IP6_ROUTE, ifindex, &deleted_route, (int) NM_PLATFORM_SIGNAL_REMOVED);
-	}
+	g_assert (NM_IS_FAKE_PLATFORM (platform));
+	g_assert (NM_IN_SET (NMP_OBJECT_GET_TYPE (obj), NMP_OBJECT_TYPE_IP4_ROUTE,
+	                                                NMP_OBJECT_TYPE_IP6_ROUTE));
 
-	return TRUE;
+	return ipx_route_delete (platform, AF_UNSPEC, -1, obj);
 }
 
-static gboolean
-ip4_route_add (NMPlatform *platform, const NMPlatformIP4Route *route)
-{
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	NMPlatformIP4Route rt = *route;
-	guint i;
-
-	rt.rt_source = nmp_utils_ip_config_source_round_trip_rtprot (rt.rt_source);
-	rt.network = nm_utils_ip4_address_clear_host_address (rt.network, rt.plen);
-	rt.scope_inv = nm_platform_route_scope_inv (rt.gateway ? RT_SCOPE_UNIVERSE : RT_SCOPE_LINK);
+static NMPlatformError
+ip_route_add (NMPlatform *platform,
+              NMPNlmFlags flags,
+              int addr_family,
+              const NMPlatformIPRoute *route)
+{
+	NMDedupMultiIter iter;
+	nm_auto_nmpobj NMPObject *obj = NULL;
+	NMPCacheOpsType cache_op;
+	const NMPObject *o = NULL;
+	nm_auto_nmpobj const NMPObject *obj_old = NULL;
+	nm_auto_nmpobj const NMPObject *obj_new = NULL;
+	nm_auto_nmpobj const NMPObject *obj_replace = NULL;
+	NMPCache *cache = nm_platform_get_cache (platform);
+	gboolean has_gateway = FALSE;
+	NMPlatformIPRoute *r = NULL;
+	NMPlatformIP4Route *r4 = NULL;
+	NMPlatformIP6Route *r6 = NULL;
+	gboolean has_same_weak_id;
+	gboolean only_dirty;
+	guint16 nlmsgflags;
+
+	g_assert (NM_IN_SET (addr_family, AF_INET, AF_INET6));
+
+	flags = NM_FLAGS_UNSET (flags, NMP_NLM_FLAG_SUPPRESS_NETLINK_FAILURE);
+
+	/* currently, only replace is implemented. */
+	g_assert (flags == NMP_NLM_FLAG_REPLACE);
+
+	obj = nmp_object_new (addr_family == AF_INET
+	                        ? NMP_OBJECT_TYPE_IP4_ROUTE
+	                        : NMP_OBJECT_TYPE_IP6_ROUTE,
+	                      (const NMPlatformObject *) route);
+	r = NMP_OBJECT_CAST_IP_ROUTE (obj);
+	nm_platform_ip_route_normalize (addr_family, r);
+
+	switch (addr_family) {
+	case AF_INET:
+		r4 = NMP_OBJECT_CAST_IP4_ROUTE (obj);
+		if (r4->gateway)
+			has_gateway = TRUE;
+		break;
+	case AF_INET6:
+		r6 = NMP_OBJECT_CAST_IP6_ROUTE (obj);
+		if (!IN6_IS_ADDR_UNSPECIFIED (&r6->gateway))
+			has_gateway = TRUE;
+		break;
+	default:
+		nm_assert_not_reached ();
+	}
 
-	if (rt.gateway) {
-		for (i = 0; i < priv->ip4_routes->len; i++) {
-			NMPlatformIP4Route *item = &g_array_index (priv->ip4_routes,
-			                                           NMPlatformIP4Route, i);
-			guint32 gate = ntohl (item->network) >> (32 - item->plen);
-			guint32 host = ntohl (rt.gateway) >> (32 - item->plen);
+	if (has_gateway) {
+		gboolean has_route_to_gw = FALSE;
+
+		nmp_cache_iter_for_each (&iter,
+		                         nm_platform_lookup_addrroute (platform,
+		                                                       NMP_OBJECT_GET_TYPE (obj),
+		                                                       0),
+		                         &o) {
+			if (addr_family == AF_INET) {
+				const NMPlatformIP4Route *item = NMP_OBJECT_CAST_IP4_ROUTE (o);
+				guint32 n = nm_utils_ip4_address_clear_host_address (item->network, item->plen);
+				guint32 g = nm_utils_ip4_address_clear_host_address (r4->gateway, item->plen);
+
+				if (   r->ifindex == item->ifindex
+				    && n == g) {
+					has_route_to_gw = TRUE;
+					break;
+				}
+			} else {
+				const NMPlatformIP6Route *item = NMP_OBJECT_CAST_IP6_ROUTE (o);
 
-			if (rt.ifindex == item->ifindex && gate == host)
-				break;
+				if (   r->ifindex == item->ifindex
+				    && nm_utils_ip6_address_same_prefix (&r6->gateway, &item->network, item->plen)) {
+					has_route_to_gw = TRUE;
+					break;
+				}
+			}
 		}
-		if (i == priv->ip4_routes->len) {
-			nm_log_warn (LOGD_PLATFORM, "Fake platform: failure adding ip4-route '%d: %s/%d %d': Network Unreachable",
-			             rt.ifindex, nm_utils_inet4_ntop (rt.network, NULL), rt.plen, rt.metric);
-			return FALSE;
+		if (!has_route_to_gw) {
+			if (addr_family == AF_INET) {
+				nm_log_warn (LOGD_PLATFORM, "Fake platform: failure adding ip4-route '%d: %s/%d %d': Network Unreachable",
+				             r->ifindex, nm_utils_inet4_ntop (r4->network, NULL), r->plen, r->metric);
+			} else {
+				nm_log_warn (LOGD_PLATFORM, "Fake platform: failure adding ip6-route '%d: %s/%d %d': Network Unreachable",
+				             r->ifindex, nm_utils_inet6_ntop (&r6->network, NULL), r->plen, r->metric);
+			}
+			return NM_PLATFORM_ERROR_UNSPECIFIED;
 		}
 	}
 
-	for (i = 0; i < priv->ip4_routes->len; i++) {
-		NMPlatformIP4Route *item = &g_array_index (priv->ip4_routes, NMPlatformIP4Route, i);
-
-		if (item->network != rt.network)
-			continue;
-		if (item->plen != rt.plen)
-			continue;
-		if (item->metric != rt.metric)
-			continue;
-
-		if (item->ifindex != rt.ifindex) {
-			ip4_route_delete (platform, item->ifindex, item->network, item->plen, item->metric);
-			i--;
-			continue;
+	has_same_weak_id = FALSE;
+	nmp_cache_iter_for_each (&iter,
+	                         nm_platform_lookup_all (platform,
+	                                                 NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID,
+	                                                 obj),
+	                         &o) {
+		if (addr_family == AF_INET) {
+			if (nm_platform_ip4_route_cmp (NMP_OBJECT_CAST_IP4_ROUTE (o), r4, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) == 0)
+				continue;
+		} else {
+			if (nm_platform_ip6_route_cmp (NMP_OBJECT_CAST_IP6_ROUTE (o), r6, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) == 0)
+				continue;
 		}
-
-		memcpy (item, &rt, sizeof (rt));
-		g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED, (int) NMP_OBJECT_TYPE_IP4_ROUTE,
-		                       rt.ifindex, &rt, (int) NM_PLATFORM_SIGNAL_CHANGED);
-		return TRUE;
+		has_same_weak_id = TRUE;
 	}
 
-	g_array_append_val (priv->ip4_routes, rt);
-	g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED, (int) NMP_OBJECT_TYPE_IP4_ROUTE,
-	                       rt.ifindex, &rt, (int) NM_PLATFORM_SIGNAL_ADDED);
-
-	return TRUE;
-}
-
-static gboolean
-ip6_route_add (NMPlatform *platform, const NMPlatformIP6Route *route)
-{
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	NMPlatformIP6Route rt = *route;
-	guint i;
-
-	rt.metric = nm_utils_ip6_route_metric_normalize (rt.metric);
-	rt.rt_source = nmp_utils_ip_config_source_round_trip_rtprot (rt.rt_source);
-	nm_utils_ip6_address_clear_host_address (&rt.network, &rt.network, rt.plen);
-
-	if (!IN6_IS_ADDR_UNSPECIFIED (&rt.gateway)) {
-		for (i = 0; i < priv->ip6_routes->len; i++) {
-			NMPlatformIP6Route *item = &g_array_index (priv->ip6_routes,
-			                                           NMPlatformIP6Route, i);
-			guint8 gate_bits = rt.gateway.s6_addr[item->plen / 8] >> (8 - item->plen % 8);
-			guint8 host_bits = item->network.s6_addr[item->plen / 8] >> (8 - item->plen % 8);
-
-			if (   rt.ifindex == item->ifindex
-			    && memcmp (&rt.gateway, &item->network, item->plen / 8) == 0
-			    && gate_bits == host_bits)
-				break;
-		}
-		if (i == priv->ip6_routes->len) {
-			nm_log_warn (LOGD_PLATFORM, "Fake platform: failure adding ip6-route '%d: %s/%d %d': Network Unreachable",
-			             rt.ifindex, nm_utils_inet6_ntop (&rt.network, NULL), rt.plen, rt.metric);
-			return FALSE;
+	nlmsgflags = 0;
+	if (has_same_weak_id) {
+		switch (flags) {
+		case NMP_NLM_FLAG_REPLACE:
+			nlmsgflags = NLM_F_REPLACE;
+			break;
+		default:
+			g_assert_not_reached ();
+			break;
 		}
 	}
 
-	for (i = 0; i < priv->ip6_routes->len; i++) {
-		NMPlatformIP6Route *item = &g_array_index (priv->ip6_routes, NMPlatformIP6Route, i);
-
-		if (!IN6_ARE_ADDR_EQUAL (&item->network, &rt.network))
-			continue;
-		if (item->plen != rt.plen)
-			continue;
-		if (item->metric != rt.metric)
-			continue;
-
-		if (item->ifindex != rt.ifindex) {
-			ip6_route_delete (platform, item->ifindex, item->network, item->plen, item->metric);
-			i--;
-			continue;
+	/* we manipulate the cache the same was as NMLinuxPlatform does it. */
+	cache_op = nmp_cache_update_netlink_route (cache,
+	                                           obj,
+	                                           FALSE,
+	                                           nlmsgflags,
+	                                           &obj_old,
+	                                           &obj_new,
+	                                           &obj_replace,
+	                                           NULL);
+	only_dirty = FALSE;
+	if (cache_op != NMP_CACHE_OPS_UNCHANGED) {
+		if (obj_replace) {
+			const NMDedupMultiEntry *entry_replace;
+
+			entry_replace = nmp_cache_lookup_entry (cache, obj_replace);
+			nm_assert (entry_replace && entry_replace->obj == obj_replace);
+			nm_dedup_multi_entry_set_dirty (entry_replace, TRUE);
+			only_dirty = TRUE;
 		}
-
-		memcpy (item, &rt, sizeof (rt));
-		g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED, (int) NMP_OBJECT_TYPE_IP6_ROUTE,
-		                       rt.ifindex, &rt, (int) NM_PLATFORM_SIGNAL_CHANGED);
-		return TRUE;
+		nm_platform_cache_update_emit_signal (platform, cache_op, obj_old, obj_new);
 	}
 
-	g_array_append_val (priv->ip6_routes, rt);
-	g_signal_emit_by_name (platform, NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED, (int) NMP_OBJECT_TYPE_IP6_ROUTE,
-	                       rt.ifindex, &rt, (int) NM_PLATFORM_SIGNAL_ADDED);
-
-	return TRUE;
-}
-
-static const NMPlatformIP4Route *
-ip4_route_get (NMPlatform *platform, int ifindex, in_addr_t network, guint8 plen, guint32 metric)
-{
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	int i;
-
-	for (i = 0; i < priv->ip4_routes->len; i++) {
-		NMPlatformIP4Route *route = &g_array_index (priv->ip4_routes, NMPlatformIP4Route, i);
-
-		if (route->ifindex == ifindex
-				&& route->network == network
-				&& route->plen == plen
-				&& route->metric == metric)
-			return route;
-	}
-
-	return NULL;
-}
-
-static const NMPlatformIP6Route *
-ip6_route_get (NMPlatform *platform, int ifindex, struct in6_addr network, guint8 plen, guint32 metric)
-{
-	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
-	int i;
-
-	metric = nm_utils_ip6_route_metric_normalize (metric);
-
-	for (i = 0; i < priv->ip6_routes->len; i++) {
-		NMPlatformIP6Route *route = &g_array_index (priv->ip6_routes, NMPlatformIP6Route, i);
-
-		if (route->ifindex == ifindex
-				&& IN6_ARE_ADDR_EQUAL (&route->network, &network)
-				&& route->plen == plen
-				&& route->metric == metric)
-			return route;
+	if (obj_replace) {
+		cache_op = nmp_cache_remove (cache, obj_replace, TRUE, only_dirty, NULL);
+		if (cache_op != NMP_CACHE_OPS_UNCHANGED) {
+			nm_assert (cache_op == NMP_CACHE_OPS_REMOVED);
+			nm_platform_cache_update_emit_signal (platform, cache_op, obj_replace, NULL);
+		}
 	}
 
-	return NULL;
+	return NM_PLATFORM_ERROR_SUCCESS;
 }
 
 /*****************************************************************************/
@@ -1409,12 +1358,8 @@ nm_fake_platform_init (NMFakePlatform *fake_platform)
 {
 	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE (fake_platform);
 
-	priv->options = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+	priv->options = g_hash_table_new_full (nm_str_hash, g_str_equal, g_free, g_free);
 	priv->links = g_array_new (TRUE, TRUE, sizeof (NMFakePlatformLink));
-	priv->ip4_addresses = g_array_new (TRUE, TRUE, sizeof (NMPlatformIP4Address));
-	priv->ip6_addresses = g_array_new (TRUE, TRUE, sizeof (NMPlatformIP6Address));
-	priv->ip4_routes = g_array_new (TRUE, TRUE, sizeof (NMPlatformIP4Route));
-	priv->ip6_routes = g_array_new (TRUE, TRUE, sizeof (NMPlatformIP6Route));
 }
 
 void
@@ -1428,16 +1373,13 @@ nm_fake_platform_setup (void)
 
 	nm_platform_setup (platform);
 
-	/* skip zero element */
-	link_add (platform, NULL, NM_LINK_TYPE_NONE, NULL, 0, NULL);
-
 	/* add loopback interface */
-	link_add (platform, "lo", NM_LINK_TYPE_LOOPBACK, NULL, 0, NULL);
+	link_add (platform, "lo", NM_LINK_TYPE_LOOPBACK, NULL, NULL, 0, NULL);
 
 	/* add some ethernets */
-	link_add (platform, "eth0", NM_LINK_TYPE_ETHERNET, NULL, 0, NULL);
-	link_add (platform, "eth1", NM_LINK_TYPE_ETHERNET, NULL, 0, NULL);
-	link_add (platform, "eth2", NM_LINK_TYPE_ETHERNET, NULL, 0, NULL);
+	link_add (platform, "eth0", NM_LINK_TYPE_ETHERNET, NULL, NULL, 0, NULL);
+	link_add (platform, "eth1", NM_LINK_TYPE_ETHERNET, NULL, NULL, 0, NULL);
+	link_add (platform, "eth2", NM_LINK_TYPE_ETHERNET, NULL, NULL, 0, NULL);
 }
 
 static void
@@ -1451,13 +1393,9 @@ finalize (GObject *object)
 		NMFakePlatformLink *device = &g_array_index (priv->links, NMFakePlatformLink, i);
 
 		g_free (device->udi);
-		g_clear_pointer (&device->lnk, nmp_object_unref);
+		g_clear_pointer (&device->obj, nmp_object_unref);
 	}
 	g_array_unref (priv->links);
-	g_array_unref (priv->ip4_addresses);
-	g_array_unref (priv->ip6_addresses);
-	g_array_unref (priv->ip4_routes);
-	g_array_unref (priv->ip6_routes);
 
 	G_OBJECT_CLASS (nm_fake_platform_parent_class)->finalize (object);
 }
@@ -1473,15 +1411,8 @@ nm_fake_platform_class_init (NMFakePlatformClass *klass)
 	platform_class->sysctl_set = sysctl_set;
 	platform_class->sysctl_get = sysctl_get;
 
-	platform_class->link_get = _nm_platform_link_get;
-	platform_class->link_get_by_ifname = _nm_platform_link_get_by_ifname;
-	platform_class->link_get_by_address = _nm_platform_link_get_by_address;
-	platform_class->link_get_all = link_get_all;
 	platform_class->link_add = link_add;
 	platform_class->link_delete = link_delete;
-	platform_class->link_get_type_name = link_get_type_name;
-
-	platform_class->link_get_lnk = link_get_lnk;
 
 	platform_class->link_get_udi = link_get_udi;
 
@@ -1525,21 +1456,11 @@ nm_fake_platform_class_init (NMFakePlatformClass *klass)
 	platform_class->mesh_set_channel = mesh_set_channel;
 	platform_class->mesh_set_ssid = mesh_set_ssid;
 
-	platform_class->ip4_address_get = ip4_address_get;
-	platform_class->ip6_address_get = ip6_address_get;
-	platform_class->ip4_address_get_all = ip4_address_get_all;
-	platform_class->ip6_address_get_all = ip6_address_get_all;
 	platform_class->ip4_address_add = ip4_address_add;
 	platform_class->ip6_address_add = ip6_address_add;
 	platform_class->ip4_address_delete = ip4_address_delete;
 	platform_class->ip6_address_delete = ip6_address_delete;
 
-	platform_class->ip4_route_get = ip4_route_get;
-	platform_class->ip6_route_get = ip6_route_get;
-	platform_class->ip4_route_get_all = ip4_route_get_all;
-	platform_class->ip6_route_get_all = ip6_route_get_all;
-	platform_class->ip4_route_add = ip4_route_add;
-	platform_class->ip6_route_add = ip6_route_add;
-	platform_class->ip4_route_delete = ip4_route_delete;
-	platform_class->ip6_route_delete = ip6_route_delete;
+	platform_class->ip_route_add = ip_route_add;
+	platform_class->ip_route_delete = ip_route_delete;
 }
diff --git a/src/platform/nm-linux-platform.c b/src/platform/nm-linux-platform.c
index 6b84c185..c4c93ed3 100644
--- a/src/platform/nm-linux-platform.c
+++ b/src/platform/nm-linux-platform.c
@@ -48,6 +48,7 @@
 #include "nmp-object.h"
 #include "nmp-netns.h"
 #include "nm-platform-utils.h"
+#include "nm-platform-private.h"
 #include "wifi/wifi-utils.h"
 #include "wifi/wifi-utils-wext.h"
 #include "nm-utils/unaligned.h"
@@ -103,6 +104,14 @@
 #define IFLA_IPTUN_MAX                  (__IFLA_IPTUN_MAX - 1)
 #endif
 
+
+static const gboolean RTA_PREF_SUPPORTED_AT_COMPILETIME = (RTA_MAX >= 20 /* RTA_PREF */);
+
+G_STATIC_ASSERT (RTA_MAX == (__RTA_MAX - 1));
+#define RTA_PREF                        20
+#undef  RTA_MAX
+#define RTA_MAX                        (MAX ((__RTA_MAX - 1), RTA_PREF))
+
 #ifndef MACVLAN_FLAG_NOPROMISC
 #define MACVLAN_FLAG_NOPROMISC          1
 #endif
@@ -197,6 +206,21 @@ typedef enum {
 	INFINIBAND_ACTION_DELETE_CHILD,
 } InfinibandAction;
 
+typedef enum {
+	CHANGE_LINK_TYPE_UNSPEC,
+	CHANGE_LINK_TYPE_SET_MTU,
+	CHANGE_LINK_TYPE_SET_ADDRESS,
+} ChangeLinkType;
+
+typedef struct {
+	union {
+		struct {
+			gconstpointer address;
+			gsize length;
+		} set_address;
+	};
+} ChangeLinkData;
+
 enum {
 	DELAYED_ACTION_IDX_REFRESH_ALL_LINKS,
 	DELAYED_ACTION_IDX_REFRESH_ALL_IP4_ADDRESSES,
@@ -255,13 +279,25 @@ static void delayed_action_schedule (NMPlatform *platform, DelayedActionType act
 static gboolean delayed_action_handle_all (NMPlatform *platform, gboolean read_netlink);
 static void do_request_link_no_delayed_actions (NMPlatform *platform, int ifindex, const char *name);
 static void do_request_all_no_delayed_actions (NMPlatform *platform, DelayedActionType action_type);
-static void cache_pre_hook (NMPCache *cache, const NMPObject *old, const NMPObject *new, NMPCacheOpsType ops_type, gpointer user_data);
-static void cache_prune_candidates_prune (NMPlatform *platform);
+static void cache_on_change (NMPlatform *platform,
+                             NMPCacheOpsType cache_op,
+                             const NMPObject *obj_old,
+                             const NMPObject *obj_new);
+static void cache_prune_all (NMPlatform *platform);
 static gboolean event_handler_read_netlink (NMPlatform *platform, gboolean wait_for_acks);
-static void ASSERT_NETNS_CURRENT (NMPlatform *platform);
 
 /*****************************************************************************/
 
+static NMPlatformError
+wait_for_nl_response_to_plerr (WaitForNlResponseResult seq_result)
+{
+	if (seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK)
+		return NM_PLATFORM_ERROR_SUCCESS;
+	if (seq_result < 0)
+		return (NMPlatformError) seq_result;
+	return NM_PLATFORM_ERROR_NETLINK;
+}
+
 static const char *
 wait_for_nl_response_to_string (WaitForNlResponseResult seq_result, char *buf, gsize buf_size)
 {
@@ -287,35 +323,117 @@ wait_for_nl_response_to_string (WaitForNlResponseResult seq_result, char *buf, g
 	return buf0;
 }
 
-/******************************************************************
+/*****************************************************************************
  * Support IFLA_INET6_ADDR_GEN_MODE
- ******************************************************************/
+ *****************************************************************************/
 
 static int _support_user_ipv6ll = 0;
 #define _support_user_ipv6ll_still_undecided() (G_UNLIKELY (_support_user_ipv6ll == 0))
 
+static void
+_support_user_ipv6ll_detect (struct nlattr **tb)
+{
+	gboolean supported;
+
+	nm_assert (_support_user_ipv6ll_still_undecided ());
+
+	/* IFLA_INET6_ADDR_GEN_MODE was added in kernel 3.17, dated 5 October, 2014. */
+	supported = !!tb[IFLA_INET6_ADDR_GEN_MODE];
+	_support_user_ipv6ll = supported ? 1 : -1;
+	_LOG2D ("kernel-support: IFLA_INET6_ADDR_GEN_MODE: %s",
+	        supported ? "detected" : "not detected");
+}
+
 static gboolean
 _support_user_ipv6ll_get (void)
 {
 	if (_support_user_ipv6ll_still_undecided ()) {
-		_support_user_ipv6ll = -1;
-		_LOG2D ("kernel-support: IFLA_INET6_ADDR_GEN_MODE: %s", "failed to detect; assume no support");
-		return FALSE;
+		_support_user_ipv6ll = 1;
+		_LOG2D ("kernel-support: IFLA_INET6_ADDR_GEN_MODE: %s", "failed to detect; assume support");
 	}
-	return _support_user_ipv6ll > 0;
+	return _support_user_ipv6ll >= 0;
+}
+
+/*****************************************************************************
+ * extended IFA_FLAGS support
+ *****************************************************************************/
 
+static int _support_kernel_extended_ifa_flags = 0;
+
+#define _support_kernel_extended_ifa_flags_still_undecided() (G_UNLIKELY (_support_kernel_extended_ifa_flags == 0))
+
+static void
+_support_kernel_extended_ifa_flags_detect (struct nl_msg *msg)
+{
+	struct nlmsghdr *msg_hdr;
+	gboolean support;
+
+	nm_assert (_support_kernel_extended_ifa_flags_still_undecided ());
+	nm_assert (msg);
+
+	msg_hdr = nlmsg_hdr (msg);
+
+	nm_assert (msg_hdr && msg_hdr->nlmsg_type == RTM_NEWADDR);
+
+	/* IFA_FLAGS is set for IPv4 and IPv6 addresses. It was added first to IPv6,
+	 * but if we encounter an IPv4 address with IFA_FLAGS, we surely have support. */
+	if (NM_IN_SET (((struct ifaddrmsg *) nlmsg_data (msg_hdr))->ifa_family, AF_INET, AF_INET6))
+		return;
+
+	/* see if the nl_msg contains the IFA_FLAGS attribute. If it does,
+	 * we assume, that the kernel supports extended flags, IFA_F_MANAGETEMPADDR
+	 * and IFA_F_NOPREFIXROUTE for IPv6. They were added together in kernel 3.14,
+	 * dated 30 March, 2014.
+	 *
+	 * For IPv4, IFA_F_NOPREFIXROUTE was added later, but there is no easy
+	 * way to detect kernel support. */
+	support = !!nlmsg_find_attr (msg_hdr, sizeof (struct ifaddrmsg), IFA_FLAGS);
+	_support_kernel_extended_ifa_flags = support ? 1 : -1;
+	_LOG2D ("kernel-support: extended-ifa-flags: %s", support ? "detected" : "not detected");
 }
 
+static gboolean
+_support_kernel_extended_ifa_flags_get (void)
+{
+	if (_support_kernel_extended_ifa_flags_still_undecided ()) {
+		_LOG2D ("kernel-support: extended-ifa-flags: %s", "unable to detect kernel support for handling IPv6 temporary addresses. Assume support");
+		_support_kernel_extended_ifa_flags = 1;
+	}
+	return _support_kernel_extended_ifa_flags >= 0;
+}
+
+/*****************************************************************************
+ * Support RTA_PREF
+ *****************************************************************************/
+
+static int _support_rta_pref = 0;
+#define _support_rta_pref_still_undecided() (G_UNLIKELY (_support_rta_pref == 0))
+
 static void
-_support_user_ipv6ll_detect (struct nlattr **tb)
+_support_rta_pref_detect (struct nlattr **tb)
 {
-	if (_support_user_ipv6ll_still_undecided ()) {
-		gboolean supported = !!tb[IFLA_INET6_ADDR_GEN_MODE];
+	gboolean supported;
+
+	nm_assert (_support_rta_pref_still_undecided ());
 
-		_support_user_ipv6ll = supported ? 1 : -1;
-		_LOG2D ("kernel-support: IFLA_INET6_ADDR_GEN_MODE: %s",
-		        supported ? "detected" : "not detected");
+	/* RTA_PREF was added in kernel 4.1, dated 21 June, 2015. */
+	supported = !!tb[RTA_PREF];
+	_support_rta_pref = supported ? 1 : -1;
+	_LOG2D ("kernel-support: RTA_PREF: ability to set router preference for IPv6 routes: %s",
+	        supported ? "detected" : "not detected");
+}
+
+static gboolean
+_support_rta_pref_get (void)
+{
+	if (_support_rta_pref_still_undecided ()) {
+		/* if we couldn't detect support, we fallback on compile-time check, whether
+		 * RTA_PREF is present in the kernel headers. */
+		_support_rta_pref = RTA_PREF_SUPPORTED_AT_COMPILETIME ? 1 : -1;
+		_LOG2D ("kernel-support: RTA_PREF: ability to set router preference for IPv6 routes: %s",
+		        RTA_PREF_SUPPORTED_AT_COMPILETIME ? "assume support" : "assume no support");
 	}
+	return _support_rta_pref >= 0;
 }
 
 /******************************************************************
@@ -377,6 +495,7 @@ static const LinkDesc linktypes[] = {
 	{ NM_LINK_TYPE_WWAN_NET,      "wwan",        NULL,          "wwan" },
 	{ NM_LINK_TYPE_WIMAX,         "wimax",       "wimax",       "wimax" },
 
+	{ NM_LINK_TYPE_BNEP,          "bluetooth",   NULL,          "bluetooth" },
 	{ NM_LINK_TYPE_DUMMY,         "dummy",       "dummy",       NULL },
 	{ NM_LINK_TYPE_GRE,           "gre",         "gre",         NULL },
 	{ NM_LINK_TYPE_GRETAP,        "gretap",      "gretap",      NULL },
@@ -388,13 +507,13 @@ static const LinkDesc linktypes[] = {
 	{ NM_LINK_TYPE_MACVLAN,       "macvlan",     "macvlan",     NULL },
 	{ NM_LINK_TYPE_MACVTAP,       "macvtap",     "macvtap",     NULL },
 	{ NM_LINK_TYPE_OPENVSWITCH,   "openvswitch", "openvswitch", NULL },
+	{ NM_LINK_TYPE_PPP,           "ppp",         NULL,          "ppp" },
 	{ NM_LINK_TYPE_SIT,           "sit",         "sit",         NULL },
 	{ NM_LINK_TYPE_TAP,           "tap",         NULL,          NULL },
 	{ NM_LINK_TYPE_TUN,           "tun",         NULL,          NULL },
 	{ NM_LINK_TYPE_VETH,          "veth",        "veth",        NULL },
 	{ NM_LINK_TYPE_VLAN,          "vlan",        "vlan",        "vlan" },
 	{ NM_LINK_TYPE_VXLAN,         "vxlan",       "vxlan",       "vxlan" },
-	{ NM_LINK_TYPE_BNEP,          "bluetooth",   NULL,          "bluetooth" },
 
 	{ NM_LINK_TYPE_BRIDGE,        "bridge",      "bridge",      "bridge" },
 	{ NM_LINK_TYPE_BOND,          "bond",        "bond",        "bond" },
@@ -628,7 +747,7 @@ _linktype_get_type (NMPlatform *platform,
 {
 	guint i;
 
-	ASSERT_NETNS_CURRENT (platform);
+	NMTST_ASSERT_PLATFORM_NETNS_CURRENT (platform);
 	nm_assert (ifname);
 
 	if (completed_from_cache) {
@@ -693,6 +812,8 @@ _linktype_get_type (NMPlatform *platform,
 		return NM_LINK_TYPE_SIT;
 	else if (arptype == ARPHRD_TUNNEL6)
 		return NM_LINK_TYPE_IP6TNL;
+	else if (arptype == ARPHRD_PPP)
+		return NM_LINK_TYPE_PPP;
 
 	{
 		NMPUtilsEthtoolDriverInfo driver_info;
@@ -762,6 +883,10 @@ _linktype_get_type (NMPlatform *platform,
 			 * aside from the DEVTYPE. */
 			if (!g_strcmp0 (devtype, "gadget"))
 				return NM_LINK_TYPE_ETHERNET;
+
+			/* Distributed Switch Architecture switch chips */
+			if (!g_strcmp0 (devtype, "dsa"))
+				return NM_LINK_TYPE_ETHERNET;
 		}
 	}
 
@@ -772,32 +897,149 @@ _linktype_get_type (NMPlatform *platform,
  * libnl unility functions and wrappers
  ******************************************************************/
 
-#define nm_auto_nlmsg __attribute__((cleanup(_nm_auto_nl_msg_cleanup)))
+#define NLMSG_TAIL(nmsg) \
+    ((struct rtattr *) (((char *) (nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len)))
+
+/* copied from iproute2's addattr_l(). */
+static gboolean
+_nl_addattr_l (struct nlmsghdr *n,
+               int maxlen,
+               int type,
+               const void *data,
+               int alen)
+{
+	int len = RTA_LENGTH (alen);
+	struct rtattr *rta;
+
+	if (NLMSG_ALIGN (n->nlmsg_len) + RTA_ALIGN (len) > maxlen)
+		return FALSE;
+
+	rta = NLMSG_TAIL (n);
+	rta->rta_type = type;
+	rta->rta_len = len;
+	memcpy (RTA_DATA (rta), data, alen);
+	n->nlmsg_len = NLMSG_ALIGN (n->nlmsg_len) + RTA_ALIGN (len);
+	return TRUE;
+}
+
 static void
 _nm_auto_nl_msg_cleanup (void *ptr)
 {
 	nlmsg_free (*((struct nl_msg **) ptr));
 }
+#define nm_auto_nlmsg nm_auto(_nm_auto_nl_msg_cleanup)
 
 static const char *
-_nl_nlmsg_type_to_str (guint16 type, char *buf, gsize len)
+_nl_nlmsghdr_to_str (const struct nlmsghdr *hdr, char *buf, gsize len)
 {
-	const char *str_type = NULL;
+	const char *b;
+	const char *s;
+	guint flags, flags_before;
+	const char *prefix;
 
-	switch (type) {
-	case RTM_NEWLINK:  str_type = "NEWLINK";  break;
-	case RTM_DELLINK:  str_type = "DELLINK";  break;
-	case RTM_NEWADDR:  str_type = "NEWADDR";  break;
-	case RTM_DELADDR:  str_type = "DELADDR";  break;
-	case RTM_NEWROUTE: str_type = "NEWROUTE"; break;
-	case RTM_DELROUTE: str_type = "DELROUTE"; break;
+	nm_utils_to_string_buffer_init (&buf, &len);
+	b = buf;
+
+	switch (hdr->nlmsg_type) {
+	case RTM_NEWLINK:  s = "NEWLINK";  break;
+	case RTM_DELLINK:  s = "DELLINK";  break;
+	case RTM_NEWADDR:  s = "NEWADDR";  break;
+	case RTM_DELADDR:  s = "DELADDR";  break;
+	case RTM_NEWROUTE: s = "NEWROUTE"; break;
+	case RTM_DELROUTE: s = "DELROUTE"; break;
+	default:           s = NULL;       break;
 	}
-	if (str_type)
-		g_strlcpy (buf, str_type, len);
+
+	if (s)
+		nm_utils_strbuf_append (&buf, &len, "RTM_%s", s);
 	else
-		g_snprintf (buf, len, "(%d)", type);
-	return buf;
+		nm_utils_strbuf_append (&buf, &len, "(%u)", (unsigned) hdr->nlmsg_type);
+
+	flags = hdr->nlmsg_flags;
+
+	if (!flags) {
+		nm_utils_strbuf_append_str (&buf, &len, ", flags 0");
+		goto flags_done;
+	}
+
+#define _F(f, n) \
+	G_STMT_START { \
+		if (NM_FLAGS_ALL (flags, f)) { \
+			flags &= ~(f); \
+			nm_utils_strbuf_append (&buf, &len, "%s%s", prefix, n); \
+			if (!flags) \
+				goto flags_done; \
+			prefix = ","; \
+		} \
+	} G_STMT_END
+
+	prefix = ", flags ";
+	flags_before = flags;
+	_F (NLM_F_REQUEST, "request");
+	_F (NLM_F_MULTI, "multi");
+	_F (NLM_F_ACK, "ack");
+	_F (NLM_F_ECHO, "echo");
+	_F (NLM_F_DUMP_INTR, "dump_intr");
+	_F (0x20 /*NLM_F_DUMP_FILTERED*/, "dump_filtered");
+
+	if (flags_before != flags)
+		prefix = ";";
+
+	switch (hdr->nlmsg_type) {
+	case RTM_NEWLINK:
+	case RTM_NEWADDR:
+	case RTM_NEWROUTE:
+		_F (NLM_F_REPLACE, "replace");
+		_F (NLM_F_EXCL, "excl");
+		_F (NLM_F_CREATE, "create");
+		_F (NLM_F_APPEND, "append");
+		break;
+	case RTM_GETLINK:
+	case RTM_GETADDR:
+	case RTM_GETROUTE:
+		_F (NLM_F_DUMP, "dump");
+		_F (NLM_F_ROOT, "root");
+		_F (NLM_F_MATCH, "match");
+		_F (NLM_F_ATOMIC, "atomic");
+		break;
+	}
+
+#undef _F
+
+	if (flags_before != flags)
+		prefix = ";";
+	nm_utils_strbuf_append (&buf, &len, "%s0x%04x", prefix, flags);
+
+flags_done:
+
+	nm_utils_strbuf_append (&buf, &len, ", seq %u", (unsigned) hdr->nlmsg_seq);
+
+	return b;
+}
+
+static int
+_nl_nla_parse (struct nlattr *tb[], int maxtype, struct nlattr *head, int len,
+               const struct nla_policy *policy)
+{
+	return nla_parse (tb, maxtype, head, len, (struct nla_policy *) policy);
+}
+#define nla_parse(...) _nl_nla_parse(__VA_ARGS__)
+
+static int
+_nl_nlmsg_parse (struct nlmsghdr *nlh, int hdrlen, struct nlattr *tb[],
+                 int maxtype, const struct nla_policy *policy)
+{
+	return nlmsg_parse (nlh, hdrlen, tb, maxtype, (struct nla_policy *) policy);
 }
+#define nlmsg_parse(...) _nl_nlmsg_parse(__VA_ARGS__)
+
+static int
+_nl_nla_parse_nested (struct nlattr *tb[], int maxtype, struct nlattr *nla,
+                      const struct nla_policy *policy)
+{
+	return nla_parse_nested (tb, maxtype, nla, (struct nla_policy *) policy);
+}
+#define nla_parse_nested(...) _nl_nla_parse_nested(__VA_ARGS__)
 
 /******************************************************************
  * NMPObject/netlink functions
@@ -826,7 +1068,7 @@ _parse_af_inet6 (NMPlatform *platform,
                  guint8 *out_addr_gen_mode_inv,
                  gboolean *out_addr_gen_mode_valid)
 {
-	static struct nla_policy policy[IFLA_INET6_MAX+1] = {
+	static const struct nla_policy policy[IFLA_INET6_MAX+1] = {
 		[IFLA_INET6_FLAGS]              = { .type = NLA_U32 },
 		[IFLA_INET6_CACHEINFO]          = { .minlen = nm_offsetofend (struct ifla_cacheinfo, retrans_time) },
 		[IFLA_INET6_CONF]               = { .minlen = 4 },
@@ -862,7 +1104,8 @@ _parse_af_inet6 (NMPlatform *platform,
 	/* Hack to detect support addrgenmode of the kernel. We only parse
 	 * netlink messages that we receive from kernel, hence this check
 	 * is valid. */
-	_support_user_ipv6ll_detect (tb);
+	if (_support_user_ipv6ll_still_undecided ())
+		_support_user_ipv6ll_detect (tb);
 
 	if (tb[IFLA_INET6_ADDR_GEN_MODE]) {
 		i6_addr_gen_mode_inv = _nm_platform_uint8_inv (nla_get_u8 (tb[IFLA_INET6_ADDR_GEN_MODE]));
@@ -892,7 +1135,7 @@ errout:
 static NMPObject *
 _parse_lnk_gre (const char *kind, struct nlattr *info_data)
 {
-	static struct nla_policy policy[IFLA_GRE_MAX + 1] = {
+	static const struct nla_policy policy[IFLA_GRE_MAX + 1] = {
 		[IFLA_GRE_LINK]     = { .type = NLA_U32 },
 		[IFLA_GRE_IFLAGS]   = { .type = NLA_U16 },
 		[IFLA_GRE_OFLAGS]   = { .type = NLA_U16 },
@@ -952,7 +1195,7 @@ _parse_lnk_gre (const char *kind, struct nlattr *info_data)
 static NMPObject *
 _parse_lnk_infiniband (const char *kind, struct nlattr *info_data)
 {
-	static struct nla_policy policy[IFLA_IPOIB_MAX + 1] = {
+	static const struct nla_policy policy[IFLA_IPOIB_MAX + 1] = {
 		[IFLA_IPOIB_PKEY]   = { .type = NLA_U16 },
 		[IFLA_IPOIB_MODE]   = { .type = NLA_U16 },
 		[IFLA_IPOIB_UMCAST] = { .type = NLA_U16 },
@@ -998,7 +1241,7 @@ _parse_lnk_infiniband (const char *kind, struct nlattr *info_data)
 static NMPObject *
 _parse_lnk_ip6tnl (const char *kind, struct nlattr *info_data)
 {
-	static struct nla_policy policy[IFLA_IPTUN_MAX + 1] = {
+	static const struct nla_policy policy[IFLA_IPTUN_MAX + 1] = {
 		[IFLA_IPTUN_LINK]        = { .type = NLA_U32 },
 		[IFLA_IPTUN_LOCAL]       = { .type = NLA_UNSPEC,
 		                             .minlen = sizeof (struct in6_addr)},
@@ -1051,7 +1294,7 @@ _parse_lnk_ip6tnl (const char *kind, struct nlattr *info_data)
 static NMPObject *
 _parse_lnk_ipip (const char *kind, struct nlattr *info_data)
 {
-	static struct nla_policy policy[IFLA_IPTUN_MAX + 1] = {
+	static const struct nla_policy policy[IFLA_IPTUN_MAX + 1] = {
 		[IFLA_IPTUN_LINK]     = { .type = NLA_U32 },
 		[IFLA_IPTUN_LOCAL]    = { .type = NLA_U32 },
 		[IFLA_IPTUN_REMOTE]   = { .type = NLA_U32 },
@@ -1089,7 +1332,7 @@ _parse_lnk_ipip (const char *kind, struct nlattr *info_data)
 static NMPObject *
 _parse_lnk_macvlan (const char *kind, struct nlattr *info_data)
 {
-	static struct nla_policy policy[IFLA_MACVLAN_MAX + 1] = {
+	static const struct nla_policy policy[IFLA_MACVLAN_MAX + 1] = {
 		[IFLA_MACVLAN_MODE]  = { .type = NLA_U32 },
 		[IFLA_MACVLAN_FLAGS] = { .type = NLA_U16 },
 	};
@@ -1132,7 +1375,7 @@ _parse_lnk_macvlan (const char *kind, struct nlattr *info_data)
 static NMPObject *
 _parse_lnk_macsec (const char *kind, struct nlattr *info_data)
 {
-	static struct nla_policy policy[__IFLA_MACSEC_MAX] = {
+	static const struct nla_policy policy[__IFLA_MACSEC_MAX] = {
 		[IFLA_MACSEC_SCI]            = { .type = NLA_U64 },
 		[IFLA_MACSEC_ICV_LEN]        = { .type = NLA_U8 },
 		[IFLA_MACSEC_CIPHER_SUITE]   = { .type = NLA_U64 },
@@ -1182,7 +1425,7 @@ _parse_lnk_macsec (const char *kind, struct nlattr *info_data)
 static NMPObject *
 _parse_lnk_sit (const char *kind, struct nlattr *info_data)
 {
-	static struct nla_policy policy[IFLA_IPTUN_MAX + 1] = {
+	static const struct nla_policy policy[IFLA_IPTUN_MAX + 1] = {
 		[IFLA_IPTUN_LINK]     = { .type = NLA_U32 },
 		[IFLA_IPTUN_LOCAL]    = { .type = NLA_U32 },
 		[IFLA_IPTUN_REMOTE]   = { .type = NLA_U32 },
@@ -1284,7 +1527,7 @@ _vlan_qos_mapping_from_nla (struct nlattr *nlattr,
 static NMPObject *
 _parse_lnk_vlan (const char *kind, struct nlattr *info_data)
 {
-	static struct nla_policy policy[IFLA_VLAN_MAX+1] = {
+	static const struct nla_policy policy[IFLA_VLAN_MAX+1] = {
 		[IFLA_VLAN_ID]          = { .type = NLA_U16 },
 		[IFLA_VLAN_FLAGS]       = { .minlen = nm_offsetofend (struct ifla_vlan_flags, flags) },
 		[IFLA_VLAN_INGRESS_QOS] = { .type = NLA_NESTED },
@@ -1370,7 +1613,7 @@ struct nm_ifla_vxlan_port_range {
 static NMPObject *
 _parse_lnk_vxlan (const char *kind, struct nlattr *info_data)
 {
-	static struct nla_policy policy[IFLA_VXLAN_MAX + 1] = {
+	static const struct nla_policy policy[IFLA_VXLAN_MAX + 1] = {
 		[IFLA_VXLAN_ID]         = { .type = NLA_U32 },
 		[IFLA_VXLAN_GROUP]      = { .type = NLA_U32 },
 		[IFLA_VXLAN_GROUP6]     = { .type = NLA_UNSPEC,
@@ -1460,7 +1703,7 @@ _parse_lnk_vxlan (const char *kind, struct nlattr *info_data)
 static NMPObject *
 _new_from_nl_link (NMPlatform *platform, const NMPCache *cache, struct nlmsghdr *nlh, gboolean id_only)
 {
-	static struct nla_policy policy[IFLA_MAX+1] = {
+	static const struct nla_policy policy[IFLA_MAX+1] = {
 		[IFLA_IFNAME]           = { .type = NLA_STRING,
 		                            .maxlen = IFNAMSIZ },
 		[IFLA_MTU]              = { .type = NLA_U32 },
@@ -1488,7 +1731,7 @@ _new_from_nl_link (NMPlatform *platform, const NMPCache *cache, struct nlmsghdr
 		[IFLA_NET_NS_PID]       = { .type = NLA_U32 },
 		[IFLA_NET_NS_FD]        = { .type = NLA_U32 },
 	};
-	static struct nla_policy policy_link_info[IFLA_INFO_MAX+1] = {
+	static const struct nla_policy policy_link_info[IFLA_INFO_MAX+1] = {
 		[IFLA_INFO_KIND]        = { .type = NLA_STRING },
 		[IFLA_INFO_DATA]        = { .type = NLA_NESTED },
 		[IFLA_INFO_XSTATS]      = { .type = NLA_NESTED },
@@ -1514,6 +1757,9 @@ _new_from_nl_link (NMPlatform *platform, const NMPCache *cache, struct nlmsghdr
 		return NULL;
 	ifi = nlmsg_data(nlh);
 
+	if (ifi->ifi_family != AF_UNSPEC)
+		return NULL;
+
 	obj = nmp_object_new_link (ifi->ifi_index);
 
 	if (id_only)
@@ -1680,7 +1926,7 @@ _new_from_nl_link (NMPlatform *platform, const NMPCache *cache, struct nlmsghdr
 				 * Also, sometimes the info-data is missing for updates. In this case
 				 * we want to keep the previously received lnk_data. */
 				nmp_object_unref (lnk_data);
-				lnk_data = nmp_object_ref (link_cached->_link.netlink.lnk);
+				lnk_data = (NMPObject *) nmp_object_ref (link_cached->_link.netlink.lnk);
 			}
 			if (address_complete_from_cache)
 				obj->link.addr = link_cached->link.addr;
@@ -1711,7 +1957,7 @@ errout:
 static NMPObject *
 _new_from_nl_addr (struct nlmsghdr *nlh, gboolean id_only)
 {
-	static struct nla_policy policy[IFA_MAX+1] = {
+	static const struct nla_policy policy[IFA_MAX+1] = {
 		[IFA_LABEL]     = { .type = NLA_STRING,
 		                     .maxlen = IFNAMSIZ },
 		[IFA_CACHEINFO] = { .minlen = nm_offsetofend (struct ifa_cacheinfo, tstamp) },
@@ -1733,7 +1979,7 @@ _new_from_nl_addr (struct nlmsghdr *nlh, gboolean id_only)
 		goto errout;
 	is_v4 = ifa->ifa_family == AF_INET;
 
-	err = nlmsg_parse(nlh, sizeof(*ifa), tb, IFA_MAX, policy);
+	err = nlmsg_parse (nlh, sizeof(*ifa), tb, IFA_MAX, policy);
 	if (err < 0)
 		goto errout;
 
@@ -1826,10 +2072,12 @@ errout:
 static NMPObject *
 _new_from_nl_route (struct nlmsghdr *nlh, gboolean id_only)
 {
-	static struct nla_policy policy[RTA_MAX+1] = {
+	static const struct nla_policy policy[RTA_MAX+1] = {
+		[RTA_TABLE]     = { .type = NLA_U32 },
 		[RTA_IIF]       = { .type = NLA_U32 },
 		[RTA_OIF]       = { .type = NLA_U32 },
 		[RTA_PRIORITY]  = { .type = NLA_U32 },
+		[RTA_PREF]      = { .type = NLA_U8 },
 		[RTA_FLOW]      = { .type = NLA_U32 },
 		[RTA_CACHEINFO] = { .minlen = nm_offsetofend (struct rta_cacheinfo, rta_tsage) },
 		[RTA_METRICS]   = { .type = NLA_NESTED },
@@ -1849,7 +2097,6 @@ _new_from_nl_route (struct nlmsghdr *nlh, gboolean id_only)
 	} nh;
 	guint32 mss;
 	guint32 window = 0, cwnd = 0, initcwnd = 0, initrwnd = 0, mtu = 0, lock = 0;
-	guint32 table;
 
 	if (!nlmsg_valid_hdr (nlh, sizeof (*rtm)))
 		return NULL;
@@ -1869,12 +2116,6 @@ _new_from_nl_route (struct nlmsghdr *nlh, gboolean id_only)
 	if (err < 0)
 		goto errout;
 
-	table = tb[RTA_TABLE]
-	        ? nla_get_u32 (tb[RTA_TABLE])
-	        : (guint32) rtm->rtm_table;
-	if (table != RT_TABLE_MAIN)
-		goto errout;
-
 	/*****************************************************************/
 
 	is_v4 = rtm->rtm_family == AF_INET;
@@ -1928,7 +2169,7 @@ _new_from_nl_route (struct nlmsghdr *nlh, gboolean id_only)
 	    || tb[RTA_GATEWAY]
 	    || tb[RTA_FLOW]) {
 		int ifindex = 0;
-		NMIPAddr gateway = NMIPAddrInit;
+		NMIPAddr gateway = { };
 
 		if (tb[RTA_OIF])
 			ifindex = nla_get_u32 (tb[RTA_OIF]);
@@ -1956,7 +2197,7 @@ _new_from_nl_route (struct nlmsghdr *nlh, gboolean id_only)
 	mss = 0;
 	if (tb[RTA_METRICS]) {
 		struct nlattr *mtb[RTAX_MAX + 1];
-		static struct nla_policy rtax_policy[RTAX_MAX + 1] = {
+		static const struct nla_policy rtax_policy[RTAX_MAX + 1] = {
 			[RTAX_LOCK]        = { .type = NLA_U32 },
 			[RTAX_ADVMSS]      = { .type = NLA_U32 },
 			[RTAX_WINDOW]      = { .type = NLA_U32 },
@@ -1990,6 +2231,10 @@ _new_from_nl_route (struct nlmsghdr *nlh, gboolean id_only)
 
 	obj = nmp_object_new (is_v4 ? NMP_OBJECT_TYPE_IP4_ROUTE : NMP_OBJECT_TYPE_IP6_ROUTE, NULL);
 
+	obj->ip_route.table_coerced = nm_platform_route_table_coerce (  tb[RTA_TABLE]
+	                                                              ? nla_get_u32 (tb[RTA_TABLE])
+	                                                              : (guint32) rtm->rtm_table);
+
 	obj->ip_route.ifindex = nh.ifindex;
 
 	if (_check_addr_or_errout (tb, RTA_DST, addr_len))
@@ -2015,9 +2260,13 @@ _new_from_nl_route (struct nlmsghdr *nlh, gboolean id_only)
 			memcpy (&obj->ip6_route.pref_src, nla_data (tb[RTA_PREFSRC]), addr_len);
 	}
 
-	if (!is_v4 && tb[RTA_SRC]) {
-		_check_addr_or_errout (tb, RTA_SRC, addr_len);
-		memcpy (&obj->ip6_route.src, nla_data (tb[RTA_SRC]), addr_len);
+	if (is_v4)
+		obj->ip4_route.tos = rtm->rtm_tos;
+	else {
+		if (tb[RTA_SRC]) {
+			_check_addr_or_errout (tb, RTA_SRC, addr_len);
+			memcpy (&obj->ip6_route.src, nla_data (tb[RTA_SRC]), addr_len);
+		}
 		obj->ip6_route.src_plen = rtm->rtm_src_len;
 	}
 
@@ -2027,12 +2276,20 @@ _new_from_nl_route (struct nlmsghdr *nlh, gboolean id_only)
 	obj->ip_route.initcwnd = initcwnd;
 	obj->ip_route.initrwnd = initrwnd;
 	obj->ip_route.mtu = mtu;
-	obj->ip_route.tos = rtm->rtm_tos;
-	obj->ip_route.lock_window = NM_FLAGS_HAS (lock, 1 << RTAX_WINDOW);
-	obj->ip_route.lock_cwnd = NM_FLAGS_HAS (lock, 1 << RTAX_CWND);
+	obj->ip_route.lock_window   = NM_FLAGS_HAS (lock, 1 << RTAX_WINDOW);
+	obj->ip_route.lock_cwnd     = NM_FLAGS_HAS (lock, 1 << RTAX_CWND);
 	obj->ip_route.lock_initcwnd = NM_FLAGS_HAS (lock, 1 << RTAX_INITCWND);
 	obj->ip_route.lock_initrwnd = NM_FLAGS_HAS (lock, 1 << RTAX_INITRWND);
-	obj->ip_route.lock_mtu  = NM_FLAGS_HAS (lock, 1 << RTAX_MTU);
+	obj->ip_route.lock_mtu      = NM_FLAGS_HAS (lock, 1 << RTAX_MTU);
+
+	if (!is_v4) {
+		/* Detect support for RTA_PREF by inspecting the netlink message. */
+		if (_support_rta_pref_still_undecided ())
+			_support_rta_pref_detect (tb);
+
+		if (tb[RTA_PREF])
+			obj->ip6_route.rt_pref = nla_get_u8 (tb[RTA_PREF]);
+	}
 
 	if (NM_FLAGS_HAS (rtm->rtm_flags, RTM_F_CLONED)) {
 		/* we must not straight way reject cloned routes, because we might have cached
@@ -2135,12 +2392,14 @@ nla_put_failure:
 
 static gboolean
 _nl_msg_new_link_set_linkinfo (struct nl_msg *msg,
-                               NMLinkType link_type)
+                               NMLinkType link_type,
+                               const char *veth_peer)
 {
 	struct nlattr *info;
 	const char *kind;
 
 	nm_assert (msg);
+	nm_assert (!!veth_peer == (link_type == NM_LINK_TYPE_VETH));
 
 	kind = nm_link_type_to_rtnl_type_string (link_type);
 	if (!kind)
@@ -2151,11 +2410,26 @@ _nl_msg_new_link_set_linkinfo (struct nl_msg *msg,
 
 	NLA_PUT_STRING (msg, IFLA_INFO_KIND, kind);
 
+	if (veth_peer) {
+		struct ifinfomsg ifi = { };
+		struct nlattr *data, *info_peer;
+
+		if (!(data = nla_nest_start (msg, IFLA_INFO_DATA)))
+			goto nla_put_failure;
+		if (!(info_peer = nla_nest_start (msg, 1 /*VETH_INFO_PEER*/)))
+			goto nla_put_failure;
+		if (nlmsg_append (msg, &ifi, sizeof (ifi), NLMSG_ALIGNTO) < 0)
+			goto nla_put_failure;
+		NLA_PUT_STRING (msg, IFLA_IFNAME, veth_peer);
+		nla_nest_end (msg, info_peer);
+		nla_nest_end (msg, data);
+	}
+
 	nla_nest_end (msg, info);
 
 	return TRUE;
 nla_put_failure:
-	return FALSE;
+	g_return_val_if_reached (FALSE);
 }
 
 static gboolean
@@ -2363,7 +2637,7 @@ _nl_msg_new_address (int nlmsg_type,
 	    && *((in_addr_t *) address) != 0) {
 		in_addr_t broadcast;
 
-		broadcast = *((in_addr_t *) address) | ~nm_utils_ip4_prefix_to_netmask (plen);
+		broadcast = *((in_addr_t *) address) | ~_nm_utils_ip4_prefix_to_netmask (plen);
 		NLA_PUT (msg, IFA_BROADCAST, addr_len, &broadcast);
 	}
 
@@ -2394,87 +2668,109 @@ nla_put_failure:
 	g_return_val_if_reached (NULL);
 }
 
+static guint32
+ip_route_get_lock_flag (const NMPlatformIPRoute *route)
+{
+	return   (((guint32) route->lock_window)   << RTAX_WINDOW)
+	       | (((guint32) route->lock_cwnd)     << RTAX_CWND)
+	       | (((guint32) route->lock_initcwnd) << RTAX_INITCWND)
+	       | (((guint32) route->lock_initrwnd) << RTAX_INITRWND)
+	       | (((guint32) route->lock_mtu)      << RTAX_MTU);
+}
+
 /* Copied and modified from libnl3's build_route_msg() and rtnl_route_build_msg(). */
 static struct nl_msg *
 _nl_msg_new_route (int nlmsg_type,
-                   int nlmsg_flags,
-                   int family,
-                   int ifindex,
-                   NMIPConfigSource source,
-                   unsigned char scope,
-                   gconstpointer network,
-                   guint8 plen,
-                   gconstpointer gateway,
-                   guint32 metric,
-                   guint32 mss,
-                   gconstpointer pref_src,
-                   gconstpointer src,
-                   guint8 src_plen,
-                   guint8 tos,
-                   guint32 window,
-                   guint32 cwnd,
-                   guint32 initcwnd,
-                   guint32 initrwnd,
-                   guint32 mtu,
-                   guint32 lock)
+                   guint16 nlmsgflags,
+                   const NMPObject *obj)
 {
 	struct nl_msg *msg;
+	const NMPClass *klass = NMP_OBJECT_GET_CLASS (obj);
+	gboolean is_v4 = klass->addr_family == AF_INET;
+	const guint32 lock = ip_route_get_lock_flag (NMP_OBJECT_CAST_IP_ROUTE (obj));
+	const guint32 table = nm_platform_route_table_uncoerce (NMP_OBJECT_CAST_IP_ROUTE (obj)->table_coerced, TRUE);
 	struct rtmsg rtmsg = {
-		.rtm_family = family,
-		.rtm_tos = tos,
-		.rtm_table = RT_TABLE_MAIN, /* omit setting RTA_TABLE attribute */
-		.rtm_protocol = nmp_utils_ip_config_source_coerce_to_rtprot (source),
-		.rtm_scope = scope,
+		.rtm_family = klass->addr_family,
+		.rtm_tos = is_v4
+		           ? obj->ip4_route.tos
+		           : 0,
+		.rtm_table = table <= 0xFF ? table : RT_TABLE_UNSPEC,
+		.rtm_protocol = nmp_utils_ip_config_source_coerce_to_rtprot (obj->ip_route.rt_source),
+		.rtm_scope = is_v4
+		             ? nm_platform_route_scope_inv (obj->ip4_route.scope_inv)
+		             : RT_SCOPE_NOWHERE,
 		.rtm_type = RTN_UNICAST,
 		.rtm_flags = 0,
-		.rtm_dst_len = plen,
-		.rtm_src_len = src ? src_plen : 0,
+		.rtm_dst_len = obj->ip_route.plen,
+		.rtm_src_len = is_v4
+		               ? 0
+		               : NMP_OBJECT_CAST_IP6_ROUTE (obj)->src_plen,
 	};
 
 	gsize addr_len;
 
-	nm_assert (NM_IN_SET (family, AF_INET, AF_INET6));
+	nm_assert (NM_IN_SET (NMP_OBJECT_GET_TYPE (obj), NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE));
 	nm_assert (NM_IN_SET (nlmsg_type, RTM_NEWROUTE, RTM_DELROUTE));
-	nm_assert (network);
 
-	msg = nlmsg_alloc_simple (nlmsg_type, nlmsg_flags);
+	msg = nlmsg_alloc_simple (nlmsg_type, (int) nlmsgflags);
 	if (!msg)
 		g_return_val_if_reached (NULL);
 
 	if (nlmsg_append (msg, &rtmsg, sizeof (rtmsg), NLMSG_ALIGNTO) < 0)
 		goto nla_put_failure;
 
-	addr_len = family == AF_INET ? sizeof (in_addr_t) : sizeof (struct in6_addr);
+	addr_len = is_v4
+	             ? sizeof (in_addr_t)
+	             : sizeof (struct in6_addr);
 
-	NLA_PUT (msg, RTA_DST, addr_len, network);
+	NLA_PUT (msg, RTA_DST, addr_len,
+	         is_v4
+	           ? (gconstpointer) &obj->ip4_route.network
+	           : (gconstpointer) &obj->ip6_route.network);
 
-	if (src)
-		NLA_PUT (msg, RTA_SRC, addr_len, src);
+	if (!is_v4) {
+		if (!IN6_IS_ADDR_UNSPECIFIED (&NMP_OBJECT_CAST_IP6_ROUTE (obj)->src))
+			NLA_PUT (msg, RTA_SRC, addr_len, &obj->ip6_route.src);
+	}
 
-	NLA_PUT_U32 (msg, RTA_PRIORITY, metric);
+	NLA_PUT_U32 (msg, RTA_PRIORITY, obj->ip_route.metric);
 
-	if (pref_src)
-		NLA_PUT (msg, RTA_PREFSRC, addr_len, pref_src);
+	if (table > 0xFF)
+		NLA_PUT_U32 (msg, RTA_TABLE, table);
+
+	if (is_v4) {
+		if (NMP_OBJECT_CAST_IP4_ROUTE (obj)->pref_src)
+			NLA_PUT (msg, RTA_PREFSRC, addr_len, &obj->ip4_route.pref_src);
+	} else {
+		if (!IN6_IS_ADDR_UNSPECIFIED (&NMP_OBJECT_CAST_IP6_ROUTE (obj)->pref_src))
+			NLA_PUT (msg, RTA_PREFSRC, addr_len, &obj->ip6_route.pref_src);
+	}
 
-	if (mss || window || cwnd || initcwnd || initrwnd || mtu || lock) {
+	if (   obj->ip_route.mss
+	    || obj->ip_route.window
+	    || obj->ip_route.cwnd
+	    || obj->ip_route.initcwnd
+	    || obj->ip_route.initrwnd
+	    || obj->ip_route.mtu
+	    || lock) {
 		struct nlattr *metrics;
 
 		metrics = nla_nest_start (msg, RTA_METRICS);
 		if (!metrics)
 			goto nla_put_failure;
 
-		if (mss)
-			NLA_PUT_U32 (msg, RTAX_ADVMSS, mss);
-		if (window)
-			NLA_PUT_U32 (msg, RTAX_WINDOW, window);
-		if (cwnd)
-			NLA_PUT_U32 (msg, RTAX_CWND, cwnd);
-		if (initcwnd)
-			NLA_PUT_U32 (msg, RTAX_INITCWND, initcwnd);
-		if (initrwnd)
-			NLA_PUT_U32 (msg, RTAX_INITRWND, initrwnd);
-		if (mtu)
-			NLA_PUT_U32 (msg, RTAX_MTU, mtu);
+		if (obj->ip_route.mss)
+			NLA_PUT_U32 (msg, RTAX_ADVMSS, obj->ip_route.mss);
+		if (obj->ip_route.window)
+			NLA_PUT_U32 (msg, RTAX_WINDOW, obj->ip_route.window);
+		if (obj->ip_route.cwnd)
+			NLA_PUT_U32 (msg, RTAX_CWND, obj->ip_route.cwnd);
+		if (obj->ip_route.initcwnd)
+			NLA_PUT_U32 (msg, RTAX_INITCWND, obj->ip_route.initcwnd);
+		if (obj->ip_route.initrwnd)
+			NLA_PUT_U32 (msg, RTAX_INITRWND, obj->ip_route.initrwnd);
+		if (obj->ip_route.mtu)
+			NLA_PUT_U32 (msg, RTAX_MTU, obj->ip_route.mtu);
 		if (lock)
 			NLA_PUT_U32 (msg, RTAX_LOCK, lock);
 
@@ -2482,10 +2778,17 @@ _nl_msg_new_route (int nlmsg_type,
 	}
 
 	/* We currently don't have need for multi-hop routes... */
-	if (   gateway
-	    && memcmp (gateway, &nm_ip_addr_zero, addr_len) != 0)
-		NLA_PUT (msg, RTA_GATEWAY, addr_len, gateway);
-	NLA_PUT_U32 (msg, RTA_OIF, ifindex);
+	if (is_v4) {
+		NLA_PUT (msg, RTA_GATEWAY, addr_len, &obj->ip4_route.gateway);
+	} else {
+		if (!IN6_IS_ADDR_UNSPECIFIED (&obj->ip6_route.gateway))
+			NLA_PUT (msg, RTA_GATEWAY, addr_len, &obj->ip6_route.gateway);
+	}
+	NLA_PUT_U32 (msg, RTA_OIF, obj->ip_route.ifindex);
+
+	if (   !is_v4
+	    && obj->ip6_route.rt_pref != NM_ICMPV6_ROUTER_PREF_MEDIUM)
+		NLA_PUT_U8 (msg, RTA_PREF, obj->ip6_route.rt_pref);
 
 	return msg;
 
@@ -2494,56 +2797,27 @@ nla_put_failure:
 	g_return_val_if_reached (NULL);
 }
 
-/*****************************************************************************/
-
-static int _support_kernel_extended_ifa_flags = -1;
-
-#define _support_kernel_extended_ifa_flags_still_undecided() (G_UNLIKELY (_support_kernel_extended_ifa_flags == -1))
-
-static void
-_support_kernel_extended_ifa_flags_detect (struct nl_msg *msg)
-{
-	struct nlmsghdr *msg_hdr;
-
-	if (!_support_kernel_extended_ifa_flags_still_undecided ())
-		return;
-
-	msg_hdr = nlmsg_hdr (msg);
-	if (msg_hdr->nlmsg_type != RTM_NEWADDR)
-		return;
-
-	/* the extended address flags are only set for AF_INET6 */
-	if (((struct ifaddrmsg *) nlmsg_data (msg_hdr))->ifa_family != AF_INET6)
-		return;
-
-	/* see if the nl_msg contains the IFA_FLAGS attribute. If it does,
-	 * we assume, that the kernel supports extended flags, IFA_F_MANAGETEMPADDR
-	 * and IFA_F_NOPREFIXROUTE (they were added together).
-	 **/
-	_support_kernel_extended_ifa_flags = !!nlmsg_find_attr (msg_hdr, sizeof (struct ifaddrmsg), IFA_FLAGS);
-	_LOG2D ("kernel-support: extended-ifa-flags: %s", _support_kernel_extended_ifa_flags ? "detected" : "not detected");
-}
-
-static gboolean
-_support_kernel_extended_ifa_flags_get (void)
-{
-	if (_support_kernel_extended_ifa_flags_still_undecided ()) {
-		_LOG2D ("kernel-support: extended-ifa-flags: %s", "unable to detect kernel support for handling IPv6 temporary addresses. Assume support");
-		_support_kernel_extended_ifa_flags = 1;
-	}
-	return _support_kernel_extended_ifa_flags;
-}
-
 /******************************************************************
  * NMPlatform types and functions
  ******************************************************************/
 
+typedef enum {
+	DELAYED_ACTION_RESPONSE_TYPE_VOID                       = 0,
+	DELAYED_ACTION_RESPONSE_TYPE_REFRESH_ALL_IN_PROGRESS    = 1,
+	DELAYED_ACTION_RESPONSE_TYPE_ROUTE_GET                  = 2,
+} DelayedActionWaitForNlResponseType;
+
 typedef struct {
 	guint32 seq_number;
 	WaitForNlResponseResult seq_result;
+	DelayedActionWaitForNlResponseType response_type;
 	gint64 timeout_abs_ns;
 	WaitForNlResponseResult *out_seq_result;
-	gint *out_refresh_all_in_progess;
+	union {
+		gint *out_refresh_all_in_progess;
+		NMPObject **out_route_get;
+		gpointer out_data;
+	} response;
 } DelayedActionWaitForNlResponseData;
 
 typedef struct {
@@ -2553,11 +2827,12 @@ typedef struct {
 	guint32 nlh_seq_last_handled;
 #endif
 	guint32 nlh_seq_last_seen;
-	NMPCache *cache;
 	GIOChannel *event_channel;
 	guint event_id;
 
-	gboolean sysctl_get_warned;
+	bool pruning[_DELAYED_ACTION_IDX_REFRESH_ALL_NUM];
+
+	bool sysctl_get_warned;
 	GHashTable *sysctl_get_prev_values;
 
 	NMUdevClient *udev_client;
@@ -2578,8 +2853,6 @@ typedef struct {
 		gint is_handling;
 	} delayed_action;
 
-	GHashTable *prune_candidates;
-
 	GHashTable *wifi_data;
 } NMLinuxPlatformPrivate;
 
@@ -2599,8 +2872,15 @@ G_DEFINE_TYPE (NMLinuxPlatform, nm_linux_platform, NM_TYPE_PLATFORM)
 NMPlatform *
 nm_linux_platform_new (gboolean log_with_ptr, gboolean netns_support)
 {
+	gboolean use_udev = FALSE;
+
+	if (   nmp_netns_is_initial ()
+	    && access ("/sys", W_OK) == 0)
+		use_udev = TRUE;
+
 	return g_object_new (NM_TYPE_LINUX_PLATFORM,
 	                     NM_PLATFORM_LOG_WITH_PTR, log_with_ptr,
+	                     NM_PLATFORM_USE_UDEV, use_udev,
 	                     NM_PLATFORM_NETNS_SUPPORT, netns_support,
 	                     NULL);
 }
@@ -2611,13 +2891,6 @@ nm_linux_platform_setup (void)
 	nm_platform_setup (nm_linux_platform_new (FALSE, FALSE));
 }
 
-static void
-ASSERT_NETNS_CURRENT (NMPlatform *platform)
-{
-	nm_assert (NM_IS_LINUX_PLATFORM (platform));
-	nm_assert (NM_IN_SET (nm_platform_netns_get (platform), NULL, nmp_netns_get_current ()));
-}
-
 /*****************************************************************************/
 
 #define ASSERT_SYSCTL_ARGS(pathid, dirfd, path) \
@@ -2676,7 +2949,7 @@ sysctl_set (NMPlatform *platform, const char *pathid, int dirfd, const char *pat
 	nm_auto_pop_netns NMPNetns *netns = NULL;
 	int fd, tries;
 	gssize nwrote;
-	gsize len;
+	gssize len;
 	char *actual;
 	gs_free char *actual_free = NULL;
 	int errsv;
@@ -2731,6 +3004,7 @@ sysctl_set (NMPlatform *platform, const char *pathid, int dirfd, const char *pat
 	 * about to write.
 	 */
 	len = strlen (value) + 1;
+	nm_assert (len > 0);
 	if (len > 512)
 		actual = actual_free = g_malloc (len + 1);
 	else
@@ -2752,16 +3026,27 @@ sysctl_set (NMPlatform *platform, const char *pathid, int dirfd, const char *pat
 			break;
 		}
 	}
-	if (nwrote == -1 && errsv != EEXIST) {
-		_LOGE ("sysctl: failed to set '%s' to '%s': (%d) %s",
-		       path, value, errsv, strerror (errsv));
+	if (nwrote == -1) {
+		NMLogLevel level = LOGL_ERR;
+
+		if (errsv == EEXIST) {
+			level = LOGL_DEBUG;
+		} else if (   errsv == EINVAL
+		           && nm_utils_sysctl_ip_conf_is_path (AF_INET6, path, NULL, "mtu")) {
+			/* setting the MTU can fail under regular conditions. Suppress
+			 * logging a warning. */
+			level = LOGL_DEBUG;
+		}
+
+		_NMLOG (level, "sysctl: failed to set '%s' to '%s': (%d) %s",
+		        path, value, errsv, strerror (errsv));
 	} else if (nwrote < len - 1) {
 		_LOGE ("sysctl: failed to set '%s' to '%s' after three attempts",
 		       path, value);
 	}
 
 	if (nwrote < len - 1) {
-		if (close (fd) != 0) {
+		if (nm_close (fd) != 0) {
 			if (errsv != 0)
 				errno = errsv;
 		} else if (errsv != 0)
@@ -2770,7 +3055,7 @@ sysctl_set (NMPlatform *platform, const char *pathid, int dirfd, const char *pat
 			errno = EIO;
 		return FALSE;
 	}
-	if (close (fd) != 0) {
+	if (nm_close (fd) != 0) {
 		/* errno is already properly set. */
 		return FALSE;
 	}
@@ -2804,7 +3089,7 @@ _log_dbg_sysctl_get_impl (NMPlatform *platform, const char *pathid, const char *
 	if (!priv->sysctl_get_prev_values) {
 		_nm_logging_clear_platform_logging_cache = _nm_logging_clear_platform_logging_cache_impl;
 		sysctl_clear_cache_list = g_slist_prepend (sysctl_clear_cache_list, platform);
-		priv->sysctl_get_prev_values = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+		priv->sysctl_get_prev_values = g_hash_table_new_full (nm_str_hash, g_str_equal, g_free, g_free);
 	} else
 		prev_value = g_hash_table_lookup (priv->sysctl_get_prev_values, pathid);
 
@@ -2872,20 +3157,30 @@ sysctl_get (NMPlatform *platform, const char *pathid, int dirfd, const char *pat
 
 /*****************************************************************************/
 
-static gboolean
-check_support_kernel_extended_ifa_flags (NMPlatform *platform)
+static NMPlatformKernelSupportFlags
+check_kernel_support (NMPlatform *platform,
+                      NMPlatformKernelSupportFlags request_flags)
 {
-	g_return_val_if_fail (NM_IS_LINUX_PLATFORM (platform), FALSE);
+	NMPlatformKernelSupportFlags response = 0;
 
-	return _support_kernel_extended_ifa_flags_get ();
-}
+	nm_assert (NM_IS_LINUX_PLATFORM (platform));
 
-static gboolean
-check_support_user_ipv6ll (NMPlatform *platform)
-{
-	g_return_val_if_fail (NM_IS_LINUX_PLATFORM (platform), FALSE);
+	if (NM_FLAGS_HAS (request_flags, NM_PLATFORM_KERNEL_SUPPORT_EXTENDED_IFA_FLAGS)) {
+		if (_support_kernel_extended_ifa_flags_get ())
+			response |= NM_PLATFORM_KERNEL_SUPPORT_EXTENDED_IFA_FLAGS;
+	}
 
-	return _support_user_ipv6ll_get ();
+	if (NM_FLAGS_HAS (request_flags, NM_PLATFORM_KERNEL_SUPPORT_USER_IPV6LL)) {
+		if (_support_user_ipv6ll_get ())
+			response |= NM_PLATFORM_KERNEL_SUPPORT_USER_IPV6LL;
+	}
+
+	if (NM_FLAGS_HAS (request_flags, NM_PLATFORM_KERNEL_SUPPORT_RTA_PREF)) {
+		if (_support_rta_pref_get ())
+			response |= NM_PLATFORM_KERNEL_SUPPORT_RTA_PREF;
+	}
+
+	return response;
 }
 
 static void
@@ -2896,86 +3191,6 @@ process_events (NMPlatform *platform)
 
 /*****************************************************************************/
 
-#define cache_lookup_all_objects(type, platform, obj_type, visible_only) \
-	({ \
-		NMPCacheId _cache_id; \
-		\
-		((const type *const*) nmp_cache_lookup_multi (NM_LINUX_PLATFORM_GET_PRIVATE ((platform))->cache, \
-		                                              nmp_cache_id_init_object_type (&_cache_id, (obj_type), (visible_only)), \
-		                                              NULL)); \
-	})
-
-/*****************************************************************************/
-
-static void
-do_emit_signal (NMPlatform *platform, const NMPObject *obj, NMPCacheOpsType cache_op, gboolean was_visible)
-{
-	gboolean is_visible;
-	NMPObject obj_clone;
-	const NMPClass *klass;
-
-	nm_assert (NM_IN_SET ((NMPlatformSignalChangeType) cache_op, (NMPlatformSignalChangeType) NMP_CACHE_OPS_UNCHANGED, NM_PLATFORM_SIGNAL_ADDED, NM_PLATFORM_SIGNAL_CHANGED, NM_PLATFORM_SIGNAL_REMOVED));
-
-	nm_assert (obj || cache_op == NMP_CACHE_OPS_UNCHANGED);
-	nm_assert (!obj || cache_op == NMP_CACHE_OPS_REMOVED || obj == nmp_cache_lookup_obj (NM_LINUX_PLATFORM_GET_PRIVATE (platform)->cache, obj));
-	nm_assert (!obj || cache_op != NMP_CACHE_OPS_REMOVED || obj != nmp_cache_lookup_obj (NM_LINUX_PLATFORM_GET_PRIVATE (platform)->cache, obj));
-
-	ASSERT_NETNS_CURRENT (platform);
-
-	switch (cache_op) {
-	case NMP_CACHE_OPS_ADDED:
-		if (!nmp_object_is_visible (obj))
-			return;
-		break;
-	case NMP_CACHE_OPS_UPDATED:
-		is_visible = nmp_object_is_visible (obj);
-		if (!was_visible && is_visible)
-			cache_op = NMP_CACHE_OPS_ADDED;
-		else if (was_visible && !is_visible) {
-			/* This is a bit ugly. The object was visible and changed in a way that it became invisible.
-			 * We raise a removed signal, but contrary to a real 'remove', @obj is already changed to be
-			 * different from what it was when the user saw it the last time.
-			 *
-			 * The more correct solution would be to have cache_pre_hook() create a clone of the original
-			 * value before it was changed to become invisible.
-			 *
-			 * But, don't bother. Probably nobody depends on the original values and only cares about the
-			 * id properties (which are still correct).
-			 */
-			cache_op = NMP_CACHE_OPS_REMOVED;
-		} else if (!is_visible)
-			return;
-		break;
-	case NMP_CACHE_OPS_REMOVED:
-		if (!was_visible)
-			return;
-		break;
-	default:
-		g_assert (cache_op == NMP_CACHE_OPS_UNCHANGED);
-		return;
-	}
-
-	klass = NMP_OBJECT_GET_CLASS (obj);
-
-	_LOGt ("emit signal %s %s: %s",
-	       klass->signal_type,
-	       nm_platform_signal_change_type_to_string ((NMPlatformSignalChangeType) cache_op),
-	       nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
-
-	/* don't expose @obj directly, but clone the public fields. A signal handler might
-	 * call back into NMPlatform which could invalidate (or modify) @obj. */
-	memcpy (&obj_clone.object, &obj->object, klass->sizeof_public);
-	g_signal_emit (platform,
-	               _nm_platform_signal_id_get (klass->signal_type_id),
-	               0,
-	               (int) klass->obj_type,
-	               obj_clone.object.ifindex,
-	               &obj_clone.object,
-	               (int) cache_op);
-}
-
-/*****************************************************************************/
-
 _NM_UTILS_LOOKUP_DEFINE (static, delayed_action_refresh_from_object_type, NMPObjectType, DelayedActionType,
 	NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT (DELAYED_ACTION_TYPE_NONE),
 	NM_UTILS_LOOKUP_ITEM (NMP_OBJECT_TYPE_LINK,        DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS),
@@ -3043,11 +3258,12 @@ delayed_action_to_string_full (DelayedActionType action_type, gpointer user_data
 			gint64 timeout = data->timeout_abs_ns - nm_utils_get_monotonic_timestamp_ns ();
 			char b[255];
 
-			nm_utils_strbuf_append (&buf, &buf_size, " (seq %u, timeout in %s%"G_GINT64_FORMAT".%09"G_GINT64_FORMAT"%s%s)",
+			nm_utils_strbuf_append (&buf, &buf_size, " (seq %u, timeout in %s%"G_GINT64_FORMAT".%09"G_GINT64_FORMAT", response-type %d%s%s)",
 			                        data->seq_number,
 			                        timeout < 0 ? "-" : "",
 			                        (timeout < 0 ? -timeout : timeout) / NM_UTILS_NS_PER_SECOND,
 			                        (timeout < 0 ? -timeout : timeout) % NM_UTILS_NS_PER_SECOND,
+			                        (int) data->response_type,
 			                        data->seq_result ? ", " : "",
 			                        data->seq_result ? wait_for_nl_response_to_string (data->seq_result, b, sizeof (b)) : "");
 		} else
@@ -3109,9 +3325,22 @@ delayed_action_wait_for_nl_response_complete (NMPlatform *platform,
 		priv->delayed_action.flags &= ~DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE;
 	if (data->out_seq_result)
 		*data->out_seq_result = seq_result;
-	if (data->out_refresh_all_in_progess) {
-		nm_assert (*data->out_refresh_all_in_progess > 0);
-		*data->out_refresh_all_in_progess -= 1;
+	switch (data->response_type) {
+	case DELAYED_ACTION_RESPONSE_TYPE_VOID:
+		break;
+	case DELAYED_ACTION_RESPONSE_TYPE_REFRESH_ALL_IN_PROGRESS:
+		if (data->response.out_refresh_all_in_progess) {
+			nm_assert (*data->response.out_refresh_all_in_progess > 0);
+			*data->response.out_refresh_all_in_progess -= 1;
+			data->response.out_refresh_all_in_progess = NULL;
+		}
+		break;
+	case DELAYED_ACTION_RESPONSE_TYPE_ROUTE_GET:
+		if (data->response.out_route_get) {
+			nm_assert (!*data->response.out_route_get);
+			data->response.out_route_get = NULL;
+		}
+		break;
 	}
 
 	g_array_remove_index_fast (priv->delayed_action.list_wait_for_nl_response, idx);
@@ -3146,13 +3375,15 @@ delayed_action_wait_for_nl_response_complete_all (NMPlatform *platform,
 static void
 delayed_action_handle_MASTER_CONNECTED (NMPlatform *platform, int master_ifindex)
 {
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
-	nm_auto_nmpobj NMPObject *obj_cache = NULL;
-	gboolean was_visible;
+	nm_auto_nmpobj const NMPObject *obj_old = NULL;
+	nm_auto_nmpobj const NMPObject *obj_new = NULL;
 	NMPCacheOpsType cache_op;
 
-	cache_op = nmp_cache_update_link_master_connected (priv->cache, master_ifindex, &obj_cache, &was_visible, cache_pre_hook, platform);
-	do_emit_signal (platform, obj_cache, cache_op, was_visible);
+	cache_op = nmp_cache_update_link_master_connected (nm_platform_get_cache (platform), master_ifindex, &obj_old, &obj_new);
+	if (cache_op == NMP_CACHE_OPS_UNCHANGED)
+		return;
+	cache_on_change (platform, cache_op, obj_old, obj_new);
+	nm_platform_cache_update_emit_signal (platform, cache_op, obj_old, obj_new);
 }
 
 static void
@@ -3273,7 +3504,7 @@ delayed_action_handle_all (NMPlatform *platform, gboolean read_netlink)
 		any = TRUE;
 	priv->delayed_action.is_handling--;
 
-	cache_prune_candidates_prune (platform);
+	cache_prune_all (platform);
 
 	return any;
 }
@@ -3319,13 +3550,15 @@ static void
 delayed_action_schedule_WAIT_FOR_NL_RESPONSE (NMPlatform *platform,
                                               guint32 seq_number,
                                               WaitForNlResponseResult *out_seq_result,
-                                              gint *out_refresh_all_in_progess)
+                                              DelayedActionWaitForNlResponseType response_type,
+                                              gpointer response_out_data)
 {
 	DelayedActionWaitForNlResponseData data = {
 		.seq_number = seq_number,
 		.timeout_abs_ns = nm_utils_get_monotonic_timestamp_ns () + (200 * (NM_UTILS_NS_PER_SECOND / 1000)),
 		.out_seq_result = out_seq_result,
-		.out_refresh_all_in_progess = out_refresh_all_in_progess,
+		.response_type = response_type,
+		.response.out_data = response_out_data,
 	};
 
 	delayed_action_schedule (platform,
@@ -3336,146 +3569,111 @@ delayed_action_schedule_WAIT_FOR_NL_RESPONSE (NMPlatform *platform,
 /*****************************************************************************/
 
 static void
-cache_prune_candidates_record_all (NMPlatform *platform, NMPObjectType obj_type)
-{
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
-	NMPCacheId cache_id;
-
-	priv->prune_candidates = nmp_cache_lookup_all_to_hash (priv->cache,
-	                                                       nmp_cache_id_init_object_type (&cache_id, obj_type, FALSE),
-	                                                       priv->prune_candidates);
-	_LOGt ("cache-prune: record %s (now %u candidates)", nmp_class_from_type (obj_type)->obj_type_name,
-	       priv->prune_candidates ? g_hash_table_size (priv->prune_candidates) : 0);
-}
-
-static void
-cache_prune_candidates_record_one (NMPlatform *platform, NMPObject *obj)
-{
-	NMLinuxPlatformPrivate *priv;
-
-	if (!obj)
-		return;
-
-	priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
-
-	if (!priv->prune_candidates)
-		priv->prune_candidates = g_hash_table_new_full (NULL, NULL, (GDestroyNotify) nmp_object_unref, NULL);
-
-	if (_LOGt_ENABLED () && !g_hash_table_contains (priv->prune_candidates, obj))
-		_LOGt ("cache-prune: record-one: %s", nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_ALL, NULL, 0));
-	g_hash_table_add (priv->prune_candidates, nmp_object_ref (obj));
-}
-
-static void
-cache_prune_candidates_drop (NMPlatform *platform, const NMPObject *obj)
+cache_prune_one_type (NMPlatform *platform, NMPObjectType obj_type)
 {
-	NMLinuxPlatformPrivate *priv;
-
-	if (!obj)
-		return;
-
-	priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
-	if (priv->prune_candidates) {
-		if (_LOGt_ENABLED () && g_hash_table_contains (priv->prune_candidates, obj))
-			_LOGt ("cache-prune: drop-one: %s", nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_ALL, NULL, 0));
-		g_hash_table_remove (priv->prune_candidates, obj);
+	NMDedupMultiIter iter;
+	const NMPObject *obj;
+	NMPCacheOpsType cache_op;
+	NMPLookup lookup;
+	NMPCache *cache = nm_platform_get_cache (platform);
+
+	nmp_lookup_init_obj_type (&lookup,
+	                          obj_type);
+	nm_dedup_multi_iter_init (&iter,
+	                          nmp_cache_lookup (cache,
+	                                            &lookup));
+	while (nm_dedup_multi_iter_next (&iter)) {
+		if (iter.current->dirty) {
+			nm_auto_nmpobj const NMPObject *obj_old = NULL;
+
+			obj = iter.current->obj;
+			_LOGt ("cache-prune: prune %s", nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_ALL, NULL, 0));
+			cache_op = nmp_cache_remove (cache, obj, TRUE, TRUE, &obj_old);
+			nm_assert (cache_op == NMP_CACHE_OPS_REMOVED);
+			cache_on_change (platform, cache_op, obj_old, NULL);
+			nm_platform_cache_update_emit_signal (platform, cache_op, obj_old, NULL);
+		}
 	}
 }
 
 static void
-cache_prune_candidates_prune (NMPlatform *platform)
+cache_prune_all (NMPlatform *platform)
 {
 	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
-	GHashTable *prune_candidates;
-	GHashTableIter iter;
-	const NMPObject *obj;
-	gboolean was_visible;
-	NMPCacheOpsType cache_op;
-
-	if (!priv->prune_candidates)
-		return;
-
-	prune_candidates = priv->prune_candidates;
-	priv->prune_candidates = NULL;
+	DelayedActionType iflags, action_type;
 
-	g_hash_table_iter_init (&iter, prune_candidates);
-	while (g_hash_table_iter_next (&iter, (gpointer *)&obj, NULL)) {
-		nm_auto_nmpobj NMPObject *obj_cache = NULL;
+	action_type = DELAYED_ACTION_TYPE_REFRESH_ALL;
+	FOR_EACH_DELAYED_ACTION (iflags, action_type) {
+		bool *p = &priv->pruning[delayed_action_refresh_all_to_idx (iflags)];
 
-		_LOGt ("cache-prune: prune %s", nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_ALL, NULL, 0));
-		cache_op = nmp_cache_remove (priv->cache, obj, TRUE, &obj_cache, &was_visible, cache_pre_hook, platform);
-		do_emit_signal (platform, obj_cache, cache_op, was_visible);
+		if (*p) {
+			*p = FALSE;
+			cache_prune_one_type (platform, delayed_action_refresh_to_object_type (iflags));
+		}
 	}
-
-	g_hash_table_unref (prune_candidates);
 }
 
 static void
-cache_pre_hook (NMPCache *cache, const NMPObject *old, const NMPObject *new, NMPCacheOpsType ops_type, gpointer user_data)
+cache_on_change (NMPlatform *platform,
+                 NMPCacheOpsType cache_op,
+                 const NMPObject *obj_old,
+                 const NMPObject *obj_new)
 {
-	NMPlatform *platform = user_data;
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
 	const NMPClass *klass;
 	char str_buf[sizeof (_nm_utils_to_string_buffer)];
 	char str_buf2[sizeof (_nm_utils_to_string_buffer)];
+	NMPCache *cache = nm_platform_get_cache (platform);
 
-	nm_assert (old || new);
-	nm_assert (NM_IN_SET (ops_type, NMP_CACHE_OPS_ADDED, NMP_CACHE_OPS_REMOVED, NMP_CACHE_OPS_UPDATED));
-	nm_assert (ops_type != NMP_CACHE_OPS_ADDED   || (old == NULL && NMP_OBJECT_IS_VALID (new) && nmp_object_is_alive (new)));
-	nm_assert (ops_type != NMP_CACHE_OPS_REMOVED || (new == NULL && NMP_OBJECT_IS_VALID (old) && nmp_object_is_alive (old)));
-	nm_assert (ops_type != NMP_CACHE_OPS_UPDATED || (NMP_OBJECT_IS_VALID (old) && nmp_object_is_alive (old) && NMP_OBJECT_IS_VALID (new) && nmp_object_is_alive (new)));
-	nm_assert (new == NULL || old == NULL || nmp_object_id_equal (new, old));
-	nm_assert (!old || !new || NMP_OBJECT_GET_CLASS (old) == NMP_OBJECT_GET_CLASS (new));
+	ASSERT_nmp_cache_ops (cache, cache_op, obj_old, obj_new);
+	nm_assert (cache_op != NMP_CACHE_OPS_UNCHANGED);
 
-	klass = old ? NMP_OBJECT_GET_CLASS (old) : NMP_OBJECT_GET_CLASS (new);
-
-	nm_assert (klass == (new ? NMP_OBJECT_GET_CLASS (new) : NMP_OBJECT_GET_CLASS (old)));
+	klass = obj_old ? NMP_OBJECT_GET_CLASS (obj_old) : NMP_OBJECT_GET_CLASS (obj_new);
 
 	_LOGt ("update-cache-%s: %s: %s%s%s",
 	       klass->obj_type_name,
-	       (ops_type == NMP_CACHE_OPS_UPDATED
+	       (cache_op == NMP_CACHE_OPS_UPDATED
 	           ? "UPDATE"
-	           : (ops_type == NMP_CACHE_OPS_REMOVED
+	           : (cache_op == NMP_CACHE_OPS_REMOVED
 	                 ? "REMOVE"
-	                 : (ops_type == NMP_CACHE_OPS_ADDED) ? "ADD" : "???")),
-	       (ops_type != NMP_CACHE_OPS_ADDED
-	           ? nmp_object_to_string (old, NMP_OBJECT_TO_STRING_ALL, str_buf2, sizeof (str_buf2))
-	           : nmp_object_to_string (new, NMP_OBJECT_TO_STRING_ALL, str_buf2, sizeof (str_buf2))),
-	       (ops_type == NMP_CACHE_OPS_UPDATED) ? " -> " : "",
-	       (ops_type == NMP_CACHE_OPS_UPDATED
-	           ? nmp_object_to_string (new, NMP_OBJECT_TO_STRING_ALL, str_buf, sizeof (str_buf))
+	                 : (cache_op == NMP_CACHE_OPS_ADDED) ? "ADD" : "???")),
+	       (cache_op != NMP_CACHE_OPS_ADDED
+	           ? nmp_object_to_string (obj_old, NMP_OBJECT_TO_STRING_ALL, str_buf2, sizeof (str_buf2))
+	           : nmp_object_to_string (obj_new, NMP_OBJECT_TO_STRING_ALL, str_buf2, sizeof (str_buf2))),
+	       (cache_op == NMP_CACHE_OPS_UPDATED) ? " -> " : "",
+	       (cache_op == NMP_CACHE_OPS_UPDATED
+	           ? nmp_object_to_string (obj_new, NMP_OBJECT_TO_STRING_ALL, str_buf, sizeof (str_buf))
 	           : ""));
 
 	switch (klass->obj_type) {
 	case NMP_OBJECT_TYPE_LINK:
 		{
 			/* check whether changing a slave link can cause a master link (bridge or bond) to go up/down */
-			if (   old
-			    && nmp_cache_link_connected_needs_toggle_by_ifindex (priv->cache, old->link.master, new, old))
-				delayed_action_schedule (platform, DELAYED_ACTION_TYPE_MASTER_CONNECTED, GINT_TO_POINTER (old->link.master));
-			if (   new
-			    && (!old || old->link.master != new->link.master)
-			    && nmp_cache_link_connected_needs_toggle_by_ifindex (priv->cache, new->link.master, new, old))
-				delayed_action_schedule (platform, DELAYED_ACTION_TYPE_MASTER_CONNECTED, GINT_TO_POINTER (new->link.master));
+			if (   obj_old
+			    && nmp_cache_link_connected_needs_toggle_by_ifindex (cache, obj_old->link.master, obj_new, obj_old))
+				delayed_action_schedule (platform, DELAYED_ACTION_TYPE_MASTER_CONNECTED, GINT_TO_POINTER (obj_old->link.master));
+			if (   obj_new
+			    && (!obj_old || obj_old->link.master != obj_new->link.master)
+			    && nmp_cache_link_connected_needs_toggle_by_ifindex (cache, obj_new->link.master, obj_new, obj_old))
+				delayed_action_schedule (platform, DELAYED_ACTION_TYPE_MASTER_CONNECTED, GINT_TO_POINTER (obj_new->link.master));
 		}
 		{
 			/* check whether we are about to change a master link that needs toggling connected state. */
-			if (   new /* <-- nonsensical, make coverity happy */
-			    && nmp_cache_link_connected_needs_toggle (cache, new, new, old))
-				delayed_action_schedule (platform, DELAYED_ACTION_TYPE_MASTER_CONNECTED, GINT_TO_POINTER (new->link.ifindex));
+			if (   obj_new /* <-- nonsensical, make coverity happy */
+			    && nmp_cache_link_connected_needs_toggle (cache, obj_new, obj_new, obj_old))
+				delayed_action_schedule (platform, DELAYED_ACTION_TYPE_MASTER_CONNECTED, GINT_TO_POINTER (obj_new->link.ifindex));
 		}
 		{
 			int ifindex = 0;
 
 			/* if we remove a link (from netlink), we must refresh the addresses and routes */
-			if (   ops_type == NMP_CACHE_OPS_REMOVED
-			    && old /* <-- nonsensical, make coverity happy */)
-				ifindex = old->link.ifindex;
-			else if (   ops_type == NMP_CACHE_OPS_UPDATED
-			         && old && new /* <-- nonsensical, make coverity happy */
-			         && !new->_link.netlink.is_in_netlink
-			         && new->_link.netlink.is_in_netlink != old->_link.netlink.is_in_netlink)
-				ifindex = new->link.ifindex;
+			if (   cache_op == NMP_CACHE_OPS_REMOVED
+			    && obj_old /* <-- nonsensical, make coverity happy */)
+				ifindex = obj_old->link.ifindex;
+			else if (   cache_op == NMP_CACHE_OPS_UPDATED
+			         && obj_old && obj_new /* <-- nonsensical, make coverity happy */
+			         && !obj_new->_link.netlink.is_in_netlink
+			         && obj_new->_link.netlink.is_in_netlink != obj_old->_link.netlink.is_in_netlink)
+				ifindex = obj_new->link.ifindex;
 
 			if (ifindex > 0) {
 				delayed_action_schedule (platform,
@@ -3494,40 +3692,40 @@ cache_pre_hook (NMPCache *cache, const NMPObject *old, const NMPObject *new, NMP
 			 * Currently, kernel misses to sent us a notification in this case
 			 * (https://bugzilla.redhat.com/show_bug.cgi?id=1262908). */
 
-			if (   ops_type == NMP_CACHE_OPS_REMOVED
-			    && old /* <-- nonsensical, make coverity happy */
-			    && old->_link.netlink.is_in_netlink)
-				ifindex = old->link.ifindex;
-			else if (   ops_type == NMP_CACHE_OPS_UPDATED
-			         && old && new /* <-- nonsensical, make coverity happy */
-			         && old->_link.netlink.is_in_netlink
-			         && !new->_link.netlink.is_in_netlink)
-				ifindex = new->link.ifindex;
+			if (   cache_op == NMP_CACHE_OPS_REMOVED
+			    && obj_old /* <-- nonsensical, make coverity happy */
+			    && obj_old->_link.netlink.is_in_netlink)
+				ifindex = obj_old->link.ifindex;
+			else if (   cache_op == NMP_CACHE_OPS_UPDATED
+			         && obj_old && obj_new /* <-- nonsensical, make coverity happy */
+			         && obj_old->_link.netlink.is_in_netlink
+			         && !obj_new->_link.netlink.is_in_netlink)
+				ifindex = obj_new->link.ifindex;
 
 			if (ifindex > 0) {
-				const NMPlatformLink *const *links;
-
-				links = cache_lookup_all_objects (NMPlatformLink, platform, NMP_OBJECT_TYPE_LINK, FALSE);
-				if (links) {
-					for (; *links; links++) {
-						const NMPlatformLink *l = (*links);
-
-						if (l->parent == ifindex)
-							delayed_action_schedule (platform, DELAYED_ACTION_TYPE_REFRESH_LINK, GINT_TO_POINTER (l->ifindex));
-					}
+				NMPLookup lookup;
+				NMDedupMultiIter iter;
+				const NMPlatformLink *l;
+
+				nmp_lookup_init_obj_type (&lookup, NMP_OBJECT_TYPE_LINK);
+				nmp_cache_iter_for_each_link (&iter,
+				                              nmp_cache_lookup (cache, &lookup),
+				                              &l) {
+					if (l->parent == ifindex)
+						delayed_action_schedule (platform, DELAYED_ACTION_TYPE_REFRESH_LINK, GINT_TO_POINTER (l->ifindex));
 				}
 			}
 		}
 		{
 			/* if a link goes down, we must refresh routes */
-			if (   ops_type == NMP_CACHE_OPS_UPDATED
-			    && old && new /* <-- nonsensical, make coverity happy */
-			    && old->_link.netlink.is_in_netlink
-			    && new->_link.netlink.is_in_netlink
-			    && (   (   NM_FLAGS_HAS (old->link.n_ifi_flags, IFF_UP)
-			            && !NM_FLAGS_HAS (new->link.n_ifi_flags, IFF_UP))
-			        || (   NM_FLAGS_HAS (old->link.n_ifi_flags, IFF_LOWER_UP)
-			            && !NM_FLAGS_HAS (new->link.n_ifi_flags, IFF_LOWER_UP)))) {
+			if (   cache_op == NMP_CACHE_OPS_UPDATED
+			    && obj_old && obj_new /* <-- nonsensical, make coverity happy */
+			    && obj_old->_link.netlink.is_in_netlink
+			    && obj_new->_link.netlink.is_in_netlink
+			    && (   (   NM_FLAGS_HAS (obj_old->link.n_ifi_flags, IFF_UP)
+			            && !NM_FLAGS_HAS (obj_new->link.n_ifi_flags, IFF_UP))
+			        || (   NM_FLAGS_HAS (obj_old->link.n_ifi_flags, IFF_LOWER_UP)
+			            && !NM_FLAGS_HAS (obj_new->link.n_ifi_flags, IFF_LOWER_UP)))) {
 				/* FIXME: I suspect that IFF_LOWER_UP must not be considered, and I
 				 * think kernel does send RTM_DELROUTE events for IPv6 routes, so
 				 * we might not need to refresh IPv6 routes. */
@@ -3537,17 +3735,17 @@ cache_pre_hook (NMPCache *cache, const NMPObject *old, const NMPObject *new, NMP
 				                         NULL);
 			}
 		}
-		if (   NM_IN_SET (ops_type, NMP_CACHE_OPS_ADDED, NMP_CACHE_OPS_UPDATED)
-		    && (new && new->_link.netlink.is_in_netlink)
-		    && (!old || !old->_link.netlink.is_in_netlink))
+		if (   NM_IN_SET (cache_op, NMP_CACHE_OPS_ADDED, NMP_CACHE_OPS_UPDATED)
+		    && (obj_new && obj_new->_link.netlink.is_in_netlink)
+		    && (!obj_old || !obj_old->_link.netlink.is_in_netlink))
 		{
-			if (!new->_link.netlink.lnk) {
+			if (!obj_new->_link.netlink.lnk) {
 				/* certain link-types also come with a IFLA_INFO_DATA/lnk_data. It may happen that
 				 * kernel didn't send this notification, thus when we first learn about a link
 				 * that lacks an lnk_data we re-request it again.
 				 *
 				 * For example https://bugzilla.redhat.com/show_bug.cgi?id=1284001 */
-				switch (new->link.type) {
+				switch (obj_new->link.type) {
 				case NM_LINK_TYPE_GRE:
 				case NM_LINK_TYPE_IP6TNL:
 				case NM_LINK_TYPE_INFINIBAND:
@@ -3558,23 +3756,23 @@ cache_pre_hook (NMPCache *cache, const NMPObject *old, const NMPObject *new, NMP
 				case NM_LINK_TYPE_VXLAN:
 					delayed_action_schedule (platform,
 					                         DELAYED_ACTION_TYPE_REFRESH_LINK,
-					                         GINT_TO_POINTER (new->link.ifindex));
+					                         GINT_TO_POINTER (obj_new->link.ifindex));
 					break;
 				default:
 					break;
 				}
 			}
-			if (   new->link.type == NM_LINK_TYPE_VETH
-			    && new->link.parent == 0) {
+			if (   obj_new->link.type == NM_LINK_TYPE_VETH
+			    && obj_new->link.parent == 0) {
 				/* the initial notification when adding a veth pair can lack the parent/IFLA_LINK
 				 * (https://bugzilla.redhat.com/show_bug.cgi?id=1285827).
 				 * Request it again. */
 				delayed_action_schedule (platform,
 				                         DELAYED_ACTION_TYPE_REFRESH_LINK,
-				                         GINT_TO_POINTER (new->link.ifindex));
+				                         GINT_TO_POINTER (obj_new->link.ifindex));
 			}
-			if (   new->link.type == NM_LINK_TYPE_ETHERNET
-			    && new->link.addr.len == 0) {
+			if (   obj_new->link.type == NM_LINK_TYPE_ETHERNET
+			    && obj_new->link.addr.len == 0) {
 				/* Due to a kernel bug, we sometimes receive spurious NEWLINK
 				 * messages after a wifi interface has disappeared. Since the
 				 * link is not present anymore we can't determine its type and
@@ -3584,7 +3782,7 @@ cache_pre_hook (NMPCache *cache, const NMPObject *old, const NMPObject *new, NMP
 				 */
 				delayed_action_schedule (platform,
 				                         DELAYED_ACTION_TYPE_REFRESH_LINK,
-				                         GINT_TO_POINTER (new->link.ifindex));
+				                         GINT_TO_POINTER (obj_new->link.ifindex));
 			}
 		}
 		{
@@ -3592,14 +3790,14 @@ cache_pre_hook (NMPCache *cache, const NMPObject *old, const NMPObject *new, NMP
 			int ifindex1 = 0, ifindex2 = 0;
 			gboolean changed_master, changed_connected;
 
-			changed_master =    (new && new->_link.netlink.is_in_netlink && new->link.master > 0 ? new->link.master : 0)
-			                 != (old && old->_link.netlink.is_in_netlink && old->link.master > 0 ? old->link.master : 0);
-			changed_connected =    (new && new->_link.netlink.is_in_netlink ? NM_FLAGS_HAS (new->link.n_ifi_flags, IFF_LOWER_UP) : 2)
-			                    != (old && old->_link.netlink.is_in_netlink ? NM_FLAGS_HAS (old->link.n_ifi_flags, IFF_LOWER_UP) : 2);
+			changed_master =    (obj_new && obj_new->_link.netlink.is_in_netlink && obj_new->link.master > 0 ? obj_new->link.master : 0)
+			                 != (obj_old && obj_old->_link.netlink.is_in_netlink && obj_old->link.master > 0 ? obj_old->link.master : 0);
+			changed_connected =    (obj_new && obj_new->_link.netlink.is_in_netlink ? NM_FLAGS_HAS (obj_new->link.n_ifi_flags, IFF_LOWER_UP) : 2)
+			                    != (obj_old && obj_old->_link.netlink.is_in_netlink ? NM_FLAGS_HAS (obj_old->link.n_ifi_flags, IFF_LOWER_UP) : 2);
 
 			if (changed_master || changed_connected) {
-				ifindex1 = (old && old->_link.netlink.is_in_netlink && old->link.master > 0) ? old->link.master : 0;
-				ifindex2 = (new && new->_link.netlink.is_in_netlink && new->link.master > 0) ? new->link.master : 0;
+				ifindex1 = (obj_old && obj_old->_link.netlink.is_in_netlink && obj_old->link.master > 0) ? obj_old->link.master : 0;
+				ifindex2 = (obj_new && obj_new->_link.netlink.is_in_netlink && obj_new->link.master > 0) ? obj_new->link.master : 0;
 
 				if (ifindex1 > 0)
 					delayed_action_schedule (platform, DELAYED_ACTION_TYPE_REFRESH_LINK, GINT_TO_POINTER (ifindex1));
@@ -3607,29 +3805,13 @@ cache_pre_hook (NMPCache *cache, const NMPObject *old, const NMPObject *new, NMP
 					delayed_action_schedule (platform, DELAYED_ACTION_TYPE_REFRESH_LINK, GINT_TO_POINTER (ifindex2));
 			}
 		}
-		{
-			if (   (       (ops_type == NMP_CACHE_OPS_REMOVED)
-			        || (   (ops_type == NMP_CACHE_OPS_UPDATED)
-			            && new
-			            && !new->_link.netlink.is_in_netlink))
-			    && old
-			    && old->_link.netlink.is_in_netlink
-			    && old->link.master) {
-				/* sometimes we receive a wrong RTM_DELLINK message when unslaving
-				 * a device. Refetch the link again to check whether the device
-				 * is really gone.
-				 *
-				 * https://bugzilla.redhat.com/show_bug.cgi?id=1285719#c2 */
-				delayed_action_schedule (platform, DELAYED_ACTION_TYPE_REFRESH_LINK, GINT_TO_POINTER (old->link.ifindex));
-			}
-		}
 		break;
 	case NMP_OBJECT_TYPE_IP4_ADDRESS:
 	case NMP_OBJECT_TYPE_IP6_ADDRESS:
 		{
 			/* Address deletion is sometimes accompanied by route deletion. We need to
 			 * check all routes belonging to the same interface. */
-			if (ops_type == NMP_CACHE_OPS_REMOVED) {
+			if (cache_op == NMP_CACHE_OPS_REMOVED) {
 				delayed_action_schedule (platform,
 				                         (klass->obj_type == NMP_OBJECT_TYPE_IP4_ADDRESS)
 				                             ? DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES
@@ -3643,69 +3825,113 @@ cache_pre_hook (NMPCache *cache, const NMPObject *old, const NMPObject *new, NMP
 	}
 }
 
-static void
-cache_post (NMPlatform *platform,
-            struct nlmsghdr *msghdr,
-            NMPCacheOpsType cache_op,
-            NMPObject *obj,
-            NMPObject *obj_cache)
+/*****************************************************************************/
+
+static guint32
+_nlh_seq_next_get (NMLinuxPlatformPrivate *priv)
+{
+	/* generate a new sequence number, but skip zero. */
+	return priv->nlh_seq_next++ ?: priv->nlh_seq_next++;
+}
+
+/**
+ * _nl_send_nlmsghdr:
+ * @platform:
+ * @nlhdr:
+ * @out_seq_result:
+ * @response_type:
+ * @response_out_data:
+ *
+ * Returns: 0 on success or a negative errno. Beware, it's an errno, not nlerror.
+ */
+static int
+_nl_send_nlmsghdr (NMPlatform *platform,
+                   struct nlmsghdr *nlhdr,
+                   WaitForNlResponseResult *out_seq_result,
+                   DelayedActionWaitForNlResponseType response_type,
+                   gpointer response_out_data)
 {
 	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
+	guint32 seq;
+	int nle;
 
-	nm_assert (NMP_OBJECT_IS_VALID (obj));
-	nm_assert (!obj_cache || nmp_object_id_equal (obj, obj_cache));
-
-	if (msghdr->nlmsg_type == RTM_NEWROUTE) {
-		DelayedActionType action_type;
-
-		action_type = NMP_OBJECT_GET_TYPE (obj) == NMP_OBJECT_TYPE_IP4_ROUTE
-		                  ? DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES
-		                  : DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES;
-		if (   !delayed_action_refresh_all_in_progress (platform, action_type)
-		    && nmp_cache_find_other_route_for_same_destination (priv->cache, obj)) {
-			/* via `iproute route change` the user can update an existing route which effectively
-			 * means that a new object (with a different ID) comes into existance, replacing the
-			 * old on. In other words, as the ID of the object changes, we really see a new
-			 * object with the old one deleted.
-			 * However, kernel decides not to send a RTM_DELROUTE event for that.
-			 *
-			 * To hack around that, check if the update leaves us with multiple routes for the
-			 * same network/plen,metric part. In that case, we cannot do better then requesting
-			 * all routes anew, which sucks.
-			 *
-			 * One mitigation to avoid a dump is only to request a new dump, if we are not in
-			 * the middle of an ongoing dump (delayed_action_refresh_all_in_progress). */
-			delayed_action_schedule (platform, action_type, NULL);
+	nm_assert (nlhdr);
+
+	seq = _nlh_seq_next_get (priv);
+	nlhdr->nlmsg_seq = seq;
+
+	{
+		struct sockaddr_nl nladdr = {
+			.nl_family = AF_NETLINK,
+		};
+		struct iovec iov = {
+			.iov_base = nlhdr,
+			.iov_len = nlhdr->nlmsg_len
+		};
+		struct msghdr msg = {
+			.msg_name = &nladdr,
+			.msg_namelen = sizeof(nladdr),
+			.msg_iov = &iov,
+			.msg_iovlen = 1,
+		};
+		int try_count;
+
+		if (!nlhdr->nlmsg_pid)
+			nlhdr->nlmsg_pid = nl_socket_get_local_port (priv->nlh);
+		nlhdr->nlmsg_flags |= (NLM_F_REQUEST | NLM_F_ACK);
+
+		try_count = 0;
+again:
+		nle = sendmsg (nl_socket_get_fd (priv->nlh), &msg, 0);
+		if (nle < 0) {
+			nle = errno;
+			if (nle == EINTR && try_count++ < 100)
+				goto again;
+			_LOGD ("netlink: nl-send-nlmsghdr: failed sending message: %s (%d)", g_strerror (nle), nle);
+			return -nle;
 		}
 	}
-}
 
-/*****************************************************************************/
+	delayed_action_schedule_WAIT_FOR_NL_RESPONSE (platform, seq, out_seq_result,
+	                                              response_type, response_out_data);
+	return 0;
+}
 
+/**
+ * _nl_send_nlmsg:
+ * @platform:
+ * @nlmsg:
+ * @out_seq_result:
+ * @response_type:
+ * @response_out_data:
+ *
+ * Returns: 0 on success, or a negative libnl3 error code (beware, it's not an errno).
+ */
 static int
-_nl_send_auto_with_seq (NMPlatform *platform,
-                        struct nl_msg *nlmsg,
-                        WaitForNlResponseResult *out_seq_result,
-                        gint *out_refresh_all_in_progess)
+_nl_send_nlmsg (NMPlatform *platform,
+                struct nl_msg *nlmsg,
+                WaitForNlResponseResult *out_seq_result,
+                DelayedActionWaitForNlResponseType response_type,
+                gpointer response_out_data)
 {
 	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
+	struct nlmsghdr *nlhdr;
 	guint32 seq;
 	int nle;
 
-	/* complete the message with a sequence number (ensuring it's not zero). */
-	seq = priv->nlh_seq_next++ ?: priv->nlh_seq_next++;
-
-	nlmsg_hdr (nlmsg)->nlmsg_seq = seq;
+	nlhdr = nlmsg_hdr (nlmsg);
+	seq = _nlh_seq_next_get (priv);
+	nlhdr->nlmsg_seq = seq;
 
 	nle = nl_send_auto (priv->nlh, nlmsg);
+	if (nle < 0) {
+		_LOGD ("netlink: nl-send-nlmsg: failed sending message: %s (%d)", nl_geterror (nle), nle);
+		return nle;
+	}
 
-	if (nle >= 0) {
-		nle = 0;
-		delayed_action_schedule_WAIT_FOR_NL_RESPONSE (platform, seq, out_seq_result, out_refresh_all_in_progess);
-	} else
-		_LOGD ("netlink: send: failed sending message: %s (%d)", nl_geterror (nle), nle);
-
-	return nle;
+	delayed_action_schedule_WAIT_FOR_NL_RESPONSE (platform, seq, out_seq_result,
+	                                              response_type, response_out_data);
+	return 0;
 }
 
 static void
@@ -3713,17 +3939,23 @@ do_request_link_no_delayed_actions (NMPlatform *platform, int ifindex, const cha
 {
 	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
 	nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
+	int nle;
 
 	if (name && !name[0])
 		name = NULL;
 
 	g_return_if_fail (ifindex > 0 || name);
 
-	_LOGD ("do-request-link: %d %s", ifindex, name ? name : "");
+	_LOGD ("do-request-link: %d %s", ifindex, name ?: "");
 
 	if (ifindex > 0) {
-		cache_prune_candidates_record_one (platform,
-		                                   (NMPObject *) nmp_cache_lookup_link (priv->cache, ifindex));
+		const NMDedupMultiEntry *entry;
+
+		entry = nmp_cache_lookup_entry_link (nm_platform_get_cache (platform), ifindex);
+		if (entry) {
+			priv->pruning[DELAYED_ACTION_IDX_REFRESH_ALL_LINKS] = TRUE;
+			nm_dedup_multi_entry_set_dirty (entry, TRUE);
+		}
 	}
 
 	event_handler_read_netlink (platform, FALSE);
@@ -3734,8 +3966,15 @@ do_request_link_no_delayed_actions (NMPlatform *platform, int ifindex, const cha
 	                          name,
 	                          0,
 	                          0);
-	if (nlmsg)
-		_nl_send_auto_with_seq (platform, nlmsg, NULL, NULL);
+	if (nlmsg) {
+		nle = _nl_send_nlmsg (platform, nlmsg, NULL, DELAYED_ACTION_RESPONSE_TYPE_VOID, NULL);
+		if (nle < 0) {
+			_LOGE ("do-request-link: %d %s: failed sending netlink request \"%s\" (%d)",
+			       ifindex, name ?: "",
+			       nl_geterror (nle), -nle);
+			return;
+		}
+	}
 }
 
 static void
@@ -3755,7 +3994,9 @@ do_request_all_no_delayed_actions (NMPlatform *platform, DelayedActionType actio
 	action_type &= DELAYED_ACTION_TYPE_REFRESH_ALL;
 
 	FOR_EACH_DELAYED_ACTION (iflags, action_type) {
-		cache_prune_candidates_record_all (platform, delayed_action_refresh_to_object_type (iflags));
+		priv->pruning[delayed_action_refresh_all_to_idx (iflags)] = TRUE;
+		nmp_cache_dirty_set_all (nm_platform_get_cache (platform),
+		                         delayed_action_refresh_to_object_type (iflags));
 	}
 
 	FOR_EACH_DELAYED_ACTION (iflags, action_type) {
@@ -3795,7 +4036,7 @@ do_request_all_no_delayed_actions (NMPlatform *platform, DelayedActionType actio
 		if (nle < 0)
 			continue;
 
-		if (_nl_send_auto_with_seq (platform, nlmsg, NULL, out_refresh_all_in_progess) < 0) {
+		if (_nl_send_nlmsg (platform, nlmsg, NULL, DELAYED_ACTION_RESPONSE_TYPE_REFRESH_ALL_IN_PROGRESS, out_refresh_all_in_progess) < 0) {
 			nm_assert (*out_refresh_all_in_progess > 0);
 			*out_refresh_all_in_progess -= 1;
 		}
@@ -3825,13 +4066,12 @@ event_seq_check_refresh_all (NMPlatform *platform, guint32 seq_number)
 		for (i = 0; i < priv->delayed_action.list_wait_for_nl_response->len; i++) {
 			data = &g_array_index (priv->delayed_action.list_wait_for_nl_response, DelayedActionWaitForNlResponseData, i);
 
-			if (data->seq_number == priv->nlh_seq_last_seen) {
-				if (data->out_refresh_all_in_progess) {
-					nm_assert (*data->out_refresh_all_in_progess > 0);
-					*data->out_refresh_all_in_progess -= 1;
-					data->out_refresh_all_in_progess = NULL;
-					break;
-				}
+			if (   data->response_type == DELAYED_ACTION_RESPONSE_TYPE_REFRESH_ALL_IN_PROGRESS
+			    && data->response.out_refresh_all_in_progess
+			    && data->seq_number == priv->nlh_seq_last_seen) {
+				*data->response.out_refresh_all_in_progess -= 1;
+				data->response.out_refresh_all_in_progess = NULL;
+				break;
 			}
 		}
 	}
@@ -3879,18 +4119,19 @@ event_seq_check (NMPlatform *platform, guint32 seq_number, WaitForNlResponseResu
 static void
 event_valid_msg (NMPlatform *platform, struct nl_msg *msg, gboolean handle_events)
 {
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
+	NMLinuxPlatformPrivate *priv;
 	nm_auto_nmpobj NMPObject *obj = NULL;
-	nm_auto_nmpobj NMPObject *obj_cache = NULL;
 	NMPCacheOpsType cache_op;
 	struct nlmsghdr *msghdr;
-	char buf_nlmsg_type[16];
+	char buf_nlmsghdr[400];
 	gboolean id_only = FALSE;
-	gboolean was_visible;
+	NMPCache *cache = nm_platform_get_cache (platform);
+	gboolean is_dump;
 
 	msghdr = nlmsg_hdr (msg);
 
-	if (_support_kernel_extended_ifa_flags_still_undecided () && msghdr->nlmsg_type == RTM_NEWADDR)
+	if (   _support_kernel_extended_ifa_flags_still_undecided ()
+	    && msghdr->nlmsg_type == RTM_NEWADDR)
 		_support_kernel_extended_ifa_flags_detect (msg);
 
 	if (!handle_events)
@@ -3902,156 +4143,144 @@ event_valid_msg (NMPlatform *platform, struct nl_msg *msg, gboolean handle_event
 		id_only = TRUE;
 	}
 
-	obj = nmp_object_new_from_nl (platform, priv->cache, msg, id_only);
+	obj = nmp_object_new_from_nl (platform, cache, msg, id_only);
 	if (!obj) {
-		_LOGT ("event-notification: %s, seq %u: ignore",
-		       _nl_nlmsg_type_to_str (msghdr->nlmsg_type, buf_nlmsg_type, sizeof (buf_nlmsg_type)),
-		       msghdr->nlmsg_seq);
+		_LOGT ("event-notification: %s: ignore",
+		       _nl_nlmsghdr_to_str (msghdr, buf_nlmsghdr, sizeof (buf_nlmsghdr)));
 		return;
 	}
 
-	_LOGT ("event-notification: %s, seq %u: %s",
-	       _nl_nlmsg_type_to_str (msghdr->nlmsg_type, buf_nlmsg_type, sizeof (buf_nlmsg_type)),
-	       msghdr->nlmsg_seq, nmp_object_to_string (obj,
-	           id_only ? NMP_OBJECT_TO_STRING_ID : NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
-
 	switch (msghdr->nlmsg_type) {
-
-	case RTM_NEWLINK:
 	case RTM_NEWADDR:
+	case RTM_NEWLINK:
 	case RTM_NEWROUTE:
-	case RTM_GETLINK:
-		cache_op = nmp_cache_update_netlink (priv->cache, obj, &obj_cache, &was_visible, cache_pre_hook, platform);
-
-		cache_post (platform, msghdr, cache_op, obj, obj_cache);
-
-		do_emit_signal (platform, obj_cache, cache_op, was_visible);
-		break;
-
-	case RTM_DELLINK:
-	case RTM_DELADDR:
-	case RTM_DELROUTE:
-		cache_op = nmp_cache_remove_netlink (priv->cache, obj, &obj_cache, &was_visible, cache_pre_hook, platform);
-		do_emit_signal (platform, obj_cache, cache_op, was_visible);
+		is_dump = delayed_action_refresh_all_in_progress (platform,
+		                                                  delayed_action_refresh_from_object_type (NMP_OBJECT_GET_TYPE (obj)));
 		break;
-
 	default:
-		break;
+		is_dump = FALSE;
 	}
 
-	cache_prune_candidates_drop (platform, obj_cache);
-}
-
-/*****************************************************************************/
-
-static const NMPObject *
-cache_lookup_link (NMPlatform *platform, int ifindex)
-{
-	const NMPObject *obj_cache;
-
-	obj_cache = nmp_cache_lookup_link (NM_LINUX_PLATFORM_GET_PRIVATE (platform)->cache, ifindex);
-	if (!nmp_object_is_visible (obj_cache))
-		return NULL;
-
-	return obj_cache;
-}
-
-const NMPlatformObject *const*
-nm_linux_platform_lookup (NMPlatform *platform, const NMPCacheId *cache_id, guint *out_len)
-{
-	g_return_val_if_fail (NM_IS_LINUX_PLATFORM (platform), NULL);
-	g_return_val_if_fail (cache_id, NULL);
+	_LOGT ("event-notification: %s%s: %s",
+	       _nl_nlmsghdr_to_str (msghdr, buf_nlmsghdr, sizeof (buf_nlmsghdr)),
+	       is_dump ? ", in-dump" : "",
+	       nmp_object_to_string (obj,
+	                             id_only ? NMP_OBJECT_TO_STRING_ID : NMP_OBJECT_TO_STRING_PUBLIC,
+	                             NULL, 0));
 
-	return nmp_cache_lookup_multi (NM_LINUX_PLATFORM_GET_PRIVATE (platform)->cache,
-	                               cache_id, out_len);
-}
+	{
+		nm_auto_nmpobj const NMPObject *obj_old = NULL;
+		nm_auto_nmpobj const NMPObject *obj_new = NULL;
+
+		switch (msghdr->nlmsg_type) {
+
+		case RTM_NEWLINK:
+		case RTM_NEWADDR:
+		case RTM_GETLINK:
+			cache_op = nmp_cache_update_netlink (cache, obj, is_dump, &obj_old, &obj_new);
+			if (cache_op != NMP_CACHE_OPS_UNCHANGED) {
+				cache_on_change (platform, cache_op, obj_old, obj_new);
+				nm_platform_cache_update_emit_signal (platform, cache_op, obj_old, obj_new);
+			}
+			break;
 
-static GArray *
-link_get_all (NMPlatform *platform)
-{
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
-	NMPCacheId cache_id;
+		case RTM_NEWROUTE: {
+			nm_auto_nmpobj const NMPObject *obj_replace = NULL;
+			gboolean resync_required = FALSE;
+			gboolean only_dirty = FALSE;
+
+			if (obj->ip_route.rt_cloned) {
+				/* a cloned route might be a response for RTM_GETROUTE. Check, whether it is. */
+				nm_assert (!nmp_object_is_alive (obj));
+				priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
+				if (NM_FLAGS_HAS (priv->delayed_action.flags, DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE)) {
+					guint i;
+
+					nm_assert (priv->delayed_action.list_wait_for_nl_response->len > 0);
+					for (i = 0; i < priv->delayed_action.list_wait_for_nl_response->len; i++) {
+						DelayedActionWaitForNlResponseData *data = &g_array_index (priv->delayed_action.list_wait_for_nl_response, DelayedActionWaitForNlResponseData, i);
+
+						if (   data->response_type == DELAYED_ACTION_RESPONSE_TYPE_ROUTE_GET
+						    && data->response.out_route_get) {
+							nm_assert (!*data->response.out_route_get);
+							if (data->seq_number == nlmsg_hdr (msg)->nlmsg_seq) {
+								*data->response.out_route_get = nmp_object_clone (obj, FALSE);
+								data->response.out_route_get = NULL;
+								break;
+							}
+						}
+					}
+				}
+			}
 
-	return nmp_cache_lookup_multi_to_array (priv->cache,
-	                                        NMP_OBJECT_TYPE_LINK,
-	                                        nmp_cache_id_init_object_type (&cache_id, NMP_OBJECT_TYPE_LINK, TRUE));
-}
+			cache_op = nmp_cache_update_netlink_route (cache,
+			                                           obj,
+			                                           is_dump,
+			                                           msghdr->nlmsg_flags,
+			                                           &obj_old,
+			                                           &obj_new,
+			                                           &obj_replace,
+			                                           &resync_required);
+			if (cache_op != NMP_CACHE_OPS_UNCHANGED) {
+				if (obj_replace) {
+					const NMDedupMultiEntry *entry_replace;
+
+					/* we found an object that is to be replaced by the RTM_NEWROUTE message.
+					 * While we invoke the signal, the platform cache might change and invalidate
+					 * the findings. Mitigate that (for the most part), by marking the entry as
+					 * dirty and only delete @obj_replace if it is still dirty afterwards.
+					 *
+					 * Yes, there is a tiny tiny chance for still getting it wrong. But in practice,
+					 * the signal handlers do not cause to call the platform again, so the cache
+					 * is not really changing. -- if they would, it would anyway be dangerous to overflow
+					 * the stack and it's not ensured that the processing of netlink messages is
+					 * reentrant (maybe it is).
+					 */
+					entry_replace = nmp_cache_lookup_entry (cache, obj_replace);
+					nm_assert (entry_replace && entry_replace->obj == obj_replace);
+					nm_dedup_multi_entry_set_dirty (entry_replace, TRUE);
+					only_dirty = TRUE;
+				}
+				cache_on_change (platform, cache_op, obj_old, obj_new);
+				nm_platform_cache_update_emit_signal (platform, cache_op, obj_old, obj_new);
+			}
 
-static const NMPlatformLink *
-_nm_platform_link_get (NMPlatform *platform, int ifindex)
-{
-	const NMPObject *obj;
+			if (obj_replace) {
+				/* the RTM_NEWROUTE message indicates that another route was replaced.
+				 * Remove it now. */
+				cache_op = nmp_cache_remove (cache, obj_replace, TRUE, only_dirty, NULL);
+				if (cache_op != NMP_CACHE_OPS_UNCHANGED) {
+					nm_assert (cache_op == NMP_CACHE_OPS_REMOVED);
+					cache_on_change (platform, cache_op, obj_replace, NULL);
+					nm_platform_cache_update_emit_signal (platform, cache_op, obj_replace, NULL);
+				}
+			}
 
-	obj = cache_lookup_link (platform, ifindex);
-	return obj ? &obj->link : NULL;
-}
+			if (resync_required) {
+				/* we'd like to avoid such resyncs as they are expensive and we should only rely on the
+				 * netlink events. This needs investigation. */
+				_LOGT ("schedule resync of routes after RTM_NEWROUTE");
+				delayed_action_schedule (platform,
+				                         delayed_action_refresh_from_object_type (NMP_OBJECT_GET_TYPE (obj)),
+				                         NULL);
+			}
+			break;
+		}
 
-static const NMPlatformLink *
-_nm_platform_link_get_by_ifname (NMPlatform *platform,
-                                 const char *ifname)
-{
-	const NMPObject *obj = NULL;
+		case RTM_DELLINK:
+		case RTM_DELADDR:
+		case RTM_DELROUTE:
+			cache_op = nmp_cache_remove_netlink (cache, obj, &obj_old, &obj_new);
+			if (cache_op != NMP_CACHE_OPS_UNCHANGED) {
+				cache_on_change (platform, cache_op, obj_old, obj_new);
+				nm_platform_cache_update_emit_signal (platform, cache_op, obj_old, obj_new);
+			}
+			break;
 
-	if (ifname && *ifname) {
-		obj = nmp_cache_lookup_link_full (NM_LINUX_PLATFORM_GET_PRIVATE (platform)->cache,
-		                                  0, ifname, TRUE, NM_LINK_TYPE_NONE, NULL, NULL);
+		default:
+			break;
+		}
 	}
-	return obj ? &obj->link : NULL;
-}
-
-struct _nm_platform_link_get_by_address_data {
-	gconstpointer address;
-	guint8 length;
-};
-
-static gboolean
-_nm_platform_link_get_by_address_match_link (const NMPObject *obj, struct _nm_platform_link_get_by_address_data *d)
-{
-	return obj->link.addr.len == d->length && !memcmp (obj->link.addr.data, d->address, d->length);
-}
-
-static const NMPlatformLink *
-_nm_platform_link_get_by_address (NMPlatform *platform,
-                                  gconstpointer address,
-                                  size_t length)
-{
-	const NMPObject *obj;
-	struct _nm_platform_link_get_by_address_data d = {
-		.address = address,
-		.length = length,
-	};
-
-	if (length <= 0 || length > NM_UTILS_HWADDR_LEN_MAX)
-		return NULL;
-	if (!address)
-		return NULL;
-
-	obj = nmp_cache_lookup_link_full (NM_LINUX_PLATFORM_GET_PRIVATE (platform)->cache,
-	                                  0, NULL, TRUE, NM_LINK_TYPE_NONE,
-	                                  (NMPObjectMatchFn) _nm_platform_link_get_by_address_match_link, &d);
-	return obj ? &obj->link : NULL;
-}
-
-/*****************************************************************************/
-
-static const NMPObject *
-link_get_lnk (NMPlatform *platform, int ifindex, NMLinkType link_type, const NMPlatformLink **out_link)
-{
-	const NMPObject *obj = cache_lookup_link (platform, ifindex);
-
-	if (!obj)
-		return NULL;
-
-	NM_SET_OUT (out_link, &obj->link);
-
-	if (!obj->_link.netlink.lnk)
-		return NULL;
-	if (   link_type != NM_LINK_TYPE_NONE
-	    && (   link_type != obj->link.type
-	        || link_type != NMP_OBJECT_GET_CLASS (obj->_link.netlink.lnk)->lnk_link_type))
-		return NULL;
-
-	return obj->_link.netlink.lnk;
 }
 
 /*****************************************************************************/
@@ -4063,34 +4292,21 @@ do_add_link_with_lookup (NMPlatform *platform,
                          struct nl_msg *nlmsg,
                          const NMPlatformLink **out_link)
 {
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
 	const NMPObject *obj = NULL;
 	WaitForNlResponseResult seq_result = WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN;
 	int nle;
 	char s_buf[256];
+	NMPCache *cache = nm_platform_get_cache (platform);
 
 	event_handler_read_netlink (platform, FALSE);
 
-	if (nmp_cache_lookup_link_full (priv->cache, 0, name, FALSE, NM_LINK_TYPE_NONE, NULL, NULL)) {
-		/* hm, a link with such a name already exists. Try reloading first. */
-		do_request_link (platform, 0, name);
-
-		obj = nmp_cache_lookup_link_full (priv->cache, 0, name, FALSE, NM_LINK_TYPE_NONE, NULL, NULL);
-		if (obj) {
-			_LOGE ("do-add-link[%s/%s]: link already exists: %s",
-			       name,
-			       nm_link_type_to_string (link_type),
-			       nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_ID, NULL, 0));
-			return FALSE;
-		}
-	}
-
-	nle = _nl_send_auto_with_seq (platform, nlmsg, &seq_result, NULL);
+	nle = _nl_send_nlmsg (platform, nlmsg, &seq_result, DELAYED_ACTION_RESPONSE_TYPE_VOID, NULL);
 	if (nle < 0) {
 		_LOGE ("do-add-link[%s/%s]: failed sending netlink request \"%s\" (%d)",
 		       name,
 		       nm_link_type_to_string (link_type),
 		       nl_geterror (nle), -nle);
+		NM_SET_OUT (out_link, NULL);
 		return FALSE;
 	}
 
@@ -4100,35 +4316,29 @@ do_add_link_with_lookup (NMPlatform *platform,
 
 	_NMLOG (seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK
 	            ? LOGL_DEBUG
-	            : LOGL_ERR,
+	            : LOGL_WARN,
 	        "do-add-link[%s/%s]: %s",
 	        name,
 	        nm_link_type_to_string (link_type),
 	        wait_for_nl_response_to_string (seq_result, s_buf, sizeof (s_buf)));
 
-	if (seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK)
-		obj = nmp_cache_lookup_link_full (priv->cache, 0, name, FALSE, link_type, NULL, NULL);
-
-	if (!obj) {
-		/* either kernel signaled failure, or it signaled success and the link object
-		 * is not (yet) in the cache. Try to reload it... */
-		do_request_link (platform, 0, name);
-		obj = nmp_cache_lookup_link_full (priv->cache, 0, name, FALSE, link_type, NULL, NULL);
+	if (out_link) {
+		obj = nmp_cache_lookup_link_full (cache, 0, name, FALSE, link_type, NULL, NULL);
+		*out_link = NMP_OBJECT_CAST_LINK (obj);
 	}
 
-	if (out_link)
-		*out_link = obj ? &obj->link : NULL;
-	return !!obj;
+	return seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK;
 }
 
-static gboolean
-do_add_addrroute (NMPlatform *platform, const NMPObject *obj_id, struct nl_msg *nlmsg)
+static NMPlatformError
+do_add_addrroute (NMPlatform *platform,
+                  const NMPObject *obj_id,
+                  struct nl_msg *nlmsg,
+                  gboolean suppress_netlink_failure)
 {
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
 	WaitForNlResponseResult seq_result = WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN;
 	int nle;
 	char s_buf[256];
-	const NMPObject *obj;
 
 	nm_assert (NM_IN_SET (NMP_OBJECT_GET_TYPE (obj_id),
 	                      NMP_OBJECT_TYPE_IP4_ADDRESS, NMP_OBJECT_TYPE_IP6_ADDRESS,
@@ -4136,72 +4346,69 @@ do_add_addrroute (NMPlatform *platform, const NMPObject *obj_id, struct nl_msg *
 
 	event_handler_read_netlink (platform, FALSE);
 
-	nle = _nl_send_auto_with_seq (platform, nlmsg, &seq_result, NULL);
+	nle = _nl_send_nlmsg (platform, nlmsg, &seq_result, DELAYED_ACTION_RESPONSE_TYPE_VOID, NULL);
 	if (nle < 0) {
 		_LOGE ("do-add-%s[%s]: failure sending netlink request \"%s\" (%d)",
 		       NMP_OBJECT_GET_CLASS (obj_id)->obj_type_name,
 		       nmp_object_to_string (obj_id, NMP_OBJECT_TO_STRING_ID, NULL, 0),
 		       nl_geterror (nle), -nle);
-		return FALSE;
+		return NM_PLATFORM_ERROR_NETLINK;
 	}
 
 	delayed_action_handle_all (platform, FALSE);
 
 	nm_assert (seq_result);
 
-	_NMLOG (seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK
+	_NMLOG ((   seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK
+	         || (   suppress_netlink_failure
+	             && seq_result < 0))
 	            ? LOGL_DEBUG
-	            : LOGL_ERR,
+	            : LOGL_WARN,
 	        "do-add-%s[%s]: %s",
 	        NMP_OBJECT_GET_CLASS (obj_id)->obj_type_name,
 	        nmp_object_to_string (obj_id, NMP_OBJECT_TO_STRING_ID, NULL, 0),
 	        wait_for_nl_response_to_string (seq_result, s_buf, sizeof (s_buf)));
 
-	/* In rare cases, the object is not yet ready as we received the ACK from
-	 * kernel. Need to refetch.
-	 *
-	 * We want to safe the expensive refetch, thus we look first into the cache
-	 * whether the object exists.
-	 *
-	 * FIXME: if the object already existed previously, we might not notice a
-	 * missing update. It's not clear how to fix that reliably without refechting
-	 * all the time. */
-	obj = nmp_cache_lookup_obj (priv->cache, obj_id);
-	if (!obj) {
-		do_request_one_type (platform, NMP_OBJECT_GET_TYPE (obj_id));
-		obj = nmp_cache_lookup_obj (priv->cache, obj_id);
+	if (NMP_OBJECT_GET_TYPE (obj_id) == NMP_OBJECT_TYPE_IP6_ADDRESS) {
+		/* In rare cases, the object is not yet ready as we received the ACK from
+		 * kernel. Need to refetch.
+		 *
+		 * We want to safe the expensive refetch, thus we look first into the cache
+		 * whether the object exists.
+		 *
+		 * rh#1484434 */
+		if (!nmp_cache_lookup_obj (nm_platform_get_cache (platform), obj_id))
+			do_request_one_type (platform, NMP_OBJECT_GET_TYPE (obj_id));
 	}
 
-	/* Adding is only successful, if kernel reported success *and* we have the
-	 * expected object in cache afterwards. */
-	return obj && seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK;
+	return wait_for_nl_response_to_plerr (seq_result);
 }
 
 static gboolean
 do_delete_object (NMPlatform *platform, const NMPObject *obj_id, struct nl_msg *nlmsg)
 {
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
 	WaitForNlResponseResult seq_result = WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN;
 	int nle;
 	char s_buf[256];
-	gboolean success = TRUE;
+	gboolean success;
 	const char *log_detail = "";
 
 	event_handler_read_netlink (platform, FALSE);
 
-	nle = _nl_send_auto_with_seq (platform, nlmsg, &seq_result, NULL);
+	nle = _nl_send_nlmsg (platform, nlmsg, &seq_result, DELAYED_ACTION_RESPONSE_TYPE_VOID, NULL);
 	if (nle < 0) {
 		_LOGE ("do-delete-%s[%s]: failure sending netlink request \"%s\" (%d)",
 		       NMP_OBJECT_GET_CLASS (obj_id)->obj_type_name,
 		       nmp_object_to_string (obj_id, NMP_OBJECT_TO_STRING_ID, NULL, 0),
 		       nl_geterror (nle), -nle);
-		goto out;
+		return FALSE;
 	}
 
 	delayed_action_handle_all (platform, FALSE);
 
 	nm_assert (seq_result);
 
+	success = TRUE;
 	if (seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK) {
 		/* ok */
 	} else if (NM_IN_SET (-((int) seq_result), ESRCH, ENOENT))
@@ -4216,42 +4423,60 @@ do_delete_object (NMPlatform *platform, const NMPObject *obj_id, struct nl_msg *
 	else
 		success = FALSE;
 
-	_NMLOG (success ? LOGL_DEBUG : LOGL_ERR,
+	_NMLOG (success ? LOGL_DEBUG : LOGL_WARN,
 	        "do-delete-%s[%s]: %s%s",
 	        NMP_OBJECT_GET_CLASS (obj_id)->obj_type_name,
 	        nmp_object_to_string (obj_id, NMP_OBJECT_TO_STRING_ID, NULL, 0),
 	        wait_for_nl_response_to_string (seq_result, s_buf, sizeof (s_buf)),
 	        log_detail);
 
-out:
-	if (!nmp_cache_lookup_obj (priv->cache, obj_id))
-		return TRUE;
+	if (NMP_OBJECT_GET_TYPE (obj_id) == NMP_OBJECT_TYPE_IP6_ADDRESS) {
+		/* In rare cases, the object is still there after we receive the ACK from
+		 * kernel. Need to refetch.
+		 *
+		 * We want to safe the expensive refetch, thus we look first into the cache
+		 * whether the object exists.
+		 *
+		 * rh#1484434 */
+		if (nmp_cache_lookup_obj (nm_platform_get_cache (platform), obj_id))
+			do_request_one_type (platform, NMP_OBJECT_GET_TYPE (obj_id));
+	}
 
-	/* such an object still exists in the cache. To be sure, refetch it (and
-	 * hope it's gone) */
-	do_request_one_type (platform, NMP_OBJECT_GET_TYPE (obj_id));
-	return !nmp_cache_lookup_obj (priv->cache, obj_id);
+	return success;
 }
 
-static WaitForNlResponseResult
-do_change_link_request (NMPlatform *platform,
-                        int ifindex,
-                        struct nl_msg *nlmsg)
+static NMPlatformError
+do_change_link (NMPlatform *platform,
+                ChangeLinkType change_link_type,
+                int ifindex,
+                struct nl_msg *nlmsg,
+                const ChangeLinkData *data)
 {
 	nm_auto_pop_netns NMPNetns *netns = NULL;
-	WaitForNlResponseResult seq_result = WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN;
 	int nle;
+	WaitForNlResponseResult seq_result = WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN;
+	char s_buf[256];
+	NMPlatformError result = NM_PLATFORM_ERROR_SUCCESS;
+	NMLogLevel log_level = LOGL_DEBUG;
+	const char *log_result = "failure";
+	const char *log_detail = "";
+	gs_free char *log_detail_free = NULL;
+	const NMPObject *obj_cache;
 
-	if (!nm_platform_netns_push (platform, &netns))
-		return WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN;
+	if (!nm_platform_netns_push (platform, &netns)) {
+		log_level = LOGL_ERR;
+		log_detail = ", failure to change network namespace";
+		goto out;
+	}
 
 retry:
-	nle = _nl_send_auto_with_seq (platform, nlmsg, &seq_result, NULL);
+	nle = _nl_send_nlmsg (platform, nlmsg, &seq_result, DELAYED_ACTION_RESPONSE_TYPE_VOID, NULL);
 	if (nle < 0) {
-		_LOGE ("do-change-link[%d]: failure sending netlink request \"%s\" (%d)",
-		       ifindex,
-		       nl_geterror (nle), -nle);
-		return WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN;
+		log_level = LOGL_ERR;
+		log_detail_free = g_strdup_printf (", failure sending netlink request: %s (%d)",
+		                                   nl_geterror (nle), -nle);
+		log_detail = log_detail_free;
+		goto out;
 	}
 
 	/* always refetch the link after changing it. There seems to be issues
@@ -4267,18 +4492,6 @@ retry:
 		nlmsg_hdr (nlmsg)->nlmsg_type = RTM_SETLINK;
 		goto retry;
 	}
-	return seq_result;
-}
-
-static NMPlatformError
-do_change_link_result (NMPlatform *platform,
-                       int ifindex,
-                       WaitForNlResponseResult seq_result)
-{
-	char s_buf[256];
-	NMPlatformError result = NM_PLATFORM_ERROR_SUCCESS;
-	NMLogLevel log_level = LOGL_DEBUG;
-	const char *log_result = "failure", *log_detail = "";
 
 	if (seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK) {
 		log_result = "success";
@@ -4287,38 +4500,43 @@ do_change_link_result (NMPlatform *platform,
 	} else if (NM_IN_SET (-((int) seq_result), ESRCH, ENOENT)) {
 		log_detail = ", firmware not found";
 		result = NM_PLATFORM_ERROR_NO_FIRMWARE;
+	} else if (   NM_IN_SET (-((int) seq_result), ERANGE)
+	           && change_link_type == CHANGE_LINK_TYPE_SET_MTU) {
+		log_detail = ", setting MTU to requested size is not possible";
+		result = NM_PLATFORM_ERROR_CANT_SET_MTU;
+	} else if (   NM_IN_SET (-((int) seq_result), ENFILE)
+	           && change_link_type == CHANGE_LINK_TYPE_SET_ADDRESS
+	           && (obj_cache = nmp_cache_lookup_link (nm_platform_get_cache (platform), ifindex))
+	           && obj_cache->link.addr.len == data->set_address.length
+	           && memcmp (obj_cache->link.addr.data, data->set_address.address, data->set_address.length) == 0) {
+		/* workaround ENFILE which may be wrongly returned (bgo #770456).
+		 * If the MAC address is as expected, assume success? */
+		log_result = "success";
+		log_detail = " (assume success changing address)";
+		result = NM_PLATFORM_ERROR_SUCCESS;
 	} else if (NM_IN_SET (-((int) seq_result), ENODEV)) {
 		log_level = LOGL_DEBUG;
 		result = NM_PLATFORM_ERROR_NOT_FOUND;
 	} else {
-		log_level = LOGL_ERR;
+		log_level = LOGL_WARN;
 		result = NM_PLATFORM_ERROR_UNSPECIFIED;
 	}
+
+out:
 	_NMLOG (log_level,
 	        "do-change-link[%d]: %s changing link: %s%s",
 	        ifindex,
 	        log_result,
 	        wait_for_nl_response_to_string (seq_result, s_buf, sizeof (s_buf)),
 	        log_detail);
-
 	return result;
 }
 
-static NMPlatformError
-do_change_link (NMPlatform *platform,
-                int ifindex,
-                struct nl_msg *nlmsg)
-{
-	WaitForNlResponseResult seq_result;
-
-	seq_result = do_change_link_request (platform, ifindex, nlmsg);
-	return do_change_link_result (platform, ifindex, seq_result);
-}
-
 static gboolean
 link_add (NMPlatform *platform,
           const char *name,
           NMLinkType type,
+          const char *veth_peer,
           const void *address,
           size_t address_len,
           const NMPlatformLink **out_link)
@@ -4337,9 +4555,6 @@ link_add (NMPlatform *platform,
 			(void) nm_utils_modprobe (NULL, TRUE, "bonding", "max_bonds=0", NULL);
 	}
 
-	_LOGD ("link: add link '%s' of type '%s' (%d)",
-	       name, nm_link_type_to_string (type), (int) type);
-
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          NLM_F_CREATE | NLM_F_EXCL,
 	                          0,
@@ -4352,7 +4567,7 @@ link_add (NMPlatform *platform,
 	if (address && address_len)
 		NLA_PUT (nlmsg, IFLA_ADDRESS, address_len, address);
 
-	if (!_nl_msg_new_link_set_linkinfo (nlmsg, type))
+	if (!_nl_msg_new_link_set_linkinfo (nlmsg, type, veth_peer))
 		return FALSE;
 
 	return do_add_link_with_lookup (platform, type, name, nlmsg, out_link);
@@ -4364,11 +4579,10 @@ static gboolean
 link_delete (NMPlatform *platform, int ifindex)
 {
 	nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
 	NMPObject obj_id;
 	const NMPObject *obj;
 
-	obj = nmp_cache_lookup_link (priv->cache, ifindex);
+	obj = nmp_cache_lookup_link (nm_platform_get_cache (platform), ifindex);
 	if (!obj || !obj->_link.netlink.is_in_netlink)
 		return FALSE;
 
@@ -4383,56 +4597,11 @@ link_delete (NMPlatform *platform, int ifindex)
 	return do_delete_object (platform, &obj_id, nlmsg);
 }
 
-static const char *
-link_get_type_name (NMPlatform *platform, int ifindex)
-{
-	const NMPObject *obj = cache_lookup_link (platform, ifindex);
-
-	if (!obj)
-		return NULL;
-
-	if (obj->link.type != NM_LINK_TYPE_UNKNOWN) {
-		/* We could detect the @link_type. In this case the function returns
-		 * our internel module names, which differs from rtnl_link_get_type():
-		 *   - NM_LINK_TYPE_INFINIBAND (gives "infiniband", instead of "ipoib")
-		 *   - NM_LINK_TYPE_TAP (gives "tap", instead of "tun").
-		 * Note that this functions is only used by NMDeviceGeneric to
-		 * set type_description. */
-		return nm_link_type_to_string (obj->link.type);
-	}
-	/* Link type not detected. Fallback to rtnl_link_get_type()/IFLA_INFO_KIND. */
-	return obj->link.kind ?: "unknown";
-}
-
-static gboolean
-link_get_unmanaged (NMPlatform *platform, int ifindex, gboolean *unmanaged)
-{
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
-	const NMPObject *link;
-	struct udev_device *udevice = NULL;
-	const char *uproperty;
-
-	link = nmp_cache_lookup_link (priv->cache, ifindex);
-	if (!link)
-		return FALSE;
-
-	udevice = link->_link.udev.device;
-	if (!udevice)
-		return FALSE;
-
-	uproperty = udev_device_get_property_value (udevice, "NM_UNMANAGED");
-	if (!uproperty)
-		return FALSE;
-
-	*unmanaged = nm_udev_utils_property_as_boolean (uproperty);
-	return TRUE;
-}
-
 static gboolean
 link_refresh (NMPlatform *platform, int ifindex)
 {
 	do_request_link (platform, ifindex, NULL);
-	return !!cache_lookup_link (platform, ifindex);
+	return !!nm_platform_link_get_obj (platform, ifindex, TRUE);
 }
 
 static gboolean
@@ -4454,7 +4623,7 @@ link_set_netns (NMPlatform *platform,
 		return FALSE;
 
 	NLA_PUT (nlmsg, IFLA_NET_NS_FD, 4, &netns_fd);
-	return do_change_link (platform, ifindex, nlmsg) == NM_PLATFORM_ERROR_SUCCESS;
+	return do_change_link (platform, CHANGE_LINK_TYPE_UNSPEC, ifindex, nlmsg, NULL) == NM_PLATFORM_ERROR_SUCCESS;
 
 nla_put_failure:
 	g_return_val_if_reached (FALSE);
@@ -4484,7 +4653,7 @@ link_change_flags (NMPlatform *platform,
 	                          flags_set);
 	if (!nlmsg)
 		return NM_PLATFORM_ERROR_UNSPECIFIED;
-	return do_change_link (platform, ifindex, nlmsg);
+	return do_change_link (platform, CHANGE_LINK_TYPE_UNSPEC, ifindex, nlmsg, NULL);
 }
 
 static gboolean
@@ -4519,7 +4688,7 @@ link_set_noarp (NMPlatform *platform, int ifindex)
 static const char *
 link_get_udi (NMPlatform *platform, int ifindex)
 {
-	const NMPObject *obj = cache_lookup_link (platform, ifindex);
+	const NMPObject *obj = nm_platform_link_get_obj (platform, ifindex, TRUE);
 
 	if (   !obj
 	    || !obj->_link.netlink.is_in_netlink
@@ -4528,35 +4697,21 @@ link_get_udi (NMPlatform *platform, int ifindex)
 	return udev_device_get_syspath (obj->_link.udev.device);
 }
 
-static struct udev_device *
-link_get_udev_device (NMPlatform *platform, int ifindex)
-{
-	const NMPObject *obj_cache;
-
-	/* we don't use cache_lookup_link() because this would return NULL
-	 * if the link is not visible in libnl. For link_get_udev_device()
-	 * we want to return whatever we have, even if the link itself
-	 * appears invisible via other platform functions. */
-
-	obj_cache = nmp_cache_lookup_link (NM_LINUX_PLATFORM_GET_PRIVATE (platform)->cache, ifindex);
-	return obj_cache ? obj_cache->_link.udev.device : NULL;
-}
-
 static NMPlatformError
 link_set_user_ipv6ll_enabled (NMPlatform *platform, int ifindex, gboolean enabled)
 {
 	nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
 	guint8 mode = enabled ? NM_IN6_ADDR_GEN_MODE_NONE : NM_IN6_ADDR_GEN_MODE_EUI64;
 
+	_LOGD ("link: change %d: user-ipv6ll: set IPv6 address generation mode to %s",
+	       ifindex,
+	       nm_platform_link_inet6_addrgenmode2str (mode, NULL, 0));
+
 	if (!_support_user_ipv6ll_get ()) {
 		_LOGD ("link: change %d: user-ipv6ll: not supported", ifindex);
 		return NM_PLATFORM_ERROR_OPNOTSUPP;
 	}
 
-	_LOGD ("link: change %d: user-ipv6ll: set IPv6 address generation mode to %s",
-	       ifindex,
-	       nm_platform_link_inet6_addrgenmode2str (mode, NULL, 0));
-
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          0,
 	                          ifindex,
@@ -4567,7 +4722,7 @@ link_set_user_ipv6ll_enabled (NMPlatform *platform, int ifindex, gboolean enable
 	    || !_nl_msg_new_link_set_afspec (nlmsg, mode, NULL))
 		g_return_val_if_reached (NM_PLATFORM_ERROR_BUG);
 
-	return do_change_link (platform, ifindex, nlmsg);
+	return do_change_link (platform, CHANGE_LINK_TYPE_UNSPEC, ifindex, nlmsg, NULL);
 }
 
 static gboolean
@@ -4582,7 +4737,7 @@ link_set_token (NMPlatform *platform, int ifindex, NMUtilsIPv6IfaceId iid)
 	if (!nlmsg || !_nl_msg_new_link_set_afspec (nlmsg, -1, &iid))
 		g_return_val_if_reached (FALSE);
 
-	return do_change_link (platform, ifindex, nlmsg) == NM_PLATFORM_ERROR_SUCCESS;
+	return do_change_link (platform, CHANGE_LINK_TYPE_UNSPEC, ifindex, nlmsg, NULL) == NM_PLATFORM_ERROR_SUCCESS;
 }
 
 static gboolean
@@ -4606,7 +4761,7 @@ link_supports_vlans (NMPlatform *platform, int ifindex)
 	nm_auto_pop_netns NMPNetns *netns = NULL;
 	const NMPObject *obj;
 
-	obj = cache_lookup_link (platform, ifindex);
+	obj = nm_platform_link_get_obj (platform, ifindex, TRUE);
 
 	/* Only ARPHRD_ETHER links can possibly support VLANs. */
 	if (!obj || obj->link.arptype != ARPHRD_ETHER)
@@ -4647,8 +4802,12 @@ link_set_address (NMPlatform *platform, int ifindex, gconstpointer address, size
 {
 	nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
 	gs_free char *mac = NULL;
-	WaitForNlResponseResult seq_result;
-	char s_buf[256];
+	const ChangeLinkData d = {
+		.set_address = {
+			.address = address,
+			.length = length,
+		},
+	};
 
 	if (!address || !length)
 		g_return_val_if_reached (NM_PLATFORM_ERROR_BUG);
@@ -4668,32 +4827,32 @@ link_set_address (NMPlatform *platform, int ifindex, gconstpointer address, size
 
 	NLA_PUT (nlmsg, IFLA_ADDRESS, length, address);
 
-	seq_result = do_change_link_request (platform, ifindex, nlmsg);
+	return do_change_link (platform, CHANGE_LINK_TYPE_SET_ADDRESS, ifindex, nlmsg, &d);
+nla_put_failure:
+	g_return_val_if_reached (NM_PLATFORM_ERROR_UNSPECIFIED);
+}
 
-	if (NM_IN_SET (-((int) seq_result), ENFILE)) {
-		const NMPObject *obj_cache;
+static NMPlatformError
+link_set_name (NMPlatform *platform, int ifindex, const char *name)
+{
+	nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
 
-		/* workaround ENFILE which may be wrongly returned (bgo #770456).
-		 * If the MAC address is as expected, assume success? */
+	_LOGD ("link: change %d: name: %s", ifindex, name);
 
-		obj_cache = nmp_cache_lookup_link (NM_LINUX_PLATFORM_GET_PRIVATE (platform)->cache, ifindex);
-		if (   obj_cache
-		    && obj_cache->link.addr.len == length
-		    && memcmp (obj_cache->link.addr.data, address, length) == 0) {
-			_NMLOG (LOGL_DEBUG,
-			        "do-change-link[%d]: %s changing link: %s%s",
-			        ifindex,
-			        "success",
-			        wait_for_nl_response_to_string (seq_result, s_buf, sizeof (s_buf)),
-			        " (assume success changing address)");
-			return NM_PLATFORM_ERROR_SUCCESS;
-		}
-	}
+	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
+	                          0,
+	                          ifindex,
+	                          NULL,
+	                          0,
+	                          0);
+	if (!nlmsg)
+		g_return_val_if_reached (NM_PLATFORM_ERROR_UNSPECIFIED);
 
-	return do_change_link_result (platform, ifindex, seq_result);
+	NLA_PUT (nlmsg, IFLA_IFNAME, strlen (name) + 1, name);
 
+	return do_change_link (platform, CHANGE_LINK_TYPE_UNSPEC, ifindex, nlmsg, NULL) == NM_PLATFORM_ERROR_SUCCESS;
 nla_put_failure:
-	g_return_val_if_reached (NM_PLATFORM_ERROR_UNSPECIFIED);
+	g_return_val_if_reached (FALSE);
 }
 
 static gboolean
@@ -4710,7 +4869,7 @@ link_get_permanent_address (NMPlatform *platform,
 	return nmp_utils_ethtool_get_permanent_address (ifindex, buf, length);
 }
 
-static gboolean
+static NMPlatformError
 link_set_mtu (NMPlatform *platform, int ifindex, guint32 mtu)
 {
 	nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
@@ -4728,7 +4887,7 @@ link_set_mtu (NMPlatform *platform, int ifindex, guint32 mtu)
 
 	NLA_PUT_U32 (nlmsg, IFLA_MTU, mtu);
 
-	return do_change_link (platform, ifindex, nlmsg) == NM_PLATFORM_ERROR_SUCCESS;
+	return do_change_link (platform, CHANGE_LINK_TYPE_SET_MTU, ifindex, nlmsg, NULL);
 nla_put_failure:
 	g_return_val_if_reached (FALSE);
 }
@@ -5359,7 +5518,6 @@ link_vlan_change (NMPlatform *platform,
                   const NMVlanQosMapping *egress_map,
                   gsize n_egress_map)
 {
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
 	const NMPObject *obj_cache;
 	nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
 	const NMPObjectLnkVlan *lnk;
@@ -5371,7 +5529,7 @@ link_vlan_change (NMPlatform *platform,
 	char s_ingress[256];
 	char s_egress[256];
 
-	obj_cache = nmp_cache_lookup_link (priv->cache, ifindex);
+	obj_cache = nmp_cache_lookup_link (nm_platform_get_cache (platform), ifindex);
 	if (   !obj_cache
 	    || !obj_cache->_link.netlink.is_in_netlink) {
 		_LOGD ("link: change %d: %s: link does not exist", ifindex, "vlan");
@@ -5437,7 +5595,7 @@ link_vlan_change (NMPlatform *platform,
 	                                            new_n_egress_map))
 		g_return_val_if_reached (FALSE);
 
-	return do_change_link (platform, ifindex, nlmsg) == NM_PLATFORM_ERROR_SUCCESS;
+	return do_change_link (platform, CHANGE_LINK_TYPE_UNSPEC, ifindex, nlmsg, NULL) == NM_PLATFORM_ERROR_SUCCESS;
 }
 
 static int
@@ -5449,9 +5607,6 @@ tun_add (NMPlatform *platform, const char *name, gboolean tap,
 	struct ifreq ifr = { };
 	int fd;
 
-	_LOGD ("link: add %s '%s' owner %" G_GINT64_FORMAT " group %" G_GINT64_FORMAT,
-	       tap ? "tap" : "tun", name, owner, group);
-
 	fd = open ("/dev/net/tun", O_RDWR | O_CLOEXEC);
 	if (fd < 0)
 		return FALSE;
@@ -5467,30 +5622,30 @@ tun_add (NMPlatform *platform, const char *name, gboolean tap,
 		ifr.ifr_flags |= NM_IFF_MULTI_QUEUE;
 
 	if (ioctl (fd, TUNSETIFF, &ifr)) {
-		close (fd);
+		nm_close (fd);
 		return FALSE;
 	}
 
 	if (owner >= 0 && owner < G_MAXINT32) {
 		if (ioctl (fd, TUNSETOWNER, (uid_t) owner)) {
-			close (fd);
+			nm_close (fd);
 			return FALSE;
 		}
 	}
 
 	if (group >= 0 && group < G_MAXINT32) {
 		if (ioctl (fd, TUNSETGROUP, (gid_t) group)) {
-			close (fd);
+			nm_close (fd);
 			return FALSE;
 		}
 	}
 
 	if (ioctl (fd, TUNSETPERSIST, 1)) {
-		close (fd);
+		nm_close (fd);
 		return FALSE;
 	}
 	do_request_link (platform, 0, name);
-	obj = nmp_cache_lookup_link_full (NM_LINUX_PLATFORM_GET_PRIVATE (platform)->cache,
+	obj = nmp_cache_lookup_link_full (nm_platform_get_cache (platform),
 	                                  0, name, FALSE,
 	                                  tap ? NM_LINK_TYPE_TAP : NM_LINK_TYPE_TUN,
 	                                  NULL, NULL);
@@ -5520,7 +5675,7 @@ link_enslave (NMPlatform *platform, int master, int slave)
 
 	NLA_PUT_U32 (nlmsg, IFLA_MASTER, master);
 
-	return do_change_link (platform, ifindex, nlmsg) == NM_PLATFORM_ERROR_SUCCESS;
+	return do_change_link (platform, CHANGE_LINK_TYPE_UNSPEC, ifindex, nlmsg, NULL) == NM_PLATFORM_ERROR_SUCCESS;
 nla_put_failure:
 	g_return_val_if_reached (FALSE);
 }
@@ -5540,7 +5695,6 @@ _infiniband_partition_action (NMPlatform *platform,
                               int p_key,
                               const NMPlatformLink **out_link)
 {
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
 	nm_auto_close int dirfd = -1;
 	char ifname_parent[IFNAMSIZ];
 	const NMPObject *obj;
@@ -5576,7 +5730,7 @@ _infiniband_partition_action (NMPlatform *platform,
 	if (action == INFINIBAND_ACTION_DELETE_CHILD)
 		return TRUE;
 
-	obj = nmp_cache_lookup_link_full (priv->cache, 0, name, FALSE,
+	obj = nmp_cache_lookup_link_full (nm_platform_get_cache (platform), 0, name, FALSE,
 	                                  NM_LINK_TYPE_INFINIBAND, NULL, NULL);
 	if (out_link)
 		*out_link = obj ? &obj->link : NULL;
@@ -5714,16 +5868,15 @@ wifi_indicate_addressing_running (NMPlatform *platform, int ifindex, gboolean ru
 static gboolean
 link_can_assume (NMPlatform *platform, int ifindex)
 {
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
-	NMPCacheId cache_id;
-	const NMPlatformObject *const *objs;
-	guint i, len;
-	const NMPObject *link;
+	NMPLookup lookup;
+	const NMPObject *link, *o;
+	NMDedupMultiIter iter;
+	NMPCache *cache = nm_platform_get_cache (platform);
 
 	if (ifindex <= 0)
 		return FALSE;
 
-	link = cache_lookup_link (platform, ifindex);
+	link = nm_platform_link_get_obj (platform, ifindex, TRUE);
 	if (!link)
 		return FALSE;
 
@@ -5733,21 +5886,21 @@ link_can_assume (NMPlatform *platform, int ifindex)
 	if (link->link.master > 0)
 		return TRUE;
 
-	if (nmp_cache_lookup_multi (priv->cache,
-	                            nmp_cache_id_init_addrroute_visible_by_ifindex (&cache_id, NMP_OBJECT_TYPE_IP4_ADDRESS, ifindex),
-	                            NULL))
+	nmp_lookup_init_addrroute (&lookup,
+	                           NMP_OBJECT_TYPE_IP4_ADDRESS,
+	                           ifindex);
+	if (nmp_cache_lookup (cache, &lookup))
 		return TRUE;
 
-	objs = nmp_cache_lookup_multi (priv->cache,
-	                               nmp_cache_id_init_addrroute_visible_by_ifindex (&cache_id, NMP_OBJECT_TYPE_IP6_ADDRESS, ifindex),
-	                               &len);
-	if (objs) {
-		for (i = 0; i < len; i++) {
-			const NMPlatformIP6Address *a = (NMPlatformIP6Address *) objs[i];
-
-			if (!IN6_IS_ADDR_LINKLOCAL (&a->address))
-				return TRUE;
-		}
+	nmp_lookup_init_addrroute (&lookup,
+	                           NMP_OBJECT_TYPE_IP6_ADDRESS,
+	                           ifindex);
+	nmp_cache_iter_for_each (&iter,
+	                         nmp_cache_lookup (cache, &lookup),
+	                         &o) {
+		nm_assert (NMP_OBJECT_GET_TYPE (o) == NMP_OBJECT_TYPE_IP6_ADDRESS);
+		if (!IN6_IS_ADDR_LINKLOCAL (&o->ip6_address.address))
+			return TRUE;
 	}
 	return FALSE;
 }
@@ -5822,33 +5975,6 @@ link_get_driver_info (NMPlatform *platform,
 
 /*****************************************************************************/
 
-static GArray *
-ipx_address_get_all (NMPlatform *platform, int ifindex, NMPObjectType obj_type)
-{
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
-	NMPCacheId cache_id;
-
-	nm_assert (NM_IN_SET (obj_type, NMP_OBJECT_TYPE_IP4_ADDRESS, NMP_OBJECT_TYPE_IP6_ADDRESS));
-
-	return nmp_cache_lookup_multi_to_array (priv->cache,
-	                                        obj_type,
-	                                        nmp_cache_id_init_addrroute_visible_by_ifindex (&cache_id,
-	                                                                                        obj_type,
-	                                                                                        ifindex));
-}
-
-static GArray *
-ip4_address_get_all (NMPlatform *platform, int ifindex)
-{
-	return ipx_address_get_all (platform, ifindex, NMP_OBJECT_TYPE_IP4_ADDRESS);
-}
-
-static GArray *
-ip6_address_get_all (NMPlatform *platform, int ifindex)
-{
-	return ipx_address_get_all (platform, ifindex, NMP_OBJECT_TYPE_IP6_ADDRESS);
-}
-
 static gboolean
 ip4_address_add (NMPlatform *platform,
                  int ifindex,
@@ -5877,7 +6003,7 @@ ip4_address_add (NMPlatform *platform,
 	                             label);
 
 	nmp_object_stackinit_id_ip4_address (&obj_id, ifindex, addr, plen, peer_addr);
-	return do_add_addrroute (platform, &obj_id, nlmsg);
+	return do_add_addrroute (platform, &obj_id, nlmsg, FALSE) == NM_PLATFORM_ERROR_SUCCESS;
 }
 
 static gboolean
@@ -5906,8 +6032,8 @@ ip6_address_add (NMPlatform *platform,
 	                             preferred,
 	                             NULL);
 
-	nmp_object_stackinit_id_ip6_address (&obj_id, ifindex, &addr, plen);
-	return do_add_addrroute (platform, &obj_id, nlmsg);
+	nmp_object_stackinit_id_ip6_address (&obj_id, ifindex, &addr);
+	return do_add_addrroute (platform, &obj_id, nlmsg, FALSE) == NM_PLATFORM_ERROR_SUCCESS;
 }
 
 static gboolean
@@ -5956,303 +6082,139 @@ ip6_address_delete (NMPlatform *platform, int ifindex, struct in6_addr addr, gui
 	if (!nlmsg)
 		g_return_val_if_reached (FALSE);
 
-	nmp_object_stackinit_id_ip6_address (&obj_id, ifindex, &addr, plen);
+	nmp_object_stackinit_id_ip6_address (&obj_id, ifindex, &addr);
 	return do_delete_object (platform, &obj_id, nlmsg);
 }
 
-static const NMPlatformIP4Address *
-ip4_address_get (NMPlatform *platform, int ifindex, in_addr_t addr, guint8 plen, in_addr_t peer_address)
-{
-	NMPObject obj_id;
-	const NMPObject *obj;
-
-	nmp_object_stackinit_id_ip4_address (&obj_id, ifindex, addr, plen, peer_address);
-	obj = nmp_cache_lookup_obj (NM_LINUX_PLATFORM_GET_PRIVATE (platform)->cache, &obj_id);
-	if (nmp_object_is_visible (obj))
-		return &obj->ip4_address;
-	return NULL;
-}
-
-static const NMPlatformIP6Address *
-ip6_address_get (NMPlatform *platform, int ifindex, struct in6_addr addr, guint8 plen)
-{
-	NMPObject obj_id;
-	const NMPObject *obj;
-
-	nmp_object_stackinit_id_ip6_address (&obj_id, ifindex, &addr, plen);
-	obj = nmp_cache_lookup_obj (NM_LINUX_PLATFORM_GET_PRIVATE (platform)->cache, &obj_id);
-	if (nmp_object_is_visible (obj))
-		return &obj->ip6_address;
-	return NULL;
-}
-
 /*****************************************************************************/
 
-static GArray *
-ipx_route_get_all (NMPlatform *platform, int ifindex, NMPObjectType obj_type, NMPlatformGetRouteFlags flags)
+static NMPlatformError
+ip_route_add (NMPlatform *platform,
+              NMPNlmFlags flags,
+              int addr_family,
+              const NMPlatformIPRoute *route)
 {
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
-	NMPCacheId cache_id;
-	const NMPlatformIPRoute *const* routes;
-	GArray *array;
-	const NMPClass *klass;
-	gboolean with_rtprot_kernel;
-	guint i, len;
-
-	nm_assert (NM_IN_SET (obj_type, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE));
-
-	if (!NM_FLAGS_ANY (flags, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT | NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT))
-		flags |= NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT | NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT;
-
-	klass = nmp_class_from_type (obj_type);
-
-	nmp_cache_id_init_routes_visible (&cache_id,
-	                                  obj_type,
-	                                  NM_FLAGS_HAS (flags, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT),
-	                                  NM_FLAGS_HAS (flags, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT),
-	                                  ifindex);
-
-	routes = (const NMPlatformIPRoute *const*) nmp_cache_lookup_multi (priv->cache, &cache_id, &len);
-
-	array = g_array_sized_new (FALSE, FALSE, klass->sizeof_public, len);
-
-	with_rtprot_kernel = NM_FLAGS_HAS (flags, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_RTPROT_KERNEL);
-	for (i = 0; i < len; i++) {
-		nm_assert (NMP_OBJECT_GET_CLASS (NMP_OBJECT_UP_CAST (routes[i])) == klass);
+	nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
+	NMPObject obj;
 
-		if (   with_rtprot_kernel
-		    || routes[i]->rt_source != NM_IP_CONFIG_SOURCE_RTPROT_KERNEL)
-			g_array_append_vals (array, routes[i], 1);
+	switch (addr_family) {
+	case AF_INET:
+		nmp_object_stackinit (&obj, NMP_OBJECT_TYPE_IP4_ROUTE, (const NMPlatformObject *) route);
+		break;
+	case AF_INET6:
+		nmp_object_stackinit (&obj, NMP_OBJECT_TYPE_IP6_ROUTE, (const NMPlatformObject *) route);
+		break;
+	default:
+		nm_assert_not_reached ();
 	}
-	return array;
-}
 
-static GArray *
-ip4_route_get_all (NMPlatform *platform, int ifindex, NMPlatformGetRouteFlags flags)
-{
-	return ipx_route_get_all (platform, ifindex, NMP_OBJECT_TYPE_IP4_ROUTE, flags);
-}
+	nm_platform_ip_route_normalize (addr_family, NMP_OBJECT_CAST_IP_ROUTE (&obj));
 
-static GArray *
-ip6_route_get_all (NMPlatform *platform, int ifindex, NMPlatformGetRouteFlags flags)
-{
-	return ipx_route_get_all (platform, ifindex, NMP_OBJECT_TYPE_IP6_ROUTE, flags);
-}
-
-static guint32
-ip_route_get_lock_flag (NMPlatformIPRoute *route)
-{
-	return   (((guint32) route->lock_window) << RTAX_WINDOW)
-	       | (((guint32) route->lock_cwnd) << RTAX_CWND)
-	       | (((guint32) route->lock_initcwnd) << RTAX_INITCWND)
-	       | (((guint32) route->lock_initrwnd) << RTAX_INITRWND)
-	       | (((guint32) route->lock_mtu) << RTAX_MTU);
-}
-
-static gboolean
-ip4_route_add (NMPlatform *platform, const NMPlatformIP4Route *route)
-{
-	NMPObject obj_id;
-	nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
-	in_addr_t network;
-
-	network = nm_utils_ip4_address_clear_host_address (route->network, route->plen);
-
-	/* FIXME: take the scope from route into account */
-	nlmsg = _nl_msg_new_route (RTM_NEWROUTE,
-	                           NLM_F_CREATE | NLM_F_REPLACE,
-	                           AF_INET,
-	                           route->ifindex,
-	                           route->rt_source,
-	                           route->gateway ? RT_SCOPE_UNIVERSE : RT_SCOPE_LINK,
-	                           &network,
-	                           route->plen,
-	                           &route->gateway,
-	                           route->metric,
-	                           route->mss,
-	                           route->pref_src ? &route->pref_src : NULL,
-	                           NULL,
-	                           0,
-	                           route->tos,
-	                           route->window,
-	                           route->cwnd,
-	                           route->initcwnd,
-	                           route->initrwnd,
-	                           route->mtu,
-	                           ip_route_get_lock_flag ((NMPlatformIPRoute *) route));
-
-	nmp_object_stackinit_id_ip4_route (&obj_id, route->ifindex, network, route->plen, route->metric);
-	return do_add_addrroute (platform, &obj_id, nlmsg);
+	nlmsg = _nl_msg_new_route (RTM_NEWROUTE, flags & NMP_NLM_FLAG_FMASK, &obj);
+	if (!nlmsg)
+		g_return_val_if_reached (NM_PLATFORM_ERROR_BUG);
+	return do_add_addrroute (platform,
+	                         &obj,
+	                         nlmsg,
+	                         NM_FLAGS_HAS (flags, NMP_NLM_FLAG_SUPPRESS_NETLINK_FAILURE));
 }
 
 static gboolean
-ip6_route_add (NMPlatform *platform, const NMPlatformIP6Route *route)
+ip_route_delete (NMPlatform *platform,
+                 const NMPObject *obj)
 {
-	NMPObject obj_id;
+	nm_auto_nmpobj const NMPObject *obj_keep_alive = NULL;
 	nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
-	struct in6_addr network;
-
-	nm_utils_ip6_address_clear_host_address (&network, &route->network, route->plen);
-
-	/* FIXME: take the scope from route into account */
-	nlmsg = _nl_msg_new_route (RTM_NEWROUTE,
-	                           NLM_F_CREATE | NLM_F_REPLACE,
-	                           AF_INET6,
-	                           route->ifindex,
-	                           route->rt_source,
-	                           IN6_IS_ADDR_UNSPECIFIED (&route->gateway) ? RT_SCOPE_LINK : RT_SCOPE_UNIVERSE,
-	                           &network,
-	                           route->plen,
-	                           &route->gateway,
-	                           route->metric,
-	                           route->mss,
-	                           !IN6_IS_ADDR_UNSPECIFIED (&route->pref_src) ? &route->pref_src : NULL,
-	                           !IN6_IS_ADDR_UNSPECIFIED (&route->src) ? &route->src : NULL,
-	                           route->src_plen,
-	                           route->tos,
-	                           route->window,
-	                           route->cwnd,
-	                           route->initcwnd,
-	                           route->initrwnd,
-	                           route->mtu,
-	                           ip_route_get_lock_flag ((NMPlatformIPRoute *) route));
-
-	nmp_object_stackinit_id_ip6_route (&obj_id, route->ifindex, &network, route->plen, route->metric);
-	return do_add_addrroute (platform, &obj_id, nlmsg);
-}
 
-static gboolean
-ip4_route_delete (NMPlatform *platform, int ifindex, in_addr_t network, guint8 plen, guint32 metric)
-{
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
-	nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
-	NMPObject obj_id;
+	nm_assert (NM_IN_SET (NMP_OBJECT_GET_TYPE (obj), NMP_OBJECT_TYPE_IP4_ROUTE,
+	                                                 NMP_OBJECT_TYPE_IP6_ROUTE));
 
-	network = nm_utils_ip4_address_clear_host_address (network, plen);
+	if (!NMP_OBJECT_IS_STACKINIT (obj))
+		obj_keep_alive = nmp_object_ref (obj);
 
-	nmp_object_stackinit_id_ip4_route (&obj_id, ifindex, network, plen, metric);
+	nlmsg = _nl_msg_new_route (RTM_DELROUTE, 0, obj);
+	if (!nlmsg)
+		g_return_val_if_reached (FALSE);
+	return do_delete_object (platform, obj, nlmsg);
+}
 
-	if (metric == 0) {
-		/* Deleting an IPv4 route with metric 0 does not only delete an exectly matching route.
-		 * If no route with metric 0 exists, it might delete another route to the same destination.
-		 * For nm_platform_ip4_route_delete() we don't want this semantic.
-		 *
-		 * Instead, make sure that we have the most recent state and process all
-		 * delayed actions (including re-reading data from netlink). */
-		delayed_action_handle_all (platform, TRUE);
-
-		if (!nmp_cache_lookup_obj (priv->cache, &obj_id)) {
-			/* hmm... we are about to delete an IP4 route with metric 0. We must only
-			 * send the delete request if such a route really exists. Above we refreshed
-			 * the platform cache, still no such route exists.
-			 *
-			 * Be extra careful and reload the routes. We must be sure that such a
-			 * route doesn't exists, because when we add an IPv4 address, we immediately
-			 * afterwards try to delete the kernel-added device route with metric 0.
-			 * It might be, that we didn't yet get the notification about that route.
-			 *
-			 * FIXME: once our ip4_address_add() is sure that upon return we have
-			 * the latest state from in the platform cache, we might save this
-			 * additional expensive cache-resync. */
-			do_request_one_type (platform, NMP_OBJECT_TYPE_IP4_ROUTE);
+/*****************************************************************************/
 
-			if (!nmp_cache_lookup_obj (priv->cache, &obj_id))
-				return TRUE;
-		}
-	}
+static NMPlatformError
+ip_route_get (NMPlatform *platform,
+              int addr_family,
+              gconstpointer address,
+              int oif_ifindex,
+              NMPObject **out_route)
+{
+	const gboolean is_v4 = (addr_family == AF_INET);
+	const int addr_len = is_v4 ? 4 : 16;
+	int try_count = 0;
+	WaitForNlResponseResult seq_result;
+	int nle;
+	nm_auto_nlmsg NMPObject *route = NULL;
 
-	nlmsg = _nl_msg_new_route (RTM_DELROUTE,
-	                           0,
-	                           AF_INET,
-	                           ifindex,
-	                           NM_IP_CONFIG_SOURCE_UNKNOWN,
-	                           RT_SCOPE_NOWHERE,
-	                           &network,
-	                           plen,
-	                           NULL,
-	                           metric,
-	                           0,
-	                           NULL,
-	                           NULL,
-	                           0,
-	                           0,
-	                           0,
-	                           0,
-	                           0,
-	                           0,
-	                           0,
-	                           0);
-	if (!nlmsg)
-		return FALSE;
+	nm_assert (NM_IS_LINUX_PLATFORM (platform));
+	nm_assert (NM_IN_SET (addr_family, AF_INET, AF_INET6));
+	nm_assert (address);
+
+	do {
+		struct {
+			struct nlmsghdr n;
+			struct rtmsg r;
+			char buf[64];
+		} req = {
+			.n.nlmsg_len = NLMSG_LENGTH (sizeof (struct rtmsg)),
+			.n.nlmsg_flags = NLM_F_REQUEST,
+			.n.nlmsg_type = RTM_GETROUTE,
+			.r.rtm_family = addr_family,
+			.r.rtm_tos = 0,
+			.r.rtm_dst_len = is_v4 ? 32 : 128,
+			.r.rtm_flags = 0x1000 /* RTM_F_LOOKUP_TABLE */,
+		};
 
-	return do_delete_object (platform, &obj_id, nlmsg);
-}
+		g_clear_pointer (&route, nmp_object_unref);
 
-static gboolean
-ip6_route_delete (NMPlatform *platform, int ifindex, struct in6_addr network, guint8 plen, guint32 metric)
-{
-	nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
-	NMPObject obj_id;
+		if (!_nl_addattr_l (&req.n, sizeof (req), RTA_DST, address, addr_len))
+			nm_assert_not_reached ();
 
-	metric = nm_utils_ip6_route_metric_normalize (metric);
-
-	nm_utils_ip6_address_clear_host_address (&network, &network, plen);
-
-	nlmsg = _nl_msg_new_route (RTM_DELROUTE,
-	                           0,
-	                           AF_INET6,
-	                           ifindex,
-	                           NM_IP_CONFIG_SOURCE_UNKNOWN,
-	                           RT_SCOPE_NOWHERE,
-	                           &network,
-	                           plen,
-	                           NULL,
-	                           metric,
-	                           0,
-	                           NULL,
-	                           NULL,
-	                           0,
-	                           0,
-	                           0,
-	                           0,
-	                           0,
-	                           0,
-	                           0,
-	                           0);
-	if (!nlmsg)
-		return FALSE;
+		if (oif_ifindex > 0) {
+			gint32 ii = oif_ifindex;
 
-	nmp_object_stackinit_id_ip6_route (&obj_id, ifindex, &network, plen, metric);
+			if (!_nl_addattr_l (&req.n, sizeof (req), RTA_OIF, &ii, sizeof (ii)))
+				nm_assert_not_reached ();
+		}
 
-	return do_delete_object (platform, &obj_id, nlmsg);
-}
+		seq_result = WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN;
+		nle = _nl_send_nlmsghdr (platform, &req.n, &seq_result, DELAYED_ACTION_RESPONSE_TYPE_ROUTE_GET, &route);
+		if (nle < 0) {
+			_LOGE ("get-route: failure sending netlink request \"%s\" (%d)",
+			       g_strerror (-nle), -nle);
+			return NM_PLATFORM_ERROR_UNSPECIFIED;
+		}
 
-static const NMPlatformIP4Route *
-ip4_route_get (NMPlatform *platform, int ifindex, in_addr_t network, guint8 plen, guint32 metric)
-{
-	NMPObject obj_id;
-	const NMPObject *obj;
+		delayed_action_handle_all (platform, FALSE);
 
-	nmp_object_stackinit_id_ip4_route (&obj_id, ifindex, network, plen, metric);
-	obj = nmp_cache_lookup_obj (NM_LINUX_PLATFORM_GET_PRIVATE (platform)->cache, &obj_id);
-	if (nmp_object_is_visible (obj))
-		return &obj->ip4_route;
-	return NULL;
-}
+		/* Retry, if we failed due to a cache resync. That can happen when the netlink
+		 * socket fills up and we lost the response. */
+	} while (   seq_result == WAIT_FOR_NL_RESPONSE_RESULT_FAILED_RESYNC
+	         && ++try_count < 10);
 
-static const NMPlatformIP6Route *
-ip6_route_get (NMPlatform *platform, int ifindex, struct in6_addr network, guint8 plen, guint32 metric)
-{
-	NMPObject obj_id;
-	const NMPObject *obj;
+	if (seq_result < 0) {
+		/* negative seq_result is an errno from kernel. Map it to negative
+		 * NMPlatformError (which are also errno). */
+		return (NMPlatformError) seq_result;
+	}
 
-	metric = nm_utils_ip6_route_metric_normalize (metric);
+	if (seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK) {
+		if (route) {
+			NM_SET_OUT (out_route, g_steal_pointer (&route));
+			return NM_PLATFORM_ERROR_SUCCESS;
+		}
+		seq_result = WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_UNKNOWN;
+	}
 
-	nmp_object_stackinit_id_ip6_route (&obj_id, ifindex, &network, plen, metric);
-	obj = nmp_cache_lookup_obj (NM_LINUX_PLATFORM_GET_PRIVATE (platform)->cache, &obj_id);
-	if (nmp_object_is_visible (obj))
-		return &obj->ip6_route;
-	return NULL;
+	return NM_PLATFORM_ERROR_UNSPECIFIED;
 }
 
 /*****************************************************************************/
@@ -6351,6 +6313,7 @@ continue_reading:
 		gboolean abort_parsing = FALSE;
 		gboolean process_valid_msg = FALSE;
 		guint32 seq_number;
+		char buf_nlmsghdr[400];
 
 		msg = nlmsg_convert (hdr);
 		if (!msg) {
@@ -6370,8 +6333,8 @@ continue_reading:
 			goto stop;
 		}
 
-		_LOGt ("netlink: recvmsg: new message type %d, seq %u",
-		       hdr->nlmsg_type, hdr->nlmsg_seq);
+		_LOGt ("netlink: recvmsg: new message %s",
+		       _nl_nlmsghdr_to_str (hdr, buf_nlmsghdr, sizeof (buf_nlmsghdr)));
 
 		if (creds)
 			nlmsg_set_creds (msg, creds);
@@ -6512,7 +6475,7 @@ event_handler_read_netlink (NMPlatform *platform, gboolean wait_for_acks)
 
 			nle = event_handler_recvmsgs (platform, TRUE);
 
-			if (nle < 0)
+			if (nle < 0) {
 				switch (nle) {
 				case -NLE_AGAIN:
 					goto after_read;
@@ -6543,6 +6506,7 @@ event_handler_read_netlink (NMPlatform *platform, gboolean wait_for_acks)
 				default:
 					_LOGE ("netlink: read: failed to retrieve incoming events: %s (%d)", nl_geterror (nle), nle);
 					break;
+				}
 			}
 			any = TRUE;
 		}
@@ -6615,19 +6579,19 @@ cache_update_link_udev (NMPlatform *platform,
                         int ifindex,
                         struct udev_device *udevice)
 {
-	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
-	nm_auto_nmpobj NMPObject *obj_cache = NULL;
-	gboolean was_visible;
+	nm_auto_nmpobj const NMPObject *obj_old = NULL;
+	nm_auto_nmpobj const NMPObject *obj_new = NULL;
 	NMPCacheOpsType cache_op;
 
-	cache_op = nmp_cache_update_link_udev (priv->cache, ifindex, udevice, &obj_cache, &was_visible, cache_pre_hook, platform);
+	cache_op = nmp_cache_update_link_udev (nm_platform_get_cache (platform), ifindex, udevice, &obj_old, &obj_new);
 
 	if (cache_op != NMP_CACHE_OPS_UNCHANGED) {
 		nm_auto_pop_netns NMPNetns *netns = NULL;
 
+		cache_on_change (platform, cache_op, obj_old, obj_new);
 		if (!nm_platform_netns_push (platform, &netns))
 			return;
-		do_emit_signal (platform, obj_cache, cache_op, was_visible);
+		nm_platform_cache_update_emit_signal (platform, cache_op, obj_old, obj_new);
 	}
 }
 
@@ -6683,7 +6647,7 @@ udev_device_removed (NMPlatform *platform,
 	if (ifindex <= 0) {
 		const NMPObject *obj;
 
-		obj = nmp_cache_lookup_link_full (NM_LINUX_PLATFORM_GET_PRIVATE (platform)->cache,
+		obj = nmp_cache_lookup_link_full (nm_platform_get_cache (platform),
 		                                  0, NULL, FALSE, NM_LINK_TYPE_NONE, _udev_device_removed_match_link, udevice);
 		if (obj)
 			ifindex = obj->link.ifindex;
@@ -6735,22 +6699,12 @@ static void
 nm_linux_platform_init (NMLinuxPlatform *self)
 {
 	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (self);
-	gboolean use_udev;
-
-	use_udev =    nmp_netns_is_initial ()
-	           && access ("/sys", W_OK) == 0;
 
 	priv->nlh_seq_next = 1;
-	priv->cache = nmp_cache_new (use_udev);
 	priv->delayed_action.list_master_connected = g_ptr_array_new ();
 	priv->delayed_action.list_refresh_link = g_ptr_array_new ();
 	priv->delayed_action.list_wait_for_nl_response = g_array_new (FALSE, TRUE, sizeof (DelayedActionWaitForNlResponseData));
 	priv->wifi_data = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) wifi_utils_deinit);
-
-	if (use_udev) {
-		priv->udev_client = nm_udev_client_new ((const char *[]) { "net", NULL },
-		                                        handle_udev_event, self);
-	}
 }
 
 static void
@@ -6764,6 +6718,11 @@ constructed (GObject *_object)
 
 	nm_assert (!platform->_netns || platform->_netns == nmp_netns_get_current ());
 
+	if (nm_platform_get_use_udev (platform)) {
+		priv->udev_client = nm_udev_client_new ((const char *[]) { "net", NULL },
+		                                        handle_udev_event, platform);
+	}
+
 	_LOGD ("create (%s netns, %s, %s udev)",
 	       !platform->_netns ? "ignore" : "use",
 	       !platform->_netns && nmp_netns_is_initial ()
@@ -6773,7 +6732,7 @@ constructed (GObject *_object)
 	                : nm_sprintf_bufa (100, "in netns[%p]%s",
 	                                   nmp_netns_get_current (),
 	                                   nmp_netns_get_current () == nmp_netns_get_initial () ? "/main" : "")),
-	       nmp_cache_use_udev_get (priv->cache) ? "use" : "no");
+	       nm_platform_get_use_udev (platform) ? "use" : "no");
 
 	priv->nlh = nl_socket_alloc ();
 	g_assert (priv->nlh);
@@ -6874,8 +6833,6 @@ dispose (GObject *object)
 	g_ptr_array_set_size (priv->delayed_action.list_master_connected, 0);
 	g_ptr_array_set_size (priv->delayed_action.list_refresh_link, 0);
 
-	g_clear_pointer (&priv->prune_candidates, g_hash_table_unref);
-
 	G_OBJECT_CLASS (nm_linux_platform_parent_class)->dispose (object);
 }
 
@@ -6884,8 +6841,6 @@ finalize (GObject *object)
 {
 	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (object);
 
-	nmp_cache_free (priv->cache);
-
 	g_ptr_array_unref (priv->delayed_action.list_master_connected);
 	g_ptr_array_unref (priv->delayed_action.list_refresh_link);
 	g_array_unref (priv->delayed_action.list_wait_for_nl_response);
@@ -6919,16 +6874,8 @@ nm_linux_platform_class_init (NMLinuxPlatformClass *klass)
 	platform_class->sysctl_set = sysctl_set;
 	platform_class->sysctl_get = sysctl_get;
 
-	platform_class->link_get = _nm_platform_link_get;
-	platform_class->link_get_by_ifname = _nm_platform_link_get_by_ifname;
-	platform_class->link_get_by_address = _nm_platform_link_get_by_address;
-	platform_class->link_get_all = link_get_all;
 	platform_class->link_add = link_add;
 	platform_class->link_delete = link_delete;
-	platform_class->link_get_type_name = link_get_type_name;
-	platform_class->link_get_unmanaged = link_get_unmanaged;
-
-	platform_class->link_get_lnk = link_get_lnk;
 
 	platform_class->link_refresh = link_refresh;
 
@@ -6940,7 +6887,6 @@ nm_linux_platform_class_init (NMLinuxPlatformClass *klass)
 	platform_class->link_set_noarp = link_set_noarp;
 
 	platform_class->link_get_udi = link_get_udi;
-	platform_class->link_get_udev_device = link_get_udev_device;
 
 	platform_class->link_set_user_ipv6ll_enabled = link_set_user_ipv6ll_enabled;
 	platform_class->link_set_token = link_set_token;
@@ -6948,6 +6894,7 @@ nm_linux_platform_class_init (NMLinuxPlatformClass *klass)
 	platform_class->link_set_address = link_set_address;
 	platform_class->link_get_permanent_address = link_get_permanent_address;
 	platform_class->link_set_mtu = link_set_mtu;
+	platform_class->link_set_name = link_set_name;
 	platform_class->link_set_sriov_num_vfs = link_set_sriov_num_vfs;
 
 	platform_class->link_get_physical_port_id = link_get_physical_port_id;
@@ -6995,26 +6942,16 @@ nm_linux_platform_class_init (NMLinuxPlatformClass *klass)
 	platform_class->link_ipip_add = link_ipip_add;
 	platform_class->link_sit_add = link_sit_add;
 
-	platform_class->ip4_address_get = ip4_address_get;
-	platform_class->ip6_address_get = ip6_address_get;
-	platform_class->ip4_address_get_all = ip4_address_get_all;
-	platform_class->ip6_address_get_all = ip6_address_get_all;
 	platform_class->ip4_address_add = ip4_address_add;
 	platform_class->ip6_address_add = ip6_address_add;
 	platform_class->ip4_address_delete = ip4_address_delete;
 	platform_class->ip6_address_delete = ip6_address_delete;
 
-	platform_class->ip4_route_get = ip4_route_get;
-	platform_class->ip6_route_get = ip6_route_get;
-	platform_class->ip4_route_get_all = ip4_route_get_all;
-	platform_class->ip6_route_get_all = ip6_route_get_all;
-	platform_class->ip4_route_add = ip4_route_add;
-	platform_class->ip6_route_add = ip6_route_add;
-	platform_class->ip4_route_delete = ip4_route_delete;
-	platform_class->ip6_route_delete = ip6_route_delete;
-
-	platform_class->check_support_kernel_extended_ifa_flags = check_support_kernel_extended_ifa_flags;
-	platform_class->check_support_user_ipv6ll = check_support_user_ipv6ll;
+	platform_class->ip_route_add = ip_route_add;
+	platform_class->ip_route_delete = ip_route_delete;
+	platform_class->ip_route_get = ip_route_get;
+
+	platform_class->check_kernel_support = check_kernel_support;
 
 	platform_class->process_events = process_events;
 }
diff --git a/src/platform/nm-linux-platform.h b/src/platform/nm-linux-platform.h
index 6b66ea69..bff6c00c 100644
--- a/src/platform/nm-linux-platform.h
+++ b/src/platform/nm-linux-platform.h
@@ -39,10 +39,4 @@ NMPlatform *nm_linux_platform_new (gboolean log_with_ptr, gboolean netns_support
 
 void nm_linux_platform_setup (void);
 
-struct _NMPCacheId;
-
-const NMPlatformObject *const *nm_linux_platform_lookup (NMPlatform *platform,
-                                                         const struct _NMPCacheId *cache_id,
-                                                         guint *out_len);
-
 #endif /* __NETWORKMANAGER_LINUX_PLATFORM_H__ */
diff --git a/src/platform/nm-platform-private.h b/src/platform/nm-platform-private.h
new file mode 100644
index 00000000..b6c94baa
--- /dev/null
+++ b/src/platform/nm-platform-private.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
+/* nm-platform.c - Handle runtime kernel networking configuration
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NM_PLATFORM_PRIVATE_H__
+#define __NM_PLATFORM_PRIVATE_H__
+
+#include "nm-platform.h"
+#include "nmp-object.h"
+
+NMPCache *nm_platform_get_cache (NMPlatform *self);
+
+#define NMTST_ASSERT_PLATFORM_NETNS_CURRENT(platform) \
+	G_STMT_START { \
+		NMPlatform *_platform = (platform); \
+		\
+		nm_assert (NM_IS_PLATFORM (_platform)); \
+		nm_assert (NM_IN_SET (nm_platform_netns_get (_platform), NULL, nmp_netns_get_current ())); \
+	} G_STMT_END
+
+void nm_platform_cache_update_emit_signal (NMPlatform *platform,
+                                           NMPCacheOpsType cache_op,
+                                           const NMPObject *obj_old,
+                                           const NMPObject *obj_new);
+
+#endif /* __NM_PLATFORM_PRIVATE_H__ */
diff --git a/src/platform/nm-platform.c b/src/platform/nm-platform.c
index a244ff39..ffc4b395 100644
--- a/src/platform/nm-platform.c
+++ b/src/platform/nm-platform.c
@@ -32,12 +32,16 @@
 #include <linux/if_tun.h>
 #include <linux/if_tunnel.h>
 #include <linux/rtnetlink.h>
+#include <libudev.h>
 
 #include "nm-utils.h"
 #include "nm-core-internal.h"
+#include "nm-utils/nm-dedup-multi.h"
+#include "nm-utils/nm-udev-utils.h"
 
 #include "nm-core-utils.h"
 #include "nm-platform-utils.h"
+#include "nm-platform-private.h"
 #include "nmp-object.h"
 #include "nmp-netns.h"
 
@@ -79,12 +83,23 @@ static guint signals[_NM_PLATFORM_SIGNAL_ID_LAST] = { 0 };
 enum {
 	PROP_0,
 	PROP_NETNS_SUPPORT,
+	PROP_USE_UDEV,
 	PROP_LOG_WITH_PTR,
 	LAST_PROP,
 };
 
 typedef struct _NMPlatformPrivate {
+	bool use_udev:1;
 	bool log_with_ptr:1;
+
+	NMPlatformKernelSupportFlags support_checked;
+	NMPlatformKernelSupportFlags support_present;
+
+	guint ip4_dev_route_blacklist_check_id;
+	guint ip4_dev_route_blacklist_gc_timeout_id;
+	GHashTable *ip4_dev_route_blacklist_hash;
+	NMDedupMultiIndex *multi_idx;
+	NMPCache *cache;
 } NMPlatformPrivate;
 
 G_DEFINE_TYPE (NMPlatform, nm_platform, G_TYPE_OBJECT)
@@ -93,6 +108,16 @@ G_DEFINE_TYPE (NMPlatform, nm_platform, G_TYPE_OBJECT)
 
 /*****************************************************************************/
 
+static void _ip4_dev_route_blacklist_schedule (NMPlatform *self);
+
+/*****************************************************************************/
+
+gboolean
+nm_platform_get_use_udev (NMPlatform *self)
+{
+	return NM_PLATFORM_GET_PRIVATE (self)->use_udev;
+}
+
 gboolean
 nm_platform_get_log_with_ptr (NMPlatform *self)
 {
@@ -193,17 +218,18 @@ nm_platform_get ()
 
 /*****************************************************************************/
 
-/**
- * _nm_platform_error_to_string:
- * @error_code: the error code to stringify.
- *
- * Returns: A string representation of the error.
- * For negative numbers, this function interprets
- * the code as -errno.
- * For invalid (positive) numbers it returns NULL.
- */
-NM_UTILS_LOOKUP_STR_DEFINE (_nm_platform_error_to_string, NMPlatformError,
-	NM_UTILS_LOOKUP_DEFAULT ( val < 0 ? g_strerror (- ((int) val)) : NULL ),
+NMDedupMultiIndex *
+nm_platform_get_multi_idx (NMPlatform *self)
+{
+	g_return_val_if_fail (NM_IS_PLATFORM (self), NULL);
+
+	return NM_PLATFORM_GET_PRIVATE (self)->multi_idx;
+}
+
+/*****************************************************************************/
+
+NM_UTILS_LOOKUP_STR_DEFINE_STATIC (_nm_platform_error_to_string, NMPlatformError,
+	NM_UTILS_LOOKUP_DEFAULT (NULL),
 	NM_UTILS_LOOKUP_STR_ITEM (NM_PLATFORM_ERROR_SUCCESS,     "success"),
 	NM_UTILS_LOOKUP_STR_ITEM (NM_PLATFORM_ERROR_BUG,         "bug"),
 	NM_UTILS_LOOKUP_STR_ITEM (NM_PLATFORM_ERROR_UNSPECIFIED, "unspecified"),
@@ -213,35 +239,101 @@ NM_UTILS_LOOKUP_STR_DEFINE (_nm_platform_error_to_string, NMPlatformError,
 	NM_UTILS_LOOKUP_STR_ITEM (NM_PLATFORM_ERROR_NOT_SLAVE,   "not-slave"),
 	NM_UTILS_LOOKUP_STR_ITEM (NM_PLATFORM_ERROR_NO_FIRMWARE, "no-firmware"),
 	NM_UTILS_LOOKUP_STR_ITEM (NM_PLATFORM_ERROR_OPNOTSUPP,   "not-supported"),
+	NM_UTILS_LOOKUP_STR_ITEM (NM_PLATFORM_ERROR_NETLINK,     "netlink"),
+	NM_UTILS_LOOKUP_STR_ITEM (NM_PLATFORM_ERROR_CANT_SET_MTU, "cant-set-mtu"),
 	NM_UTILS_LOOKUP_ITEM_IGNORE (_NM_PLATFORM_ERROR_MININT),
 );
 
-/*****************************************************************************/
-
-gboolean
-nm_platform_check_support_kernel_extended_ifa_flags (NMPlatform *self)
+/**
+ * nm_platform_error_to_string:
+ * @error_code: the error code to stringify.
+ * @buf: (allow-none): buffer
+ * @buf_len: size of buffer
+ *
+ * Returns: A string representation of the error.
+ * For negative numbers, this function interprets
+ * the code as -errno.
+ * For invalid (positive) numbers it returns NULL.
+ */
+const char *
+nm_platform_error_to_string (NMPlatformError error_code, char *buf, gsize buf_len)
 {
-	_CHECK_SELF (self, klass, FALSE);
+	const char *s;
 
-	if (!klass->check_support_kernel_extended_ifa_flags)
-		return FALSE;
+	if (error_code < 0) {
+		int errsv = -((int) error_code);
 
-	return klass->check_support_kernel_extended_ifa_flags (self);
+		nm_utils_to_string_buffer_init (&buf, &buf_len);
+		g_snprintf (buf, buf_len, "%s (%d)", g_strerror (errsv), errsv);
+	} else {
+		s = _nm_platform_error_to_string (error_code);
+		if (s) {
+			if (!buf)
+				return s;
+			g_strlcpy (buf, s, buf_len);
+		} else {
+			nm_utils_to_string_buffer_init (&buf, &buf_len);
+			g_snprintf (buf, buf_len, "(%d)", (int) error_code);
+		}
+	}
+
+	return buf;
 }
 
-gboolean
-nm_platform_check_support_user_ipv6ll (NMPlatform *self)
+NM_UTILS_LOOKUP_STR_DEFINE_STATIC (_nmp_nlm_flag_to_string_lookup, NMPNlmFlags,
+	NM_UTILS_LOOKUP_DEFAULT (NULL),
+	NM_UTILS_LOOKUP_ITEM (NMP_NLM_FLAG_ADD,     "add"),
+	NM_UTILS_LOOKUP_ITEM (NMP_NLM_FLAG_CHANGE,  "change"),
+	NM_UTILS_LOOKUP_ITEM (NMP_NLM_FLAG_REPLACE, "replace"),
+	NM_UTILS_LOOKUP_ITEM (NMP_NLM_FLAG_PREPEND, "prepend"),
+	NM_UTILS_LOOKUP_ITEM (NMP_NLM_FLAG_APPEND,  "append"),
+	NM_UTILS_LOOKUP_ITEM (NMP_NLM_FLAG_TEST,    "test"),
+	NM_UTILS_LOOKUP_ITEM_IGNORE (NMP_NLM_FLAG_F_APPEND),
+	NM_UTILS_LOOKUP_ITEM_IGNORE (NMP_NLM_FLAG_FMASK),
+	NM_UTILS_LOOKUP_ITEM_IGNORE (NMP_NLM_FLAG_SUPPRESS_NETLINK_FAILURE),
+);
+
+#define _nmp_nlm_flag_to_string(flags) \
+	({ \
+		NMPNlmFlags _flags = (flags); \
+		\
+		_nmp_nlm_flag_to_string_lookup (flags) ?: nm_sprintf_bufa (100, "new[0x%x]", (unsigned) _flags); \
+	})
+
+/*****************************************************************************/
+
+NMPlatformKernelSupportFlags
+nm_platform_check_kernel_support (NMPlatform *self,
+                                  NMPlatformKernelSupportFlags request_flags)
 {
-	static int supported = -1;
+	NMPlatformPrivate *priv;
 
-	_CHECK_SELF (self, klass, FALSE);
+	_CHECK_SELF (self, klass, TRUE);
 
-	if (!klass->check_support_user_ipv6ll)
-		return FALSE;
+	priv = NM_PLATFORM_GET_PRIVATE (self);
+
+	/* we cache the response from subclasses and only request it once.
+	 * This probably gives better performance, but more importantly,
+	 * we are guaranteed that the answer for a certain request_flag
+	 * is always the same. */
+	if (G_UNLIKELY (!NM_FLAGS_ALL (priv->support_checked, request_flags))) {
+		NMPlatformKernelSupportFlags checked, response;
 
-	if (supported < 0)
-		supported = klass->check_support_user_ipv6ll (self) ? 1 : 0;
-	return !!supported;
+		checked = request_flags & ~priv->support_checked;
+		nm_assert (checked);
+
+		if (klass->check_kernel_support)
+			response = klass->check_kernel_support (self, checked);
+		else {
+			/* fake platform. Pretend no support for anything. */
+			response = 0;
+		}
+
+		priv->support_checked |= checked;
+		priv->support_present = (priv->support_present & ~checked) | (response & checked);
+	}
+
+	return priv->support_present & request_flags;
 }
 
 /**
@@ -322,6 +414,7 @@ nm_platform_sysctl_set_ip6_hop_limit_safe (NMPlatform *self, const char *iface,
 {
 	const char *path;
 	gint64 cur;
+	char buf[NM_UTILS_SYSCTL_IP_CONF_PATH_BUFSIZE];
 
 	_CHECK_SELF (self, klass, FALSE);
 
@@ -333,7 +426,7 @@ nm_platform_sysctl_set_ip6_hop_limit_safe (NMPlatform *self, const char *iface,
 	if (value < 10)
 		return FALSE;
 
-	path = nm_utils_ip6_property_path (iface, "hop_limit");
+	path = nm_utils_sysctl_ip_conf_path (AF_INET6, buf, iface, "hop_limit");
 	cur = nm_platform_sysctl_get_int_checked (self, NMP_SYSCTL_PATHID_ABSOLUTE (path), 10, 1, G_MAXINT32, -1);
 
 	/* only allow increasing the hop-limit to avoid DOS by an attacker
@@ -440,8 +533,8 @@ _link_get_all_presort (gconstpointer  p_a,
                        gconstpointer  p_b,
                        gpointer       sort_by_name)
 {
-	const NMPlatformLink *a = p_a;
-	const NMPlatformLink *b = p_b;
+	const NMPlatformLink *a = NMP_OBJECT_CAST_LINK (*((const NMPObject **) p_a));
+	const NMPlatformLink *b = NMP_OBJECT_CAST_LINK (*((const NMPObject **) p_b));
 
 	/* Loopback always first */
 	if (a->ifindex == 1)
@@ -463,43 +556,56 @@ _link_get_all_presort (gconstpointer  p_a,
 
 /**
  * nm_platform_link_get_all:
- * self: platform instance
+ * @self: platform instance
+ * @sort_by_name: whether to sort by name or ifindex.
  *
  * Retrieve a snapshot of configuration for all links at once. The result is
- * owned by the caller and should be freed with g_array_unref().
+ * owned by the caller and should be freed with g_ptr_array_unref().
  */
-GArray *
+GPtrArray *
 nm_platform_link_get_all (NMPlatform *self, gboolean sort_by_name)
 {
-	GArray *links, *result;
-	guint i, j, nresult;
-	GHashTable *unseen;
-	NMPlatformLink *item;
+	gs_unref_ptrarray GPtrArray *links = NULL;
+	GPtrArray *result;
+	guint i, nresult;
+	gs_unref_hashtable GHashTable *unseen = NULL;
+	const NMPlatformLink *item;
+	NMPLookup lookup;
 
 	_CHECK_SELF (self, klass, NULL);
 
-	links = klass->link_get_all (self);
+	nmp_lookup_init_obj_type (&lookup, NMP_OBJECT_TYPE_LINK);
+	links = nm_dedup_multi_objs_to_ptr_array_head (nm_platform_lookup (self, &lookup),
+	                                               NULL, NULL);
+	if (!links)
+		return NULL;
+
+	for (i = 0; i < links->len; ) {
+		if (!nmp_object_is_visible (links->pdata[i]))
+			g_ptr_array_remove_index_fast (links, i);
+		else
+			i++;
+	}
 
-	if (!links || links->len == 0)
-		return links;
+	if (links->len == 0)
+		return NULL;
 
 	/* first sort the links by their ifindex or name. Below we will sort
 	 * further by moving children/slaves to the end. */
-	g_array_sort_with_data (links, _link_get_all_presort, GINT_TO_POINTER (sort_by_name));
+	g_ptr_array_sort_with_data (links, _link_get_all_presort, GINT_TO_POINTER (sort_by_name));
 
 	unseen = g_hash_table_new (g_direct_hash, g_direct_equal);
 	for (i = 0; i < links->len; i++) {
-		item = &g_array_index (links, NMPlatformLink, i);
-
+		item = NMP_OBJECT_CAST_LINK (links->pdata[i]);
 		nm_assert (item->ifindex > 0);
 		if (!nm_g_hash_table_insert (unseen, GINT_TO_POINTER (item->ifindex), NULL))
 			nm_assert_not_reached ();
 	}
 
-#ifndef G_DISABLE_ASSERT
+#if NM_MORE_ASSERTS
 	/* Ensure that link_get_all returns a consistent and valid result. */
 	for (i = 0; i < links->len; i++) {
-		item = &g_array_index (links, NMPlatformLink, i);
+		item = NMP_OBJECT_CAST_LINK (links->pdata[i]);
 
 		if (!item->ifindex)
 			continue;
@@ -519,54 +625,75 @@ nm_platform_link_get_all (NMPlatform *self, gboolean sort_by_name)
 #endif
 
 	/* Re-order the links list such that children/slaves come after all ancestors */
-	nresult = g_hash_table_size (unseen);
-	result = g_array_sized_new (TRUE, TRUE, sizeof (NMPlatformLink), nresult);
-	g_array_set_size (result, nresult);
+	nm_assert (g_hash_table_size (unseen) == links->len);
+	nresult = links->len;
+	result = g_ptr_array_new_full (nresult, (GDestroyNotify) nmp_object_unref);
 
-	j = 0;
-	do {
+	while (TRUE) {
 		gboolean found_something = FALSE;
 		guint first_idx = G_MAXUINT;
 
 		for (i = 0; i < links->len; i++) {
-			item = &g_array_index (links, NMPlatformLink, i);
+			item = NMP_OBJECT_CAST_LINK (links->pdata[i]);
 
-			if (!item->ifindex)
+			if (!item)
 				continue;
 
-			if (first_idx == G_MAXUINT)
-				first_idx = i;
-
 			g_assert (g_hash_table_contains (unseen, GINT_TO_POINTER (item->ifindex)));
 
 			if (item->master > 0 && g_hash_table_contains (unseen, GINT_TO_POINTER (item->master)))
-				continue;
+				goto skip;
 			if (item->parent > 0 && g_hash_table_contains (unseen, GINT_TO_POINTER (item->parent)))
-				continue;
+				goto skip;
 
 			g_hash_table_remove (unseen, GINT_TO_POINTER (item->ifindex));
-			g_array_index (result, NMPlatformLink, j++) = *item;
-			item->ifindex = 0;
+			g_ptr_array_add (result, links->pdata[i]);
+			links->pdata[i] = NULL;
 			found_something = TRUE;
+			continue;
+skip:
+			if (first_idx == G_MAXUINT)
+				first_idx = i;
 		}
 
-		if (!found_something) {
+		if (found_something) {
+			if (first_idx == G_MAXUINT)
+				break;
+		} else {
+			nm_assert (first_idx != G_MAXUINT);
 			/* There is a loop, pop the first (remaining) element from the list.
 			 * This can happen for veth pairs where each peer is parent of the other end. */
-			item = &g_array_index (links, NMPlatformLink, first_idx);
-
+			item = NMP_OBJECT_CAST_LINK (links->pdata[first_idx]);
 			g_hash_table_remove (unseen, GINT_TO_POINTER (item->ifindex));
-			g_array_index (result, NMPlatformLink, j++) = *item;
-			item->ifindex = 0;
+			g_ptr_array_add (result, links->pdata[first_idx]);
+			links->pdata[first_idx] = NULL;
 		}
-	} while (j < nresult);
-
-	g_hash_table_destroy (unseen);
-	g_array_free (links, TRUE);
+		nm_assert (result->len < nresult);
+	}
+	nm_assert (result->len == nresult);
 
 	return result;
 }
 
+/*****************************************************************************/
+
+const NMPObject *
+nm_platform_link_get_obj (NMPlatform *self,
+                          int ifindex,
+                          gboolean visible_only)
+{
+	const NMPObject *obj_cache;
+
+	obj_cache = nmp_cache_lookup_link (nm_platform_get_cache (self), ifindex);
+	if (   !obj_cache
+	    || (   visible_only
+	        && !nmp_object_is_visible (obj_cache)))
+		return NULL;
+	return obj_cache;
+}
+
+/*****************************************************************************/
+
 /**
  * nm_platform_link_get:
  * @self: platform instance
@@ -577,16 +704,20 @@ nm_platform_link_get_all (NMPlatform *self, gboolean sort_by_name)
  * Returns: %NULL, if such a link exists or the internal
  * platform link object. Do not modify the returned value.
  * Also, be aware that any subsequent platform call might
- * invalidated/modify the returned instance.
+ * invalidate/modify the returned instance.
  **/
 const NMPlatformLink *
 nm_platform_link_get (NMPlatform *self, int ifindex)
 {
+	const NMPObject *obj;
+
 	_CHECK_SELF (self, klass, NULL);
 
-	if (ifindex > 0)
-		return klass->link_get (self, ifindex);
-	return NULL;
+	if (ifindex <= 0)
+		return NULL;
+
+	obj = nm_platform_link_get_obj (self, ifindex, TRUE);
+	return NMP_OBJECT_CAST_LINK (obj);
 }
 
 /**
@@ -599,11 +730,27 @@ nm_platform_link_get (NMPlatform *self, int ifindex)
 const NMPlatformLink *
 nm_platform_link_get_by_ifname (NMPlatform *self, const char *ifname)
 {
+	const NMPObject *obj;
+
 	_CHECK_SELF (self, klass, NULL);
 
-	if (ifname && *ifname)
-		return klass->link_get_by_ifname (self, ifname);
-	return NULL;
+	if (!ifname || !*ifname)
+		return NULL;
+
+	obj = nmp_cache_lookup_link_full (nm_platform_get_cache (self),
+	                                  0, ifname, TRUE, NM_LINK_TYPE_NONE, NULL, NULL);
+	return NMP_OBJECT_CAST_LINK (obj);
+}
+
+struct _nm_platform_link_get_by_address_data {
+	gconstpointer address;
+	guint8 length;
+};
+
+static gboolean
+_nm_platform_link_get_by_address_match_link (const NMPObject *obj, struct _nm_platform_link_get_by_address_data *d)
+{
+	return obj->link.addr.len == d->length && !memcmp (obj->link.addr.data, d->address, d->length);
 }
 
 /**
@@ -620,15 +767,26 @@ nm_platform_link_get_by_address (NMPlatform *self,
                                  gconstpointer address,
                                  size_t length)
 {
+	const NMPObject *obj;
+	struct _nm_platform_link_get_by_address_data d = {
+		.address = address,
+		.length = length,
+	};
+
 	_CHECK_SELF (self, klass, NULL);
 
-	g_return_val_if_fail (length == 0 || address, NULL);
-	if (length > 0) {
-		if (length > NM_UTILS_HWADDR_LEN_MAX)
-			g_return_val_if_reached (NULL);
-		return klass->link_get_by_address (self, address, length);
-	}
-	return NULL;
+	if (length == 0)
+		return NULL;
+
+	if (length > NM_UTILS_HWADDR_LEN_MAX)
+		g_return_val_if_reached (NULL);
+	if (!address)
+		g_return_val_if_reached (NULL);
+
+	obj = nmp_cache_lookup_link_full (nm_platform_get_cache (self),
+	                                  0, NULL, TRUE, NM_LINK_TYPE_NONE,
+	                                  (NMPObjectMatchFn) _nm_platform_link_get_by_address_match_link, &d);
+	return NMP_OBJECT_CAST_LINK (obj);
 }
 
 static NMPlatformError
@@ -662,6 +820,7 @@ _link_add_check_existing (NMPlatform *self, const char *name, NMLinkType type, c
  * @self: platform instance
  * @name: Interface name
  * @type: Interface type
+ * @veth_peer: For veths, the peer name
  * @address: (allow-none): set the mac address of the link
  * @address_len: the length of the @address
  * @out_link: on success, the link object
@@ -680,27 +839,51 @@ static NMPlatformError
 nm_platform_link_add (NMPlatform *self,
                       const char *name,
                       NMLinkType type,
+                      const char *veth_peer,
                       const void *address,
                       size_t address_len,
                       const NMPlatformLink **out_link)
 {
 	NMPlatformError plerr;
+	char addr_buf[NM_UTILS_HWADDR_LEN_MAX * 3];
 
 	_CHECK_SELF (self, klass, NM_PLATFORM_ERROR_BUG);
 
 	g_return_val_if_fail (name, NM_PLATFORM_ERROR_BUG);
-	g_return_val_if_fail ( (address != NULL) ^ (address_len == 0) , NM_PLATFORM_ERROR_BUG);
+	g_return_val_if_fail ((address != NULL) ^ (address_len == 0) , NM_PLATFORM_ERROR_BUG);
+	g_return_val_if_fail (address_len <= NM_UTILS_HWADDR_LEN_MAX, NM_PLATFORM_ERROR_BUG);
+	g_return_val_if_fail ((!!veth_peer) == (type == NM_LINK_TYPE_VETH), NM_PLATFORM_ERROR_BUG);
 
 	plerr = _link_add_check_existing (self, name, type, out_link);
 	if (plerr != NM_PLATFORM_ERROR_SUCCESS)
 		return plerr;
 
-	_LOGD ("link: adding %s '%s'", nm_link_type_to_string (type), name);
-	if (!klass->link_add (self, name, type, address, address_len, out_link))
+	_LOGD ("link: adding link '%s' of type '%s' (%d)"
+	       "%s%s" /* address */
+	       "%s%s" /* veth peer */
+	       "",
+	       name,
+	       nm_link_type_to_string (type),
+	       (int) type,
+	       address ? ", address: " : "",
+	       address ? nm_utils_hwaddr_ntoa_buf (address, address_len, FALSE, addr_buf, sizeof (addr_buf)) : "",
+	       veth_peer ? ", veth-peer: " : "",
+	       veth_peer ?: "");
+
+	if (!klass->link_add (self, name, type, veth_peer, address, address_len, out_link))
 		return NM_PLATFORM_ERROR_UNSPECIFIED;
 	return NM_PLATFORM_ERROR_SUCCESS;
 }
 
+NMPlatformError
+nm_platform_link_veth_add (NMPlatform *self,
+                            const char *name,
+                            const char *peer,
+                            const NMPlatformLink **out_link)
+{
+	return nm_platform_link_add (self, name, NM_LINK_TYPE_VETH, peer, NULL, 0, out_link);
+}
+
 /**
  * nm_platform_link_dummy_add:
  * @self: platform instance
@@ -714,7 +897,7 @@ nm_platform_link_dummy_add (NMPlatform *self,
                             const char *name,
                             const NMPlatformLink **out_link)
 {
-	return nm_platform_link_add (self, name, NM_LINK_TYPE_DUMMY, NULL, 0, out_link);
+	return nm_platform_link_add (self, name, NM_LINK_TYPE_DUMMY, NULL, NULL, 0, out_link);
 }
 
 /**
@@ -846,9 +1029,26 @@ nm_platform_link_get_type (NMPlatform *self, int ifindex)
 const char *
 nm_platform_link_get_type_name (NMPlatform *self, int ifindex)
 {
+	const NMPObject *obj;
+
 	_CHECK_SELF (self, klass, NULL);
 
-	return klass->link_get_type_name (self, ifindex);
+	obj = nm_platform_link_get_obj (self, ifindex, TRUE);
+
+	if (!obj)
+		return NULL;
+
+	if (obj->link.type != NM_LINK_TYPE_UNKNOWN) {
+		/* We could detect the @link_type. In this case the function returns
+		 * our internel module names, which differs from rtnl_link_get_type():
+		 *   - NM_LINK_TYPE_INFINIBAND (gives "infiniband", instead of "ipoib")
+		 *   - NM_LINK_TYPE_TAP (gives "tap", instead of "tun").
+		 * Note that this functions is only used by NMDeviceGeneric to
+		 * set type_description. */
+		return nm_link_type_to_string (obj->link.type);
+	}
+	/* Link type not detected. Fallback to rtnl_link_get_type()/IFLA_INFO_KIND. */
+	return obj->link.kind ?: "unknown";
 }
 
 /**
@@ -863,11 +1063,26 @@ nm_platform_link_get_type_name (NMPlatform *self, int ifindex)
 gboolean
 nm_platform_link_get_unmanaged (NMPlatform *self, int ifindex, gboolean *unmanaged)
 {
+	const NMPObject *link;
+	struct udev_device *udevice = NULL;
+	const char *uproperty;
+
 	_CHECK_SELF (self, klass, FALSE);
 
-	if (klass->link_get_unmanaged)
-		return klass->link_get_unmanaged (self, ifindex, unmanaged);
-	return FALSE;
+	link = nmp_cache_lookup_link (nm_platform_get_cache (self), ifindex);
+	if (!link)
+		return FALSE;
+
+	udevice = link->_link.udev.device;
+	if (!udevice)
+		return FALSE;
+
+	uproperty = udev_device_get_property_value (udevice, "NM_UNMANAGED");
+	if (!uproperty)
+		return FALSE;
+
+	*unmanaged = nm_udev_utils_property_as_boolean (uproperty);
+	return TRUE;
 }
 
 /**
@@ -1013,13 +1228,14 @@ nm_platform_link_get_udi (NMPlatform *self, int ifindex)
 struct udev_device *
 nm_platform_link_get_udev_device (NMPlatform *self, int ifindex)
 {
+	const NMPObject *obj_cache;
+
 	_CHECK_SELF (self, klass, FALSE);
 
 	g_return_val_if_fail (ifindex >= 0, NULL);
 
-	if (klass->link_get_udev_device)
-		return klass->link_get_udev_device (self, ifindex);
-	return NULL;
+	obj_cache = nm_platform_link_get_obj (self, ifindex, FALSE);
+	return obj_cache ? obj_cache->_link.udev.device : NULL;
 }
 
 /**
@@ -1284,7 +1500,7 @@ nm_platform_link_set_noarp (NMPlatform *self, int ifindex)
  *
  * Set interface MTU.
  */
-gboolean
+NMPlatformError
 nm_platform_link_set_mtu (NMPlatform *self, int ifindex, guint32 mtu)
 {
 	_CHECK_SELF (self, klass, FALSE);
@@ -1315,6 +1531,30 @@ nm_platform_link_get_mtu (NMPlatform *self, int ifindex)
 }
 
 /**
+ * nm_platform_link_set_name:
+ * @self: platform instance
+ * @ifindex: Interface index
+ * @name: The new interface name
+ *
+ * Set interface name.
+ */
+gboolean
+nm_platform_link_set_name (NMPlatform *self, int ifindex, const char *name)
+{
+	_CHECK_SELF (self, klass, FALSE);
+
+	g_return_val_if_fail (ifindex >= 0, FALSE);
+	g_return_val_if_fail (name, FALSE);
+
+	_LOGD ("link: setting '%s' (%d) name %s", nm_platform_link_get_name (self, ifindex), ifindex, name);
+
+	if (strlen (name) + 1 > IFNAMSIZ)
+		return FALSE;
+
+	return klass->link_set_name (self, ifindex, name);
+}
+
+/**
  * nm_platform_link_get_physical_port_id:
  * @self: platform instance
  * @ifindex: Interface index
@@ -1465,7 +1705,7 @@ nm_platform_link_release (NMPlatform *self, int master, int slave)
  * @self: platform instance
  * @slave: Interface index of the slave.
  *
- * Returns: Interfase index of the slave's master.
+ * Returns: Interface index of the slave's master.
  */
 int
 nm_platform_link_get_master (NMPlatform *self, int slave)
@@ -1519,13 +1759,28 @@ nm_platform_link_can_assume (NMPlatform *self, int ifindex)
 const NMPObject *
 nm_platform_link_get_lnk (NMPlatform *self, int ifindex, NMLinkType link_type, const NMPlatformLink **out_link)
 {
+	const NMPObject *obj;
+
 	_CHECK_SELF (self, klass, FALSE);
 
 	NM_SET_OUT (out_link, NULL);
 
 	g_return_val_if_fail (ifindex > 0, NULL);
 
-	return klass->link_get_lnk (self, ifindex, link_type, out_link);
+	obj = nm_platform_link_get_obj (self, ifindex, TRUE);
+	if (!obj)
+		return NULL;
+
+	NM_SET_OUT (out_link, &obj->link);
+
+	if (!obj->_link.netlink.lnk)
+		return NULL;
+	if (   link_type != NM_LINK_TYPE_NONE
+	    && (   link_type != obj->link.type
+	        || link_type != NMP_OBJECT_GET_CLASS (obj->_link.netlink.lnk)->lnk_link_type))
+		return NULL;
+
+	return obj->_link.netlink.lnk;
 }
 
 static gconstpointer
@@ -1616,7 +1871,7 @@ nm_platform_link_bridge_add (NMPlatform *self,
                              size_t address_len,
                              const NMPlatformLink **out_link)
 {
-	return nm_platform_link_add (self, name, NM_LINK_TYPE_BRIDGE, address, address_len, out_link);
+	return nm_platform_link_add (self, name, NM_LINK_TYPE_BRIDGE, NULL, address, address_len, out_link);
 }
 
 /**
@@ -1632,7 +1887,7 @@ nm_platform_link_bond_add (NMPlatform *self,
                            const char *name,
                            const NMPlatformLink **out_link)
 {
-	return nm_platform_link_add (self, name, NM_LINK_TYPE_BOND, NULL, 0, out_link);
+	return nm_platform_link_add (self, name, NM_LINK_TYPE_BOND, NULL, NULL, 0, out_link);
 }
 
 /**
@@ -1648,7 +1903,7 @@ nm_platform_link_team_add (NMPlatform *self,
                            const char *name,
                            const NMPlatformLink **out_link)
 {
-	return nm_platform_link_add (self, name, NM_LINK_TYPE_TEAM, NULL, 0, out_link);
+	return nm_platform_link_add (self, name, NM_LINK_TYPE_TEAM, NULL, NULL, 0, out_link);
 }
 
 /**
@@ -2617,6 +2872,82 @@ nm_platform_ethtool_get_link_settings (NMPlatform *self, int ifindex, gboolean *
 
 /*****************************************************************************/
 
+const NMDedupMultiHeadEntry *
+nm_platform_lookup_all (NMPlatform *platform,
+                        NMPCacheIdType cache_id_type,
+                        const NMPObject *obj)
+{
+	return nmp_cache_lookup_all (nm_platform_get_cache (platform),
+	                             cache_id_type,
+	                             obj);
+}
+
+const NMDedupMultiEntry *
+nm_platform_lookup_entry (NMPlatform *platform,
+                          NMPCacheIdType cache_id_type,
+                          const NMPObject *obj)
+{
+	return nmp_cache_lookup_entry_with_idx_type (nm_platform_get_cache (platform),
+	                                             cache_id_type,
+	                                             obj);
+}
+
+const NMDedupMultiHeadEntry *
+nm_platform_lookup (NMPlatform *self,
+                    const NMPLookup *lookup)
+{
+	return nmp_cache_lookup (nm_platform_get_cache (self),
+	                         lookup);
+}
+
+gboolean
+nm_platform_lookup_predicate_routes_main (const NMPObject *obj,
+                                          gpointer user_data)
+{
+	nm_assert (NM_IN_SET (NMP_OBJECT_GET_TYPE (obj), NMP_OBJECT_TYPE_IP4_ROUTE,
+	                                                 NMP_OBJECT_TYPE_IP6_ROUTE));
+	return nm_platform_route_table_is_main (obj->ip_route.table_coerced);
+}
+
+gboolean
+nm_platform_lookup_predicate_routes_main_skip_rtprot_kernel (const NMPObject *obj,
+                                                             gpointer user_data)
+{
+	nm_assert (NM_IN_SET (NMP_OBJECT_GET_TYPE (obj), NMP_OBJECT_TYPE_IP4_ROUTE,
+	                                                 NMP_OBJECT_TYPE_IP6_ROUTE));
+	return    nm_platform_route_table_is_main (obj->ip_route.table_coerced)
+	       && obj->ip_route.rt_source != NM_IP_CONFIG_SOURCE_RTPROT_KERNEL;
+}
+
+/**
+ * nm_platform_lookup_clone:
+ * @self:
+ * @lookup:
+ * @predicate: if given, only objects for which @predicate returns %TRUE are included
+ *   in the result.
+ * @user_data: user data for @predicate
+ *
+ * Returns the result of lookup in a GPtrArray. The result array contains
+ * references objects from the cache, it's destroy function will unref them.
+ *
+ * The user must unref the GPtrArray, which will also unref the NMPObject
+ * elements.
+ *
+ * The elements in the array *must* not be modified.
+ *
+ * Returns: the result of the lookup.
+ */
+GPtrArray *
+nm_platform_lookup_clone (NMPlatform *self,
+                          const NMPLookup *lookup,
+                          NMPObjectPredicateFunc predicate,
+                          gpointer user_data)
+{
+	return nm_dedup_multi_objs_to_ptr_array_head (nm_platform_lookup (self, lookup),
+	                                              (NMDedupMultiFcnSelectPredicate) predicate,
+	                                              user_data);
+}
+
 void
 nm_platform_ip4_address_set_addr (NMPlatformIP4Address *addr, in_addr_t address, guint8 plen)
 {
@@ -2636,26 +2967,6 @@ nm_platform_ip6_address_get_peer (const NMPlatformIP6Address *addr)
 	return &addr->peer_address;
 }
 
-GArray *
-nm_platform_ip4_address_get_all (NMPlatform *self, int ifindex)
-{
-	_CHECK_SELF (self, klass, NULL);
-
-	g_return_val_if_fail (ifindex > 0, NULL);
-
-	return klass->ip4_address_get_all (self, ifindex);
-}
-
-GArray *
-nm_platform_ip6_address_get_all (NMPlatform *self, int ifindex)
-{
-	_CHECK_SELF (self, klass, NULL);
-
-	g_return_val_if_fail (ifindex > 0, NULL);
-
-	return klass->ip6_address_get_all (self, ifindex);
-}
-
 gboolean
 nm_platform_ip4_address_add (NMPlatform *self,
                              int ifindex,
@@ -2768,54 +3079,41 @@ nm_platform_ip6_address_delete (NMPlatform *self, int ifindex, struct in6_addr a
 const NMPlatformIP4Address *
 nm_platform_ip4_address_get (NMPlatform *self, int ifindex, in_addr_t address, guint8 plen, guint32 peer_address)
 {
+	NMPObject obj_id;
+	const NMPObject *obj;
+
 	_CHECK_SELF (self, klass, NULL);
 
 	g_return_val_if_fail (plen <= 32, NULL);
 
-	return klass->ip4_address_get (self, ifindex, address, plen, peer_address);
+	nmp_object_stackinit_id_ip4_address (&obj_id, ifindex, address, plen, peer_address);
+	obj = nmp_cache_lookup_obj (nm_platform_get_cache (self), &obj_id);
+	nm_assert (!obj || nmp_object_is_visible (obj));
+	return NMP_OBJECT_CAST_IP4_ADDRESS (obj);
 }
 
 const NMPlatformIP6Address *
-nm_platform_ip6_address_get (NMPlatform *self, int ifindex, struct in6_addr address, guint8 plen)
+nm_platform_ip6_address_get (NMPlatform *self, int ifindex, struct in6_addr address)
 {
-	_CHECK_SELF (self, klass, NULL);
+	NMPObject obj_id;
+	const NMPObject *obj;
 
-	g_return_val_if_fail (plen <= 128, NULL);
-
-	return klass->ip6_address_get (self, ifindex, address, plen);
-}
-
-static const NMPlatformIP4Address *
-array_contains_ip4_address (const GArray *addresses, const NMPlatformIP4Address *address, gint32 now)
-{
-	guint len = addresses ? addresses->len : 0;
-	guint i;
-
-	for (i = 0; i < len; i++) {
-		const NMPlatformIP4Address *candidate = &g_array_index (addresses, NMPlatformIP4Address, i);
-
-		if (   candidate->address == address->address
-		    && candidate->plen == address->plen
-		    && ((candidate->peer_address ^ address->peer_address) & nm_utils_ip4_prefix_to_netmask (address->plen)) == 0) {
-			guint32 lifetime, preferred;
-
-			if (nm_utils_lifetime_get (candidate->timestamp, candidate->lifetime, candidate->preferred,
-			                           now, &lifetime, &preferred))
-				return candidate;
-		}
-	}
+	_CHECK_SELF (self, klass, NULL);
 
-	return NULL;
+	nmp_object_stackinit_id_ip6_address (&obj_id, ifindex, &address);
+	obj = nmp_cache_lookup_obj (nm_platform_get_cache (self), &obj_id);
+	nm_assert (!obj || nmp_object_is_visible (obj));
+	return NMP_OBJECT_CAST_IP6_ADDRESS (obj);
 }
 
 static gboolean
-array_contains_ip6_address (const GArray *addresses, const NMPlatformIP6Address *address, gint32 now)
+array_contains_ip6_address (const GPtrArray *addresses, const NMPlatformIP6Address *address, gint32 now)
 {
 	guint len = addresses ? addresses->len : 0;
 	guint i;
 
 	for (i = 0; i < len; i++) {
-		NMPlatformIP6Address *candidate = &g_array_index (addresses, NMPlatformIP6Address, i);
+		NMPlatformIP6Address *candidate = NMP_OBJECT_CAST_IP6_ADDRESS (addresses->pdata[i]);
 
 		if (IN6_ARE_ADDR_EQUAL (&candidate->address, &address->address) && candidate->plen == address->plen) {
 			guint32 lifetime, preferred;
@@ -2830,69 +3128,100 @@ array_contains_ip6_address (const GArray *addresses, const NMPlatformIP6Address
 }
 
 static gboolean
-_ptr_inside_ip4_addr_array (const GArray *array, gconstpointer needle)
+ip4_addr_subnets_is_plain_address (const GPtrArray *addresses, gconstpointer needle)
 {
-	return    needle >= (gconstpointer) &g_array_index (array, const NMPlatformIP4Address, 0)
-	       && needle <  (gconstpointer) &g_array_index (array, const NMPlatformIP4Address, array->len);
+	return    needle >= (gconstpointer) &addresses->pdata[0]
+	       && needle <  (gconstpointer) &addresses->pdata[addresses->len];
+}
+
+static const NMPObject **
+ip4_addr_subnets_addr_list_get (const GPtrArray *addr_list, guint idx)
+{
+	nm_assert (addr_list);
+	nm_assert (addr_list->len > 1);
+	nm_assert (idx < addr_list->len);
+	nm_assert (addr_list->pdata[idx]);
+	nm_assert (   !(*((gpointer *) addr_list->pdata[idx]))
+	           || NMP_OBJECT_CAST_IP4_ADDRESS (*((gpointer *) addr_list->pdata[idx])));
+	nm_assert (idx == 0 || ip4_addr_subnets_addr_list_get (addr_list, idx - 1));
+	return addr_list->pdata[idx];
 }
 
 static void
-ip4_addr_subnets_destroy_index (GHashTable *ht, const GArray *addresses)
+ip4_addr_subnets_destroy_index (GHashTable *subnets, const GPtrArray *addresses)
 {
 	GHashTableIter iter;
 	gpointer p;
 
-	g_hash_table_iter_init (&iter, ht);
+	if (!subnets)
+		return;
 
+	g_hash_table_iter_init (&iter, subnets);
 	while (g_hash_table_iter_next (&iter, NULL, &p)) {
-		if (!_ptr_inside_ip4_addr_array (addresses, p)) {
+		if (!ip4_addr_subnets_is_plain_address (addresses, p))
 			g_ptr_array_free ((GPtrArray *) p, TRUE);
-		}
 	}
 
-	g_hash_table_unref (ht);
+	g_hash_table_unref (subnets);
 }
 
 static GHashTable *
-ip4_addr_subnets_build_index (const GArray *addresses, gboolean consider_flags)
+ip4_addr_subnets_build_index (const GPtrArray *addresses,
+                              gboolean consider_flags,
+                              gboolean full_index)
 {
-	const NMPlatformIP4Address *address;
-	gpointer p;
 	GHashTable *subnets;
-	GPtrArray *ptr;
-	guint32 net;
 	guint i;
-	gint position;
 
-	if (!addresses)
-		return NULL;
+	nm_assert (addresses && addresses->len);
 
-	subnets = g_hash_table_new_full (g_direct_hash,
-	                                 g_direct_equal,
-	                                 NULL,
-	                                 NULL);
+	subnets = g_hash_table_new (NULL, NULL);
 
 	/* Build a hash table of all addresses per subnet */
 	for (i = 0; i < addresses->len; i++) {
-		address = &g_array_index (addresses, const NMPlatformIP4Address, i);
-		net = address->address & nm_utils_ip4_prefix_to_netmask (address->plen);
-		if (!g_hash_table_lookup_extended (subnets, GUINT_TO_POINTER (net), NULL, &p)) {
-			g_hash_table_insert (subnets, GUINT_TO_POINTER (net), (gpointer) address);
+		const NMPlatformIP4Address *address;
+		gpointer p_address;
+		GPtrArray *addr_list;
+		guint32 net;
+		int position;
+		gpointer p;
+
+		if (!addresses->pdata[i])
 			continue;
-		}
-		if (_ptr_inside_ip4_addr_array (addresses, p)) {
-			ptr = g_ptr_array_new ();
-			g_hash_table_insert (subnets, GUINT_TO_POINTER (net), ptr);
-			g_ptr_array_add (ptr, p);
-		} else
-			ptr = p;
 
-		if (!consider_flags || NM_FLAGS_HAS (address->n_ifa_flags, IFA_F_SECONDARY))
-			position = -1; /* append */
-		else
-			position = 0; /* prepend */
+		p_address = &addresses->pdata[i];
+		address = NMP_OBJECT_CAST_IP4_ADDRESS (addresses->pdata[i]);
 
-		g_ptr_array_insert (ptr, position, (gpointer) address);
+		net = address->address & _nm_utils_ip4_prefix_to_netmask (address->plen);
+		if (!g_hash_table_lookup_extended (subnets, GUINT_TO_POINTER (net), NULL, &p)) {
+			g_hash_table_insert (subnets, GUINT_TO_POINTER (net), p_address);
+			continue;
+		}
+		nm_assert (p);
+
+		if (full_index) {
+			if (ip4_addr_subnets_is_plain_address (addresses, p)) {
+				addr_list = g_ptr_array_new ();
+				g_hash_table_insert (subnets, GUINT_TO_POINTER (net), addr_list);
+				g_ptr_array_add (addr_list, p);
+			} else
+				addr_list = p;
+
+			if (   !consider_flags
+			    || NM_FLAGS_HAS (address->n_ifa_flags, IFA_F_SECONDARY))
+				position = -1; /* append */
+			else
+				position = 0; /* prepend */
+			g_ptr_array_insert (addr_list, position, p_address);
+		} else {
+			/* we only care about the primary. No need to track the secondaries
+			 * as a GPtrArray. */
+			nm_assert (ip4_addr_subnets_is_plain_address (addresses, p));
+			if (   consider_flags
+			    && !NM_FLAGS_HAS (address->n_ifa_flags, IFA_F_SECONDARY)) {
+				g_hash_table_insert (subnets, GUINT_TO_POINTER (net), p_address);
+			}
+		}
 	}
 
 	return subnets;
@@ -2911,23 +3240,33 @@ ip4_addr_subnets_build_index (const GArray *addresses, gboolean consider_flags)
  * Returns: %TRUE if the address is secondary, %FALSE otherwise
  */
 static gboolean
-ip4_addr_subnets_is_secondary (const NMPlatformIP4Address *address, GHashTable *subnets, const GArray *addresses, GPtrArray **out_addr_list)
-{
-	GPtrArray *addr_list;
-	gpointer p;
+ip4_addr_subnets_is_secondary (const NMPObject *address,
+                               GHashTable *subnets,
+                               const GPtrArray *addresses,
+                               const GPtrArray **out_addr_list)
+{
+	const NMPlatformIP4Address *a;
+	const GPtrArray *addr_list;
+	gconstpointer p;
 	guint32 net;
+	const NMPObject **o;
+
+	a = NMP_OBJECT_CAST_IP4_ADDRESS (address);
 
-	net = address->address & nm_utils_ip4_prefix_to_netmask (address->plen);
+	net = a->address & _nm_utils_ip4_prefix_to_netmask (a->plen);
 	p = g_hash_table_lookup (subnets, GUINT_TO_POINTER (net));
 	nm_assert (p);
-	if (!_ptr_inside_ip4_addr_array (addresses, p)) {
+	if (!ip4_addr_subnets_is_plain_address (addresses, p)) {
 		addr_list = p;
+		nm_assert (addr_list->len > 1);
 		NM_SET_OUT (out_addr_list, addr_list);
-		if (addr_list->pdata[0] != address)
+		o = ip4_addr_subnets_addr_list_get (addr_list, 0);
+		nm_assert (o && *o);
+		if (*o != address)
 			return TRUE;
 	} else {
-		nm_assert ((gconstpointer) address == p);
 		NM_SET_OUT (out_addr_list, NULL);
+		return address != *((gconstpointer *) p);
 	}
 	return FALSE;
 }
@@ -2936,11 +3275,14 @@ ip4_addr_subnets_is_secondary (const NMPlatformIP4Address *address, GHashTable *
  * nm_platform_ip4_address_sync:
  * @self: platform instance
  * @ifindex: Interface index
- * @known_addresses: List of addresses
- * @out_added_addresses: (out): (allow-none): if not %NULL, return a #GPtrArray
- *   with the addresses added. The pointers point into @known_addresses.
- *   It possibly does not contain all addresses from @known_address because
- *   some addresses might be expired.
+ * @known_addresses: List of addresses. The list will be modified and only
+ *   addresses that were successfully added will be kept in the list.
+ *   That means, expired addresses and addresses that could not be added
+ *   will be dropped.
+ *   Hence, the input argument @known_addresses is also an output argument
+ *   telling which addresses were succesfully added.
+ *   Addresses are removed by unrefing the instance via nmp_object_unref()
+ *   and leaving a NULL tombstone.
  *
  * A convenience function to synchronize addresses for a specific interface
  * with the least possible disturbance. It simply removes addresses that are
@@ -2949,102 +3291,168 @@ ip4_addr_subnets_is_secondary (const NMPlatformIP4Address *address, GHashTable *
  * Returns: %TRUE on success.
  */
 gboolean
-nm_platform_ip4_address_sync (NMPlatform *self, int ifindex, const GArray *known_addresses, GPtrArray **out_added_addresses)
+nm_platform_ip4_address_sync (NMPlatform *self,
+                              int ifindex,
+                              GPtrArray *known_addresses)
 {
-	GArray *addresses;
-	NMPlatformIP4Address *address;
+	gs_unref_ptrarray GPtrArray *plat_addresses = NULL;
 	const NMPlatformIP4Address *known_address;
 	gint32 now = nm_utils_get_monotonic_timestamp_s ();
-	GHashTable *plat_subnets;
-	GHashTable *known_subnets;
-	GPtrArray *ptr;
-	int i, j;
+	GHashTable *plat_subnets = NULL;
+	GHashTable *known_subnets = NULL;
+	gs_unref_hashtable GHashTable *known_addresses_idx = NULL;
+	guint i, j, len;
+	NMPLookup lookup;
+	guint32 lifetime, preferred;
+	guint32 ifa_flags;
 
 	_CHECK_SELF (self, klass, FALSE);
 
-	addresses = nm_platform_ip4_address_get_all (self, ifindex);
-	plat_subnets = ip4_addr_subnets_build_index (addresses, TRUE);
-	known_subnets = ip4_addr_subnets_build_index (known_addresses, FALSE);
+	if (known_addresses) {
+		/* remove all addresses that are already expired. */
+		for (i = 0; i < known_addresses->len; i++) {
+			const NMPObject *o;
+
+			o = known_addresses->pdata[i];
+			nm_assert (o);
+
+			known_address = NMP_OBJECT_CAST_IP4_ADDRESS (known_addresses->pdata[i]);
+
+			if (!nm_utils_lifetime_get (known_address->timestamp, known_address->lifetime, known_address->preferred,
+			                            now, &lifetime, &preferred))
+				goto delete_and_next;
+
+			if (G_UNLIKELY (!known_addresses_idx)) {
+				known_addresses_idx = g_hash_table_new ((GHashFunc) nmp_object_id_hash,
+				                                        (GEqualFunc) nmp_object_id_equal);
+			}
+			if (!nm_g_hash_table_insert (known_addresses_idx, (gpointer) o, (gpointer) o)) {
+				/* duplicate? Keep only the first instance. */
+				goto delete_and_next;
+			}
+
+			continue;
+delete_and_next:
+			nmp_object_unref (o);
+			known_addresses->pdata[i] = NULL;
+		}
+
+		if (   !known_addresses_idx
+		    || g_hash_table_size (known_addresses_idx) == 0)
+			known_addresses = NULL;
+	}
+
+	plat_addresses = nm_platform_lookup_clone (self,
+	                                           nmp_lookup_init_addrroute (&lookup,
+	                                                                      NMP_OBJECT_TYPE_IP4_ADDRESS,
+	                                                                      ifindex),
+	                                           NULL, NULL);
+	if (plat_addresses)
+		plat_subnets = ip4_addr_subnets_build_index (plat_addresses, TRUE, TRUE);
 
 	/* Delete unknown addresses */
-	for (i = 0; i < addresses->len; i++) {
-		address = &g_array_index (addresses, NMPlatformIP4Address, i);
+	len = plat_addresses ? plat_addresses->len : 0;
+	for (i = 0; i < len; i++) {
+		const NMPObject *plat_obj;
+		const NMPlatformIP4Address *plat_address;
+		const GPtrArray *addr_list;
 
-		if (!address->ifindex) {
+		plat_obj = plat_addresses->pdata[i];
+		if (!plat_obj) {
 			/* Already deleted */
 			continue;
 		}
 
-		known_address = array_contains_ip4_address (known_addresses, address, now);
-		if (known_address) {
-			gboolean secondary;
+		plat_address = NMP_OBJECT_CAST_IP4_ADDRESS (plat_obj);
+
+		if (known_addresses) {
+			const NMPObject *o;
+
+			o = g_hash_table_lookup (known_addresses_idx, plat_obj);
+			if (o) {
+				gboolean secondary;
+
+				if (!known_subnets)
+					known_subnets = ip4_addr_subnets_build_index (known_addresses, FALSE, FALSE);
 
-			secondary = ip4_addr_subnets_is_secondary (known_address, known_subnets, known_addresses, NULL);
-			/* Ignore the matching address if it has a different primary/slave
-			 * role. */
-			if (secondary != NM_FLAGS_HAS (address->n_ifa_flags, IFA_F_SECONDARY))
-				known_address = NULL;
+				secondary = ip4_addr_subnets_is_secondary (o, known_subnets, known_addresses, NULL);
+				if (secondary == NM_FLAGS_HAS (plat_address->n_ifa_flags, IFA_F_SECONDARY)) {
+					/* if we have an existing known-address, with matching secondary role,
+					 * do not delete the platform-address. */
+					continue;
+				}
+			}
 		}
 
-		if (!known_address) {
-			nm_platform_ip4_address_delete (self, ifindex,
-			                                address->address,
-			                                address->plen,
-			                                address->peer_address);
-			if (   !ip4_addr_subnets_is_secondary (address, plat_subnets, addresses, &ptr)
-			    && ptr) {
-				/* If we just deleted a primary addresses and there were
-				 * secondary ones the kernel can do two things, depending on
-				 * version and sysctl setting: delete also secondary addresses
-				 * or promote a secondary to primary. Ensure that secondary
-				 * addresses are deleted, so that we can start with a clean
-				 * slate and add addresses in the right order. */
-				for (j = 1; j < ptr->len; j++) {
-					address = ptr->pdata[j];
+		nm_platform_ip4_address_delete (self, ifindex,
+		                                plat_address->address,
+		                                plat_address->plen,
+		                                plat_address->peer_address);
+
+		if (   !ip4_addr_subnets_is_secondary (plat_obj, plat_subnets, plat_addresses, &addr_list)
+		    && addr_list) {
+			/* If we just deleted a primary addresses and there were
+			 * secondary ones the kernel can do two things, depending on
+			 * version and sysctl setting: delete also secondary addresses
+			 * or promote a secondary to primary. Ensure that secondary
+			 * addresses are deleted, so that we can start with a clean
+			 * slate and add addresses in the right order. */
+			for (j = 1; j < addr_list->len; j++) {
+				const NMPObject **o;
+
+				o = ip4_addr_subnets_addr_list_get (addr_list, j);
+				nm_assert (o);
+
+				if (*o) {
+					const NMPlatformIP4Address *a;
+
+					a = NMP_OBJECT_CAST_IP4_ADDRESS (*o);
 					nm_platform_ip4_address_delete (self, ifindex,
-					                                address->address,
-					                                address->plen,
-					                                address->peer_address);
-					address->ifindex = 0;
+					                                a->address,
+					                                a->plen,
+					                                a->peer_address);
+					nmp_object_unref (*o);
+					*o = NULL;
 				}
 			}
 		}
 	}
-	ip4_addr_subnets_destroy_index (plat_subnets, addresses);
-	g_array_free (addresses, TRUE);
-
-	if (out_added_addresses)
-		*out_added_addresses = NULL;
+	ip4_addr_subnets_destroy_index (plat_subnets, plat_addresses);
+	ip4_addr_subnets_destroy_index (known_subnets, known_addresses);
 
 	if (!known_addresses)
 		return TRUE;
 
+	ifa_flags =   nm_platform_check_kernel_support (self, NM_PLATFORM_KERNEL_SUPPORT_EXTENDED_IFA_FLAGS)
+	            ? IFA_F_NOPREFIXROUTE
+	            : 0;
+
 	/* Add missing addresses */
 	for (i = 0; i < known_addresses->len; i++) {
-		guint32 lifetime, preferred;
+		const NMPObject *o;
 
-		known_address = &g_array_index (known_addresses, NMPlatformIP4Address, i);
+		o = known_addresses->pdata[i];
+		if (!o)
+			continue;
+
+		known_address = NMP_OBJECT_CAST_IP4_ADDRESS (o);
 
 		if (!nm_utils_lifetime_get (known_address->timestamp, known_address->lifetime, known_address->preferred,
 		                            now, &lifetime, &preferred))
-			continue;
+			goto delete_and_next2;
 
 		if (!nm_platform_ip4_address_add (self, ifindex, known_address->address, known_address->plen,
 		                                  known_address->peer_address, lifetime, preferred,
-		                                  0, known_address->label)) {
-			ip4_addr_subnets_destroy_index (known_subnets, known_addresses);
-			return FALSE;
-		}
-
-		if (out_added_addresses) {
-			if (!*out_added_addresses)
-				*out_added_addresses = g_ptr_array_new ();
-			g_ptr_array_add (*out_added_addresses, (gpointer) known_address);
-		}
+		                                  ifa_flags,
+		                                  known_address->label))
+			goto delete_and_next2;
+
+		continue;
+delete_and_next2:
+		nmp_object_unref (o);
+		known_addresses->pdata[i] = NULL;
 	}
 
-	ip4_addr_subnets_destroy_index (known_subnets, known_addresses);
-
 	return TRUE;
 }
 
@@ -3052,7 +3460,8 @@ nm_platform_ip4_address_sync (NMPlatform *self, int ifindex, const GArray *known
  * nm_platform_ip6_address_sync:
  * @self: platform instance
  * @ifindex: Interface index
- * @known_addresses: List of addresses
+ * @known_addresses: List of IPv6 addresses, as NMPObject. The list
+ *   is not modified.
  * @keep_link_local: Don't remove link-local address
  *
  * A convenience function to synchronize addresses for a specific interface
@@ -3062,33 +3471,47 @@ nm_platform_ip4_address_sync (NMPlatform *self, int ifindex, const GArray *known
  * Returns: %TRUE on success.
  */
 gboolean
-nm_platform_ip6_address_sync (NMPlatform *self, int ifindex, const GArray *known_addresses, gboolean keep_link_local)
+nm_platform_ip6_address_sync (NMPlatform *self,
+                              int ifindex,
+                              const GPtrArray *known_addresses,
+                              gboolean keep_link_local)
 {
-	GArray *addresses;
+	gs_unref_ptrarray GPtrArray *plat_addresses = NULL;
 	NMPlatformIP6Address *address;
 	gint32 now = nm_utils_get_monotonic_timestamp_s ();
-	int i;
+	guint i;
+	NMPLookup lookup;
+	guint32 ifa_flags;
 
 	/* Delete unknown addresses */
-	addresses = nm_platform_ip6_address_get_all (self, ifindex);
-	for (i = 0; i < addresses->len; i++) {
-		address = &g_array_index (addresses, NMPlatformIP6Address, i);
-
-		/* Leave link local address management to the kernel */
-		if (keep_link_local && IN6_IS_ADDR_LINKLOCAL (&address->address))
-			continue;
+	plat_addresses = nm_platform_lookup_clone (self,
+	                                           nmp_lookup_init_addrroute (&lookup,
+	                                                                      NMP_OBJECT_TYPE_IP6_ADDRESS,
+	                                                                      ifindex),
+	                                           NULL, NULL);
+	if (plat_addresses) {
+		for (i = 0; i < plat_addresses->len; i++) {
+			address = NMP_OBJECT_CAST_IP6_ADDRESS (plat_addresses->pdata[i]);
+
+			/* Leave link local address management to the kernel */
+			if (keep_link_local && IN6_IS_ADDR_LINKLOCAL (&address->address))
+				continue;
 
-		if (!array_contains_ip6_address (known_addresses, address, now))
-			nm_platform_ip6_address_delete (self, ifindex, address->address, address->plen);
+			if (!array_contains_ip6_address (known_addresses, address, now))
+				nm_platform_ip6_address_delete (self, ifindex, address->address, address->plen);
+		}
 	}
-	g_array_free (addresses, TRUE);
 
 	if (!known_addresses)
 		return TRUE;
 
+	ifa_flags =   nm_platform_check_kernel_support (self, NM_PLATFORM_KERNEL_SUPPORT_EXTENDED_IFA_FLAGS)
+	            ? IFA_F_NOPREFIXROUTE
+	            : 0;
+
 	/* Add missing addresses */
 	for (i = 0; i < known_addresses->len; i++) {
-		const NMPlatformIP6Address *known_address = &g_array_index (known_addresses, NMPlatformIP6Address, i);
+		const NMPlatformIP6Address *known_address = NMP_OBJECT_CAST_IP6_ADDRESS (known_addresses->pdata[i]);
 		guint32 lifetime, preferred;
 
 		if (NM_FLAGS_HAS (known_address->n_ifa_flags, IFA_F_TEMPORARY)) {
@@ -3102,7 +3525,8 @@ nm_platform_ip6_address_sync (NMPlatform *self, int ifindex, const GArray *known
 
 		if (!nm_platform_ip6_address_add (self, ifindex, known_address->address,
 		                                  known_address->plen, known_address->peer_address,
-		                                  lifetime, preferred, known_address->n_ifa_flags))
+		                                  lifetime, preferred,
+		                                  ifa_flags | known_address->n_ifa_flags))
 			return FALSE;
 	}
 
@@ -3110,125 +3534,797 @@ nm_platform_ip6_address_sync (NMPlatform *self, int ifindex, const GArray *known
 }
 
 gboolean
-nm_platform_address_flush (NMPlatform *self, int ifindex)
+nm_platform_ip_address_flush (NMPlatform *self,
+                              int addr_family,
+                              int ifindex)
 {
+	gboolean success = TRUE;
+
 	_CHECK_SELF (self, klass, FALSE);
 
-	return    nm_platform_ip4_address_sync (self, ifindex, NULL, NULL)
-	       && nm_platform_ip6_address_sync (self, ifindex, NULL, FALSE);
+	nm_assert (NM_IN_SET (addr_family, AF_UNSPEC,
+	                                   AF_INET,
+	                                   AF_INET6));
+
+	if (NM_IN_SET (addr_family, AF_UNSPEC, AF_INET))
+		success &= nm_platform_ip4_address_sync (self, ifindex, NULL);
+	if (NM_IN_SET (addr_family, AF_UNSPEC, AF_INET6))
+		success &= nm_platform_ip6_address_sync (self, ifindex, NULL, FALSE);
+	return success;
 }
 
 /*****************************************************************************/
 
-GArray *
-nm_platform_ip4_route_get_all (NMPlatform *self, int ifindex, NMPlatformGetRouteFlags flags)
+static gboolean
+_err_inval_due_to_ipv6_tentative_pref_src (NMPlatform *self, const NMPObject *obj)
 {
-	_CHECK_SELF (self, klass, NULL);
+	const NMPlatformIP6Route *r;
+	const NMPlatformIP6Address *a;
 
-	g_return_val_if_fail (ifindex >= 0, NULL);
+	nm_assert (NM_IS_PLATFORM (self));
+	nm_assert (NMP_OBJECT_IS_VALID (obj));
 
-	return klass->ip4_route_get_all (self, ifindex, flags);
+	/* trying to add an IPv6 route with pref-src fails, if the address is
+	 * still tentative (rh#1452684). We need to hack around that.
+	 *
+	 * Detect it, by guessing whether that's the case. */
+
+	if (NMP_OBJECT_GET_TYPE (obj) != NMP_OBJECT_TYPE_IP6_ROUTE)
+		return FALSE;
+
+	r = NMP_OBJECT_CAST_IP6_ROUTE (obj);
+
+	/* we only allow this workaround for routes added manually by the user. */
+	if (r->rt_source != NM_IP_CONFIG_SOURCE_USER)
+		return FALSE;
+
+	if (IN6_IS_ADDR_UNSPECIFIED (&r->pref_src))
+		return FALSE;
+
+	a = nm_platform_ip6_address_get (self, r->ifindex, r->pref_src);
+	if (!a)
+		return FALSE;
+	if (   !NM_FLAGS_HAS (a->n_ifa_flags, IFA_F_TENTATIVE)
+	    || NM_FLAGS_HAS (a->n_ifa_flags, IFA_F_DADFAILED))
+		return FALSE;
+
+	return TRUE;
 }
 
-GArray *
-nm_platform_ip6_route_get_all (NMPlatform *self, int ifindex, NMPlatformGetRouteFlags flags)
-{
-	_CHECK_SELF (self, klass, NULL);
+GPtrArray *
+nm_platform_ip_route_get_prune_list (NMPlatform *self,
+                                     int addr_family,
+                                     int ifindex,
+                                     NMIPRouteTableSyncMode route_table_sync)
+{
+	NMPLookup lookup;
+	GPtrArray *routes_prune;
+	const NMDedupMultiHeadEntry *head_entry;
+	CList *iter;
+
+	nm_assert (NM_IS_PLATFORM (self));
+	nm_assert (NM_IN_SET (addr_family, AF_INET, AF_INET6));
+	nm_assert (NM_IN_SET (route_table_sync, NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN,
+	                                        NM_IP_ROUTE_TABLE_SYNC_MODE_FULL,
+	                                        NM_IP_ROUTE_TABLE_SYNC_MODE_ALL));
+
+	nmp_lookup_init_addrroute (&lookup,
+	                           addr_family == AF_INET
+	                             ? NMP_OBJECT_TYPE_IP4_ROUTE
+	                             : NMP_OBJECT_TYPE_IP6_ROUTE,
+	                           ifindex);
+	head_entry = nm_platform_lookup (self, &lookup);
+	if (!head_entry)
+		return NULL;
 
-	g_return_val_if_fail (ifindex >= 0, NULL);
+	routes_prune = g_ptr_array_new_full (head_entry->len,
+	                                     (GDestroyNotify) nm_dedup_multi_obj_unref);
+
+	c_list_for_each (iter, &head_entry->lst_entries_head) {
+		const NMPObject *obj = c_list_entry (iter, NMDedupMultiEntry, lst_entries)->obj;
+
+		if (route_table_sync == NM_IP_ROUTE_TABLE_SYNC_MODE_FULL) {
+			if (nm_platform_route_table_uncoerce (NMP_OBJECT_CAST_IP_ROUTE (obj)->table_coerced, TRUE) == RT_TABLE_LOCAL)
+				continue;
+		} else if (route_table_sync == NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN) {
+			if (!nm_platform_route_table_is_main (NMP_OBJECT_CAST_IP_ROUTE (obj)->table_coerced))
+				continue;
+		} else
+			nm_assert (route_table_sync == NM_IP_ROUTE_TABLE_SYNC_MODE_ALL);
+
+		g_ptr_array_add (routes_prune, (gpointer) nmp_object_ref (obj));
+	}
 
-	return klass->ip6_route_get_all (self, ifindex, flags);
+	if (routes_prune->len == 0) {
+		g_ptr_array_unref (routes_prune);
+		return NULL;
+	}
+	return routes_prune;
 }
 
 /**
- * nm_platform_ip4_route_add:
- * @self:
- * @route:
- *
- * For kernel, a gateway can be either explicitly set or left
- * at zero (0.0.0.0). In addition, there is the scope of the IPv4
- * route.
- * When adding a route with
- *   $ ip route add default dev $IFNAME
- * the resulting route will have gateway 0.0.0.0 and scope "link".
- * Contrary to
- *   $ ip route add default via 0.0.0.0 dev $IFNAME
- * which adds the route with scope "global".
- *
- * NetworkManager's Platform can currently only add on-link-routes with scope
- * "link" (and gateway 0.0.0.0) or gateway-routes with scope "global" (and
- * gateway not 0.0.0.0).
+ * nm_platform_ip_route_sync:
+ * @self: the #NMPlatform instance.
+ * @addr_family: AF_INET or AF_INET6.
+ * @ifindex: the @ifindex for which the routes are to be added.
+ * @routes: (allow-none): a list of routes to configure. Must contain
+ *   NMPObject instances of routes, according to @addr_family.
+ * @routes_prune: (allow-none): the list of routes to delete.
+ *   If platform has such a route configured, it will be deleted
+ *   at the end of the operation. Note that if @routes contains
+ *   the same route, then it will not be deleted. @routes overrules
+ *   @routes_prune list.
+ * @out_temporary_not_available: (allow-none): (out): routes that could
+ *   currently not be synced. The caller shall keep them and try later again.
  *
- * It does not support adding globally scoped routes via 0.0.0.0.
- *
- * Returns: %TRUE in case of success.
+ * Returns: %TRUE on success.
  */
 gboolean
-nm_platform_ip4_route_add (NMPlatform *self, const NMPlatformIP4Route *route)
-{
-	_CHECK_SELF (self, klass, FALSE);
+nm_platform_ip_route_sync (NMPlatform *self,
+                           int addr_family,
+                           int ifindex,
+                           GPtrArray *routes,
+                           GPtrArray *routes_prune,
+                           GPtrArray **out_temporary_not_available)
+{
+	const NMPlatformVTableRoute *vt;
+	gs_unref_hashtable GHashTable *routes_idx = NULL;
+	const NMPObject *conf_o;
+	const NMDedupMultiEntry *plat_entry;
+	guint i;
+	int i_type;
+	gboolean success = TRUE;
+	char sbuf1[sizeof (_nm_utils_to_string_buffer)];
+	char sbuf2[sizeof (_nm_utils_to_string_buffer)];
+	char sbuf_err[60];
+
+	nm_assert (NM_IS_PLATFORM (self));
+	nm_assert (NM_IN_SET (addr_family, AF_INET, AF_INET6));
+	nm_assert (ifindex > 0);
+
+	vt = addr_family == AF_INET
+	     ? &nm_platform_vtable_route_v4
+	     : &nm_platform_vtable_route_v6;
+
+	for (i_type = 0; routes && i_type < 2; i_type++) {
+		for (i = 0; i < routes->len; i++) {
+			NMPlatformError plerr;
+
+			conf_o = routes->pdata[i];
+
+#define VTABLE_IS_DEVICE_ROUTE(vt, o) (vt->is_ip4 \
+                                         ? (NMP_OBJECT_CAST_IP4_ROUTE (o)->gateway == 0) \
+                                         : IN6_IS_ADDR_UNSPECIFIED (&NMP_OBJECT_CAST_IP6_ROUTE (o)->gateway) )
+
+			if (   (i_type == 0 && !VTABLE_IS_DEVICE_ROUTE (vt, conf_o))
+			    || (i_type == 1 &&  VTABLE_IS_DEVICE_ROUTE (vt, conf_o))) {
+				/* we add routes in two runs over @i_type.
+				 *
+				 * First device routes, then gateway routes. */
+				continue;
+			}
+
+			if (!routes_idx) {
+				routes_idx = g_hash_table_new ((GHashFunc) nmp_object_id_hash,
+				                               (GEqualFunc) nmp_object_id_equal);
+			}
+			if (!nm_g_hash_table_insert (routes_idx, (gpointer) conf_o, (gpointer) conf_o)) {
+				_LOGD ("route-sync: skip adding duplicate route %s",
+				       nmp_object_to_string (conf_o, NMP_OBJECT_TO_STRING_PUBLIC, sbuf1, sizeof (sbuf1)));
+				continue;
+			}
+
+			plat_entry = nm_platform_lookup_entry (self,
+			                                       NMP_CACHE_ID_TYPE_OBJECT_TYPE,
+			                                       conf_o);
+			if (plat_entry) {
+				const NMPObject *plat_o;
+
+				plat_o = plat_entry->obj;
 
-	g_return_val_if_fail (route, FALSE);
-	g_return_val_if_fail (route->plen <= 32, FALSE);
+				if (vt->route_cmp (NMP_OBJECT_CAST_IPX_ROUTE (conf_o),
+				                   NMP_OBJECT_CAST_IPX_ROUTE (plat_o),
+				                   NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) == 0)
+					continue;
 
-	_LOGD ("route: adding or updating IPv4 route: %s", nm_platform_ip4_route_to_string (route, NULL, 0));
+				/* we need to replace the existing route with a (slightly) differnt
+				 * one. Delete it first. */
+				if (!nm_platform_ip_route_delete (self, plat_o)) {
+					/* ignore error. */
+				}
+			}
 
-	return klass->ip4_route_add (self, route);
+			plerr = nm_platform_ip_route_add (self,
+			                                    NMP_NLM_FLAG_APPEND
+			                                  | NMP_NLM_FLAG_SUPPRESS_NETLINK_FAILURE,
+			                                  conf_o);
+			if (plerr != NM_PLATFORM_ERROR_SUCCESS) {
+				if (-((int) plerr) == EEXIST) {
+					/* Don't fail for EEXIST. It's not clear that the existing route
+					 * is identical to the one that we were about to add. However,
+					 * above we should have deleted conflicting (non-identical) routes. */
+					if (_LOGD_ENABLED ()) {
+						plat_entry = nm_platform_lookup_entry (self,
+						                                       NMP_CACHE_ID_TYPE_OBJECT_TYPE,
+						                                       conf_o);
+						if (!plat_entry) {
+							_LOGD ("route-sync: adding route %s failed with EEXIST, however we cannot find such a route",
+							       nmp_object_to_string (conf_o, NMP_OBJECT_TO_STRING_PUBLIC, sbuf1, sizeof (sbuf1)));
+						} else if (vt->route_cmp (NMP_OBJECT_CAST_IPX_ROUTE (conf_o),
+						                          NMP_OBJECT_CAST_IPX_ROUTE (plat_entry->obj),
+						                          NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) != 0) {
+							_LOGD ("route-sync: adding route %s failed due to existing (different!) route %s",
+							       nmp_object_to_string (conf_o, NMP_OBJECT_TO_STRING_PUBLIC, sbuf1, sizeof (sbuf1)),
+							       nmp_object_to_string (plat_entry->obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf2, sizeof (sbuf2)));
+						}
+					}
+				} else if (   -((int) plerr) == EINVAL
+				           && out_temporary_not_available
+				           && _err_inval_due_to_ipv6_tentative_pref_src (self, conf_o)) {
+					_LOGD ("route-sync: ignore failure to add IPv6 route with tentative IPv6 pref-src: %s: %s",
+					       nmp_object_to_string (conf_o, NMP_OBJECT_TO_STRING_PUBLIC, sbuf1, sizeof (sbuf1)),
+					       nm_platform_error_to_string (plerr, sbuf_err, sizeof (sbuf_err)));
+					if (!*out_temporary_not_available)
+						*out_temporary_not_available = g_ptr_array_new_full (0, (GDestroyNotify) nmp_object_unref);
+					g_ptr_array_add (*out_temporary_not_available, (gpointer) nmp_object_ref (conf_o));
+				} else if (NMP_OBJECT_CAST_IP_ROUTE (conf_o)->rt_source < NM_IP_CONFIG_SOURCE_USER) {
+					_LOGD ("route-sync: ignore failure to add IPv%c route: %s: %s",
+					       vt->is_ip4 ? '4' : '6',
+					       nmp_object_to_string (conf_o, NMP_OBJECT_TO_STRING_PUBLIC, sbuf1, sizeof (sbuf1)),
+					       nm_platform_error_to_string (plerr, sbuf_err, sizeof (sbuf_err)));
+				} else {
+					const char *reason = "";
+
+					if (   -((int) plerr) == ENETUNREACH
+					    && (  vt->is_ip4
+					        ? !!NMP_OBJECT_CAST_IP4_ROUTE (conf_o)->gateway
+					        : !IN6_IS_ADDR_UNSPECIFIED (&NMP_OBJECT_CAST_IP6_ROUTE (conf_o)->gateway)))
+						reason = "; is the gateway directly reachable?";
+
+					_LOGW ("route-sync: failure to add IPv%c route: %s: %s%s",
+					       vt->is_ip4 ? '4' : '6',
+					       nmp_object_to_string (conf_o, NMP_OBJECT_TO_STRING_PUBLIC, sbuf1, sizeof (sbuf1)),
+					       nm_platform_error_to_string (plerr, sbuf_err, sizeof (sbuf_err)),
+					       reason);
+					success = FALSE;
+				}
+			}
+		}
+	}
+
+	if (routes_prune) {
+		for (i = 0; i < routes_prune->len; i++) {
+			const NMPObject *prune_o;
+
+			prune_o = routes_prune->pdata[i];
+
+			nm_assert (   (addr_family == AF_INET  && NMP_OBJECT_GET_TYPE (prune_o) == NMP_OBJECT_TYPE_IP4_ROUTE)
+			           || (addr_family == AF_INET6 && NMP_OBJECT_GET_TYPE (prune_o) == NMP_OBJECT_TYPE_IP6_ROUTE));
+
+			if (   routes_idx
+			    && g_hash_table_lookup (routes_idx, prune_o))
+				continue;
+
+			if (!nm_platform_lookup_entry (self,
+			                               NMP_CACHE_ID_TYPE_OBJECT_TYPE,
+			                               prune_o))
+				continue;
+
+			if (!nm_platform_ip_route_delete (self, prune_o)) {
+				/* ignore error... */
+			}
+		}
+	}
+
+	return success;
 }
 
 gboolean
-nm_platform_ip6_route_add (NMPlatform *self, const NMPlatformIP6Route *route)
+nm_platform_ip_route_flush (NMPlatform *self,
+                            int addr_family,
+                            int ifindex)
 {
+	gboolean success = TRUE;
+
 	_CHECK_SELF (self, klass, FALSE);
 
-	g_return_val_if_fail (route, FALSE);
-	g_return_val_if_fail (route->plen <= 128, FALSE);
+	nm_assert (NM_IN_SET (addr_family, AF_UNSPEC,
+	                                   AF_INET,
+	                                   AF_INET6));
 
-	_LOGD ("route: adding or updating IPv6 route: %s", nm_platform_ip6_route_to_string (route, NULL, 0));
+	if (NM_IN_SET (addr_family, AF_UNSPEC, AF_INET)) {
+		gs_unref_ptrarray GPtrArray *routes_prune = NULL;
 
-	return klass->ip6_route_add (self, route);
+		routes_prune = nm_platform_ip_route_get_prune_list (self,
+		                                                    AF_INET,
+		                                                    ifindex,
+		                                                    NM_IP_ROUTE_TABLE_SYNC_MODE_ALL);
+		success &= nm_platform_ip_route_sync (self, AF_INET, ifindex, NULL, routes_prune, NULL);
+	}
+	if (NM_IN_SET (addr_family, AF_UNSPEC, AF_INET6)) {
+		gs_unref_ptrarray GPtrArray *routes_prune = NULL;
+
+		routes_prune = nm_platform_ip_route_get_prune_list (self,
+		                                                    AF_INET6,
+		                                                    ifindex,
+		                                                    NM_IP_ROUTE_TABLE_SYNC_MODE_ALL);
+		success &= nm_platform_ip_route_sync (self, AF_INET6, ifindex, NULL, routes_prune, NULL);
+	}
+	return success;
 }
 
-gboolean
-nm_platform_ip4_route_delete (NMPlatform *self, int ifindex, in_addr_t network, guint8 plen, guint32 metric)
+/*****************************************************************************/
+
+static guint8
+_ip_route_scope_inv_get_normalized (const NMPlatformIP4Route *route)
+{
+	/* in kernel, you cannot set scope to RT_SCOPE_NOWHERE (255).
+	 * That means, in NM, we treat RT_SCOPE_NOWHERE as unset, and detect
+	 * it based on the presence of the gateway. In other words, when adding
+	 * a route with scope RT_SCOPE_NOWHERE (in NetworkManager) to kernel,
+	 * the resulting scope will be either "link" or "universe" (depending
+	 * on the gateway).
+	 *
+	 * Note that internally, we track @scope_inv is the inverse of scope,
+	 * so that the default equals zero (~(RT_SCOPE_NOWHERE)).
+	 **/
+	if (route->scope_inv == 0) {
+		return nm_platform_route_scope_inv (!route->gateway
+		                                    ? RT_SCOPE_LINK : RT_SCOPE_UNIVERSE);
+	}
+	return route->scope_inv;
+}
+
+static guint8
+_route_pref_normalize (guint8 pref)
 {
-	char str_dev[TO_STRING_DEV_BUF_SIZE];
+	/* for kernel (and ICMPv6) pref can only have one of 3 values. Normalize. */
+	return NM_IN_SET (pref, NM_ICMPV6_ROUTER_PREF_LOW,
+	                        NM_ICMPV6_ROUTER_PREF_HIGH)
+	       ? pref
+	       : NM_ICMPV6_ROUTER_PREF_MEDIUM;
+}
+
+/**
+ * nm_platform_ip_route_normalize:
+ * @addr_family: AF_INET or AF_INET6
+ * @route: an NMPlatformIP4Route or NMPlatformIP6Route instance, depending on @addr_family.
+ *
+ * Adding a route to kernel via nm_platform_ip_route_add() will normalize/coerce some
+ * properties of the route. This function modifies (normalizes) the route like it
+ * would be done by adding the route in kernel.
+ *
+ * Note that this function is related to NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY
+ * in that if two routes compare semantically equal, after normalizing they also shall
+ * compare equal with NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL.
+ */
+void
+nm_platform_ip_route_normalize (int addr_family,
+                                NMPlatformIPRoute *route)
+{
+	NMPlatformIP4Route *r4;
+	NMPlatformIP6Route *r6;
+
+	switch (addr_family) {
+	case AF_INET:
+		r4 = (NMPlatformIP4Route *) route;
+		r4->table_coerced = nm_platform_route_table_coerce (nm_platform_route_table_uncoerce (r4->table_coerced, TRUE));
+		r4->network = nm_utils_ip4_address_clear_host_address (r4->network, r4->plen);
+		r4->rt_source = nmp_utils_ip_config_source_round_trip_rtprot (r4->rt_source);
+		r4->scope_inv = _ip_route_scope_inv_get_normalized (r4);
+		break;
+	case AF_INET6:
+		r6 = (NMPlatformIP6Route *) route;
+		r6->table_coerced = nm_platform_route_table_coerce (nm_platform_route_table_uncoerce (r6->table_coerced, TRUE));
+		nm_utils_ip6_address_clear_host_address (&r6->network, &r6->network, r6->plen);
+		r6->rt_source = nmp_utils_ip_config_source_round_trip_rtprot (r6->rt_source),
+		r6->metric = nm_utils_ip6_route_metric_normalize (r6->metric);
+		nm_utils_ip6_address_clear_host_address (&r6->src, &r6->src, r6->src_plen);
+		break;
+	default:
+		nm_assert_not_reached ();
+		break;
+	}
+}
+
+static NMPlatformError
+_ip_route_add (NMPlatform *self,
+               NMPNlmFlags flags,
+               int addr_family,
+               gconstpointer route)
+{
+	char sbuf[sizeof (_nm_utils_to_string_buffer)];
 
 	_CHECK_SELF (self, klass, FALSE);
 
-	_LOGD ("route: deleting IPv4 route %s/%d, metric=%"G_GUINT32_FORMAT", ifindex %d%s",
-	       nm_utils_inet4_ntop (network, NULL), plen, metric, ifindex,
-	       _to_string_dev (self, ifindex, str_dev, sizeof (str_dev)));
-	return klass->ip4_route_delete (self, ifindex, network, plen, metric);
+	nm_assert (route);
+	nm_assert (NM_IN_SET (addr_family, AF_INET, AF_INET6));
+
+	_LOGD ("route: %-10s IPv%c route: %s",
+	       _nmp_nlm_flag_to_string (flags & NMP_NLM_FLAG_FMASK),
+	       nm_utils_addr_family_to_char (addr_family),
+	       addr_family == AF_INET
+	         ? nm_platform_ip4_route_to_string (route, sbuf, sizeof (sbuf))
+	         : nm_platform_ip6_route_to_string (route, sbuf, sizeof (sbuf)));
+
+	return klass->ip_route_add (self, flags, addr_family, route);
+}
+
+NMPlatformError
+nm_platform_ip_route_add (NMPlatform *self,
+                          NMPNlmFlags flags,
+                          const NMPObject *route)
+{
+	int addr_family;
+
+	switch (NMP_OBJECT_GET_TYPE (route)) {
+	case NMP_OBJECT_TYPE_IP4_ROUTE:
+		addr_family = AF_INET;
+		break;
+	case NMP_OBJECT_TYPE_IP6_ROUTE:
+		addr_family = AF_INET6;
+		break;
+	default:
+		g_return_val_if_reached (FALSE);
+	}
+
+	return _ip_route_add (self, flags, addr_family, NMP_OBJECT_CAST_IP_ROUTE (route));
+}
+
+NMPlatformError
+nm_platform_ip4_route_add (NMPlatform *self,
+                           NMPNlmFlags flags,
+                           const NMPlatformIP4Route *route)
+{
+	return _ip_route_add (self, flags, AF_INET, route);
+}
+
+NMPlatformError
+nm_platform_ip6_route_add (NMPlatform *self,
+                           NMPNlmFlags flags,
+                           const NMPlatformIP6Route *route)
+{
+	return _ip_route_add (self, flags, AF_INET6, route);
 }
 
 gboolean
-nm_platform_ip6_route_delete (NMPlatform *self, int ifindex, struct in6_addr network, guint8 plen, guint32 metric)
+nm_platform_ip_route_delete (NMPlatform *self,
+                             const NMPObject *obj)
 {
-	char str_dev[TO_STRING_DEV_BUF_SIZE];
+	_CHECK_SELF (self, klass, FALSE);
+
+	if (!NM_IN_SET (NMP_OBJECT_GET_TYPE (obj), NMP_OBJECT_TYPE_IP4_ROUTE,
+	                                           NMP_OBJECT_TYPE_IP6_ROUTE))
+		g_return_val_if_reached (FALSE);
+
+	_LOGD ("route: delete     IPv%c route %s",
+	       NMP_OBJECT_GET_TYPE (obj) == NMP_OBJECT_TYPE_IP4_ROUTE ? '4' : '6',
+	       nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+
+	return klass->ip_route_delete (self, obj);
+}
+
+/*****************************************************************************/
+
+NMPlatformError
+nm_platform_ip_route_get (NMPlatform *self,
+                          int addr_family,
+                          gconstpointer address /* in_addr_t or struct in6_addr */,
+                          int oif_ifindex,
+                          NMPObject **out_route)
+{
+	nm_auto_nmpobj NMPObject *route = NULL;
+	NMPlatformError result;
+	char buf[NM_UTILS_INET_ADDRSTRLEN];
+	char buf_err[200];
+	char buf_oif[64];
 
 	_CHECK_SELF (self, klass, FALSE);
 
-	_LOGD ("route: deleting IPv6 route %s/%d, metric=%"G_GUINT32_FORMAT", ifindex %d%s",
-	       nm_utils_inet6_ntop (&network, NULL), plen, metric, ifindex,
-	       _to_string_dev (self, ifindex, str_dev, sizeof (str_dev)));
-	return klass->ip6_route_delete (self, ifindex, network, plen, metric);
+	g_return_val_if_fail (address, NM_PLATFORM_ERROR_BUG);
+	g_return_val_if_fail (NM_IN_SET (addr_family, AF_INET,
+	                                              AF_INET6), NM_PLATFORM_ERROR_BUG);
+
+	_LOGT ("route: get IPv%c route for: %s%s",
+	       nm_utils_addr_family_to_char (addr_family),
+	       inet_ntop (addr_family, address, buf, sizeof (buf)),
+	       oif_ifindex > 0 ? nm_sprintf_buf (buf_oif, " oif %d", oif_ifindex) : "");
+
+	if (!klass->ip_route_get)
+		result = NM_PLATFORM_ERROR_OPNOTSUPP;
+	else {
+		result = klass->ip_route_get (self,
+		                              addr_family,
+		                              address,
+		                              oif_ifindex,
+		                              &route);
+	}
+
+	if (result != NM_PLATFORM_ERROR_SUCCESS) {
+		nm_assert (!route);
+		_LOGW ("route: get IPv%c route for: %s failed with %s",
+		       nm_utils_addr_family_to_char (addr_family),
+		       inet_ntop (addr_family, address, buf, sizeof (buf)),
+		       nm_platform_error_to_string (result, buf_err, sizeof (buf_err)));
+	} else {
+		nm_assert (NM_IN_SET (NMP_OBJECT_GET_TYPE (route), NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE));
+		nm_assert (!NMP_OBJECT_IS_STACKINIT (route));
+		nm_assert (route->parent._ref_count == 1);
+		_LOGD ("route: get IPv%c route for: %s succeeded: %s",
+		       nm_utils_addr_family_to_char (addr_family),
+		       inet_ntop (addr_family, address, buf, sizeof (buf)),
+		       nmp_object_to_string (route, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+		NM_SET_OUT (out_route, g_steal_pointer (&route));
+	}
+	return result;
 }
 
-const NMPlatformIP4Route *
-nm_platform_ip4_route_get (NMPlatform *self, int ifindex, in_addr_t network, guint8 plen, guint32 metric)
+/*****************************************************************************/
+
+#define IP4_DEV_ROUTE_BLACKLIST_TIMEOUT_MS   ((int) 1500)
+#define IP4_DEV_ROUTE_BLACKLIST_GC_TIMEOUT_S ((int) (((IP4_DEV_ROUTE_BLACKLIST_TIMEOUT_MS + 999) * 3) / 1000))
+
+static gint64
+_ip4_dev_route_blacklist_timeout_ms_get (gint64 timeout_ms)
 {
-	_CHECK_SELF (self, klass, FALSE);
+	return timeout_ms >> 1;
+}
 
-	return klass->ip4_route_get (self ,ifindex, network, plen, metric);
+static gint64
+_ip4_dev_route_blacklist_timeout_ms_marked (gint64 timeout_ms)
+{
+	return !!(timeout_ms & ((gint64) 1));
 }
 
-const NMPlatformIP6Route *
-nm_platform_ip6_route_get (NMPlatform *self, int ifindex, struct in6_addr network, guint8 plen, guint32 metric)
+static gboolean
+_ip4_dev_route_blacklist_check_cb (gpointer user_data)
 {
-	_CHECK_SELF (self, klass, FALSE);
+	NMPlatform *self = user_data;
+	NMPlatformPrivate *priv = NM_PLATFORM_GET_PRIVATE (self);
+	GHashTableIter iter;
+	const NMPObject *p_obj;
+	gint64 *p_timeout_ms;
+	gint64 now_ms;
+
+	priv->ip4_dev_route_blacklist_check_id = 0;
+
+again:
+	if (!priv->ip4_dev_route_blacklist_hash)
+		goto out;
+
+	now_ms = nm_utils_get_monotonic_timestamp_ms ();
+
+	g_hash_table_iter_init (&iter, priv->ip4_dev_route_blacklist_hash);
+	while (g_hash_table_iter_next (&iter, (gpointer *) &p_obj, (gpointer *) &p_timeout_ms)) {
+		if (!_ip4_dev_route_blacklist_timeout_ms_marked (*p_timeout_ms))
+			continue;
+
+		/* unmark because we checked it. */
+		*p_timeout_ms = *p_timeout_ms & ~((gint64) 1);
+
+		if (now_ms > _ip4_dev_route_blacklist_timeout_ms_get (*p_timeout_ms))
+			continue;
+
+		if (!nm_platform_lookup_entry (self,
+		                               NMP_CACHE_ID_TYPE_OBJECT_TYPE,
+		                               p_obj))
+			continue;
+
+		_LOGT ("ip4-dev-route: delete %s",
+		       nmp_object_to_string (p_obj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+		nm_platform_ip_route_delete (self, p_obj);
+		goto again;
+	}
+
+out:
+	return G_SOURCE_REMOVE;
+}
+
+static void
+_ip4_dev_route_blacklist_check_schedule (NMPlatform *self)
+{
+	NMPlatformPrivate *priv = NM_PLATFORM_GET_PRIVATE (self);
+
+	if (!priv->ip4_dev_route_blacklist_check_id) {
+		priv->ip4_dev_route_blacklist_check_id = g_idle_add_full (G_PRIORITY_HIGH,
+		                                                          _ip4_dev_route_blacklist_check_cb,
+		                                                          self,
+		                                                          NULL);
+	}
+}
+
+static void
+_ip4_dev_route_blacklist_notify_route (NMPlatform *self,
+                                       const NMPObject *obj)
+{
+	NMPlatformPrivate *priv;
+	const NMPObject *p_obj;
+	gint64 *p_timeout_ms;
+	gint64 now_ms;
+
+	nm_assert (NM_IS_PLATFORM (self));
+	nm_assert (NMP_OBJECT_GET_TYPE (obj) == NMP_OBJECT_TYPE_IP4_ROUTE);
+
+	priv = NM_PLATFORM_GET_PRIVATE (self);
+
+	nm_assert (priv->ip4_dev_route_blacklist_gc_timeout_id);
+
+	if (!g_hash_table_lookup_extended (priv->ip4_dev_route_blacklist_hash,
+	                                   obj,
+	                                   (gpointer *) &p_obj,
+	                                   (gpointer *) &p_timeout_ms))
+		return;
+
+	now_ms = nm_utils_get_monotonic_timestamp_ms ();
+	if (now_ms > _ip4_dev_route_blacklist_timeout_ms_get (*p_timeout_ms)) {
+		/* already expired. Wait for gc. */
+		return;
+	}
+
+	if (_ip4_dev_route_blacklist_timeout_ms_marked (*p_timeout_ms)) {
+		nm_assert (priv->ip4_dev_route_blacklist_check_id);
+		return;
+	}
+
+	/* We cannot delete it right away because we are in the process of receiving netlink messages.
+	 * It may be possible to do so, but complicated and error prone.
+	 *
+	 * Instead, we mark the entry and schedule an idle action (with high priority). */
+	*p_timeout_ms = (*p_timeout_ms) | ((gint64) 1);
+	_ip4_dev_route_blacklist_check_schedule (self);
+}
+
+static gboolean
+_ip4_dev_route_blacklist_gc_timeout_handle (gpointer user_data)
+{
+	NMPlatform *self = user_data;
+	NMPlatformPrivate *priv = NM_PLATFORM_GET_PRIVATE (self);
+	GHashTableIter iter;
+	const NMPObject *p_obj;
+	gint64 *p_timeout_ms;
+	gint64 now_ms;
+
+	nm_assert (priv->ip4_dev_route_blacklist_gc_timeout_id);
+
+	now_ms = nm_utils_get_monotonic_timestamp_ms ();
+
+	g_hash_table_iter_init (&iter, priv->ip4_dev_route_blacklist_hash);
+	while (g_hash_table_iter_next (&iter, (gpointer *) &p_obj, (gpointer *) &p_timeout_ms)) {
+		if (now_ms > _ip4_dev_route_blacklist_timeout_ms_get (*p_timeout_ms)) {
+			_LOGT ("ip4-dev-route: cleanup %s",
+			       nmp_object_to_string (p_obj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+			g_hash_table_iter_remove (&iter);
+		}
+	}
+
+	_ip4_dev_route_blacklist_schedule (self);
+	return G_SOURCE_CONTINUE;
+}
+
+static void
+_ip4_dev_route_blacklist_schedule (NMPlatform *self)
+{
+	NMPlatformPrivate *priv = NM_PLATFORM_GET_PRIVATE (self);
+
+	if (   !priv->ip4_dev_route_blacklist_hash
+	    || g_hash_table_size (priv->ip4_dev_route_blacklist_hash) == 0) {
+		g_clear_pointer (&priv->ip4_dev_route_blacklist_hash, g_hash_table_unref);
+		nm_clear_g_source (&priv->ip4_dev_route_blacklist_gc_timeout_id);
+	} else {
+		if (!priv->ip4_dev_route_blacklist_gc_timeout_id) {
+			/* this timeout is only to garbage collect the expired entries from priv->ip4_dev_route_blacklist_hash.
+			 * It can run infrequently, and it doesn't hurt if expired entries linger around a bit
+			 * longer then necessary. */
+			priv->ip4_dev_route_blacklist_gc_timeout_id = g_timeout_add_seconds (IP4_DEV_ROUTE_BLACKLIST_GC_TIMEOUT_S,
+			                                                                     _ip4_dev_route_blacklist_gc_timeout_handle,
+			                                                                     self);
+		}
+	}
+}
+
+/**
+ * nm_platform_ip4_dev_route_blacklist_set:
+ * @self:
+ * @ifindex:
+ * @ip4_dev_route_blacklist:
+ *
+ * When adding an IP address, kernel automatically adds a device route.
+ * This can be suppressed via the IFA_F_NOPREFIXROUTE address flag. For proper
+ * IPv6 support, we require kernel support for IFA_F_NOPREFIXROUTE and always
+ * add the device route manually.
+ *
+ * For IPv4, this flag is rather new and we don't rely on it yet. We want to use
+ * it (but currently still don't). So, for IPv4, kernel possibly adds a device
+ * route, however it has a wrong metric of zero. We add our own device route (with
+ * proper metric), but need to delete the route that kernel adds.
+ *
+ * The problem is, that kernel does not immidiately add the route, when adding
+ * the address. It only shows up some time later. So, we register here a list
+ * of blacklisted routes, and when they show up within a time out, we assume it's
+ * the kernel generated one, and we delete it.
+ *
+ * Eventually, we want to get rid of this and use IFA_F_NOPREFIXROUTE for IPv4
+ * routes as well.
+ */
+void
+nm_platform_ip4_dev_route_blacklist_set (NMPlatform *self,
+                                         int ifindex,
+                                         GPtrArray *ip4_dev_route_blacklist)
+{
+	NMPlatformPrivate *priv;
+	GHashTableIter iter;
+	const NMPObject *p_obj;
+	guint i;
+	gint64 timeout_ms;
+	gint64 timeout_ms_val;
+	gint64 *p_timeout_ms;
+	gboolean needs_check = FALSE;
+
+	nm_assert (NM_IS_PLATFORM (self));
+	nm_assert (ifindex > 0);
+
+	priv = NM_PLATFORM_GET_PRIVATE (self);
+
+	/* first, expire all for current ifindex... */
+	if (priv->ip4_dev_route_blacklist_hash) {
+		g_hash_table_iter_init (&iter, priv->ip4_dev_route_blacklist_hash);
+		while (g_hash_table_iter_next (&iter, (gpointer *) &p_obj, (gpointer *) &p_timeout_ms)) {
+			if (NMP_OBJECT_CAST_IP4_ROUTE (p_obj)->ifindex == ifindex) {
+				/* we could g_hash_table_iter_remove(&iter) the current entry.
+				 * Instead, just expire it and let _ip4_dev_route_blacklist_gc_timeout_handle()
+				 * handle it.
+				 *
+				 * The assumption is, that ip4_dev_route_blacklist contains the very same entry
+				 * again, with a new timeout. So, we can un-expire it below. */
+				*p_timeout_ms = 0;
+			}
+		}
+	}
+
+	if (   ip4_dev_route_blacklist
+	    && ip4_dev_route_blacklist->len > 0) {
+
+		if (!priv->ip4_dev_route_blacklist_hash) {
+			priv->ip4_dev_route_blacklist_hash = g_hash_table_new_full ((GHashFunc) nmp_object_id_hash,
+			                                                            (GEqualFunc) nmp_object_id_equal,
+			                                                            (GDestroyNotify) nmp_object_unref,
+			                                                            nm_g_slice_free_fcn_gint64);
+		}
+
+		timeout_ms = nm_utils_get_monotonic_timestamp_ms () + IP4_DEV_ROUTE_BLACKLIST_TIMEOUT_MS;
+		timeout_ms_val = (timeout_ms << 1) | ((gint64) 1);
+		for (i = 0; i < ip4_dev_route_blacklist->len; i++) {
+			const NMPObject *o;
+
+			needs_check = TRUE;
+			o = ip4_dev_route_blacklist->pdata[i];
+			if (g_hash_table_lookup_extended (priv->ip4_dev_route_blacklist_hash,
+			                                  o,
+			                                  (gpointer *) &p_obj,
+			                                  (gpointer *) &p_timeout_ms)) {
+				if (nmp_object_equal (p_obj, o)) {
+					/* un-expire and reuse the entry. */
+					_LOGT ("ip4-dev-route: register %s (update)",
+					       nmp_object_to_string (p_obj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+					*p_timeout_ms = timeout_ms_val;
+					continue;
+				}
+			}
+
+			_LOGT ("ip4-dev-route: register %s",
+			       nmp_object_to_string (o, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+			p_timeout_ms = g_slice_new (gint64);
+			*p_timeout_ms = timeout_ms_val;
+			g_hash_table_replace (priv->ip4_dev_route_blacklist_hash,
+			                      (gpointer) nmp_object_ref (o),
+			                      p_timeout_ms);
+		}
+	}
+
+	_ip4_dev_route_blacklist_schedule (self);
 
-	return klass->ip6_route_get (self, ifindex, network, plen, metric);
+	if (needs_check)
+		_ip4_dev_route_blacklist_check_schedule (self);
 }
 
 /*****************************************************************************/
@@ -3898,6 +4994,7 @@ nm_platform_ip4_route_to_string (const NMPlatformIP4Route *route, char *buf, gsi
 	char s_network[INET_ADDRSTRLEN], s_gateway[INET_ADDRSTRLEN];
 	char s_pref_src[INET_ADDRSTRLEN];
 	char str_dev[TO_STRING_DEV_BUF_SIZE];
+	char str_table[30];
 	char str_scope[30], s_source[50];
 	char str_tos[32], str_window[32], str_cwnd[32], str_initcwnd[32], str_initrwnd[32], str_mtu[32];
 
@@ -3909,20 +5006,9 @@ nm_platform_ip4_route_to_string (const NMPlatformIP4Route *route, char *buf, gsi
 
 	_to_string_dev (NULL, route->ifindex, str_dev, sizeof (str_dev));
 
-	if (route->tos)
-		nm_sprintf_buf (str_tos, " tos 0x%x", (unsigned) route->tos);
-	if (route->window)
-		nm_sprintf_buf (str_window, " window %s%"G_GUINT32_FORMAT, route->lock_window ? "lock " : "", route->window);
-	if (route->cwnd)
-		nm_sprintf_buf (str_cwnd, " cwnd %s%"G_GUINT32_FORMAT, route->lock_cwnd ? "lock " : "", route->cwnd);
-	if (route->initcwnd)
-		nm_sprintf_buf (str_initcwnd, " initcwnd %s%"G_GUINT32_FORMAT, route->lock_initcwnd ? "lock " : "", route->initcwnd);
-	if (route->initrwnd)
-		nm_sprintf_buf (str_initrwnd, " initrwnd %s%"G_GUINT32_FORMAT, route->lock_initrwnd ? "lock " : "", route->initrwnd);
-	if (route->mtu)
-		nm_sprintf_buf (str_mtu, " mtu %s%"G_GUINT32_FORMAT, route->lock_mtu ? "lock " : "", route->mtu);
 
 	g_snprintf (buf, len,
+	            "%s" /* table */
 	            "%s/%d"
 	            " via %s"
 	            "%s"
@@ -3939,6 +5025,7 @@ nm_platform_ip4_route_to_string (const NMPlatformIP4Route *route, char *buf, gsi
 	            "%s" /* initrwnd */
 	            "%s" /* mtu */
 	            "",
+	            route->table_coerced ? nm_sprintf_buf (str_table, "table %u ", nm_platform_route_table_uncoerce (route->table_coerced, FALSE)) : "",
 	            s_network,
 	            route->plen,
 	            s_gateway,
@@ -3951,12 +5038,12 @@ nm_platform_ip4_route_to_string (const NMPlatformIP4Route *route, char *buf, gsi
 	            route->scope_inv ? (nm_platform_route_scope2str (nm_platform_route_scope_inv (route->scope_inv), str_scope, sizeof (str_scope))) : "",
 	            route->pref_src ? " pref-src " : "",
 	            route->pref_src ? inet_ntop (AF_INET, &route->pref_src, s_pref_src, sizeof(s_pref_src)) : "",
-	            route->tos ? str_tos : "",
-	            route->window ? str_window : "",
-	            route->cwnd ? str_cwnd : "",
-	            route->initcwnd ? str_initcwnd : "",
-	            route->initrwnd ? str_initrwnd : "",
-	            route->mtu ? str_mtu : "");
+	            route->tos ? nm_sprintf_buf (str_tos, " tos 0x%x", (unsigned) route->tos) : "",
+	            route->window   || route->lock_window   ? nm_sprintf_buf (str_window,   " window %s%"G_GUINT32_FORMAT,   route->lock_window   ? "lock " : "", route->window)   : "",
+	            route->cwnd     || route->lock_cwnd     ? nm_sprintf_buf (str_cwnd,     " cwnd %s%"G_GUINT32_FORMAT,     route->lock_cwnd     ? "lock " : "", route->cwnd)     : "",
+	            route->initcwnd || route->lock_initcwnd ? nm_sprintf_buf (str_initcwnd, " initcwnd %s%"G_GUINT32_FORMAT, route->lock_initcwnd ? "lock " : "", route->initcwnd) : "",
+	            route->initrwnd || route->lock_initrwnd ? nm_sprintf_buf (str_initrwnd, " initrwnd %s%"G_GUINT32_FORMAT, route->lock_initrwnd ? "lock " : "", route->initrwnd) : "",
+	            route->mtu      || route->lock_mtu      ? nm_sprintf_buf (str_mtu,      " mtu %s%"G_GUINT32_FORMAT,      route->lock_mtu      ? "lock " : "", route->mtu)      : "");
 	return buf;
 }
 
@@ -3976,16 +5063,18 @@ const char *
 nm_platform_ip6_route_to_string (const NMPlatformIP6Route *route, char *buf, gsize len)
 {
 	char s_network[INET6_ADDRSTRLEN], s_gateway[INET6_ADDRSTRLEN], s_pref_src[INET6_ADDRSTRLEN];
-	char s_src[INET6_ADDRSTRLEN];
+	char s_src_all[INET6_ADDRSTRLEN + 40], s_src[INET6_ADDRSTRLEN];
+	char str_table[30];
+	char str_pref[40];
+	char str_pref2[30];
 	char str_dev[TO_STRING_DEV_BUF_SIZE], s_source[50];
-	char str_tos[32], str_window[32], str_cwnd[32], str_initcwnd[32], str_initrwnd[32], str_mtu[32];
+	char str_window[32], str_cwnd[32], str_initcwnd[32], str_initrwnd[32], str_mtu[32];
 
 	if (!nm_utils_to_string_buffer_init_null (route, &buf, &len))
 		return buf;
 
 	inet_ntop (AF_INET6, &route->network, s_network, sizeof (s_network));
 	inet_ntop (AF_INET6, &route->gateway, s_gateway, sizeof (s_gateway));
-	inet_ntop (AF_INET6, &route->src, s_src, sizeof (s_src));
 
 	if (IN6_IS_ADDR_UNSPECIFIED (&route->pref_src))
 		s_pref_src[0] = 0;
@@ -3994,36 +5083,25 @@ nm_platform_ip6_route_to_string (const NMPlatformIP6Route *route, char *buf, gsi
 
 	_to_string_dev (NULL, route->ifindex, str_dev, sizeof (str_dev));
 
-	if (route->tos)
-		nm_sprintf_buf (str_tos, " tos 0x%x", (unsigned) route->tos);
-	if (route->window)
-		nm_sprintf_buf (str_window, " window %s%"G_GUINT32_FORMAT, route->lock_window ? "lock " : "", route->window);
-	if (route->cwnd)
-		nm_sprintf_buf (str_cwnd, " cwnd %s%"G_GUINT32_FORMAT, route->lock_cwnd ? "lock " : "", route->cwnd);
-	if (route->initcwnd)
-		nm_sprintf_buf (str_initcwnd, " initcwnd %s%"G_GUINT32_FORMAT, route->lock_initcwnd ? "lock " : "", route->initcwnd);
-	if (route->initrwnd)
-		nm_sprintf_buf (str_initrwnd, " initrwnd %s%"G_GUINT32_FORMAT, route->lock_initrwnd ? "lock " : "", route->initrwnd);
-	if (route->mtu)
-		nm_sprintf_buf (str_mtu, " mtu %s%"G_GUINT32_FORMAT, route->lock_mtu ? "lock " : "", route->mtu);
-
 	g_snprintf (buf, len,
+	            "%s" /* table */
 	            "%s/%d"
 	            " via %s"
 	            "%s"
 	            " metric %"G_GUINT32_FORMAT
 	            " mss %"G_GUINT32_FORMAT
 	            " rt-src %s" /* protocol */
-	            " src %s/%u" /* source */
+	            "%s" /* source */
 	            "%s" /* cloned */
 	            "%s%s" /* pref-src */
-	            "%s" /* tos */
 	            "%s" /* window */
 	            "%s" /* cwnd */
 	            "%s" /* initcwnd */
 	            "%s" /* initrwnd */
 	            "%s" /* mtu */
+	            "%s" /* pref */
 	            "",
+	            route->table_coerced ? nm_sprintf_buf (str_table, "table %u ", nm_platform_route_table_uncoerce (route->table_coerced, FALSE)) : "",
 	            s_network,
 	            route->plen,
 	            s_gateway,
@@ -4031,358 +5109,724 @@ nm_platform_ip6_route_to_string (const NMPlatformIP6Route *route, char *buf, gsi
 	            route->metric,
 	            route->mss,
 	            nmp_utils_ip_config_source_to_string (route->rt_source, s_source, sizeof (s_source)),
-	            s_src, route->src_plen,
+	            route->src_plen || !IN6_IS_ADDR_UNSPECIFIED (&route->src)
+	              ? nm_sprintf_buf (s_src_all, " src %s/%u", nm_utils_inet6_ntop (&route->src, s_src), (unsigned) route->src_plen)
+	              : "",
 	            route->rt_cloned ? " cloned" : "",
 	            s_pref_src[0] ? " pref-src " : "",
 	            s_pref_src[0] ? s_pref_src : "",
-	            route->tos ? str_tos : "",
-	            route->window ? str_window : "",
-	            route->cwnd ? str_cwnd : "",
-	            route->initcwnd ? str_initcwnd : "",
-	            route->initrwnd ? str_initrwnd : "",
-	            route->mtu ? str_mtu : "");
+	            route->window   || route->lock_window   ? nm_sprintf_buf (str_window,   " window %s%"G_GUINT32_FORMAT,   route->lock_window   ? "lock " : "", route->window)   : "",
+	            route->cwnd     || route->lock_cwnd     ? nm_sprintf_buf (str_cwnd,     " cwnd %s%"G_GUINT32_FORMAT,     route->lock_cwnd     ? "lock " : "", route->cwnd)     : "",
+	            route->initcwnd || route->lock_initcwnd ? nm_sprintf_buf (str_initcwnd, " initcwnd %s%"G_GUINT32_FORMAT, route->lock_initcwnd ? "lock " : "", route->initcwnd) : "",
+	            route->initrwnd || route->lock_initrwnd ? nm_sprintf_buf (str_initrwnd, " initrwnd %s%"G_GUINT32_FORMAT, route->lock_initrwnd ? "lock " : "", route->initrwnd) : "",
+	            route->mtu      || route->lock_mtu      ? nm_sprintf_buf (str_mtu,      " mtu %s%"G_GUINT32_FORMAT,      route->lock_mtu      ? "lock " : "", route->mtu)      : "",
+	            route->rt_pref ? nm_sprintf_buf (str_pref, " pref %s", nm_icmpv6_router_pref_to_string (route->rt_pref, str_pref2, sizeof (str_pref2))) : "");
 
 	return buf;
 }
 
-#define _CMP_SELF(a, b)                                     \
-    G_STMT_START {                                          \
-        if ((a) == (b))                                     \
-            return 0;                                       \
-        if (!(a))                                           \
-            return -1;                                      \
-        if (!(b))                                           \
-            return 1;                                       \
-    } G_STMT_END
-
-#define _CMP_DIRECT(a, b)                                   \
-    G_STMT_START {                                          \
-        if ((a) != (b))                                     \
-            return ((a) < (b)) ? -1 : 1;                    \
-    } G_STMT_END
-
-#define _CMP_DIRECT_MEMCMP(a, b, size)                      \
-    G_STMT_START {                                          \
-        int c = memcmp ((a), (b), (size));                  \
-        if (c != 0)                                         \
-            return c < 0 ? -1 : 1;                          \
-    } G_STMT_END
-
-#define _CMP_FIELD(a, b, field)                             \
-    G_STMT_START {                                          \
-        if (((a)->field) != ((b)->field))                   \
-            return (((a)->field) < ((b)->field)) ? -1 : 1;  \
-    } G_STMT_END
-
-#define _CMP_FIELD_BOOL(a, b, field)                        \
-    G_STMT_START {                                          \
-        if ((!((a)->field)) != (!((b)->field)))                 \
-            return ((!((a)->field)) < (!((b)->field))) ? -1 : 1; \
-    } G_STMT_END
-
-#define _CMP_FIELD_STR(a, b, field)                         \
-    G_STMT_START {                                          \
-        int c = strcmp ((a)->field, (b)->field);            \
-        if (c != 0)                                         \
-            return c < 0 ? -1 : 1;                          \
-    } G_STMT_END
-
-#define _CMP_FIELD_STR_INTERNED(a, b, field)                \
-    G_STMT_START {                                          \
-        if (((a)->field) != ((b)->field)) {                 \
-            /* just to be sure, also do a strcmp() if the pointers don't match */ \
-            int c = g_strcmp0 ((a)->field, (b)->field);     \
-            if (c != 0)                                     \
-                return c < 0 ? -1 : 1;                      \
-        } \
-    } G_STMT_END
-
-#define _CMP_FIELD_STR0(a, b, field)                        \
-    G_STMT_START {                                          \
-        int c = g_strcmp0 ((a)->field, (b)->field);         \
-        if (c != 0)                                         \
-            return c < 0 ? -1 : 1;                          \
-    } G_STMT_END
-
-#define _CMP_FIELD_MEMCMP_LEN(a, b, field, len)             \
-    G_STMT_START {                                          \
-        int c = memcmp (&((a)->field), &((b)->field),       \
-                        MIN (len, sizeof ((a)->field)));    \
-        if (c != 0)                                         \
-            return c < 0 ? -1 : 1;                          \
-    } G_STMT_END
-
-#define _CMP_FIELD_MEMCMP(a, b, field)                      \
-    G_STMT_START {                                          \
-        int c = memcmp (&((a)->field), &((b)->field),       \
-                        sizeof ((a)->field));               \
-        if (c != 0)                                         \
-            return c < 0 ? -1 : 1;                          \
-    } G_STMT_END
+void
+nm_platform_link_hash_update (const NMPlatformLink *obj, NMHashState *h)
+{
+	nm_hash_update_vals (h,
+	                     obj->ifindex,
+	                     obj->master,
+	                     obj->parent,
+	                     obj->n_ifi_flags,
+	                     obj->mtu,
+	                     obj->type,
+	                     obj->arptype,
+	                     obj->inet6_addr_gen_mode_inv,
+	                     obj->inet6_token,
+	                     obj->rx_packets,
+	                     obj->rx_bytes,
+	                     obj->tx_packets,
+	                     obj->tx_bytes,
+	                     NM_HASH_COMBINE_BOOLS (guint8,
+	                                            obj->connected,
+	                                            obj->initialized));
+	nm_hash_update_strarr (h, obj->name);
+	nm_hash_update_str0 (h, obj->kind);
+	nm_hash_update_str0 (h, obj->driver);
+	/* nm_hash_update_mem() also hashes the length obj->addr.len */
+	nm_hash_update_mem (h, obj->addr.data, obj->addr.len);
+}
 
 int
 nm_platform_link_cmp (const NMPlatformLink *a, const NMPlatformLink *b)
 {
-	_CMP_SELF (a, b);
-	_CMP_FIELD (a, b, ifindex);
-	_CMP_FIELD (a, b, type);
-	_CMP_FIELD_STR (a, b, name);
-	_CMP_FIELD (a, b, master);
-	_CMP_FIELD (a, b, parent);
-	_CMP_FIELD (a, b, n_ifi_flags);
-	_CMP_FIELD (a, b, connected);
-	_CMP_FIELD (a, b, mtu);
-	_CMP_FIELD_BOOL (a, b, initialized);
-	_CMP_FIELD (a, b, arptype);
-	_CMP_FIELD (a, b, addr.len);
-	_CMP_FIELD (a, b, inet6_addr_gen_mode_inv);
-	_CMP_FIELD_STR_INTERNED (a, b, kind);
-	_CMP_FIELD_STR_INTERNED (a, b, driver);
+	NM_CMP_SELF (a, b);
+	NM_CMP_FIELD (a, b, ifindex);
+	NM_CMP_FIELD (a, b, type);
+	NM_CMP_FIELD_STR (a, b, name);
+	NM_CMP_FIELD (a, b, master);
+	NM_CMP_FIELD (a, b, parent);
+	NM_CMP_FIELD (a, b, n_ifi_flags);
+	NM_CMP_FIELD_UNSAFE (a, b, connected);
+	NM_CMP_FIELD (a, b, mtu);
+	NM_CMP_FIELD_BOOL (a, b, initialized);
+	NM_CMP_FIELD (a, b, arptype);
+	NM_CMP_FIELD (a, b, addr.len);
+	NM_CMP_FIELD (a, b, inet6_addr_gen_mode_inv);
+	NM_CMP_FIELD_STR_INTERNED (a, b, kind);
+	NM_CMP_FIELD_STR_INTERNED (a, b, driver);
 	if (a->addr.len)
-		_CMP_FIELD_MEMCMP_LEN (a, b, addr.data, a->addr.len);
-	_CMP_FIELD_MEMCMP (a, b, inet6_token);
-	_CMP_FIELD (a, b, rx_packets);
-	_CMP_FIELD (a, b, rx_bytes);
-	_CMP_FIELD (a, b, tx_packets);
-	_CMP_FIELD (a, b, tx_bytes);
+		NM_CMP_FIELD_MEMCMP_LEN (a, b, addr.data, a->addr.len);
+	NM_CMP_FIELD_MEMCMP (a, b, inet6_token);
+	NM_CMP_FIELD (a, b, rx_packets);
+	NM_CMP_FIELD (a, b, rx_bytes);
+	NM_CMP_FIELD (a, b, tx_packets);
+	NM_CMP_FIELD (a, b, tx_bytes);
 	return 0;
 }
 
+void
+nm_platform_lnk_gre_hash_update (const NMPlatformLnkGre *obj, NMHashState *h)
+{
+	nm_hash_update_vals (h,
+	                     obj->local,
+	                     obj->remote,
+	                     obj->parent_ifindex,
+	                     obj->input_flags,
+	                     obj->output_flags,
+	                     obj->input_key,
+	                     obj->output_key,
+	                     obj->ttl,
+	                     obj->tos,
+	                     (bool) obj->path_mtu_discovery);
+}
+
 int
 nm_platform_lnk_gre_cmp (const NMPlatformLnkGre *a, const NMPlatformLnkGre *b)
 {
-	_CMP_SELF (a, b);
-	_CMP_FIELD (a, b, parent_ifindex);
-	_CMP_FIELD (a, b, input_flags);
-	_CMP_FIELD (a, b, output_flags);
-	_CMP_FIELD (a, b, input_key);
-	_CMP_FIELD (a, b, output_key);
-	_CMP_FIELD (a, b, local);
-	_CMP_FIELD (a, b, remote);
-	_CMP_FIELD (a, b, ttl);
-	_CMP_FIELD (a, b, tos);
-	_CMP_FIELD_BOOL (a, b, path_mtu_discovery);
+	NM_CMP_SELF (a, b);
+	NM_CMP_FIELD (a, b, parent_ifindex);
+	NM_CMP_FIELD (a, b, input_flags);
+	NM_CMP_FIELD (a, b, output_flags);
+	NM_CMP_FIELD (a, b, input_key);
+	NM_CMP_FIELD (a, b, output_key);
+	NM_CMP_FIELD (a, b, local);
+	NM_CMP_FIELD (a, b, remote);
+	NM_CMP_FIELD (a, b, ttl);
+	NM_CMP_FIELD (a, b, tos);
+	NM_CMP_FIELD_BOOL (a, b, path_mtu_discovery);
 	return 0;
 }
 
+void
+nm_platform_lnk_infiniband_hash_update (const NMPlatformLnkInfiniband *obj, NMHashState *h)
+{
+	nm_hash_update_val (h, obj->p_key);
+	nm_hash_update_str0 (h, obj->mode);
+}
+
 int
 nm_platform_lnk_infiniband_cmp (const NMPlatformLnkInfiniband *a, const NMPlatformLnkInfiniband *b)
 {
-	_CMP_SELF (a, b);
-	_CMP_FIELD (a, b, p_key);
-	_CMP_FIELD_STR_INTERNED (a, b, mode);
+	NM_CMP_SELF (a, b);
+	NM_CMP_FIELD (a, b, p_key);
+	NM_CMP_FIELD_STR_INTERNED (a, b, mode);
 	return 0;
 }
 
+void
+nm_platform_lnk_ip6tnl_hash_update (const NMPlatformLnkIp6Tnl *obj, NMHashState *h)
+{
+	nm_hash_update_vals (h,
+	                     obj->local,
+	                     obj->remote,
+	                     obj->parent_ifindex,
+	                     obj->ttl,
+	                     obj->tclass,
+	                     obj->encap_limit,
+	                     obj->proto,
+	                     obj->flow_label);
+}
+
 int
 nm_platform_lnk_ip6tnl_cmp (const NMPlatformLnkIp6Tnl *a, const NMPlatformLnkIp6Tnl *b)
 {
-	_CMP_SELF (a, b);
-	_CMP_FIELD (a, b, parent_ifindex);
-	_CMP_FIELD_MEMCMP (a, b, local);
-	_CMP_FIELD_MEMCMP (a, b, remote);
-	_CMP_FIELD (a, b, ttl);
-	_CMP_FIELD (a, b, tclass);
-	_CMP_FIELD (a, b, encap_limit);
-	_CMP_FIELD (a, b, flow_label);
-	_CMP_FIELD (a, b, proto);
+	NM_CMP_SELF (a, b);
+	NM_CMP_FIELD (a, b, parent_ifindex);
+	NM_CMP_FIELD_MEMCMP (a, b, local);
+	NM_CMP_FIELD_MEMCMP (a, b, remote);
+	NM_CMP_FIELD (a, b, ttl);
+	NM_CMP_FIELD (a, b, tclass);
+	NM_CMP_FIELD (a, b, encap_limit);
+	NM_CMP_FIELD (a, b, flow_label);
+	NM_CMP_FIELD (a, b, proto);
 	return 0;
 }
 
+void
+nm_platform_lnk_ipip_hash_update (const NMPlatformLnkIpIp *obj, NMHashState *h)
+{
+	nm_hash_update_vals (h,
+	                     obj->local,
+	                     obj->remote,
+	                     obj->parent_ifindex,
+	                     obj->ttl,
+	                     obj->tos,
+	                     (bool) obj->path_mtu_discovery);
+}
+
 int
 nm_platform_lnk_ipip_cmp (const NMPlatformLnkIpIp *a, const NMPlatformLnkIpIp *b)
 {
-	_CMP_SELF (a, b);
-	_CMP_FIELD (a, b, parent_ifindex);
-	_CMP_FIELD (a, b, local);
-	_CMP_FIELD (a, b, remote);
-	_CMP_FIELD (a, b, ttl);
-	_CMP_FIELD (a, b, tos);
-	_CMP_FIELD_BOOL (a, b, path_mtu_discovery);
+	NM_CMP_SELF (a, b);
+	NM_CMP_FIELD (a, b, parent_ifindex);
+	NM_CMP_FIELD (a, b, local);
+	NM_CMP_FIELD (a, b, remote);
+	NM_CMP_FIELD (a, b, ttl);
+	NM_CMP_FIELD (a, b, tos);
+	NM_CMP_FIELD_BOOL (a, b, path_mtu_discovery);
 	return 0;
 }
 
+void
+nm_platform_lnk_macsec_hash_update (const NMPlatformLnkMacsec *obj, NMHashState *h)
+{
+	nm_hash_update_vals (h,
+	                     obj->parent_ifindex,
+	                     obj->sci,
+	                     obj->cipher_suite,
+	                     obj->window,
+	                     obj->icv_length,
+	                     obj->encoding_sa,
+	                     obj->validation,
+	                     NM_HASH_COMBINE_BOOLS (guint8,
+	                                            obj->encrypt,
+	                                            obj->protect,
+	                                            obj->include_sci,
+	                                            obj->es,
+	                                            obj->scb,
+	                                            obj->replay_protect));
+}
+
 int
 nm_platform_lnk_macsec_cmp (const NMPlatformLnkMacsec *a, const NMPlatformLnkMacsec *b)
 {
-	_CMP_SELF (a, b);
-	_CMP_FIELD (a, b, sci);
-	_CMP_FIELD (a, b, icv_length);
-	_CMP_FIELD (a, b, cipher_suite);
-	_CMP_FIELD (a, b, window);
-	_CMP_FIELD (a, b, encoding_sa);
-	_CMP_FIELD (a, b, validation);
-	_CMP_FIELD (a, b, encrypt);
-	_CMP_FIELD (a, b, protect);
-	_CMP_FIELD (a, b, include_sci);
-	_CMP_FIELD (a, b, es);
-	_CMP_FIELD (a, b, scb);
-	_CMP_FIELD (a, b, replay_protect);
+	NM_CMP_SELF (a, b);
+	NM_CMP_FIELD (a, b, parent_ifindex);
+	NM_CMP_FIELD (a, b, sci);
+	NM_CMP_FIELD (a, b, icv_length);
+	NM_CMP_FIELD (a, b, cipher_suite);
+	NM_CMP_FIELD (a, b, window);
+	NM_CMP_FIELD (a, b, encoding_sa);
+	NM_CMP_FIELD (a, b, validation);
+	NM_CMP_FIELD_UNSAFE (a, b, encrypt);
+	NM_CMP_FIELD_UNSAFE (a, b, protect);
+	NM_CMP_FIELD_UNSAFE (a, b, include_sci);
+	NM_CMP_FIELD_UNSAFE (a, b, es);
+	NM_CMP_FIELD_UNSAFE (a, b, scb);
+	NM_CMP_FIELD_UNSAFE (a, b, replay_protect);
 	return 0;
 }
 
+void
+nm_platform_lnk_macvlan_hash_update (const NMPlatformLnkMacvlan *obj, NMHashState *h )
+{
+	nm_hash_update_vals (h,
+	                     obj->mode,
+	                     NM_HASH_COMBINE_BOOLS (guint8,
+	                                            obj->no_promisc,
+	                                            obj->tap));
+}
+
 int
 nm_platform_lnk_macvlan_cmp (const NMPlatformLnkMacvlan *a, const NMPlatformLnkMacvlan *b)
 {
-	_CMP_SELF (a, b);
-	_CMP_FIELD (a, b, mode);
-	_CMP_FIELD_BOOL (a, b, no_promisc);
+	NM_CMP_SELF (a, b);
+	NM_CMP_FIELD (a, b, mode);
+	NM_CMP_FIELD_UNSAFE (a, b, no_promisc);
+	NM_CMP_FIELD_UNSAFE (a, b, tap);
 	return 0;
 }
 
+void
+nm_platform_lnk_sit_hash_update (const NMPlatformLnkSit *obj, NMHashState *h)
+{
+	nm_hash_update_vals (h,
+	                     obj->local,
+	                     obj->remote,
+	                     obj->parent_ifindex,
+	                     obj->flags,
+	                     obj->ttl,
+	                     obj->tos,
+	                     obj->proto,
+	                     (bool) obj->path_mtu_discovery);
+}
+
 int
 nm_platform_lnk_sit_cmp (const NMPlatformLnkSit *a, const NMPlatformLnkSit *b)
 {
-	_CMP_SELF (a, b);
-	_CMP_FIELD (a, b, parent_ifindex);
-	_CMP_FIELD (a, b, local);
-	_CMP_FIELD (a, b, remote);
-	_CMP_FIELD (a, b, ttl);
-	_CMP_FIELD (a, b, tos);
-	_CMP_FIELD_BOOL (a, b, path_mtu_discovery);
-	_CMP_FIELD (a, b, flags);
-	_CMP_FIELD (a, b, proto);
+	NM_CMP_SELF (a, b);
+	NM_CMP_FIELD (a, b, parent_ifindex);
+	NM_CMP_FIELD (a, b, local);
+	NM_CMP_FIELD (a, b, remote);
+	NM_CMP_FIELD (a, b, ttl);
+	NM_CMP_FIELD (a, b, tos);
+	NM_CMP_FIELD_BOOL (a, b, path_mtu_discovery);
+	NM_CMP_FIELD (a, b, flags);
+	NM_CMP_FIELD (a, b, proto);
 	return 0;
 }
 
+void
+nm_platform_lnk_vlan_hash_update (const NMPlatformLnkVlan *obj, NMHashState *h)
+{
+	nm_hash_update_vals (h,
+	                     obj->id,
+	                     obj->flags);
+}
+
 int
 nm_platform_lnk_vlan_cmp (const NMPlatformLnkVlan *a, const NMPlatformLnkVlan *b)
 {
-	_CMP_SELF (a, b);
-	_CMP_FIELD (a, b, id);
-	_CMP_FIELD (a, b, flags);
+	NM_CMP_SELF (a, b);
+	NM_CMP_FIELD (a, b, id);
+	NM_CMP_FIELD (a, b, flags);
 	return 0;
 }
 
+void
+nm_platform_lnk_vxlan_hash_update (const NMPlatformLnkVxlan *obj, NMHashState *h)
+{
+	nm_hash_update_vals (h,
+	                     obj->group6,
+	                     obj->local6,
+	                     obj->group,
+	                     obj->local,
+	                     obj->parent_ifindex,
+	                     obj->id,
+	                     obj->ageing,
+	                     obj->limit,
+	                     obj->dst_port,
+	                     obj->src_port_min,
+	                     obj->src_port_max,
+	                     obj->tos,
+	                     obj->ttl,
+	                     NM_HASH_COMBINE_BOOLS (guint8,
+	                                            obj->learning,
+	                                            obj->proxy,
+	                                            obj->rsc,
+	                                            obj->l2miss,
+	                                            obj->l3miss));
+}
+
 int
 nm_platform_lnk_vxlan_cmp (const NMPlatformLnkVxlan *a, const NMPlatformLnkVxlan *b)
 {
-	_CMP_SELF (a, b);
-	_CMP_FIELD (a, b, parent_ifindex);
-	_CMP_FIELD (a, b, id);
-	_CMP_FIELD (a, b, group);
-	_CMP_FIELD (a, b, local);
-	_CMP_FIELD_MEMCMP (a, b, group6);
-	_CMP_FIELD_MEMCMP (a, b, local6);
-	_CMP_FIELD (a, b, tos);
-	_CMP_FIELD (a, b, ttl);
-	_CMP_FIELD_BOOL (a, b, learning);
-	_CMP_FIELD (a, b, ageing);
-	_CMP_FIELD (a, b, limit);
-	_CMP_FIELD (a, b, dst_port);
-	_CMP_FIELD (a, b, src_port_min);
-	_CMP_FIELD (a, b, src_port_max);
-	_CMP_FIELD_BOOL (a, b, proxy);
-	_CMP_FIELD_BOOL (a, b, rsc);
-	_CMP_FIELD_BOOL (a, b, l2miss);
-	_CMP_FIELD_BOOL (a, b, l3miss);
+	NM_CMP_SELF (a, b);
+	NM_CMP_FIELD (a, b, parent_ifindex);
+	NM_CMP_FIELD (a, b, id);
+	NM_CMP_FIELD (a, b, group);
+	NM_CMP_FIELD (a, b, local);
+	NM_CMP_FIELD_MEMCMP (a, b, group6);
+	NM_CMP_FIELD_MEMCMP (a, b, local6);
+	NM_CMP_FIELD (a, b, tos);
+	NM_CMP_FIELD (a, b, ttl);
+	NM_CMP_FIELD_BOOL (a, b, learning);
+	NM_CMP_FIELD (a, b, ageing);
+	NM_CMP_FIELD (a, b, limit);
+	NM_CMP_FIELD (a, b, dst_port);
+	NM_CMP_FIELD (a, b, src_port_min);
+	NM_CMP_FIELD (a, b, src_port_max);
+	NM_CMP_FIELD_BOOL (a, b, proxy);
+	NM_CMP_FIELD_BOOL (a, b, rsc);
+	NM_CMP_FIELD_BOOL (a, b, l2miss);
+	NM_CMP_FIELD_BOOL (a, b, l3miss);
 	return 0;
 }
 
+void
+nm_platform_ip4_address_hash_update (const NMPlatformIP4Address *obj, NMHashState *h)
+{
+	nm_hash_update_vals (h,
+	                     obj->ifindex,
+	                     obj->addr_source,
+	                     obj->timestamp,
+	                     obj->lifetime,
+	                     obj->preferred,
+	                     obj->n_ifa_flags,
+	                     obj->plen,
+	                     obj->address,
+	                     obj->peer_address);
+	nm_hash_update_strarr (h, obj->label);
+}
+
 int
 nm_platform_ip4_address_cmp (const NMPlatformIP4Address *a, const NMPlatformIP4Address *b)
 {
-	_CMP_SELF (a, b);
-	_CMP_FIELD (a, b, ifindex);
-	_CMP_FIELD (a, b, address);
-	_CMP_FIELD (a, b, plen);
-	_CMP_FIELD (a, b, peer_address);
-	_CMP_FIELD (a, b, addr_source);
-	_CMP_FIELD (a, b, timestamp);
-	_CMP_FIELD (a, b, lifetime);
-	_CMP_FIELD (a, b, preferred);
-	_CMP_FIELD (a, b, n_ifa_flags);
-	_CMP_FIELD_STR (a, b, label);
+	NM_CMP_SELF (a, b);
+	NM_CMP_FIELD (a, b, ifindex);
+	NM_CMP_FIELD (a, b, address);
+	NM_CMP_FIELD (a, b, plen);
+	NM_CMP_FIELD (a, b, peer_address);
+	NM_CMP_FIELD (a, b, addr_source);
+	NM_CMP_FIELD (a, b, timestamp);
+	NM_CMP_FIELD (a, b, lifetime);
+	NM_CMP_FIELD (a, b, preferred);
+	NM_CMP_FIELD (a, b, n_ifa_flags);
+	NM_CMP_FIELD_STR (a, b, label);
 	return 0;
 }
 
+void
+nm_platform_ip6_address_hash_update (const NMPlatformIP6Address *obj, NMHashState *h)
+{
+	nm_hash_update_vals (h,
+	                     obj->ifindex,
+	                     obj->addr_source,
+	                     obj->timestamp,
+	                     obj->lifetime,
+	                     obj->preferred,
+	                     obj->n_ifa_flags,
+	                     obj->plen,
+	                     obj->address,
+	                     obj->peer_address);
+}
+
 int
 nm_platform_ip6_address_cmp (const NMPlatformIP6Address *a, const NMPlatformIP6Address *b)
 {
 	const struct in6_addr *p_a, *p_b;
 
-	_CMP_SELF (a, b);
-	_CMP_FIELD (a, b, ifindex);
-	_CMP_FIELD_MEMCMP (a, b, address);
-	_CMP_FIELD (a, b, plen);
+	NM_CMP_SELF (a, b);
+	NM_CMP_FIELD (a, b, ifindex);
+	NM_CMP_FIELD_MEMCMP (a, b, address);
+	NM_CMP_FIELD (a, b, plen);
 	p_a = nm_platform_ip6_address_get_peer (a);
 	p_b = nm_platform_ip6_address_get_peer (b);
-	_CMP_DIRECT_MEMCMP (p_a, p_b, sizeof (*p_a));
-	_CMP_FIELD (a, b, addr_source);
-	_CMP_FIELD (a, b, timestamp);
-	_CMP_FIELD (a, b, lifetime);
-	_CMP_FIELD (a, b, preferred);
-	_CMP_FIELD (a, b, n_ifa_flags);
+	NM_CMP_DIRECT_MEMCMP (p_a, p_b, sizeof (*p_a));
+	NM_CMP_FIELD (a, b, addr_source);
+	NM_CMP_FIELD (a, b, timestamp);
+	NM_CMP_FIELD (a, b, lifetime);
+	NM_CMP_FIELD (a, b, preferred);
+	NM_CMP_FIELD (a, b, n_ifa_flags);
 	return 0;
 }
 
+void
+nm_platform_ip4_route_hash_update (const NMPlatformIP4Route *obj, NMPlatformIPRouteCmpType cmp_type, NMHashState *h)
+{
+	switch (cmp_type) {
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID:
+		nm_hash_update_vals (h,
+		                     nm_platform_route_table_uncoerce (obj->table_coerced, TRUE),
+		                     nm_utils_ip4_address_clear_host_address (obj->network, obj->plen),
+		                     obj->plen,
+		                     obj->metric,
+		                     obj->tos);
+		break;
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID:
+		nm_hash_update_vals (h,
+		                     nm_platform_route_table_uncoerce (obj->table_coerced, TRUE),
+		                     nm_utils_ip4_address_clear_host_address (obj->network, obj->plen),
+		                     obj->plen,
+		                     obj->metric,
+		                     obj->tos,
+		                     /* on top of WEAK_ID: */
+		                     obj->ifindex,
+		                     nmp_utils_ip_config_source_round_trip_rtprot (obj->rt_source),
+		                     _ip_route_scope_inv_get_normalized (obj),
+		                     obj->gateway,
+		                     obj->mss,
+		                     obj->pref_src,
+		                     obj->window,
+		                     obj->cwnd,
+		                     obj->initcwnd,
+		                     obj->initrwnd,
+		                     obj->mtu,
+		                     NM_HASH_COMBINE_BOOLS (guint8,
+		                                            obj->lock_window,
+		                                            obj->lock_cwnd,
+		                                            obj->lock_initcwnd,
+		                                            obj->lock_initrwnd,
+		                                            obj->lock_mtu));
+		break;
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY:
+		nm_hash_update_vals (h,
+		                     nm_platform_route_table_uncoerce (obj->table_coerced, TRUE),
+		                     obj->ifindex,
+		                     nm_utils_ip4_address_clear_host_address (obj->network, obj->plen),
+		                     obj->plen,
+		                     obj->metric,
+		                     obj->gateway,
+		                     nmp_utils_ip_config_source_round_trip_rtprot (obj->rt_source),
+		                     _ip_route_scope_inv_get_normalized (obj),
+		                     obj->tos,
+		                     obj->mss,
+		                     obj->pref_src,
+		                     obj->window,
+		                     obj->cwnd,
+		                     obj->initcwnd,
+		                     obj->initrwnd,
+		                     obj->mtu,
+		                     NM_HASH_COMBINE_BOOLS (guint8,
+		                                            obj->rt_cloned,
+		                                            obj->lock_window,
+		                                            obj->lock_cwnd,
+		                                            obj->lock_initcwnd,
+		                                            obj->lock_initrwnd,
+		                                            obj->lock_mtu));
+		break;
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL:
+		nm_hash_update_vals (h,
+		                     obj->table_coerced,
+		                     obj->ifindex,
+		                     obj->network,
+		                     obj->plen,
+		                     obj->metric,
+		                     obj->gateway,
+		                     obj->rt_source,
+		                     obj->scope_inv,
+		                     obj->tos,
+		                     obj->mss,
+		                     obj->pref_src,
+		                     obj->window,
+		                     obj->cwnd,
+		                     obj->initcwnd,
+		                     obj->initrwnd,
+		                     obj->mtu,
+		                     NM_HASH_COMBINE_BOOLS (guint8,
+		                                            obj->rt_cloned,
+		                                            obj->lock_window,
+		                                            obj->lock_cwnd,
+		                                            obj->lock_initcwnd,
+		                                            obj->lock_initrwnd,
+		                                            obj->lock_mtu));
+		break;
+	}
+}
+
 int
-nm_platform_ip4_route_cmp_full (const NMPlatformIP4Route *a, const NMPlatformIP4Route *b, gboolean consider_host_part)
-{
-	_CMP_SELF (a, b);
-	_CMP_FIELD (a, b, ifindex);
-	if (consider_host_part)
-		_CMP_FIELD (a, b, network);
-	else {
-		_CMP_DIRECT (nm_utils_ip4_address_clear_host_address (a->network, a->plen),
-		             nm_utils_ip4_address_clear_host_address (b->network, b->plen));
+nm_platform_ip4_route_cmp (const NMPlatformIP4Route *a, const NMPlatformIP4Route *b, NMPlatformIPRouteCmpType cmp_type)
+{
+	NM_CMP_SELF (a, b);
+	switch (cmp_type) {
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID:
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID:
+		NM_CMP_DIRECT (nm_platform_route_table_uncoerce (a->table_coerced, TRUE),
+		               nm_platform_route_table_uncoerce (b->table_coerced, TRUE));
+		NM_CMP_DIRECT_IN4ADDR_SAME_PREFIX (a->network, b->network, MIN (a->plen, b->plen));
+		NM_CMP_FIELD (a, b, plen);
+		NM_CMP_FIELD (a, b, metric);
+		NM_CMP_FIELD (a, b, tos);
+		if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) {
+			NM_CMP_FIELD (a, b, ifindex);
+			NM_CMP_DIRECT (nmp_utils_ip_config_source_round_trip_rtprot (a->rt_source),
+			               nmp_utils_ip_config_source_round_trip_rtprot (b->rt_source));
+			NM_CMP_DIRECT (_ip_route_scope_inv_get_normalized (a),
+			               _ip_route_scope_inv_get_normalized (b));
+			NM_CMP_FIELD (a, b, gateway);
+			NM_CMP_FIELD (a, b, mss);
+			NM_CMP_FIELD (a, b, pref_src);
+			NM_CMP_FIELD (a, b, window);
+			NM_CMP_FIELD (a, b, cwnd);
+			NM_CMP_FIELD (a, b, initcwnd);
+			NM_CMP_FIELD (a, b, initrwnd);
+			NM_CMP_FIELD (a, b, mtu);
+			NM_CMP_FIELD_UNSAFE (a, b, lock_window);
+			NM_CMP_FIELD_UNSAFE (a, b, lock_cwnd);
+			NM_CMP_FIELD_UNSAFE (a, b, lock_initcwnd);
+			NM_CMP_FIELD_UNSAFE (a, b, lock_initrwnd);
+			NM_CMP_FIELD_UNSAFE (a, b, lock_mtu);
+		}
+		break;
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY:
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL:
+		if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) {
+			NM_CMP_DIRECT (nm_platform_route_table_uncoerce (a->table_coerced, TRUE),
+			               nm_platform_route_table_uncoerce (b->table_coerced, TRUE));
+		} else
+			NM_CMP_FIELD (a, b, table_coerced);
+		NM_CMP_FIELD (a, b, ifindex);
+		if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
+			NM_CMP_DIRECT_IN4ADDR_SAME_PREFIX (a->network, b->network, MIN (a->plen, b->plen));
+		else
+			NM_CMP_FIELD (a, b, network);
+		NM_CMP_FIELD (a, b, plen);
+		NM_CMP_FIELD (a, b, metric);
+		NM_CMP_FIELD (a, b, gateway);
+		if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) {
+			NM_CMP_DIRECT (nmp_utils_ip_config_source_round_trip_rtprot (a->rt_source),
+			               nmp_utils_ip_config_source_round_trip_rtprot (b->rt_source));
+			NM_CMP_DIRECT (_ip_route_scope_inv_get_normalized (a),
+			               _ip_route_scope_inv_get_normalized (b));
+		} else {
+			NM_CMP_FIELD (a, b, rt_source);
+			NM_CMP_FIELD (a, b, scope_inv);
+		}
+		NM_CMP_FIELD (a, b, mss);
+		NM_CMP_FIELD (a, b, pref_src);
+		NM_CMP_FIELD_UNSAFE (a, b, rt_cloned);
+		NM_CMP_FIELD (a, b, tos);
+		NM_CMP_FIELD_UNSAFE (a, b, lock_window);
+		NM_CMP_FIELD_UNSAFE (a, b, lock_cwnd);
+		NM_CMP_FIELD_UNSAFE (a, b, lock_initcwnd);
+		NM_CMP_FIELD_UNSAFE (a, b, lock_initrwnd);
+		NM_CMP_FIELD_UNSAFE (a, b, lock_mtu);
+		NM_CMP_FIELD (a, b, window);
+		NM_CMP_FIELD (a, b, cwnd);
+		NM_CMP_FIELD (a, b, initcwnd);
+		NM_CMP_FIELD (a, b, initrwnd);
+		NM_CMP_FIELD (a, b, mtu);
+		break;
 	}
-	_CMP_FIELD (a, b, plen);
-	_CMP_FIELD (a, b, metric);
-	_CMP_FIELD (a, b, gateway);
-	_CMP_FIELD (a, b, rt_source);
-	_CMP_FIELD (a, b, mss);
-	_CMP_FIELD (a, b, scope_inv);
-	_CMP_FIELD (a, b, pref_src);
-	_CMP_FIELD (a, b, rt_cloned);
-	_CMP_FIELD (a, b, tos);
-	_CMP_FIELD (a, b, lock_window);
-	_CMP_FIELD (a, b, lock_cwnd);
-	_CMP_FIELD (a, b, lock_initcwnd);
-	_CMP_FIELD (a, b, lock_initrwnd);
-	_CMP_FIELD (a, b, lock_mtu);
-	_CMP_FIELD (a, b, window);
-	_CMP_FIELD (a, b, cwnd);
-	_CMP_FIELD (a, b, initcwnd);
-	_CMP_FIELD (a, b, initrwnd);
-	_CMP_FIELD (a, b, mtu);
 	return 0;
 }
 
-int
-nm_platform_ip6_route_cmp_full (const NMPlatformIP6Route *a, const NMPlatformIP6Route *b, gboolean consider_host_part)
-{
-	_CMP_SELF (a, b);
-	_CMP_FIELD (a, b, ifindex);
-	if (consider_host_part)
-		_CMP_FIELD_MEMCMP (a, b, network);
-	else {
-		struct in6_addr n1, n2;
+void
+nm_platform_ip6_route_hash_update (const NMPlatformIP6Route *obj, NMPlatformIPRouteCmpType cmp_type, NMHashState *h)
+{
+	struct in6_addr a1, a2;
+
+	switch (cmp_type) {
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID:
+		nm_hash_update_vals (h,
+		                     nm_platform_route_table_uncoerce (obj->table_coerced, TRUE),
+		                     *nm_utils_ip6_address_clear_host_address (&a1, &obj->network, obj->plen),
+		                     obj->plen,
+		                     nm_utils_ip6_route_metric_normalize (obj->metric),
+		                     *nm_utils_ip6_address_clear_host_address (&a2, &obj->src, obj->src_plen),
+		                     obj->src_plen);
+		break;
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID:
+		nm_hash_update_vals (h,
+		                     nm_platform_route_table_uncoerce (obj->table_coerced, TRUE),
+		                     *nm_utils_ip6_address_clear_host_address (&a1, &obj->network, obj->plen),
+		                     obj->plen,
+		                     nm_utils_ip6_route_metric_normalize (obj->metric),
+		                     *nm_utils_ip6_address_clear_host_address (&a2, &obj->src, obj->src_plen),
+		                     obj->src_plen,
+		                     /* on top of WEAK_ID: */
+		                     obj->ifindex,
+		                     obj->gateway);
+		break;
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY:
+		nm_hash_update_vals (h,
+		                     nm_platform_route_table_uncoerce (obj->table_coerced, TRUE),
+		                     obj->ifindex,
+		                     *nm_utils_ip6_address_clear_host_address (&a1, &obj->network, obj->plen),
+		                     obj->plen,
+		                     nm_utils_ip6_route_metric_normalize (obj->metric),
+		                     obj->gateway,
+		                     obj->pref_src,
+		                     *nm_utils_ip6_address_clear_host_address (&a2, &obj->src, obj->src_plen),
+		                     obj->src_plen,
+		                     nmp_utils_ip_config_source_round_trip_rtprot (obj->rt_source),
+		                     obj->mss,
+		                     NM_HASH_COMBINE_BOOLS (guint8,
+		                                            obj->rt_cloned,
+		                                            obj->lock_window,
+		                                            obj->lock_cwnd,
+		                                            obj->lock_initcwnd,
+		                                            obj->lock_initrwnd,
+		                                            obj->lock_mtu),
+		                     obj->window,
+		                     obj->cwnd,
+		                     obj->initcwnd,
+		                     obj->initrwnd,
+		                     obj->mtu,
+		                     _route_pref_normalize (obj->rt_pref));
+		break;
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL:
+		nm_hash_update_vals (h,
+		                     obj->table_coerced,
+		                     obj->ifindex,
+		                     obj->network,
+		                     obj->plen,
+		                     obj->metric,
+		                     obj->gateway,
+		                     obj->pref_src,
+		                     obj->src,
+		                     obj->src_plen,
+		                     obj->rt_source,
+		                     obj->mss,
+		                     NM_HASH_COMBINE_BOOLS (guint8,
+		                                            obj->rt_cloned,
+		                                            obj->lock_window,
+		                                            obj->lock_cwnd,
+		                                            obj->lock_initcwnd,
+		                                            obj->lock_initrwnd,
+		                                            obj->lock_mtu),
+		                     obj->window,
+		                     obj->cwnd,
+		                     obj->initcwnd,
+		                     obj->initrwnd,
+		                     obj->mtu,
+		                     obj->rt_pref);
+		break;
+	}
+}
 
-		nm_utils_ip6_address_clear_host_address (&n1, &a->network, a->plen);
-		nm_utils_ip6_address_clear_host_address (&n2, &b->network, b->plen);
-		_CMP_DIRECT_MEMCMP (&n1, &n2, sizeof (struct in6_addr));
+int
+nm_platform_ip6_route_cmp (const NMPlatformIP6Route *a, const NMPlatformIP6Route *b, NMPlatformIPRouteCmpType cmp_type)
+{
+	NM_CMP_SELF (a, b);
+	switch (cmp_type) {
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID:
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID:
+		NM_CMP_DIRECT (nm_platform_route_table_uncoerce (a->table_coerced, TRUE),
+		               nm_platform_route_table_uncoerce (b->table_coerced, TRUE));
+		NM_CMP_DIRECT_IN6ADDR_SAME_PREFIX (&a->network, &b->network, MIN (a->plen, b->plen));
+		NM_CMP_FIELD (a, b, plen);
+		NM_CMP_DIRECT (nm_utils_ip6_route_metric_normalize (a->metric), nm_utils_ip6_route_metric_normalize (b->metric));
+		NM_CMP_DIRECT_IN6ADDR_SAME_PREFIX (&a->src, &b->src, MIN (a->src_plen, b->src_plen));
+		NM_CMP_FIELD (a, b, src_plen);
+		if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) {
+			NM_CMP_FIELD (a, b, ifindex);
+			NM_CMP_FIELD_IN6ADDR (a, b, gateway);
+		}
+		break;
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY:
+	case NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL:
+		if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) {
+			NM_CMP_DIRECT (nm_platform_route_table_uncoerce (a->table_coerced, TRUE),
+			               nm_platform_route_table_uncoerce (b->table_coerced, TRUE));
+		} else
+			NM_CMP_FIELD (a, b, table_coerced);
+		NM_CMP_FIELD (a, b, ifindex);
+		if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
+			NM_CMP_DIRECT_IN6ADDR_SAME_PREFIX (&a->network, &b->network, MIN (a->plen, b->plen));
+		else
+			NM_CMP_FIELD_IN6ADDR (a, b, network);
+		NM_CMP_FIELD (a, b, plen);
+		if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
+			NM_CMP_DIRECT (nm_utils_ip6_route_metric_normalize (a->metric), nm_utils_ip6_route_metric_normalize (b->metric));
+		else
+			NM_CMP_FIELD (a, b, metric);
+		NM_CMP_FIELD_IN6ADDR (a, b, gateway);
+		NM_CMP_FIELD_IN6ADDR (a, b, pref_src);
+		if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) {
+			NM_CMP_DIRECT_IN6ADDR_SAME_PREFIX (&a->src, &b->src, MIN (a->src_plen, b->src_plen));
+			NM_CMP_FIELD (a, b, src_plen);
+			NM_CMP_DIRECT (nmp_utils_ip_config_source_round_trip_rtprot (a->rt_source),
+			               nmp_utils_ip_config_source_round_trip_rtprot (b->rt_source));
+		} else {
+			NM_CMP_FIELD_IN6ADDR (a, b, src);
+			NM_CMP_FIELD (a, b, src_plen);
+			NM_CMP_FIELD (a, b, rt_source);
+		}
+		NM_CMP_FIELD (a, b, mss);
+		NM_CMP_FIELD_UNSAFE (a, b, rt_cloned);
+		NM_CMP_FIELD_UNSAFE (a, b, lock_window);
+		NM_CMP_FIELD_UNSAFE (a, b, lock_cwnd);
+		NM_CMP_FIELD_UNSAFE (a, b, lock_initcwnd);
+		NM_CMP_FIELD_UNSAFE (a, b, lock_initrwnd);
+		NM_CMP_FIELD_UNSAFE (a, b, lock_mtu);
+		NM_CMP_FIELD (a, b, window);
+		NM_CMP_FIELD (a, b, cwnd);
+		NM_CMP_FIELD (a, b, initcwnd);
+		NM_CMP_FIELD (a, b, initrwnd);
+		NM_CMP_FIELD (a, b, mtu);
+		if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
+			NM_CMP_DIRECT (_route_pref_normalize (a->rt_pref), _route_pref_normalize (b->rt_pref));
+		else
+			NM_CMP_FIELD (a, b, rt_pref);
+		break;
 	}
-	_CMP_FIELD (a, b, plen);
-	_CMP_FIELD (a, b, metric);
-	_CMP_FIELD_MEMCMP (a, b, gateway);
-	_CMP_FIELD_MEMCMP (a, b, pref_src);
-	_CMP_FIELD_MEMCMP (a, b, src);
-	_CMP_FIELD (a, b, src_plen);
-	_CMP_FIELD (a, b, rt_source);
-	_CMP_FIELD (a, b, mss);
-	_CMP_FIELD (a, b, rt_cloned);
-	_CMP_FIELD (a, b, tos);
-	_CMP_FIELD (a, b, lock_window);
-	_CMP_FIELD (a, b, lock_cwnd);
-	_CMP_FIELD (a, b, lock_initcwnd);
-	_CMP_FIELD (a, b, lock_initrwnd);
-	_CMP_FIELD (a, b, lock_mtu);
-	_CMP_FIELD (a, b, window);
-	_CMP_FIELD (a, b, cwnd);
-	_CMP_FIELD (a, b, initcwnd);
-	_CMP_FIELD (a, b, initrwnd);
-	_CMP_FIELD (a, b, mtu);
 	return 0;
 }
 
@@ -4404,7 +5848,7 @@ nm_platform_ip_address_cmp_expiry (const NMPlatformIPAddress *a, const NMPlatfor
 {
 	gint64 ta = 0, tb = 0;
 
-	_CMP_SELF (a, b);
+	NM_CMP_SELF (a, b);
 
 	if (a->lifetime == NM_PLATFORM_LIFETIME_PERMANENT || a->lifetime == 0)
 		ta = G_MAXINT64;
@@ -4485,120 +5929,139 @@ log_ip6_route (NMPlatform *self, NMPObjectType obj_type, int ifindex, NMPlatform
 
 /*****************************************************************************/
 
-NMPNetns *
-nm_platform_netns_get (NMPlatform *self)
+void
+nm_platform_cache_update_emit_signal (NMPlatform *self,
+                                      NMPCacheOpsType cache_op,
+                                      const NMPObject *obj_old,
+                                      const NMPObject *obj_new)
 {
-	_CHECK_SELF (self, klass, NULL);
+	gboolean visible_new;
+	gboolean visible_old;
+	const NMPObject *o;
+	const NMPClass *klass;
 
-	return self->_netns;
-}
+	nm_assert (NM_IN_SET ((NMPlatformSignalChangeType) cache_op, (NMPlatformSignalChangeType) NMP_CACHE_OPS_UNCHANGED, NM_PLATFORM_SIGNAL_ADDED, NM_PLATFORM_SIGNAL_CHANGED, NM_PLATFORM_SIGNAL_REMOVED));
 
-gboolean
-nm_platform_netns_push (NMPlatform *platform, NMPNetns **netns)
-{
-	g_return_val_if_fail (NM_IS_PLATFORM (platform), FALSE);
+	ASSERT_nmp_cache_ops (nm_platform_get_cache (self), cache_op, obj_old, obj_new);
 
-	if (   platform->_netns
-	    && !nmp_netns_push (platform->_netns)) {
-		NM_SET_OUT (netns, NULL);
-		return FALSE;
+	nm_assert (NM_IN_SET (nm_platform_netns_get (self),
+	                      NULL,
+	                      nmp_netns_get_current ()));
+
+	NMTST_ASSERT_PLATFORM_NETNS_CURRENT (self);
+
+	switch (cache_op) {
+	case NMP_CACHE_OPS_ADDED:
+		if (!nmp_object_is_visible (obj_new))
+			return;
+		o = obj_new;
+		break;
+	case NMP_CACHE_OPS_UPDATED:
+		visible_old = nmp_object_is_visible (obj_old);
+		visible_new = nmp_object_is_visible (obj_new);
+		if (!visible_old && visible_new) {
+			o = obj_new;
+			cache_op = NMP_CACHE_OPS_ADDED;
+		} else if (visible_old && !visible_new) {
+			o = obj_old;
+			cache_op = NMP_CACHE_OPS_REMOVED;
+		} else if (!visible_new) {
+			/* it was invisible and stayed invisible. Nothing to do. */
+			return;
+		} else
+			o = obj_new;
+		break;
+	case NMP_CACHE_OPS_REMOVED:
+		if (!nmp_object_is_visible (obj_old))
+			return;
+		o = obj_old;
+		break;
+	default:
+		nm_assert (cache_op == NMP_CACHE_OPS_UNCHANGED);
+		return;
 	}
 
-	NM_SET_OUT (netns, platform->_netns);
-	return TRUE;
+	klass = NMP_OBJECT_GET_CLASS (o);
+
+	if (   klass->obj_type == NMP_OBJECT_TYPE_IP4_ROUTE
+	    && NM_PLATFORM_GET_PRIVATE (self)->ip4_dev_route_blacklist_gc_timeout_id
+	    && NM_IN_SET (cache_op, NMP_CACHE_OPS_ADDED, NMP_CACHE_OPS_UPDATED))
+		_ip4_dev_route_blacklist_notify_route (self, o);
+
+	_LOGt ("emit signal %s %s: %s",
+	       klass->signal_type,
+	       nm_platform_signal_change_type_to_string ((NMPlatformSignalChangeType) cache_op),
+	       nmp_object_to_string (o, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+
+	nmp_object_ref (o);
+	g_signal_emit (self,
+	               _nm_platform_signal_id_get (klass->signal_type_id),
+	               0,
+	               (int) klass->obj_type,
+	               o->object.ifindex,
+	               &o->object,
+	               (int) cache_op);
+	nmp_object_unref (o);
 }
 
 /*****************************************************************************/
 
-static gboolean
-_vtr_v4_route_add (NMPlatform *self, int ifindex, const NMPlatformIPXRoute *route, gint64 metric)
+NMPCache *
+nm_platform_get_cache (NMPlatform *self)
 {
-	NMPlatformIP4Route rt = route->r4;
-
-	if (ifindex > 0)
-		rt.ifindex = ifindex;
-	if (metric >= 0)
-		rt.metric = metric;
-
-	return nm_platform_ip4_route_add (self, &rt);
+	return NM_PLATFORM_GET_PRIVATE (self)->cache;
 }
 
-static gboolean
-_vtr_v6_route_add (NMPlatform *self, int ifindex, const NMPlatformIPXRoute *route, gint64 metric)
+NMPNetns *
+nm_platform_netns_get (NMPlatform *self)
 {
-	NMPlatformIP6Route rt = route->r6;
-
-	if (ifindex > 0)
-		rt.ifindex = ifindex;
-	if (metric >= 0)
-		rt.metric = metric;
+	_CHECK_SELF (self, klass, NULL);
 
-	return nm_platform_ip6_route_add (self, &rt);
+	return self->_netns;
 }
 
-static gboolean
-_vtr_v4_route_delete (NMPlatform *self, int ifindex, const NMPlatformIPXRoute *route)
+gboolean
+nm_platform_netns_push (NMPlatform *self, NMPNetns **netns)
 {
-	return nm_platform_ip4_route_delete (self,
-	                                     ifindex > 0 ? ifindex : route->rx.ifindex,
-	                                     route->r4.network,
-	                                     route->rx.plen,
-	                                     route->rx.metric);
-}
+	g_return_val_if_fail (NM_IS_PLATFORM (self), FALSE);
 
-static gboolean
-_vtr_v6_route_delete (NMPlatform *self, int ifindex, const NMPlatformIPXRoute *route)
-{
-	return nm_platform_ip6_route_delete (self,
-	                                     ifindex > 0 ? ifindex : route->rx.ifindex,
-	                                     route->r6.network,
-	                                     route->rx.plen,
-	                                     route->rx.metric);
+	if (   self->_netns
+	    && !nmp_netns_push (self->_netns)) {
+		NM_SET_OUT (netns, NULL);
+		return FALSE;
+	}
+
+	NM_SET_OUT (netns, self->_netns);
+	return TRUE;
 }
 
+/*****************************************************************************/
+
 static guint32
 _vtr_v4_metric_normalize (guint32 metric)
 {
 	return metric;
 }
 
-static gboolean
-_vtr_v4_route_delete_default (NMPlatform *self, int ifindex, guint32 metric)
-{
-	return nm_platform_ip4_route_delete (self, ifindex, 0, 0, metric);
-}
-
-static gboolean
-_vtr_v6_route_delete_default (NMPlatform *self, int ifindex, guint32 metric)
-{
-	return nm_platform_ip6_route_delete (self, ifindex, in6addr_any, 0, metric);
-}
-
 /*****************************************************************************/
 
 const NMPlatformVTableRoute nm_platform_vtable_route_v4 = {
 	.is_ip4                         = TRUE,
+	.obj_type                       = NMP_OBJECT_TYPE_IP4_ROUTE,
 	.addr_family                    = AF_INET,
 	.sizeof_route                   = sizeof (NMPlatformIP4Route),
-	.route_cmp                      = (int (*) (const NMPlatformIPXRoute *a, const NMPlatformIPXRoute *b, gboolean consider_host_part)) nm_platform_ip4_route_cmp_full,
+	.route_cmp                      = (int (*) (const NMPlatformIPXRoute *a, const NMPlatformIPXRoute *b, NMPlatformIPRouteCmpType cmp_type)) nm_platform_ip4_route_cmp,
 	.route_to_string                = (const char *(*) (const NMPlatformIPXRoute *route, char *buf, gsize len)) nm_platform_ip4_route_to_string,
-	.route_get_all                  = nm_platform_ip4_route_get_all,
-	.route_add                      = _vtr_v4_route_add,
-	.route_delete                   = _vtr_v4_route_delete,
-	.route_delete_default           = _vtr_v4_route_delete_default,
 	.metric_normalize               = _vtr_v4_metric_normalize,
 };
 
 const NMPlatformVTableRoute nm_platform_vtable_route_v6 = {
 	.is_ip4                         = FALSE,
+	.obj_type                       = NMP_OBJECT_TYPE_IP6_ROUTE,
 	.addr_family                    = AF_INET6,
 	.sizeof_route                   = sizeof (NMPlatformIP6Route),
-	.route_cmp                      = (int (*) (const NMPlatformIPXRoute *a, const NMPlatformIPXRoute *b, gboolean consider_host_part)) nm_platform_ip6_route_cmp_full,
+	.route_cmp                      = (int (*) (const NMPlatformIPXRoute *a, const NMPlatformIPXRoute *b, NMPlatformIPRouteCmpType cmp_type)) nm_platform_ip6_route_cmp,
 	.route_to_string                = (const char *(*) (const NMPlatformIPXRoute *route, char *buf, gsize len)) nm_platform_ip6_route_to_string,
-	.route_get_all                  = nm_platform_ip6_route_get_all,
-	.route_add                      = _vtr_v6_route_add,
-	.route_delete                   = _vtr_v6_route_delete,
-	.route_delete_default           = _vtr_v6_route_delete_default,
 	.metric_normalize               = nm_utils_ip6_route_metric_normalize,
 };
 
@@ -4622,6 +6085,10 @@ set_property (GObject *object, guint prop_id,
 				self->_netns = g_object_ref (netns);
 		}
 		break;
+	case PROP_USE_UDEV:
+		/* construct-only */
+		priv->use_udev = g_value_get_boolean (value);
+		break;
 	case PROP_LOG_WITH_PTR:
 		/* construct-only */
 		priv->log_with_ptr = g_value_get_boolean (value);
@@ -4638,12 +6105,40 @@ nm_platform_init (NMPlatform *self)
 	self->_priv = G_TYPE_INSTANCE_GET_PRIVATE (self, NM_TYPE_PLATFORM, NMPlatformPrivate);
 }
 
+static GObject *
+constructor (GType type,
+             guint n_construct_params,
+             GObjectConstructParam *construct_params)
+{
+	GObject *object;
+	NMPlatform *self;
+	NMPlatformPrivate *priv;
+
+	object = G_OBJECT_CLASS (nm_platform_parent_class)->constructor (type,
+	                                                                 n_construct_params,
+	                                                                 construct_params);
+	self = NM_PLATFORM (object);
+	priv = NM_PLATFORM_GET_PRIVATE (self);
+
+	priv->multi_idx = nm_dedup_multi_index_new ();
+
+	priv->cache = nmp_cache_new (nm_platform_get_multi_idx (self),
+	                             priv->use_udev);
+	return object;
+}
+
 static void
 finalize (GObject *object)
 {
 	NMPlatform *self = NM_PLATFORM (object);
+	NMPlatformPrivate *priv = NM_PLATFORM_GET_PRIVATE (self);
 
+	nm_clear_g_source (&priv->ip4_dev_route_blacklist_check_id);
+	nm_clear_g_source (&priv->ip4_dev_route_blacklist_gc_timeout_id);
+	g_clear_pointer (&priv->ip4_dev_route_blacklist_hash, g_hash_table_unref);
 	g_clear_object (&self->_netns);
+	nm_dedup_multi_index_unref (priv->multi_idx);
+	nmp_cache_free (priv->cache);
 }
 
 static void
@@ -4653,6 +6148,7 @@ nm_platform_class_init (NMPlatformClass *platform_class)
 
 	g_type_class_add_private (object_class, sizeof (NMPlatformPrivate));
 
+	object_class->constructor = constructor;
 	object_class->set_property = set_property;
 	object_class->finalize = finalize;
 
@@ -4667,6 +6163,14 @@ nm_platform_class_init (NMPlatformClass *platform_class)
 	                           G_PARAM_STATIC_STRINGS));
 
 	g_object_class_install_property
+	 (object_class, PROP_USE_UDEV,
+	     g_param_spec_boolean (NM_PLATFORM_USE_UDEV, "", "",
+	                           FALSE,
+	                           G_PARAM_WRITABLE |
+	                           G_PARAM_CONSTRUCT_ONLY |
+	                           G_PARAM_STATIC_STRINGS));
+
+	g_object_class_install_property
 	 (object_class, PROP_LOG_WITH_PTR,
 	     g_param_spec_boolean (NM_PLATFORM_LOG_WITH_PTR, "", "",
 	                           TRUE,
diff --git a/src/platform/nm-platform.h b/src/platform/nm-platform.h
index 1b8fa133..d155c109 100644
--- a/src/platform/nm-platform.h
+++ b/src/platform/nm-platform.h
@@ -45,12 +45,16 @@
 /*****************************************************************************/
 
 #define NM_PLATFORM_NETNS_SUPPORT      "netns-support"
+#define NM_PLATFORM_USE_UDEV           "use-udev"
 #define NM_PLATFORM_LOG_WITH_PTR       "log-with-ptr"
 
 /*****************************************************************************/
 
 struct udev_device;
 
+typedef gboolean (*NMPObjectPredicateFunc) (const NMPObject *obj,
+                                            gpointer user_data);
+
 /* workaround for older libnl version, that does not define these flags. */
 #ifndef IFA_F_MANAGETEMPADDR
 #define IFA_F_MANAGETEMPADDR 0x100
@@ -59,6 +63,8 @@ struct udev_device;
 #define IFA_F_NOPREFIXROUTE 0x200
 #endif
 
+#define NM_RT_SCOPE_LINK                       253  /* RT_SCOPE_LINK */
+
 /* Define of the IN6_ADDR_GEN_MODE_* values to workaround old kernel headers
  * that don't define it. */
 #define NM_IN6_ADDR_GEN_MODE_UNKNOWN           255  /* no corresponding value.  */
@@ -72,6 +78,74 @@ struct udev_device;
 /* Redefine this in host's endianness */
 #define NM_GRE_KEY      0x2000
 
+typedef enum {
+	/* use our own platform enum for the nlmsg-flags. Otherwise, we'd have
+	 * to include <linux/netlink.h> */
+	NMP_NLM_FLAG_F_REPLACE      = 0x100, /* NLM_F_REPLACE, Override existing */
+	NMP_NLM_FLAG_F_EXCL         = 0x200, /* NLM_F_EXCL, Do not touch, if it exists */
+	NMP_NLM_FLAG_F_CREATE       = 0x400, /* NLM_F_CREATE, Create, if it does not exist */
+	NMP_NLM_FLAG_F_APPEND       = 0x800, /* NLM_F_APPEND, Add to end of list */
+
+	NMP_NLM_FLAG_FMASK          = 0xFFFF, /* a mask for all NMP_NLM_FLAG_F_* flags */
+
+	/* instructs NM to suppress logging an error message for any failures
+	 * received from kernel.
+	 *
+	 * It will still log with debug-level, and it will still log
+	 * other failures aside the kernel response. */
+	NMP_NLM_FLAG_SUPPRESS_NETLINK_FAILURE = 0x10000,
+
+	/* the following aliases correspond to iproute2's `ip route CMD` for
+	 * RTM_NEWROUTE, with CMD being one of add, change, replace, prepend,
+	 * append and test. */
+	NMP_NLM_FLAG_ADD            = NMP_NLM_FLAG_F_CREATE                          | NMP_NLM_FLAG_F_EXCL,
+	NMP_NLM_FLAG_CHANGE         =                         NMP_NLM_FLAG_F_REPLACE,
+	NMP_NLM_FLAG_REPLACE        = NMP_NLM_FLAG_F_CREATE | NMP_NLM_FLAG_F_REPLACE,
+	NMP_NLM_FLAG_PREPEND        = NMP_NLM_FLAG_F_CREATE,
+	NMP_NLM_FLAG_APPEND         = NMP_NLM_FLAG_F_CREATE                                                | NMP_NLM_FLAG_F_APPEND,
+	NMP_NLM_FLAG_TEST           =                                                  NMP_NLM_FLAG_F_EXCL,
+} NMPNlmFlags;
+
+typedef enum {
+	/* compare fields which kernel considers as similar routes.
+	 * It is a looser comparisong then NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID
+	 * and means that `ip route add` would fail to add two routes
+	 * that have the same NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID.
+	 * On the other hand, `ip route append` would allow that, as
+	 * long as NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID differs. */
+	NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID,
+
+	/* compare two routes as kernel would allow to add them with
+	 * `ip route append`. In other words, kernel does not allow you to
+	 * add two routes (at the same time) which compare equal according
+	 * to NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID.
+	 *
+	 * For the ID we can only recognize route fields that we actually implement.
+	 * However, kernel supports more routing options, some of them also part of
+	 * the ID. NetworkManager is oblivious to these options and will wrongly think
+	 * that two routes are idential, while they are not. That can lead to an
+	 * inconsistent platform cache. Not much what we can do about that, except
+	 * implementing all options that kernel supports *sigh*. See rh#1337860.
+	 */
+	NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID,
+
+	/* compare all fields as they make sense for kernel. For example,
+	 * a route destination 192.168.1.5/24 is not accepted by kernel and
+	 * we treat it identical to 192.168.1.0/24. Semantically these
+	 * routes are identical, but NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL will
+	 * report them as different.
+	 *
+	 * The result shall be identical to call first nm_platform_ip_route_normalize()
+	 * on both routes and then doing a full comparison. */
+	NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY,
+
+	/* compare all fields. This should have the same effect as memcmp(),
+	 * except allowing for undefined data in holes between field alignment.
+	 */
+	NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL,
+
+} NMPlatformIPRouteCmpType;
+
 typedef enum { /*< skip >*/
 
 	/* dummy value, to enforce that the enum type is signed and has a size
@@ -91,6 +165,8 @@ typedef enum { /*< skip >*/
 	NM_PLATFORM_ERROR_NOT_SLAVE,
 	NM_PLATFORM_ERROR_NO_FIRMWARE,
 	NM_PLATFORM_ERROR_OPNOTSUPP,
+	NM_PLATFORM_ERROR_NETLINK,
+	NM_PLATFORM_ERROR_CANT_SET_MTU,
 } NMPlatformError;
 
 #define NM_PLATFORM_LINK_OTHER_NETNS    (-1)
@@ -172,21 +248,9 @@ typedef enum {
 	NM_PLATFORM_SIGNAL_REMOVED,
 } NMPlatformSignalChangeType;
 
-typedef enum { /*< skip >*/
-	NM_PLATFORM_GET_ROUTE_FLAGS_NONE                            = 0,
-
-	/* Whether to include default-routes/non-default-routes. Omitting
-	 * both WITH_DEFAULT and WITH_NON_DEFAULT, is equal to specifying
-	 * both of them. */
-	NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT                    = (1LL << 0),
-	NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT                = (1LL << 1),
-
-	NM_PLATFORM_GET_ROUTE_FLAGS_WITH_RTPROT_KERNEL              = (1LL << 2),
-} NMPlatformGetRouteFlags;
-
-typedef struct {
+struct _NMPlatformObject {
 	__NMPlatformObject_COMMON;
-} NMPlatformObject;
+};
 
 
 #define __NMPlatformIPAddress_COMMON \
@@ -298,7 +362,15 @@ typedef union {
 	/* The NMIPConfigSource. For routes that we receive from cache this corresponds
 	 * to the rtm_protocol field (and is one of the NM_IP_CONFIG_SOURCE_RTPROT_* values).
 	 * When adding a route, the source will be coerced to the protocol using
-	 * nmp_utils_ip_config_source_coerce_to_rtprot(). */ \
+	 * nmp_utils_ip_config_source_coerce_to_rtprot().
+	 *
+	 * rtm_protocol is part of the primary key of an IPv4 route (meaning, you can add
+	 * two IPv4 routes that only differ in their rtm_protocol. For IPv6, that is not
+	 * the case.
+	 *
+	 * When deleting an IPv4/IPv6 route, the rtm_protocol field must match (even
+	 * if it is not part of the primary key for IPv6) -- unless rtm_protocol is set
+	 * to zero, in which case the first matching route (with proto ignored) is deleted. */ \
 	NMIPConfigSource rt_source; \
 	\
 	guint8 plen; \
@@ -308,21 +380,58 @@ typedef union {
 	 * of platform users. This flag is internal to track those hidden
 	 * routes. Such a route is not alive, according to nmp_object_is_alive(). */ \
 	bool rt_cloned:1; \
+	\
+	\
+	/* RTA_METRICS:
+	 *
+	 * For IPv4 routes, these properties are part of their
+	 * ID (meaning: you can add otherwise idential IPv4 routes that
+	 * only differ by the metric property).
+	 * On the other hand, for IPv6 you cannot add two IPv6 routes that only differ
+	 * by an RTA_METRICS property.
+	 *
+	 * When deleting a route, kernel seems to ignore the RTA_METRICS propeties.
+	 * That is a problem/bug for IPv4 because you cannot explicitly select which
+	 * route to delete. Kernel just picks the first. See rh#1475642. */ \
+	\
+	/* RTA_METRICS.RTAX_LOCK (iproute2: "lock" arguments) */ \
 	bool lock_window:1; \
 	bool lock_cwnd:1; \
 	bool lock_initcwnd:1; \
 	bool lock_initrwnd:1; \
 	bool lock_mtu:1; \
 	\
-	guint32 metric; \
+	/* RTA_METRICS.RTAX_ADVMSS (iproute2: advmss) */ \
 	guint32 mss; \
-	guint32 tos; \
+	\
+	/* RTA_METRICS.RTAX_WINDOW (iproute2: window) */ \
 	guint32 window; \
+	\
+	/* RTA_METRICS.RTAX_CWND (iproute2: cwnd) */ \
 	guint32 cwnd; \
+	\
+	/* RTA_METRICS.RTAX_INITCWND (iproute2: initcwnd) */ \
 	guint32 initcwnd; \
+	\
+	/* RTA_METRICS.RTAX_INITRWND (iproute2: initrwnd) */ \
 	guint32 initrwnd; \
+	\
+	/* RTA_METRICS.RTAX_MTU (iproute2: mtu) */ \
 	guint32 mtu; \
-	;
+	\
+	\
+	/* RTA_PRIORITY (iproute2: metric) */ \
+	guint32 metric; \
+	\
+	/* rtm_table, RTA_TABLE.
+	 *
+	 * This is not the original table ID. Instead, 254 (RT_TABLE_MAIN) and
+	 * zero (RT_TABLE_UNSPEC) are swapped, so that the default is the main
+	 * table. Use nm_platform_route_table_coerce()/nm_platform_route_table_uncoerce(). */ \
+	guint32 table_coerced; \
+	\
+	/*end*/
+
 
 typedef struct {
 	__NMPlatformIPRoute_COMMON;
@@ -332,30 +441,89 @@ typedef struct {
 	};
 } NMPlatformIPRoute;
 
+#if _NM_CC_SUPPORT_GENERIC
+#define NM_PLATFORM_IP_ROUTE_IS_DEFAULT(route) \
+	(_Generic ((route), \
+	           const NMPlatformIPRoute  *: ((const NMPlatformIPRoute *) (route))->plen, \
+	                 NMPlatformIPRoute  *: ((const NMPlatformIPRoute *) (route))->plen, \
+	           const NMPlatformIPXRoute *: ((const NMPlatformIPRoute *) (route))->plen, \
+	                 NMPlatformIPXRoute *: ((const NMPlatformIPRoute *) (route))->plen, \
+	           const NMPlatformIP4Route *: ((const NMPlatformIPRoute *) (route))->plen, \
+	                 NMPlatformIP4Route *: ((const NMPlatformIPRoute *) (route))->plen, \
+	           const NMPlatformIP6Route *: ((const NMPlatformIPRoute *) (route))->plen, \
+	                 NMPlatformIP6Route *: ((const NMPlatformIPRoute *) (route))->plen, \
+	           const void               *: ((const NMPlatformIPRoute *) (route))->plen, \
+	                 void               *: ((const NMPlatformIPRoute *) (route))->plen) == 0)
+#else
 #define NM_PLATFORM_IP_ROUTE_IS_DEFAULT(route) \
 	( ((const NMPlatformIPRoute *) (route))->plen <= 0 )
+#endif
 
 struct _NMPlatformIP4Route {
 	__NMPlatformIPRoute_COMMON;
 	in_addr_t network;
-	in_addr_t gateway;
 
-	/* The bitwise inverse of the route scope. It is inverted so that the
-	 * default value (RT_SCOPE_NOWHERE) is nul. */
-	guint8 scope_inv;
+	/* RTA_GATEWAY. The gateway is part of the primary key for a route */
+	in_addr_t gateway;
 
-	/* RTA_PREFSRC/rtnl_route_get_pref_src(). A value of zero means that
-	 * no pref-src is set.  */
+	/* RTA_PREFSRC (called "src" by iproute2).
+	 *
+	 * pref_src is part of the ID of an IPv4 route. When deleting a route,
+	 * pref_src must match, unless set to 0.0.0.0 to match any. */
 	in_addr_t pref_src;
+
+	/* rtm_tos (iproute2: tos)
+	 *
+	 * For IPv4, tos is part of the weak-id (like metric).
+	 *
+	 * For IPv6, tos is ignored by kernel.  */
+	guint8 tos;
+
+	/* The bitwise inverse of the route scope rtm_scope. It is inverted so that the
+	 * default value (RT_SCOPE_NOWHERE) is zero. Use nm_platform_route_scope_inv()
+	 * to convert back and forth between the inverese representation and the
+	 * real value.
+	 *
+	 * rtm_scope is part of the primary key for IPv4 routes. When deleting a route,
+	 * the scope must match, unless it is left at RT_SCOPE_NOWHERE, in which case the first
+	 * matching route is deleted.
+	 *
+	 * For IPv6 routes, the scope is ignored and kernel always assumes global scope.
+	 * Hence, this field is only in NMPlatformIP4Route. */
+	guint8 scope_inv;
 };
 
 struct _NMPlatformIP6Route {
 	__NMPlatformIPRoute_COMMON;
 	struct in6_addr network;
+
+	/* RTA_GATEWAY. The gateway is part of the primary key for a route */
 	struct in6_addr gateway;
+
+	/* RTA_PREFSRC (called "src" by iproute2).
+	 *
+	 * pref_src is not part of the ID for an IPv6 route. You cannot add two
+	 * routes that only differ by pref_src.
+	 *
+	 * When deleting a route, pref_src is ignored by kernel. */
 	struct in6_addr pref_src;
+
+	/* RTA_SRC and rtm_src_len (called "from" by iproute2).
+	 *
+	 * Kernel clears the host part of src/src_plen.
+	 *
+	 * src/src_plen is part of the ID of a route just like network/plen. That is,
+	 * Not only `ip route append`, but also `ip route add` allows to add routes that only
+	 * differ in their src/src_plen.
+	 */
 	struct in6_addr src;
 	guint8 src_plen;
+
+	/* RTA_PREF router preference.
+	 *
+	 * The type is guint8 to keep the struct size small. But the values are compatible with
+	 * the NMIcmpv6RouterPref enum. */
+	guint8 rt_pref;
 };
 
 typedef union {
@@ -372,14 +540,11 @@ typedef union {
 
 typedef struct {
 	gboolean is_ip4;
+	NMPObjectType obj_type;
 	int addr_family;
 	gsize sizeof_route;
-	int (*route_cmp) (const NMPlatformIPXRoute *a, const NMPlatformIPXRoute *b, gboolean consider_host_part);
+	int (*route_cmp) (const NMPlatformIPXRoute *a, const NMPlatformIPXRoute *b, NMPlatformIPRouteCmpType cmp_type);
 	const char *(*route_to_string) (const NMPlatformIPXRoute *route, char *buf, gsize len);
-	GArray *(*route_get_all) (NMPlatform *self, int ifindex, NMPlatformGetRouteFlags flags);
-	gboolean (*route_add) (NMPlatform *self, int ifindex, const NMPlatformIPXRoute *route, gint64 metric);
-	gboolean (*route_delete) (NMPlatform *self, int ifindex, const NMPlatformIPXRoute *route);
-	gboolean (*route_delete_default) (NMPlatform *self, int ifindex, guint32 metric);
 	guint32 (*metric_normalize) (guint32 metric);
 } NMPlatformVTableRoute;
 
@@ -426,7 +591,7 @@ typedef struct {
 
 typedef struct {
 	int parent_ifindex;
-	guint64 sci;	/* host byte order */
+	guint64 sci;                    /* host byte order */
 	guint64 cipher_suite;
 	guint32 window;
 	guint8 icv_length;
@@ -452,11 +617,11 @@ typedef struct {
 	in_addr_t local;
 	in_addr_t remote;
 	int parent_ifindex;
+	guint16 flags;
 	guint8 ttl;
 	guint8 tos;
 	guint8 proto;
 	bool path_mtu_discovery:1;
-	guint16 flags;
 } NMPlatformLnkSit;
 
 typedef struct {
@@ -501,6 +666,12 @@ typedef enum {
 	NM_PLATFORM_LINK_DUPLEX_FULL,
 } NMPlatformLinkDuplexType;
 
+typedef enum {
+	NM_PLATFORM_KERNEL_SUPPORT_EXTENDED_IFA_FLAGS               = (1LL <<  0),
+	NM_PLATFORM_KERNEL_SUPPORT_USER_IPV6LL                      = (1LL <<  1),
+	NM_PLATFORM_KERNEL_SUPPORT_RTA_PREF                         = (1LL <<  2),
+} NMPlatformKernelSupportFlags;
+
 /*****************************************************************************/
 
 struct _NMPlatformPrivate;
@@ -517,22 +688,14 @@ typedef struct {
 	gboolean (*sysctl_set) (NMPlatform *, const char *pathid, int dirfd, const char *path, const char *value);
 	char * (*sysctl_get) (NMPlatform *, const char *pathid, int dirfd, const char *path);
 
-	const NMPlatformLink *(*link_get) (NMPlatform *platform, int ifindex);
-	const NMPlatformLink *(*link_get_by_ifname) (NMPlatform *platform, const char *ifname);
-	const NMPlatformLink *(*link_get_by_address) (NMPlatform *platform, gconstpointer address, size_t length);
-
-	const NMPObject *(*link_get_lnk) (NMPlatform *platform, int ifindex, NMLinkType link_type, const NMPlatformLink **out_link);
-
-	GArray *(*link_get_all) (NMPlatform *);
 	gboolean (*link_add) (NMPlatform *,
 	                      const char *name,
 	                      NMLinkType type,
+	                      const char *veth_peer,
 	                      const void *address,
 	                      size_t address_len,
 	                      const NMPlatformLink **out_link);
 	gboolean (*link_delete) (NMPlatform *, int ifindex);
-	const char *(*link_get_type_name) (NMPlatform *, int ifindex);
-	gboolean (*link_get_unmanaged) (NMPlatform *, int ifindex, gboolean *unmanaged);
 
 	gboolean (*link_refresh) (NMPlatform *, int ifindex);
 
@@ -556,7 +719,8 @@ typedef struct {
 	                                        guint8 *buf,
 	                                        size_t *length);
 	NMPlatformError (*link_set_address) (NMPlatform *, int ifindex, gconstpointer address, size_t length);
-	gboolean (*link_set_mtu) (NMPlatform *, int ifindex, guint32 mtu);
+	NMPlatformError (*link_set_mtu) (NMPlatform *, int ifindex, guint32 mtu);
+	gboolean (*link_set_name) (NMPlatform *, int ifindex, const char *name);
 	gboolean (*link_set_sriov_num_vfs) (NMPlatform *, int ifindex, guint num_vfs);
 
 	char *   (*link_get_physical_port_id) (NMPlatform *, int ifindex);
@@ -641,8 +805,6 @@ typedef struct {
 	gboolean    (*mesh_set_channel)      (NMPlatform *, int ifindex, guint32 channel);
 	gboolean    (*mesh_set_ssid)         (NMPlatform *, int ifindex, const guint8 *ssid, gsize len);
 
-	GArray * (*ip4_address_get_all) (NMPlatform *, int ifindex);
-	GArray * (*ip6_address_get_all) (NMPlatform *, int ifindex);
 	gboolean (*ip4_address_add) (NMPlatform *,
 	                             int ifindex,
 	                             in_addr_t address,
@@ -662,20 +824,21 @@ typedef struct {
 	                             guint32 flags);
 	gboolean (*ip4_address_delete) (NMPlatform *, int ifindex, in_addr_t address, guint8 plen, in_addr_t peer_address);
 	gboolean (*ip6_address_delete) (NMPlatform *, int ifindex, struct in6_addr address, guint8 plen);
-	const NMPlatformIP4Address *(*ip4_address_get) (NMPlatform *, int ifindex, in_addr_t address, guint8 plen, in_addr_t peer_address);
-	const NMPlatformIP6Address *(*ip6_address_get) (NMPlatform *, int ifindex, struct in6_addr address, guint8 plen);
-
-	GArray * (*ip4_route_get_all) (NMPlatform *, int ifindex, NMPlatformGetRouteFlags flags);
-	GArray * (*ip6_route_get_all) (NMPlatform *, int ifindex, NMPlatformGetRouteFlags flags);
-	gboolean (*ip4_route_add) (NMPlatform *, const NMPlatformIP4Route *route);
-	gboolean (*ip6_route_add) (NMPlatform *, const NMPlatformIP6Route *route);
-	gboolean (*ip4_route_delete) (NMPlatform *, int ifindex, in_addr_t network, guint8 plen, guint32 metric);
-	gboolean (*ip6_route_delete) (NMPlatform *, int ifindex, struct in6_addr network, guint8 plen, guint32 metric);
-	const NMPlatformIP4Route *(*ip4_route_get) (NMPlatform *, int ifindex, in_addr_t network, guint8 plen, guint32 metric);
-	const NMPlatformIP6Route *(*ip6_route_get) (NMPlatform *, int ifindex, struct in6_addr network, guint8 plen, guint32 metric);
-
-	gboolean (*check_support_kernel_extended_ifa_flags) (NMPlatform *);
-	gboolean (*check_support_user_ipv6ll) (NMPlatform *);
+
+	NMPlatformError (*ip_route_add) (NMPlatform *,
+	                                 NMPNlmFlags flags,
+	                                 int addr_family,
+	                                 const NMPlatformIPRoute *route);
+	gboolean (*ip_route_delete) (NMPlatform *, const NMPObject *obj);
+
+	NMPlatformError (*ip_route_get) (NMPlatform *self,
+	                                 int addr_family,
+	                                 gconstpointer address,
+	                                 int oif_ifindex,
+	                                 NMPObject **out_route);
+
+	NMPlatformKernelSupportFlags (*check_kernel_support) (NMPlatform * self,
+	                                                      NMPlatformKernelSupportFlags request_flags);
 } NMPlatformClass;
 
 /* NMPlatform signals
@@ -709,6 +872,72 @@ NMPlatform *nm_platform_get (void);
 /*****************************************************************************/
 
 /**
+ * nm_platform_route_table_coerce:
+ * @table: the route table, in its original value as received
+ *   from rtm_table/RTA_TABLE.
+ *
+ * Returns: returns the coerced table id, that can be stored in
+ *   NMPlatformIPRoute.table_coerced.
+ */
+static inline guint32
+nm_platform_route_table_coerce (guint32 table)
+{
+	/* For kernel, the default table is RT_TABLE_MAIN (254).
+	 * We want that in NMPlatformIPRoute.table_coerced a numeric
+	 * zero is the default. Hence, @table_coerced swaps the
+	 * value 0 and 254. Use nm_platform_route_table_coerce()
+	 * and nm_platform_route_table_uncoerce() to convert between
+	 * the two domains. */
+	switch (table) {
+	case 0 /* RT_TABLE_UNSPEC */:
+		return 254;
+	case 254 /* RT_TABLE_MAIN */:
+		return 0;
+	default:
+		return table;
+	}
+}
+
+/**
+ * nm_platform_route_table_uncoerce:
+ * @table: the route table, in its coerced value
+ * @normalize: whether to normalize RT_TABLE_UNSPEC to
+ *   RT_TABLE_MAIN. For kernel, routes with a table id
+ *   RT_TABLE_UNSPEC do not exist and are treated like
+ *   RT_TABLE_MAIN.
+ *
+ * Returns: reverts the coerced table ID in NMPlatformIPRoute.table_coerced
+ *   to the original value as kernel understands it.
+ */
+static inline guint32
+nm_platform_route_table_uncoerce (guint32 table_coerced, gboolean normalize)
+{
+	/* this undoes nm_platform_route_table_coerce().  */
+	switch (table_coerced) {
+	case 0 /* RT_TABLE_UNSPEC */:
+		return 254;
+	case 254 /* RT_TABLE_MAIN */:
+		return normalize ? 254 : 0;
+	default:
+		return table_coerced;
+	}
+}
+
+static inline gboolean
+nm_platform_route_table_is_main (guint32 table)
+{
+	/* same as
+	 *   nm_platform_route_table_uncoerce (table, TRUE) == RT_TABLE_MAIN
+	 * and
+	 *   nm_platform_route_table_uncoerce (nm_platform_route_table_coerce (table), TRUE) == RT_TABLE_MAIN
+	 *
+	 * That is, the function operates the same on @table and its coerced
+	 * form.
+	 */
+	return table == 0 || table == 254;
+}
+
+/**
  * nm_platform_route_scope_inv:
  * @scope: the route scope, either its original value, or its inverse.
  *
@@ -725,6 +954,7 @@ _nm_platform_uint8_inv (guint8 scope)
 	return (guint8) ~scope;
 }
 
+gboolean nm_platform_get_use_udev (NMPlatform *self);
 gboolean nm_platform_get_log_with_ptr (NMPlatform *self);
 
 NMPNetns *nm_platform_netns_get (NMPlatform *self);
@@ -732,8 +962,11 @@ gboolean nm_platform_netns_push (NMPlatform *platform, NMPNetns **netns);
 
 const char *nm_link_type_to_string (NMLinkType link_type);
 
-const char *_nm_platform_error_to_string (NMPlatformError error);
-#define nm_platform_error_to_string(error) NM_UTILS_LOOKUP_STR (_nm_platform_error_to_string, error)
+const char *nm_platform_error_to_string (NMPlatformError error,
+                                         char *buf,
+                                         gsize buf_len);
+#define nm_platform_error_to_string_a(error) \
+	(nm_platform_error_to_string ((error), g_alloca (30), 30))
 
 #define NMP_SYSCTL_PATHID_ABSOLUTE(path) \
 	((const char *) NULL), -1, (path)
@@ -759,19 +992,39 @@ gboolean nm_platform_sysctl_set_ip6_hop_limit_safe (NMPlatform *self, const char
 const char *nm_platform_if_indextoname (NMPlatform *self, int ifindex, char *out_ifname/* of size IFNAMSIZ */);
 int nm_platform_if_nametoindex (NMPlatform *self, const char *ifname);
 
+const NMPObject *nm_platform_link_get_obj (NMPlatform *self,
+                                           int ifindex,
+                                           gboolean visible_only);
 const NMPlatformLink *nm_platform_link_get (NMPlatform *self, int ifindex);
 const NMPlatformLink *nm_platform_link_get_by_ifname (NMPlatform *self, const char *ifname);
 const NMPlatformLink *nm_platform_link_get_by_address (NMPlatform *self, gconstpointer address, size_t length);
 
-GArray *nm_platform_link_get_all (NMPlatform *self, gboolean sort_by_name);
+GPtrArray *nm_platform_link_get_all (NMPlatform *self, gboolean sort_by_name);
 NMPlatformError nm_platform_link_dummy_add (NMPlatform *self, const char *name, const NMPlatformLink **out_link);
 NMPlatformError nm_platform_link_bridge_add (NMPlatform *self, const char *name, const void *address, size_t address_len, const NMPlatformLink **out_link);
 NMPlatformError nm_platform_link_bond_add (NMPlatform *self, const char *name, const NMPlatformLink **out_link);
 NMPlatformError nm_platform_link_team_add (NMPlatform *self, const char *name, const NMPlatformLink **out_link);
+NMPlatformError nm_platform_link_veth_add (NMPlatform *self, const char *name, const char *peer, const NMPlatformLink **out_link);
+
 gboolean nm_platform_link_delete (NMPlatform *self, int ifindex);
 
 gboolean nm_platform_link_set_netns (NMPlatform *self, int ifindex, int netns_fd);
 
+struct _NMDedupMultiHeadEntry;
+struct _NMPLookup;
+const struct _NMDedupMultiHeadEntry *nm_platform_lookup (NMPlatform *platform,
+                                                         const struct _NMPLookup *lookup);
+
+gboolean nm_platform_lookup_predicate_routes_main (const NMPObject *obj,
+                                                   gpointer user_data);
+gboolean nm_platform_lookup_predicate_routes_main_skip_rtprot_kernel (const NMPObject *obj,
+                                                                      gpointer user_data);
+
+GPtrArray *nm_platform_lookup_clone (NMPlatform *platform,
+                                     const struct _NMPLookup *lookup,
+                                     NMPObjectPredicateFunc predicate,
+                                     gpointer user_data);
+
 /* convienience methods to lookup the link and access fields of NMPlatformLink. */
 int nm_platform_link_get_ifindex (NMPlatform *self, const char *name);
 const char *nm_platform_link_get_name (NMPlatform *self, int ifindex);
@@ -808,7 +1061,8 @@ gboolean nm_platform_link_set_ipv6_token (NMPlatform *self, int ifindex, NMUtils
 
 gboolean nm_platform_link_get_permanent_address (NMPlatform *self, int ifindex, guint8 *buf, size_t *length);
 NMPlatformError nm_platform_link_set_address (NMPlatform *self, int ifindex, const void *address, size_t length);
-gboolean nm_platform_link_set_mtu (NMPlatform *self, int ifindex, guint32 mtu);
+NMPlatformError nm_platform_link_set_mtu (NMPlatform *self, int ifindex, guint32 mtu);
+gboolean nm_platform_link_set_name (NMPlatform *self, int ifindex, const char *name);
 gboolean nm_platform_link_set_sriov_num_vfs (NMPlatform *self, int ifindex, guint num_vfs);
 
 char    *nm_platform_link_get_physical_port_id (NMPlatform *self, int ifindex);
@@ -938,9 +1192,8 @@ NMPlatformError nm_platform_link_sit_add (NMPlatform *self,
                                           const NMPlatformLnkSit *props,
                                           const NMPlatformLink **out_link);
 
-const NMPlatformIP6Address *nm_platform_ip6_address_get (NMPlatform *self, int ifindex, struct in6_addr address, guint8 plen);
-GArray *nm_platform_ip4_address_get_all (NMPlatform *self, int ifindex);
-GArray *nm_platform_ip6_address_get_all (NMPlatform *self, int ifindex);
+const NMPlatformIP6Address *nm_platform_ip6_address_get (NMPlatform *self, int ifindex, struct in6_addr address);
+
 gboolean nm_platform_ip4_address_add (NMPlatform *self,
                                       int ifindex,
                                       in_addr_t address,
@@ -960,18 +1213,44 @@ gboolean nm_platform_ip6_address_add (NMPlatform *self,
                                       guint32 flags);
 gboolean nm_platform_ip4_address_delete (NMPlatform *self, int ifindex, in_addr_t address, guint8 plen, in_addr_t peer_address);
 gboolean nm_platform_ip6_address_delete (NMPlatform *self, int ifindex, struct in6_addr address, guint8 plen);
-gboolean nm_platform_ip4_address_sync (NMPlatform *self, int ifindex, const GArray *known_addresses, GPtrArray **out_added_addresses);
-gboolean nm_platform_ip6_address_sync (NMPlatform *self, int ifindex, const GArray *known_addresses, gboolean keep_link_local);
-gboolean nm_platform_address_flush (NMPlatform *self, int ifindex);
-
-const NMPlatformIP4Route *nm_platform_ip4_route_get (NMPlatform *self, int ifindex, in_addr_t network, guint8 plen, guint32 metric);
-const NMPlatformIP6Route *nm_platform_ip6_route_get (NMPlatform *self, int ifindex, struct in6_addr network, guint8 plen, guint32 metric);
-GArray *nm_platform_ip4_route_get_all (NMPlatform *self, int ifindex, NMPlatformGetRouteFlags flags);
-GArray *nm_platform_ip6_route_get_all (NMPlatform *self, int ifindex, NMPlatformGetRouteFlags flags);
-gboolean nm_platform_ip4_route_add (NMPlatform *self, const NMPlatformIP4Route *route);
-gboolean nm_platform_ip6_route_add (NMPlatform *self, const NMPlatformIP6Route *route);
-gboolean nm_platform_ip4_route_delete (NMPlatform *self, int ifindex, in_addr_t network, guint8 plen, guint32 metric);
-gboolean nm_platform_ip6_route_delete (NMPlatform *self, int ifindex, struct in6_addr network, guint8 plen, guint32 metric);
+gboolean nm_platform_ip4_address_sync (NMPlatform *self, int ifindex, GPtrArray *known_addresse);
+gboolean nm_platform_ip6_address_sync (NMPlatform *self, int ifindex, const GPtrArray *known_addresses, gboolean keep_link_local);
+gboolean nm_platform_ip_address_flush (NMPlatform *self,
+                                       int addr_family,
+                                       int ifindex);
+
+void nm_platform_ip_route_normalize (int addr_family,
+                                     NMPlatformIPRoute *route);
+
+NMPlatformError nm_platform_ip_route_add (NMPlatform *self,
+                                          NMPNlmFlags flags,
+                                          const NMPObject *route);
+NMPlatformError nm_platform_ip4_route_add (NMPlatform *self, NMPNlmFlags flags, const NMPlatformIP4Route *route);
+NMPlatformError nm_platform_ip6_route_add (NMPlatform *self, NMPNlmFlags flags, const NMPlatformIP6Route *route);
+
+gboolean nm_platform_ip_route_delete (NMPlatform *self, const NMPObject *route);
+
+GPtrArray *nm_platform_ip_route_get_prune_list (NMPlatform *self,
+                                                int addr_family,
+                                                int ifindex,
+                                                NMIPRouteTableSyncMode route_table_sync);
+
+gboolean nm_platform_ip_route_sync (NMPlatform *self,
+                                    int addr_family,
+                                    int ifindex,
+                                    GPtrArray *routes,
+                                    GPtrArray *routes_prune,
+                                    GPtrArray **out_temporary_not_available);
+
+gboolean nm_platform_ip_route_flush (NMPlatform *self,
+                                     int addr_family,
+                                     int ifindex);
+
+NMPlatformError nm_platform_ip_route_get (NMPlatform *self,
+                                          int addr_family,
+                                          gconstpointer address,
+                                          int oif_ifindex,
+                                          NMPObject **out_route);
 
 const char *nm_platform_link_to_string (const NMPlatformLink *link, char *buf, gsize len);
 const char *nm_platform_lnk_gre_to_string (const NMPlatformLnkGre *lnk, char *buf, gsize len);
@@ -1006,23 +1285,39 @@ int nm_platform_lnk_vlan_cmp (const NMPlatformLnkVlan *a, const NMPlatformLnkVla
 int nm_platform_lnk_vxlan_cmp (const NMPlatformLnkVxlan *a, const NMPlatformLnkVxlan *b);
 int nm_platform_ip4_address_cmp (const NMPlatformIP4Address *a, const NMPlatformIP4Address *b);
 int nm_platform_ip6_address_cmp (const NMPlatformIP6Address *a, const NMPlatformIP6Address *b);
-int nm_platform_ip4_route_cmp_full (const NMPlatformIP4Route *a, const NMPlatformIP4Route *b, gboolean consider_host_part);
-int nm_platform_ip6_route_cmp_full (const NMPlatformIP6Route *a, const NMPlatformIP6Route *b, gboolean consider_host_part);
+
+int nm_platform_ip4_route_cmp (const NMPlatformIP4Route *a, const NMPlatformIP4Route *b, NMPlatformIPRouteCmpType cmp_type);
+int nm_platform_ip6_route_cmp (const NMPlatformIP6Route *a, const NMPlatformIP6Route *b, NMPlatformIPRouteCmpType cmp_type);
 
 static inline int
-nm_platform_ip4_route_cmp (const NMPlatformIP4Route *a, const NMPlatformIP4Route *b)
+nm_platform_ip4_route_cmp_full (const NMPlatformIP4Route *a, const NMPlatformIP4Route *b)
 {
-	return nm_platform_ip4_route_cmp_full (a, b, TRUE);
+	return nm_platform_ip4_route_cmp (a, b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL);
 }
 
 static inline int
-nm_platform_ip6_route_cmp (const NMPlatformIP6Route *a, const NMPlatformIP6Route *b)
+nm_platform_ip6_route_cmp_full (const NMPlatformIP6Route *a, const NMPlatformIP6Route *b)
 {
-	return nm_platform_ip6_route_cmp_full (a, b, TRUE);
+	return nm_platform_ip6_route_cmp (a, b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL);
 }
 
-gboolean nm_platform_check_support_kernel_extended_ifa_flags (NMPlatform *self);
-gboolean nm_platform_check_support_user_ipv6ll (NMPlatform *self);
+void nm_platform_link_hash_update (const NMPlatformLink *obj, NMHashState *h);
+void nm_platform_ip4_address_hash_update (const NMPlatformIP4Address *obj, NMHashState *h);
+void nm_platform_ip6_address_hash_update (const NMPlatformIP6Address *obj, NMHashState *h);
+void nm_platform_ip4_route_hash_update (const NMPlatformIP4Route *obj, NMPlatformIPRouteCmpType cmp_type, NMHashState *h);
+void nm_platform_ip6_route_hash_update (const NMPlatformIP6Route *obj, NMPlatformIPRouteCmpType cmp_type, NMHashState *h);
+void nm_platform_lnk_gre_hash_update (const NMPlatformLnkGre *obj, NMHashState *h);
+void nm_platform_lnk_infiniband_hash_update (const NMPlatformLnkInfiniband *obj, NMHashState *h);
+void nm_platform_lnk_ip6tnl_hash_update (const NMPlatformLnkIp6Tnl *obj, NMHashState *h);
+void nm_platform_lnk_ipip_hash_update (const NMPlatformLnkIpIp *obj, NMHashState *h);
+void nm_platform_lnk_macsec_hash_update (const NMPlatformLnkMacsec *obj, NMHashState *h);
+void nm_platform_lnk_macvlan_hash_update (const NMPlatformLnkMacvlan *obj, NMHashState *h);
+void nm_platform_lnk_sit_hash_update (const NMPlatformLnkSit *obj, NMHashState *h);
+void nm_platform_lnk_vlan_hash_update (const NMPlatformLnkVlan *obj, NMHashState *h);
+void nm_platform_lnk_vxlan_hash_update (const NMPlatformLnkVxlan *obj, NMHashState *h);
+
+NMPlatformKernelSupportFlags nm_platform_check_kernel_support (NMPlatform *self,
+                                                               NMPlatformKernelSupportFlags request_flags);
 
 const char *nm_platform_link_flags2str (unsigned flags, char *buf, gsize len);
 const char *nm_platform_link_inet6_addrgenmode2str (guint8 mode, char *buf, gsize len);
@@ -1035,4 +1330,10 @@ gboolean nm_platform_ethtool_set_wake_on_lan (NMPlatform *self, int ifindex, NMS
 gboolean nm_platform_ethtool_set_link_settings (NMPlatform *self, int ifindex, gboolean autoneg, guint32 speed, NMPlatformLinkDuplexType duplex);
 gboolean nm_platform_ethtool_get_link_settings (NMPlatform *self, int ifindex, gboolean *out_autoneg, guint32 *out_speed, NMPlatformLinkDuplexType *out_duplex);
 
+void nm_platform_ip4_dev_route_blacklist_set (NMPlatform *self,
+                                              int ifindex,
+                                              GPtrArray *ip4_dev_route_blacklist);
+
+struct _NMDedupMultiIndex *nm_platform_get_multi_idx (NMPlatform *self);
+
 #endif /* __NETWORKMANAGER_PLATFORM_H__ */
diff --git a/src/platform/nmp-netns.c b/src/platform/nmp-netns.c
index 4acd4761..34215828 100644
--- a/src/platform/nmp-netns.c
+++ b/src/platform/nmp-netns.c
@@ -299,7 +299,7 @@ _netns_new (GError **error)
 		g_set_error (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN,
 		             "Failed opening mntns: %s",
 		             g_strerror (errsv));
-		close (fd_net);
+		nm_close (fd_net);
 		return NULL;
 	}
 
@@ -620,7 +620,7 @@ nmp_netns_bind_to_path (NMPNetns *self, const char *filename, int *out_fd)
 		       filename, g_strerror (errsv));
 		return FALSE;
 	}
-	close (fd);
+	nm_close (fd);
 
 	if (mount (PROC_SELF_NS_NET, filename, "none", MS_BIND, NULL) != 0) {
 		errsv = errno;
@@ -702,15 +702,11 @@ dispose (GObject *object)
 	NMPNetns *self = NMP_NETNS (object);
 	NMPNetnsPrivate *priv = NMP_NETNS_GET_PRIVATE (self);
 
-	if (priv->fd_net > 0) {
-		close (priv->fd_net);
-		priv->fd_net = 0;
-	}
+	nm_close (priv->fd_net);
+	priv->fd_net = -1;
 
-	if (priv->fd_mnt > 0) {
-		close (priv->fd_mnt);
-		priv->fd_mnt = 0;
-	}
+	nm_close (priv->fd_mnt);
+	priv->fd_mnt = -1;
 
 	G_OBJECT_CLASS (nmp_netns_parent_class)->dispose (object);
 }
diff --git a/src/platform/nmp-netns.h b/src/platform/nmp-netns.h
index 56c1e7e8..55a4b95f 100644
--- a/src/platform/nmp-netns.h
+++ b/src/platform/nmp-netns.h
@@ -60,8 +60,7 @@ _nm_auto_pop_netns (NMPNetns **p)
 		errno = errsv;
 	}
 }
-
-#define nm_auto_pop_netns __attribute__((cleanup(_nm_auto_pop_netns)))
+#define nm_auto_pop_netns nm_auto(_nm_auto_pop_netns)
 
 gboolean nmp_netns_bind_to_path (NMPNetns *self, const char *filename, int *out_fd);
 gboolean nmp_netns_bind_to_path_destroy (NMPNetns *self, const char *filename);
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,
 	},
 };
diff --git a/src/platform/nmp-object.h b/src/platform/nmp-object.h
index b69680f6..41fd08cb 100644
--- a/src/platform/nmp-object.h
+++ b/src/platform/nmp-object.h
@@ -21,8 +21,9 @@
 #ifndef __NMP_OBJECT_H__
 #define __NMP_OBJECT_H__
 
+#include "nm-utils/nm-obj.h"
+#include "nm-utils/nm-dedup-multi.h"
 #include "nm-platform.h"
-#include "nm-multi-index.h"
 
 struct udev_device;
 
@@ -47,36 +48,42 @@ typedef enum { /*< skip >*/
  * but only route objects can be indexed by NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_NO_DEFAULT.
  *
  * Of one index type, there can be multiple indexes or not.
- * For example, of the index type NMP_CACHE_ID_TYPE_ADDRROUTE_VISIBLE_BY_IFINDEX there
+ * For example, of the index type NMP_CACHE_ID_TYPE_ADDRROUTE_BY_IFINDEX there
  * are multiple instances (for different route/addresses, v4/v6, per-ifindex).
  *
  * But one object, can only be indexed by one particular index of a
  * type. For example, a certain address instance is only indexed by
- * the index NMP_CACHE_ID_TYPE_ADDRROUTE_VISIBLE_BY_IFINDEX with
+ * the index NMP_CACHE_ID_TYPE_ADDRROUTE_BY_IFINDEX with
  * matching v4/v6 and ifindex -- or maybe not at all if it isn't visible.
  * */
 typedef enum { /*< skip >*/
 	NMP_CACHE_ID_TYPE_NONE,
 
-	/* all the objects of a certain type */
+	/* all the objects of a certain type.
+	 *
+	 * This index is special. It is the only one that contains *all* object.
+	 * Other indexes may consider some object as non "partitionable", hence
+	 * they don't track all objects.
+	 *
+	 * Hence, this index type is used when looking at all objects (still
+	 * partitioned by type).
+	 *
+	 * Also, note that links may be considered invisible. This index type
+	 * expose all links, even invisible ones. For addresses/routes, this
+	 * distiction doesn't exist, as all addresses/routes that are alive
+	 * are visible as well. */
 	NMP_CACHE_ID_TYPE_OBJECT_TYPE,
 
 	/* index for the link objects by ifname. */
 	NMP_CACHE_ID_TYPE_LINK_BY_IFNAME,
 
-	/* all the visible objects of a certain type */
-	NMP_CACHE_ID_TYPE_OBJECT_TYPE_VISIBLE_ONLY,
+	/* indeces for the visible default-routes, ignoring ifindex.
+	 * This index only contains two partitions: all visible default-routes,
+	 * separate for IPv4 and IPv6. */
+	NMP_CACHE_ID_TYPE_DEFAULT_ROUTES,
 
-	/* indeces for the visible routes, ignoring ifindex. */
-	NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_NO_DEFAULT,
-	NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_ONLY_DEFAULT,
-
-	/* all the visible addresses/routes (by object-type) for an ifindex. */
-	NMP_CACHE_ID_TYPE_ADDRROUTE_VISIBLE_BY_IFINDEX,
-
-	/* three indeces for the visible routes, per ifindex. */
-	NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_BY_IFINDEX_NO_DEFAULT,
-	NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_BY_IFINDEX_ONLY_DEFAULT,
+	/* all the addresses/routes (by object-type) for an ifindex. */
+	NMP_CACHE_ID_TYPE_ADDRROUTE_BY_IFINDEX,
 
 	/* Consider all the destination fields of a route, that is, the ID without the ifindex
 	 * and gateway (meaning: network/plen,metric).
@@ -86,65 +93,21 @@ typedef enum { /*< skip >*/
 	 * sends one RTM_NEWADDR notification without notifying about the deletion. We detect
 	 * that by having this index to contain overlapping routes which require special
 	 * cache-resync. */
-	NMP_CACHE_ID_TYPE_ROUTES_BY_DESTINATION_IP4,
-	NMP_CACHE_ID_TYPE_ROUTES_BY_DESTINATION_IP6,
+	NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID,
 
 	__NMP_CACHE_ID_TYPE_MAX,
 	NMP_CACHE_ID_TYPE_MAX = __NMP_CACHE_ID_TYPE_MAX - 1,
 } NMPCacheIdType;
 
-typedef struct _NMPCacheId NMPCacheId;
-
-struct _NMPCacheId {
-	union {
-		NMMultiIndexId base;
-		guint8 _id_type; /* NMPCacheIdType as guint8 */
-		struct _nm_packed {
-			/* NMP_CACHE_ID_TYPE_OBJECT_TYPE */
-			/* NMP_CACHE_ID_TYPE_OBJECT_TYPE_VISIBLE_ONLY */
-			/* NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_NO_DEFAULT */
-			/* NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_ONLY_DEFAULT */
-			guint8 _id_type;
-			guint8 obj_type; /* NMPObjectType as guint8 */
-		} object_type;
-		struct _nm_packed {
-			/* NMP_CACHE_ID_TYPE_ADDRROUTE_VISIBLE_BY_IFINDEX */
-			/* NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_BY_IFINDEX_NO_DEFAULT */
-			/* NMP_CACHE_ID_TYPE_ROUTES_VISIBLE_BY_IFINDEX_ONLY_DEFAULT */
-			guint8 _id_type;
-			guint8 obj_type; /* NMPObjectType as guint8 */
-			int _misaligned_ifindex;
-		} object_type_by_ifindex;
-		struct _nm_packed {
-			/* NMP_CACHE_ID_TYPE_LINK_BY_IFNAME */
-			guint8 _id_type;
-			char ifname_short[IFNAMSIZ - 1]; /* don't include the trailing NUL so the struct fits in 4 bytes. */
-		} link_by_ifname;
-		struct _nm_packed {
-			/* NMP_CACHE_ID_TYPE_ROUTES_BY_DESTINATION_IP4 */
-			guint8 _id_type;
-			guint8 plen;
-			guint32 _misaligned_metric;
-			guint32 _misaligned_network;
-		} routes_by_destination_ip4;
-		struct _nm_packed {
-			/* NMP_CACHE_ID_TYPE_ROUTES_BY_DESTINATION_IP6 */
-			guint8 _id_type;
-			guint8 plen;
-			guint32 _misaligned_metric;
-			struct in6_addr _misaligned_network;
-		} routes_by_destination_ip6;
-	};
-};
-
 typedef struct {
+	NMDedupMultiObjClass parent;
+	const char *obj_type_name;
+	int sizeof_data;
+	int sizeof_public;
 	NMPObjectType obj_type;
 	int addr_family;
 	int rtm_gettype;
-	int sizeof_data;
-	int sizeof_public;
 	NMPlatformSignalIdType signal_type_id;
-	const char *obj_type_name;
 	const char *signal_type;
 
 	const guint8 *supported_cache_ids;
@@ -152,13 +115,9 @@ typedef struct {
 	/* Only for NMPObjectLnk* types. */
 	NMLinkType lnk_link_type;
 
-	/* returns %FALSE, if the obj type would never have an entry for index type @id_type. If @obj has an index,
-	 * initialize @id and set @out_id to it. Otherwise, @out_id is NULL. */
-	gboolean (*cmd_obj_init_cache_id) (const NMPObject *obj, NMPCacheIdType id_type, NMPCacheId *id, const NMPCacheId **out_id);
-
+	void (*cmd_obj_hash_update) (const NMPObject *obj, NMHashState *h);
 	int (*cmd_obj_cmp) (const NMPObject *obj1, const NMPObject *obj2);
 	void (*cmd_obj_copy) (NMPObject *dst, const NMPObject *src);
-	void (*cmd_obj_stackinit_id) (NMPObject *obj, const NMPObject *src);
 	void (*cmd_obj_dispose) (NMPObject *obj);
 	gboolean (*cmd_obj_is_alive) (const NMPObject *obj);
 	gboolean (*cmd_obj_is_visible) (const NMPObject *obj);
@@ -166,10 +125,11 @@ typedef struct {
 
 	/* functions that operate on NMPlatformObject */
 	void (*cmd_plobj_id_copy) (NMPlatformObject *dst, const NMPlatformObject *src);
-	gboolean (*cmd_plobj_id_equal) (const NMPlatformObject *obj1, const NMPlatformObject *obj2);
-	guint (*cmd_plobj_id_hash) (const NMPlatformObject *obj);
+	int (*cmd_plobj_id_cmp) (const NMPlatformObject *obj1, const NMPlatformObject *obj2);
+	void (*cmd_plobj_id_hash_update) (const NMPlatformObject *obj, NMHashState *h);
 	const char *(*cmd_plobj_to_string_id) (const NMPlatformObject *obj, char *buf, gsize buf_size);
 	const char *(*cmd_plobj_to_string) (const NMPlatformObject *obj, char *buf, gsize len);
+	void (*cmd_plobj_hash_update) (const NMPlatformObject *obj, NMHashState *h);
 	int (*cmd_plobj_cmp) (const NMPlatformObject *obj1, const NMPlatformObject *obj2);
 } NMPClass;
 
@@ -182,7 +142,7 @@ typedef struct {
 		bool is_in_netlink;
 
 		/* Additional data that depends on the link-type (IFLA_INFO_DATA) */
-		NMPObject *lnk;
+		const NMPObject *lnk;
 	} netlink;
 
 	struct {
@@ -266,9 +226,10 @@ typedef struct {
 } NMPObjectIP6Route;
 
 struct _NMPObject {
-	const NMPClass *_class;
-	int _ref_count;
-	bool is_cached;
+	union {
+		NMDedupMultiObj parent;
+		const NMPClass *_class;
+	};
 	union {
 		NMPlatformObject        object;
 
@@ -326,8 +287,6 @@ NMP_CLASS_IS_VALID (const NMPClass *klass)
 	    && ((((char *) klass) - ((char *) _nmp_classes)) % (sizeof (_nmp_classes[0]))) == 0;
 }
 
-#define NMP_REF_COUNT_STACKINIT (G_MAXINT)
-
 static inline NMPObject *
 NMP_OBJECT_UP_CAST(const NMPlatformObject *plobj)
 {
@@ -336,7 +295,7 @@ NMP_OBJECT_UP_CAST(const NMPlatformObject *plobj)
 	obj = plobj
 	      ? (NMPObject *) ( &(((char *) plobj)[-((int) G_STRUCT_OFFSET (NMPObject, object))]) )
 	      : NULL;
-	nm_assert (!obj || (obj->_ref_count > 0 && NMP_CLASS_IS_VALID (obj->_class)));
+	nm_assert (!obj || (obj->parent._ref_count > 0 && NMP_CLASS_IS_VALID (obj->_class)));
 	return obj;
 }
 #define NMP_OBJECT_UP_CAST(plobj) (NMP_OBJECT_UP_CAST ((const NMPlatformObject *) (plobj)))
@@ -345,7 +304,7 @@ static inline gboolean
 NMP_OBJECT_IS_VALID (const NMPObject *obj)
 {
 	nm_assert (!obj || (   obj
-	                    && obj->_ref_count > 0
+	                    && obj->parent._ref_count > 0
 	                    && NMP_CLASS_IS_VALID (obj->_class)));
 
 	/* There isn't really much to check. Either @obj is NULL, or we must
@@ -358,7 +317,7 @@ NMP_OBJECT_IS_STACKINIT (const NMPObject *obj)
 {
 	nm_assert (!obj || NMP_OBJECT_IS_VALID (obj));
 
-	return obj && obj->_ref_count == NMP_REF_COUNT_STACKINIT;
+	return obj && obj->parent._ref_count == NM_OBJ_REF_COUNT_STACKINIT;
 }
 
 static inline const NMPClass *
@@ -377,65 +336,264 @@ NMP_OBJECT_GET_TYPE (const NMPObject *obj)
 	return obj ? obj->_class->obj_type : NMP_OBJECT_TYPE_UNKNOWN;
 }
 
-
+#define NMP_OBJECT_CAST_LINK(obj) \
+	({ \
+		typeof (obj) _obj = (obj); \
+		\
+		nm_assert (!_obj || NMP_OBJECT_GET_TYPE ((const NMPObject *) _obj) == NMP_OBJECT_TYPE_LINK); \
+		_obj ? &_NM_CONSTCAST (NMPObject, _obj)->link : NULL; \
+	})
+
+#define NMP_OBJECT_CAST_IP_ADDRESS(obj) \
+	({ \
+		typeof (obj) _obj = (obj); \
+		\
+		nm_assert (!_obj || NM_IN_SET (NMP_OBJECT_GET_TYPE (_obj), NMP_OBJECT_TYPE_IP4_ADDRESS, NMP_OBJECT_TYPE_IP6_ADDRESS)); \
+		_obj ? &_NM_CONSTCAST (NMPObject, _obj)->ip_address : NULL; \
+	})
+
+#define NMP_OBJECT_CAST_IPX_ADDRESS(obj) \
+	({ \
+		typeof (obj) _obj = (obj); \
+		\
+		nm_assert (!_obj || NM_IN_SET (NMP_OBJECT_GET_TYPE (_obj), NMP_OBJECT_TYPE_IP4_ADDRESS, NMP_OBJECT_TYPE_IP6_ADDRESS)); \
+		_obj ? &_NM_CONSTCAST (NMPObject, _obj)->ipx_address : NULL; \
+	})
+
+#define NMP_OBJECT_CAST_IP4_ADDRESS(obj) \
+	({ \
+		typeof (obj) _obj = (obj); \
+		\
+		nm_assert (!_obj || NMP_OBJECT_GET_TYPE ((const NMPObject *) _obj) == NMP_OBJECT_TYPE_IP4_ADDRESS); \
+		_obj ? &_NM_CONSTCAST (NMPObject, _obj)->ip4_address : NULL; \
+	})
+
+#define NMP_OBJECT_CAST_IP6_ADDRESS(obj) \
+	({ \
+		typeof (obj) _obj = (obj); \
+		\
+		nm_assert (!_obj || NMP_OBJECT_GET_TYPE ((const NMPObject *) _obj) == NMP_OBJECT_TYPE_IP6_ADDRESS); \
+		_obj ? &_NM_CONSTCAST (NMPObject, _obj)->ip6_address : NULL; \
+	})
+
+#define NMP_OBJECT_CAST_IPX_ROUTE(obj) \
+	({ \
+		typeof (obj) _obj = (obj); \
+		\
+		nm_assert (!_obj || NM_IN_SET (NMP_OBJECT_GET_TYPE (_obj), NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE)); \
+		_obj ? &_NM_CONSTCAST (NMPObject, _obj)->ipx_route : NULL; \
+	})
+
+#define NMP_OBJECT_CAST_IP_ROUTE(obj) \
+	({ \
+		typeof (obj) _obj = (obj); \
+		\
+		nm_assert (!_obj || NM_IN_SET (NMP_OBJECT_GET_TYPE (_obj), NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE)); \
+		_obj ? &_NM_CONSTCAST (NMPObject, _obj)->ip_route : NULL; \
+	})
+
+#define NMP_OBJECT_CAST_IP4_ROUTE(obj) \
+	({ \
+		typeof (obj) _obj = (obj); \
+		\
+		nm_assert (!_obj || NMP_OBJECT_GET_TYPE ((const NMPObject *) _obj) == NMP_OBJECT_TYPE_IP4_ROUTE); \
+		_obj ? &_NM_CONSTCAST (NMPObject, _obj)->ip4_route : NULL; \
+	})
+
+#define NMP_OBJECT_CAST_IP6_ROUTE(obj) \
+	({ \
+		typeof (obj) _obj = (obj); \
+		\
+		nm_assert (!_obj || NMP_OBJECT_GET_TYPE ((const NMPObject *) _obj) == NMP_OBJECT_TYPE_IP6_ROUTE); \
+		_obj ? &_NM_CONSTCAST (NMPObject, _obj)->ip6_route : NULL; \
+	})
 
 const NMPClass *nmp_class_from_type (NMPObjectType obj_type);
 
-NMPObject *nmp_object_ref (NMPObject *object);
-void nmp_object_unref (NMPObject *object);
+static inline const NMPObject *
+nmp_object_ref (const NMPObject *obj)
+{
+	if (!obj) {
+		/* for convenience, allow NULL. */
+		return NULL;
+	}
+
+	/* ref and unref accept const pointers. NMPObject is supposed to be shared
+	 * and kept immutable. Disallowing to take/retrun a reference to a const
+	 * NMPObject is cumbersome, because callers are precisely expected to
+	 * keep a ref on the otherwise immutable object. */
+	g_return_val_if_fail (NMP_OBJECT_IS_VALID (obj), NULL);
+	g_return_val_if_fail (obj->parent._ref_count != NM_OBJ_REF_COUNT_STACKINIT, NULL);
+
+	return (const NMPObject *) nm_dedup_multi_obj_ref ((const NMDedupMultiObj *) obj);
+}
+
+static inline const NMPObject *
+nmp_object_unref (const NMPObject *obj)
+{
+	nm_dedup_multi_obj_unref ((const NMDedupMultiObj *) obj);
+	return NULL;
+}
+
+#define nm_clear_nmp_object(ptr) \
+	({ \
+		typeof (ptr) _ptr = (ptr); \
+		typeof (*_ptr) _pptr; \
+		gboolean _changed = FALSE; \
+		\
+		if (   _ptr \
+		    && (_pptr = *_ptr)) { \
+			*_ptr = NULL; \
+			nmp_object_unref (_pptr); \
+			_changed = TRUE; \
+		} \
+		_changed; \
+	})
+
 NMPObject *nmp_object_new (NMPObjectType obj_type, const NMPlatformObject *plob);
 NMPObject *nmp_object_new_link (int ifindex);
 
 const NMPObject *nmp_object_stackinit (NMPObject *obj, NMPObjectType obj_type, const NMPlatformObject *plobj);
+
+static inline NMPObject *
+nmp_object_stackinit_obj (NMPObject *obj, const NMPObject *src)
+{
+	return obj == src
+	         ? obj
+	         : (NMPObject *) nmp_object_stackinit (obj, NMP_OBJECT_GET_TYPE (src), &src->object);
+}
+
 const NMPObject *nmp_object_stackinit_id  (NMPObject *obj, const NMPObject *src);
 const NMPObject *nmp_object_stackinit_id_link (NMPObject *obj, int ifindex);
 const NMPObject *nmp_object_stackinit_id_ip4_address (NMPObject *obj, int ifindex, guint32 address, guint8 plen, guint32 peer_address);
-const NMPObject *nmp_object_stackinit_id_ip6_address (NMPObject *obj, int ifindex, const struct in6_addr *address, guint8 plen);
-const NMPObject *nmp_object_stackinit_id_ip4_route (NMPObject *obj, int ifindex, guint32 network, guint8 plen, guint32 metric);
-const NMPObject *nmp_object_stackinit_id_ip6_route (NMPObject *obj, int ifindex, const struct in6_addr *network, guint8 plen, guint32 metric);
+const NMPObject *nmp_object_stackinit_id_ip6_address (NMPObject *obj, int ifindex, const struct in6_addr *address);
 
 const char *nmp_object_to_string (const NMPObject *obj, NMPObjectToStringMode to_string_mode, char *buf, gsize buf_size);
+void nmp_object_hash_update (const NMPObject *obj, NMHashState *h);
 int nmp_object_cmp (const NMPObject *obj1, const NMPObject *obj2);
 gboolean nmp_object_equal (const NMPObject *obj1, const NMPObject *obj2);
 void nmp_object_copy (NMPObject *dst, const NMPObject *src, gboolean id_only);
 NMPObject *nmp_object_clone (const NMPObject *obj, gboolean id_only);
-gboolean nmp_object_id_equal (const NMPObject *obj1, const NMPObject *obj2);
+
+int nmp_object_id_cmp (const NMPObject *obj1, const NMPObject *obj2);
+void nmp_object_id_hash_update (const NMPObject *obj, NMHashState *h);
 guint nmp_object_id_hash (const NMPObject *obj);
+
+static inline gboolean
+nmp_object_id_equal (const NMPObject *obj1, const NMPObject *obj2)
+{
+	return nmp_object_id_cmp (obj1, obj2) == 0;
+}
+
 gboolean nmp_object_is_alive (const NMPObject *obj);
 gboolean nmp_object_is_visible (const NMPObject *obj);
 
-void _nmp_object_fixup_link_udev_fields (NMPObject *obj, gboolean use_udev);
+void _nmp_object_fixup_link_udev_fields (NMPObject **obj_new, NMPObject *obj_orig, gboolean use_udev);
 
-#define nm_auto_nmpobj __attribute__((cleanup(_nm_auto_nmpobj_cleanup)))
 static inline void
-_nm_auto_nmpobj_cleanup (NMPObject **pobj)
+_nm_auto_nmpobj_cleanup (gpointer p)
 {
-	nmp_object_unref (*pobj);
+	nmp_object_unref (*((const NMPObject **) p));
 }
+#define nm_auto_nmpobj nm_auto(_nm_auto_nmpobj_cleanup)
 
 typedef struct _NMPCache NMPCache;
 
 typedef void (*NMPCachePreHook) (NMPCache *cache, const NMPObject *old, const NMPObject *new, NMPCacheOpsType ops_type, gpointer user_data);
 typedef gboolean (*NMPObjectMatchFn) (const NMPObject *obj, gpointer user_data);
 
-gboolean nmp_cache_id_equal (const NMPCacheId *a, const NMPCacheId *b);
-guint nmp_cache_id_hash (const NMPCacheId *id);
-NMPCacheId *nmp_cache_id_clone (const NMPCacheId *id);
-void nmp_cache_id_destroy (NMPCacheId *id);
+const NMDedupMultiEntry *nmp_cache_lookup_entry (const NMPCache *cache,
+                                                 const NMPObject *obj);
+const NMDedupMultiEntry *nmp_cache_lookup_entry_with_idx_type (const NMPCache *cache,
+                                                               NMPCacheIdType cache_id_type,
+                                                               const NMPObject *obj);
+const NMDedupMultiEntry *nmp_cache_lookup_entry_link (const NMPCache *cache,
+                                                      int ifindex);
+const NMPObject *nmp_cache_lookup_obj (const NMPCache *cache,
+                                       const NMPObject *obj);
+const NMPObject *nmp_cache_lookup_link (const NMPCache *cache,
+                                        int ifindex);
+
+typedef struct _NMPLookup NMPLookup;
+
+struct _NMPLookup {
+	NMPCacheIdType cache_id_type;
+	NMPObject selector_obj;
+};
+
+const NMDedupMultiHeadEntry *nmp_cache_lookup_all (const NMPCache *cache,
+                                                   NMPCacheIdType cache_id_type,
+                                                   const NMPObject *select_obj);
+
+static inline const NMDedupMultiHeadEntry *
+nmp_cache_lookup (const NMPCache *cache,
+                  const NMPLookup *lookup)
+{
+	return nmp_cache_lookup_all (cache, lookup->cache_id_type, &lookup->selector_obj);
+}
+
+const NMPLookup *nmp_lookup_init_obj_type (NMPLookup *lookup,
+                                           NMPObjectType obj_type);
+const NMPLookup *nmp_lookup_init_link_by_ifname (NMPLookup *lookup,
+                                                 const char *ifname);
+const NMPLookup *nmp_lookup_init_addrroute (NMPLookup *lookup,
+                                            NMPObjectType obj_type,
+                                            int ifindex);
+const NMPLookup *nmp_lookup_init_route_default (NMPLookup *lookup,
+                                                NMPObjectType obj_type);
+const NMPLookup *nmp_lookup_init_route_by_weak_id (NMPLookup *lookup,
+                                                   const NMPObject *obj);
+const NMPLookup *nmp_lookup_init_ip4_route_by_weak_id (NMPLookup *lookup,
+                                                       in_addr_t network,
+                                                       guint plen,
+                                                       guint32 metric,
+                                                       guint8 tos);
+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);
+
+GArray *nmp_cache_lookup_to_array (const NMDedupMultiHeadEntry *head_entry,
+                                   NMPObjectType obj_type,
+                                   gboolean visible_only);
+
+static inline gboolean
+nmp_cache_iter_next (NMDedupMultiIter *iter, const NMPObject **out_obj)
+{
+	gboolean has_next;
+
+	has_next = nm_dedup_multi_iter_next (iter);
+	nm_assert (!has_next || NMP_OBJECT_IS_VALID (iter->current->obj));
+	if (out_obj)
+		*out_obj = has_next ? iter->current->obj : NULL;
+	return has_next;
+}
+
+static inline gboolean
+nmp_cache_iter_next_link (NMDedupMultiIter *iter, const NMPlatformLink **out_obj)
+{
+	gboolean has_next;
 
-NMPCacheId *nmp_cache_id_init_object_type (NMPCacheId *id, NMPObjectType obj_type, gboolean visible_only);
-NMPCacheId *nmp_cache_id_init_addrroute_visible_by_ifindex (NMPCacheId *id, NMPObjectType obj_type, int ifindex);
-NMPCacheId *nmp_cache_id_init_routes_visible (NMPCacheId *id, NMPObjectType obj_type, gboolean with_default, gboolean with_non_default, int ifindex);
-NMPCacheId *nmp_cache_id_init_link_by_ifname (NMPCacheId *id, const char *ifname);
-NMPCacheId *nmp_cache_id_init_routes_by_destination_ip4 (NMPCacheId *id, guint32 network, guint8 plen, guint32 metric);
-NMPCacheId *nmp_cache_id_init_routes_by_destination_ip6 (NMPCacheId *id, const struct in6_addr *network, guint8 plen, guint32 metric);
+	has_next = nm_dedup_multi_iter_next (iter);
+	nm_assert (!has_next || NMP_OBJECT_GET_TYPE (iter->current->obj) == NMP_OBJECT_TYPE_LINK);
+	if (out_obj)
+		*out_obj = has_next ? &(((const NMPObject *) iter->current->obj)->link) : NULL;
+	return has_next;
+}
 
-const NMPlatformObject *const *nmp_cache_lookup_multi (const NMPCache *cache, const NMPCacheId *cache_id, guint *out_len);
-GArray *nmp_cache_lookup_multi_to_array (const NMPCache *cache, NMPObjectType obj_type, const NMPCacheId *cache_id);
-const NMPObject *nmp_cache_lookup_obj (const NMPCache *cache, const NMPObject *obj);
-const NMPObject *nmp_cache_lookup_link (const NMPCache *cache, int ifindex);
+#define nmp_cache_iter_for_each(iter, head, obj) \
+	for (nm_dedup_multi_iter_init ((iter), \
+	                               (head)); \
+	     nmp_cache_iter_next ((iter), (obj)); \
+	     )
 
-const NMPObject *nmp_cache_find_other_route_for_same_destination (const NMPCache *cache, const NMPObject *route);
+#define nmp_cache_iter_for_each_link(iter, head, obj) \
+	for (nm_dedup_multi_iter_init ((iter), \
+	                               (head)); \
+	     nmp_cache_iter_next_link ((iter), (obj)); \
+	     )
 
 const NMPObject *nmp_cache_lookup_link_full (const NMPCache *cache,
                                              int ifindex,
@@ -444,10 +602,8 @@ const NMPObject *nmp_cache_lookup_link_full (const NMPCache *cache,
                                              NMLinkType link_type,
                                              NMPObjectMatchFn match_fn,
                                              gpointer user_data);
-GHashTable *nmp_cache_lookup_all_to_hash (const NMPCache *cache,
-                                          NMPCacheId *cache_id,
-                                          GHashTable *hash);
 
+gboolean nmp_cache_link_connected_for_slave (int ifindex_master, const NMPObject *slave);
 gboolean nmp_cache_link_connected_needs_toggle (const NMPCache *cache, const NMPObject *master, const NMPObject *potential_slave, const NMPObject *ignore_slave);
 const NMPObject *nmp_cache_link_connected_needs_toggle_by_ifindex (const NMPCache *cache, int master_ifindex, const NMPObject *potential_slave, const NMPObject *ignore_slave);
 
@@ -455,13 +611,182 @@ gboolean nmp_cache_use_udev_get (const NMPCache *cache);
 
 void ASSERT_nmp_cache_is_consistent (const NMPCache *cache);
 
-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);
-NMPCacheOpsType nmp_cache_remove_netlink (NMPCache *cache, const NMPObject *obj, NMPObject **out_obj, gboolean *out_was_visible, NMPCachePreHook pre_hook, gpointer user_data);
-NMPCacheOpsType nmp_cache_update_netlink (NMPCache *cache, NMPObject *obj, NMPObject **out_obj, gboolean *out_was_visible, NMPCachePreHook pre_hook, gpointer user_data);
-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);
-NMPCacheOpsType nmp_cache_update_link_master_connected (NMPCache *cache, int ifindex, NMPObject **out_obj, gboolean *out_was_visible, NMPCachePreHook pre_hook, gpointer user_data);
-
-NMPCache *nmp_cache_new (gboolean use_udev);
+NMPCacheOpsType nmp_cache_remove (NMPCache *cache,
+                                  const NMPObject *obj_needle,
+                                  gboolean equals_by_ptr,
+                                  gboolean only_dirty,
+                                  const NMPObject **out_obj_old);
+NMPCacheOpsType nmp_cache_remove_netlink (NMPCache *cache,
+                                          const NMPObject *obj_needle,
+                                          const NMPObject **out_obj_old,
+                                          const NMPObject **out_obj_new);
+NMPCacheOpsType nmp_cache_update_netlink (NMPCache *cache,
+                                          NMPObject *obj_hand_over,
+                                          gboolean is_dump,
+                                          const NMPObject **out_obj_old,
+                                          const NMPObject **out_obj_new);
+NMPCacheOpsType 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);
+NMPCacheOpsType nmp_cache_update_link_udev (NMPCache *cache,
+                                            int ifindex,
+                                            struct udev_device *udevice,
+                                            const NMPObject **out_obj_old,
+                                            const NMPObject **out_obj_new);
+NMPCacheOpsType nmp_cache_update_link_master_connected (NMPCache *cache,
+                                                        int ifindex,
+                                                        const NMPObject **out_obj_old,
+                                                        const NMPObject **out_obj_new);
+
+void nmp_cache_dirty_set_all (NMPCache *cache, NMPObjectType obj_type);
+
+NMPCache *nmp_cache_new (NMDedupMultiIndex *multi_idx, gboolean use_udev);
 void nmp_cache_free (NMPCache *cache);
 
+static inline void
+ASSERT_nmp_cache_ops (const NMPCache *cache,
+                      NMPCacheOpsType ops_type,
+                      const NMPObject *obj_old,
+                      const NMPObject *obj_new)
+{
+#if NM_MORE_ASSERTS
+	nm_assert (cache);
+	nm_assert (obj_old || obj_new);
+	nm_assert (!obj_old || (   NMP_OBJECT_IS_VALID (obj_old)
+	                        && !NMP_OBJECT_IS_STACKINIT (obj_old)
+	                        && nmp_object_is_alive (obj_old)));
+	nm_assert (!obj_new || (   NMP_OBJECT_IS_VALID (obj_new)
+	                        && !NMP_OBJECT_IS_STACKINIT (obj_new)
+	                        && nmp_object_is_alive (obj_new)));
+
+	switch (ops_type) {
+	case NMP_CACHE_OPS_UNCHANGED:
+		nm_assert (obj_old == obj_new);
+		break;
+	case NMP_CACHE_OPS_ADDED:
+		nm_assert (!obj_old && obj_new);
+		break;
+	case NMP_CACHE_OPS_UPDATED:
+		nm_assert (obj_old &&  obj_new && obj_old != obj_new);
+		break;
+	case NMP_CACHE_OPS_REMOVED:
+		nm_assert (obj_old && !obj_new);
+		break;
+	default:
+		nm_assert_not_reached ();
+	}
+
+	nm_assert (obj_new == NULL || obj_old == NULL || nmp_object_id_equal (obj_new, obj_old));
+	nm_assert (!obj_old || !obj_new || NMP_OBJECT_GET_CLASS (obj_old) == NMP_OBJECT_GET_CLASS (obj_new));
+
+	nm_assert (obj_new == nmp_cache_lookup_obj (cache, obj_new ?: obj_old));
+#endif
+}
+
+const NMDedupMultiHeadEntry *nm_platform_lookup_all (NMPlatform *platform,
+                                                     NMPCacheIdType cache_id_type,
+                                                     const NMPObject *obj);
+
+const NMDedupMultiEntry *nm_platform_lookup_entry (NMPlatform *platform,
+                                                   NMPCacheIdType cache_id_type,
+                                                   const NMPObject *obj);
+
+static inline const NMDedupMultiHeadEntry *
+nm_platform_lookup_obj_type (NMPlatform *platform,
+                             NMPObjectType obj_type)
+{
+	NMPLookup lookup;
+
+	nmp_lookup_init_obj_type (&lookup, obj_type);
+	return nm_platform_lookup (platform, &lookup);
+}
+
+static inline const NMDedupMultiHeadEntry *
+nm_platform_lookup_link_by_ifname (NMPlatform *platform,
+                                   const char *ifname)
+{
+	NMPLookup lookup;
+
+	nmp_lookup_init_link_by_ifname (&lookup, ifname);
+	return nm_platform_lookup (platform, &lookup);
+}
+
+static inline const NMDedupMultiHeadEntry *
+nm_platform_lookup_addrroute (NMPlatform *platform,
+                              NMPObjectType obj_type,
+                              int ifindex)
+{
+	NMPLookup lookup;
+
+	nmp_lookup_init_addrroute (&lookup, obj_type, ifindex);
+	return nm_platform_lookup (platform, &lookup);
+}
+
+static inline GPtrArray *
+nm_platform_lookup_addrroute_clone (NMPlatform *platform,
+                                    NMPObjectType obj_type,
+                                    int ifindex,
+                                    NMPObjectPredicateFunc predicate,
+                                    gpointer user_data)
+{
+	NMPLookup lookup;
+
+	nmp_lookup_init_addrroute (&lookup, obj_type, ifindex);
+	return nm_platform_lookup_clone (platform, &lookup, predicate, user_data);
+}
+
+static inline const NMDedupMultiHeadEntry *
+nm_platform_lookup_route_default (NMPlatform *platform,
+                                  NMPObjectType obj_type)
+{
+	NMPLookup lookup;
+
+	nmp_lookup_init_route_default (&lookup, obj_type);
+	return nm_platform_lookup (platform, &lookup);
+}
+
+static inline GPtrArray *
+nm_platform_lookup_route_default_clone (NMPlatform *platform,
+                                        NMPObjectType obj_type,
+                                        NMPObjectPredicateFunc predicate,
+                                        gpointer user_data)
+{
+	NMPLookup lookup;
+
+	nmp_lookup_init_route_default (&lookup, obj_type);
+	return nm_platform_lookup_clone (platform, &lookup, predicate, user_data);
+}
+
+static inline const NMDedupMultiHeadEntry *
+nm_platform_lookup_ip4_route_by_weak_id (NMPlatform *platform,
+                                         in_addr_t network,
+                                         guint plen,
+                                         guint32 metric,
+                                         guint8 tos)
+{
+	NMPLookup lookup;
+
+	nmp_lookup_init_ip4_route_by_weak_id (&lookup, network, plen, metric, tos);
+	return nm_platform_lookup (platform, &lookup);
+}
+
+static inline const NMDedupMultiHeadEntry *
+nm_platform_lookup_ip6_route_by_weak_id (NMPlatform *platform,
+                                         const struct in6_addr *network,
+                                         guint plen,
+                                         guint32 metric,
+                                         const struct in6_addr *src,
+                                         guint8 src_plen)
+{
+	NMPLookup lookup;
+
+	nmp_lookup_init_ip6_route_by_weak_id (&lookup, network, plen, metric, src, src_plen);
+	return nm_platform_lookup (platform, &lookup);
+}
+
 #endif /* __NMP_OBJECT_H__ */
diff --git a/src/platform/tests/monitor.c b/src/platform/tests/monitor.c
index d0c58aeb..e1220052 100644
--- a/src/platform/tests/monitor.c
+++ b/src/platform/tests/monitor.c
@@ -78,6 +78,8 @@ main (int argc, char **argv)
 
 	nm_linux_platform_setup ();
 
+	nm_platform_check_kernel_support (NM_PLATFORM_GET, ~((NMPlatformKernelSupportFlags) 0));
+
 	if (global_opt.persist)
 		g_main_loop_run (loop);
 
diff --git a/src/platform/tests/test-address.c b/src/platform/tests/test-address.c
index 4c139ef0..93851ff7 100644
--- a/src/platform/tests/test-address.c
+++ b/src/platform/tests/test-address.c
@@ -22,7 +22,6 @@
 
 #include "test-common.h"
 
-#define DEVICE_NAME "nm-test-device"
 #define IP4_ADDRESS "192.0.2.1"
 #define IP4_ADDRESS_PEER "192.0.2.2"
 #define IP4_ADDRESS_PEER2 "192.0.3.1"
@@ -30,8 +29,8 @@
 #define IP6_ADDRESS "2001:db8:a:b:1:2:3:4"
 #define IP6_PLEN 64
 
-static int DEVICE_IFINDEX = -1;
-static int EX = -1;
+#define DEVICE_IFINDEX NMTSTP_ENV1_IFINDEX
+#define EX             NMTSTP_ENV1_EX
 
 /*****************************************************************************/
 
@@ -103,7 +102,7 @@ test_ip4_address_general (void)
 	accept_signals (address_changed, 0, 1);
 
 	/* Test address listing */
-	addresses = nm_platform_ip4_address_get_all (NM_PLATFORM_GET, ifindex);
+	addresses = nmtstp_platform_ip4_address_get_all (NM_PLATFORM_GET, ifindex);
 	g_assert (addresses);
 	g_assert_cmpint (addresses->len, ==, 1);
 	address = &g_array_index (addresses, NMPlatformIP4Address, 0);
@@ -143,9 +142,9 @@ test_ip6_address_general (void)
 	inet_pton (AF_INET6, IP6_ADDRESS, &addr);
 
 	/* Add address */
-	g_assert (!nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, addr, IP6_PLEN));
+	g_assert (!nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, addr));
 	nmtstp_ip6_address_add (NULL, EX, ifindex, addr, IP6_PLEN, in6addr_any, lifetime, preferred, flags);
-	g_assert (nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, addr, IP6_PLEN));
+	g_assert (nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, addr));
 	accept_signal (address_added);
 
 	/* Add address again (aka update) */
@@ -153,7 +152,7 @@ test_ip6_address_general (void)
 	accept_signals (address_changed, 0, 1);
 
 	/* Test address listing */
-	addresses = nm_platform_ip6_address_get_all (NM_PLATFORM_GET, ifindex);
+	addresses = nmtstp_platform_ip6_address_get_all (NM_PLATFORM_GET, ifindex);
 	g_assert (addresses);
 	g_assert_cmpint (addresses->len, ==, 1);
 	address = &g_array_index (addresses, NMPlatformIP6Address, 0);
@@ -164,7 +163,7 @@ test_ip6_address_general (void)
 
 	/* Remove address */
 	nmtstp_ip6_address_del (NULL, EX, ifindex, addr, IP6_PLEN);
-	g_assert (!nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, addr, IP6_PLEN));
+	g_assert (!nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, addr));
 	accept_signal (address_removed);
 
 	/* Remove address again */
@@ -229,20 +228,20 @@ test_ip6_address_general_2 (void)
 	/* Add/delete notification */
 	nmtstp_ip6_address_add (NULL, EX, ifindex, addr, IP6_PLEN, in6addr_any, lifetime, preferred, 0);
 	accept_signal (address_added);
-	g_assert (nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, addr, IP6_PLEN));
+	g_assert (nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, addr));
 
 	nmtstp_ip6_address_del (NULL, EX, ifindex, addr, IP6_PLEN);
 	accept_signal (address_removed);
-	g_assert (!nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, addr, IP6_PLEN));
+	g_assert (!nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, addr));
 
 	/* Add/delete conflict */
 	nmtstp_ip6_address_add (NULL, EX, ifindex, addr, IP6_PLEN, in6addr_any, lifetime, preferred, 0);
 	accept_signal (address_added);
-	g_assert (nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, addr, IP6_PLEN));
+	g_assert (nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, addr));
 
 	nmtstp_ip6_address_add (NULL, EX, ifindex, addr, IP6_PLEN, in6addr_any, lifetime, preferred, flags);
 	ensure_no_signal (address_added);
-	g_assert (nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, addr, IP6_PLEN));
+	g_assert (nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, addr));
 
 	free_signal (address_added);
 	free_signal (address_removed);
@@ -330,7 +329,7 @@ test_ip4_address_peer_zero (void)
 
 		nmtstp_ip4_address_add (NULL, EX, ifindex, addr, plen, r_peers[i], lifetime, preferred, 0, label);
 
-		addrs = nm_platform_ip4_address_get_all (NM_PLATFORM_GET, ifindex);
+		addrs = nmtstp_platform_ip4_address_get_all (NM_PLATFORM_GET, ifindex);
 		g_assert (addrs);
 		g_assert_cmpint (addrs->len, ==, i + 1);
 		g_array_unref (addrs);
@@ -345,7 +344,7 @@ test_ip4_address_peer_zero (void)
 
 		nmtstp_ip4_address_del (NULL, EX, ifindex, addr, plen, r_peers[i]);
 
-		addrs = nm_platform_ip4_address_get_all (NM_PLATFORM_GET, ifindex);
+		addrs = nmtstp_platform_ip4_address_get_all (NM_PLATFORM_GET, ifindex);
 		g_assert (addrs);
 		g_assert_cmpint (addrs->len, ==, G_N_ELEMENTS (peers) - i - 1);
 		g_array_unref (addrs);
@@ -366,62 +365,16 @@ _nmtstp_init_tests (int *argc, char ***argv)
  * SETUP TESTS
  *****************************************************************************/
 
-typedef struct {
-	const char *testpath;
-	GTestFunc test_func;
-} TestSetup;
-
-static void
-_g_test_run (gconstpointer user_data)
-{
-	const TestSetup *s = user_data;
-	int ifindex;
-
-	_LOGT ("TEST: start %s", s->testpath);
-
-	nm_platform_link_delete (NM_PLATFORM_GET, nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME));
-	g_assert (!nm_platform_link_get_by_ifname (NM_PLATFORM_GET, DEVICE_NAME));
-	g_assert_cmpint (nm_platform_link_dummy_add (NM_PLATFORM_GET, DEVICE_NAME, NULL), ==, NM_PLATFORM_ERROR_SUCCESS);
-
-	ifindex = nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME);
-	g_assert_cmpint (ifindex, >, 0);
-	g_assert_cmpint (DEVICE_IFINDEX, ==, -1);
-
-	DEVICE_IFINDEX = ifindex;
-	EX = nmtstp_run_command_check_external_global ();
-
-	s->test_func ();
-
-	g_assert_cmpint (DEVICE_IFINDEX, ==, ifindex);
-	DEVICE_IFINDEX = -1;
-
-	g_assert_cmpint (ifindex, ==, nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME));
-	g_assert (nm_platform_link_delete (NM_PLATFORM_GET, ifindex));
-	_LOGT ("TEST: finished %s", s->testpath);
-}
-
-static void
-_g_test_add_func (const char *testpath,
-                  GTestFunc   test_func)
-{
-	TestSetup *s;
-
-	s = g_new0 (TestSetup, 1);
-	s->testpath = testpath;
-	s->test_func = test_func;
-
-	g_test_add_data_func_full (testpath, s, _g_test_run, g_free);
-}
-
 void
 _nmtstp_setup_tests (void)
 {
-	_g_test_add_func ("/address/ipv4/general", test_ip4_address_general);
-	_g_test_add_func ("/address/ipv6/general", test_ip6_address_general);
+#define add_test_func(testpath, test_func) nmtstp_env1_add_test_func(testpath, test_func, FALSE)
+	add_test_func ("/address/ipv4/general", test_ip4_address_general);
+	add_test_func ("/address/ipv6/general", test_ip6_address_general);
 
-	_g_test_add_func ("/address/ipv4/general-2", test_ip4_address_general_2);
-	_g_test_add_func ("/address/ipv6/general-2", test_ip6_address_general_2);
+	add_test_func ("/address/ipv4/general-2", test_ip4_address_general_2);
+	add_test_func ("/address/ipv6/general-2", test_ip6_address_general_2);
 
-	_g_test_add_func ("/address/ipv4/peer", test_ip4_address_peer);
-	_g_test_add_func ("/address/ipv4/peer/zero", test_ip4_address_peer_zero);
+	add_test_func ("/address/ipv4/peer", test_ip4_address_peer);
+	add_test_func ("/address/ipv4/peer/zero", test_ip4_address_peer_zero);
 }
diff --git a/src/platform/tests/test-cleanup.c b/src/platform/tests/test-cleanup.c
index 71a92cbf..937cd12c 100644
--- a/src/platform/tests/test-cleanup.c
+++ b/src/platform/tests/test-cleanup.c
@@ -22,8 +22,6 @@
 
 #include "test-common.h"
 
-#define DEVICE_NAME "nm-test-device"
-
 static void
 test_cleanup_internal (void)
 {
@@ -31,8 +29,8 @@ test_cleanup_internal (void)
 	int ifindex;
 	GArray *addresses4;
 	GArray *addresses6;
-	GArray *routes4;
-	GArray *routes6;
+	GPtrArray *routes4;
+	GPtrArray *routes6;
 	in_addr_t addr4;
 	in_addr_t network4;
 	int plen4 = 24;
@@ -72,10 +70,10 @@ test_cleanup_internal (void)
 	nmtstp_ip6_route_add (NM_PLATFORM_GET, ifindex, NM_IP_CONFIG_SOURCE_USER, network6, plen6, gateway6, in6addr_any, metric, mss);
 	nmtstp_ip6_route_add (NM_PLATFORM_GET, ifindex, NM_IP_CONFIG_SOURCE_USER, in6addr_any, 0, gateway6, in6addr_any, metric, mss);
 
-	addresses4 = nm_platform_ip4_address_get_all (NM_PLATFORM_GET, ifindex);
-	addresses6 = nm_platform_ip6_address_get_all (NM_PLATFORM_GET, ifindex);
-	routes4 = nm_platform_ip4_route_get_all (NM_PLATFORM_GET, ifindex, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT | NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT);
-	routes6 = nm_platform_ip6_route_get_all (NM_PLATFORM_GET, ifindex, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT | NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT);
+	addresses4 = nmtstp_platform_ip4_address_get_all (NM_PLATFORM_GET, ifindex);
+	addresses6 = nmtstp_platform_ip6_address_get_all (NM_PLATFORM_GET, ifindex);
+	routes4 = nmtstp_ip4_route_get_all (NM_PLATFORM_GET, ifindex);
+	routes6 = nmtstp_ip6_route_get_all (NM_PLATFORM_GET, ifindex);
 
 	g_assert_cmpint (addresses4->len, ==, 1);
 	g_assert_cmpint (addresses6->len, ==, 2); /* also has a IPv6 LL address. */
@@ -84,26 +82,24 @@ test_cleanup_internal (void)
 
 	g_array_unref (addresses4);
 	g_array_unref (addresses6);
-	g_array_unref (routes4);
-	g_array_unref (routes6);
+	g_ptr_array_unref (routes4);
+	g_ptr_array_unref (routes6);
 
 	/* Delete interface with all addresses and routes */
 	g_assert (nm_platform_link_delete (NM_PLATFORM_GET, ifindex));
 
-	addresses4 = nm_platform_ip4_address_get_all (NM_PLATFORM_GET, ifindex);
-	addresses6 = nm_platform_ip6_address_get_all (NM_PLATFORM_GET, ifindex);
-	routes4 = nm_platform_ip4_route_get_all (NM_PLATFORM_GET, ifindex, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT | NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT);
-	routes6 = nm_platform_ip6_route_get_all (NM_PLATFORM_GET, ifindex, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT | NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT);
+	addresses4 = nmtstp_platform_ip4_address_get_all (NM_PLATFORM_GET, ifindex);
+	addresses6 = nmtstp_platform_ip6_address_get_all (NM_PLATFORM_GET, ifindex);
+	routes4 = nmtstp_ip4_route_get_all (NM_PLATFORM_GET, ifindex);
+	routes6 = nmtstp_ip6_route_get_all (NM_PLATFORM_GET, ifindex);
 
 	g_assert_cmpint (addresses4->len, ==, 0);
 	g_assert_cmpint (addresses6->len, ==, 0);
-	g_assert_cmpint (routes4->len, ==, 0);
-	g_assert_cmpint (routes6->len, ==, 0);
+	g_assert (!routes4);
+	g_assert (!routes6);
 
 	g_array_unref (addresses4);
 	g_array_unref (addresses6);
-	g_array_unref (routes4);
-	g_array_unref (routes6);
 }
 
 NMTstpSetupFunc const _nmtstp_setup_platform_func = SETUP;
diff --git a/src/platform/tests/test-common.c b/src/platform/tests/test-common.c
index a9d0694d..18076712 100644
--- a/src/platform/tests/test-common.c
+++ b/src/platform/tests/test-common.c
@@ -30,6 +30,9 @@
 #define SIGNAL_DATA_FMT "'%s-%s' ifindex %d%s%s%s (%d times received)"
 #define SIGNAL_DATA_ARG(data) (data)->name, nm_platform_signal_change_type_to_string ((data)->change_type), (data)->ifindex, (data)->ifname ? " ifname '" : "", (data)->ifname ? (data)->ifname : "", (data)->ifname ? "'" : "", (data)->received_count
 
+int NMTSTP_ENV1_IFINDEX = -1;
+int NMTSTP_ENV1_EX = -1;
+
 /*****************************************************************************/
 
 void
@@ -67,6 +70,90 @@ _init_platform (NMPlatform **platform, gboolean external_command)
 
 /*****************************************************************************/
 
+static GArray *
+_ipx_address_get_all (NMPlatform *self, int ifindex, NMPObjectType obj_type)
+{
+	NMPLookup lookup;
+
+	g_assert (NM_IS_PLATFORM (self));
+	g_assert (ifindex > 0);
+	g_assert (NM_IN_SET (obj_type, NMP_OBJECT_TYPE_IP4_ADDRESS, NMP_OBJECT_TYPE_IP6_ADDRESS));
+	nmp_lookup_init_addrroute (&lookup,
+	                           obj_type,
+	                           ifindex);
+	return nmp_cache_lookup_to_array (nm_platform_lookup (self, &lookup),
+	                                  obj_type,
+	                                  FALSE /*addresses are always visible. */);
+}
+
+GArray *
+nmtstp_platform_ip4_address_get_all (NMPlatform *self, int ifindex)
+{
+	return _ipx_address_get_all (self, ifindex, NMP_OBJECT_TYPE_IP4_ADDRESS);
+}
+
+GArray *
+nmtstp_platform_ip6_address_get_all (NMPlatform *self, int ifindex)
+{
+	return _ipx_address_get_all (self, ifindex, NMP_OBJECT_TYPE_IP6_ADDRESS);
+}
+
+/*****************************************************************************/
+
+gboolean
+nmtstp_platform_ip4_route_delete (NMPlatform *platform, int ifindex, in_addr_t network, guint8 plen, guint32 metric)
+{
+	NMDedupMultiIter iter;
+
+	nm_platform_process_events (platform);
+
+	nm_dedup_multi_iter_for_each (&iter,
+	                              nm_platform_lookup_addrroute (platform,
+	                                                            NMP_OBJECT_TYPE_IP4_ROUTE,
+	                                                            ifindex)) {
+		const NMPlatformIP4Route *r = NMP_OBJECT_CAST_IP4_ROUTE (iter.current->obj);
+
+		if (   r->ifindex != ifindex
+		    || r->network != network
+		    || r->plen != plen
+		    || r->metric != metric) {
+			continue;
+		}
+
+		return nm_platform_ip_route_delete (platform, NMP_OBJECT_UP_CAST (r));
+	}
+
+	return TRUE;
+}
+
+gboolean
+nmtstp_platform_ip6_route_delete (NMPlatform *platform, int ifindex, struct in6_addr network, guint8 plen, guint32 metric)
+{
+	NMDedupMultiIter iter;
+
+	nm_platform_process_events (platform);
+
+	nm_dedup_multi_iter_for_each (&iter,
+	                              nm_platform_lookup_addrroute (platform,
+	                                                            NMP_OBJECT_TYPE_IP6_ROUTE,
+	                                                            ifindex)) {
+		const NMPlatformIP6Route *r = NMP_OBJECT_CAST_IP6_ROUTE (iter.current->obj);
+
+		if (   r->ifindex != ifindex
+		    || !IN6_ARE_ADDR_EQUAL (&r->network, &network)
+		    || r->plen != plen
+		    || r->metric != metric) {
+			continue;
+		}
+
+		return nm_platform_ip_route_delete (platform, NMP_OBJECT_UP_CAST (r));
+	}
+
+	return TRUE;
+}
+
+/*****************************************************************************/
+
 SignalData *
 add_signal_full (const char *name, NMPlatformSignalChangeType change_type, GCallback callback, int ifindex, const char *ifname)
 {
@@ -153,9 +240,9 @@ link_callback (NMPlatform *platform, int obj_type_i, int ifindex, NMPlatformLink
 {
 	const NMPObjectType obj_type = obj_type_i;
 	const NMPlatformSignalChangeType change_type = change_type_i;
-	GArray *links;
-	NMPlatformLink *cached;
-	int i;
+	NMPLookup lookup;
+	NMDedupMultiIter iter;
+	const NMPlatformLink *cached;
 
 	g_assert_cmpint (obj_type, ==, NMP_OBJECT_TYPE_LINK);
 	g_assert (received);
@@ -185,19 +272,21 @@ link_callback (NMPlatform *platform, int obj_type_i, int ifindex, NMPlatformLink
 
 	/* Check the data */
 	g_assert (received->ifindex > 0);
-	links = nm_platform_link_get_all (NM_PLATFORM_GET, TRUE);
-	for (i = 0; i < links->len; i++) {
-		cached = &g_array_index (links, NMPlatformLink, i);
+
+	nmp_lookup_init_obj_type (&lookup, NMP_OBJECT_TYPE_LINK);
+	nmp_cache_iter_for_each_link (&iter,
+	                              nm_platform_lookup (platform, &lookup),
+	                              &cached) {
+		if (!nmp_object_is_visible (NMP_OBJECT_UP_CAST (cached)))
+			continue;
 		if (cached->ifindex == received->ifindex) {
 			g_assert_cmpint (nm_platform_link_cmp (cached, received), ==, 0);
 			g_assert (!memcmp (cached, received, sizeof (*cached)));
 			if (data->change_type == NM_PLATFORM_SIGNAL_REMOVED)
 				g_error ("Deleted link still found in the local cache.");
-			g_array_unref (links);
 			return;
 		}
 	}
-	g_array_unref (links);
 
 	if (data->change_type != NM_PLATFORM_SIGNAL_REMOVED)
 		g_error ("Added/changed link not found in the local cache.");
@@ -205,113 +294,219 @@ link_callback (NMPlatform *platform, int obj_type_i, int ifindex, NMPlatformLink
 
 /*****************************************************************************/
 
-gboolean
-nmtstp_ip4_route_exists (const char *ifname, guint32 network, int plen, guint32 metric)
+static const NMPlatformIP4Route *
+_ip4_route_get (NMPlatform *platform,
+                int ifindex,
+                guint32 network,
+                int plen,
+                guint32 metric,
+                guint8 tos,
+                guint *out_c_exists)
 {
-	gs_free char *arg_network = NULL;
-	const char *argv[] = {
-		NULL,
-		"route",
-		"list",
-		"dev",
-		ifname,
-		"exact",
-		NULL,
-		NULL,
-	};
-	int exit_status;
-	gs_free char *std_out = NULL, *std_err = NULL;
-	char *out;
-	gboolean success;
-	gs_free_error GError *error = NULL;
-	gs_free char *metric_pattern = NULL;
+	NMDedupMultiIter iter;
+	NMPLookup lookup;
+	const NMPObject *o = NULL;
+	guint c;
+	const NMPlatformIP4Route *r = NULL;
 
-	g_assert (ifname && nm_utils_is_valid_iface_name (ifname, NULL));
-	g_assert (!strstr (ifname, " metric "));
-	g_assert (plen >= 0 && plen <= 32);
+	_init_platform (&platform, FALSE);
 
-	if (!nmtstp_is_root_test ()) {
-		/* If we don't test against linux-platform, we don't actually configure any
-		 * routes in the system. */
-		return -1;
+	nmp_lookup_init_ip4_route_by_weak_id (&lookup,
+	                                      network,
+	                                      plen,
+	                                      metric,
+	                                      tos);
+
+	c = 0;
+	nmp_cache_iter_for_each (&iter,
+	                         nm_platform_lookup (platform, &lookup),
+	                         &o) {
+		if (   NMP_OBJECT_CAST_IP4_ROUTE (o)->ifindex != ifindex
+		    && ifindex > 0)
+			continue;
+		if (!r)
+			r = NMP_OBJECT_CAST_IP4_ROUTE (o);
+		c++;
 	}
 
-	argv[0] = nm_utils_file_search_in_paths ("ip", NULL,
-	                                         (const char *[]) { "/sbin", "/usr/sbin", NULL },
-	                                         G_FILE_TEST_IS_EXECUTABLE, NULL, NULL, NULL);
-	argv[6] = arg_network = g_strdup_printf ("%s/%d", nm_utils_inet4_ntop (network, NULL), plen);
+	NM_SET_OUT (out_c_exists, c);
+	return r;
+}
 
-	if (!argv[0]) {
-		/* Hm. There is no 'ip' binary. Return *unknown* */
-		return -1;
+const NMPlatformIP4Route *
+_nmtstp_assert_ip4_route_exists (const char *file,
+                                 guint line,
+                                 const char *func,
+                                 NMPlatform *platform,
+                                 int c_exists,
+                                 const char *ifname,
+                                 guint32 network,
+                                 int plen,
+                                 guint32 metric,
+                                 guint8 tos)
+{
+	int ifindex;
+	guint c;
+	const NMPlatformIP4Route *r = NULL;
+
+	_init_platform (&platform, FALSE);
+
+	ifindex = -1;
+	if (ifname) {
+		ifindex = nm_platform_link_get_ifindex (platform, ifname);
+		g_assert (ifindex > 0);
 	}
 
-	success = g_spawn_sync (NULL,
-	                        (char **) argv,
-	                        (char *[]) { NULL },
-	                        0,
-	                        NULL,
-	                        NULL,
-	                        &std_out,
-	                        &std_err,
-	                        &exit_status,
-	                        &error);
-	g_assert_no_error (error);
-	g_assert (success);
-	g_assert_cmpstr (std_err, ==, "");
-	g_assert (std_out);
-
-	metric_pattern = g_strdup_printf (" metric %u", metric);
-	out = std_out;
-	while (out) {
-		char *eol = strchr (out, '\n');
-		gs_free char *line = eol ? g_strndup (out, eol - out) : g_strdup (out);
-		const char *p;
-
-		out = eol ? &eol[1] : NULL;
-		if (!line[0])
-			continue;
+	r = _ip4_route_get (platform,
+	                    ifindex,
+	                    network,
+	                    plen,
+	                    metric,
+	                    tos,
+	                    &c);
 
-		if (metric == 0) {
-			if (!strstr (line, " metric "))
-				return TRUE;
-		}
-		p = strstr (line, metric_pattern);
-		if (p && NM_IN_SET (p[strlen (metric_pattern)], ' ', '\0'))
-			return TRUE;
+	if (c != c_exists && c_exists != -1) {
+		g_error ("[%s:%u] %s(): The ip4 route %s/%d metric %u tos %u shall exist %u times, but platform has it %u times",
+		         file, line, func,
+		         nm_utils_inet4_ntop (network, NULL), plen,
+		         metric,
+		         tos,
+		         c_exists,
+		         c);
 	}
-	return FALSE;
+
+	return r;
 }
 
-void
-_nmtstp_assert_ip4_route_exists (const char *file, guint line, const char *func, NMPlatform *platform, gboolean exists, const char *ifname, guint32 network, int plen, guint32 metric)
+const NMPlatformIP4Route *
+nmtstp_ip4_route_get (NMPlatform *platform,
+                      int ifindex,
+                      guint32 network,
+                      int plen,
+                      guint32 metric,
+                      guint8 tos)
+{
+	return _ip4_route_get (platform,
+	                       ifindex,
+	                       network,
+	                       plen,
+	                       metric,
+	                       tos,
+	                       NULL);
+}
+
+/*****************************************************************************/
+
+static const NMPlatformIP6Route *
+_ip6_route_get (NMPlatform *platform,
+                int ifindex,
+                const struct in6_addr *network,
+                guint plen,
+                guint32 metric,
+                const struct in6_addr *src,
+                guint8 src_plen,
+                guint *out_c_exists)
+{
+	NMDedupMultiIter iter;
+	NMPLookup lookup;
+	const NMPObject *o = NULL;
+	guint c;
+	const NMPlatformIP6Route *r = NULL;
+
+	_init_platform (&platform, FALSE);
+
+	nmp_lookup_init_ip6_route_by_weak_id (&lookup,
+	                                      network,
+	                                      plen,
+	                                      metric,
+	                                      src,
+	                                      src_plen);
+
+	c = 0;
+	nmp_cache_iter_for_each (&iter,
+	                         nm_platform_lookup (platform, &lookup),
+	                         &o) {
+		if (   NMP_OBJECT_CAST_IP6_ROUTE (o)->ifindex != ifindex
+		    && ifindex > 0)
+			continue;
+		if (!r)
+			r = NMP_OBJECT_CAST_IP6_ROUTE (o);
+		c++;
+	}
+
+	NM_SET_OUT (out_c_exists, c);
+	return r;
+}
+
+const NMPlatformIP6Route *
+_nmtstp_assert_ip6_route_exists (const char *file,
+                                 guint line,
+                                 const char *func,
+                                 NMPlatform *platform,
+                                 int c_exists,
+                                 const char *ifname,
+                                 const struct in6_addr *network,
+                                 guint plen,
+                                 guint32 metric,
+                                 const struct in6_addr *src,
+                                 guint8 src_plen)
 {
 	int ifindex;
-	gboolean exists_checked;
+	guint c;
+	const NMPlatformIP6Route *r = NULL;
 
 	_init_platform (&platform, FALSE);
 
-	/* Check for existance of the route by spawning iproute2. Do this because platform
-	 * code might be entirely borked, but we expect ip-route to give a correct result.
-	 * If the ip command cannot be found, we accept this as success. */
-	exists_checked = nmtstp_ip4_route_exists (ifname, network, plen, metric);
-	if (exists_checked != -1 && !exists_checked != !exists) {
-		g_error ("[%s:%u] %s(): We expect the ip4 route %s/%d metric %u %s, but it %s",
-		         file, line, func,
-		         nm_utils_inet4_ntop (network, NULL), plen, metric,
-		         exists ? "to exist" : "not to exist",
-		         exists ? "doesn't" : "does");
+	ifindex = -1;
+	if (ifname) {
+		ifindex = nm_platform_link_get_ifindex (platform, ifname);
+		g_assert (ifindex > 0);
 	}
 
-	ifindex = nm_platform_link_get_ifindex (platform, ifname);
-	g_assert (ifindex > 0);
-	if (!nm_platform_ip4_route_get (platform, ifindex, network, plen, metric) != !exists) {
-		g_error ("[%s:%u] %s(): The ip4 route %s/%d metric %u %s, but platform thinks %s",
+	r = _ip6_route_get (platform,
+	                    ifindex,
+	                    network,
+	                    plen,
+	                    metric,
+	                    src,
+	                    src_plen,
+	                    &c);
+
+	if (c != c_exists && c_exists != -1) {
+		char s_src[NM_UTILS_INET_ADDRSTRLEN];
+		char s_network[NM_UTILS_INET_ADDRSTRLEN];
+
+		g_error ("[%s:%u] %s(): The ip6 route %s/%d metric %u src %s/%d shall exist %u times, but platform has it %u times",
 		         file, line, func,
-		         nm_utils_inet4_ntop (network, NULL), plen, metric,
-		         exists ? "exists" : "does not exist",
-		         exists ? "it doesn't" : "it does");
+		         nm_utils_inet6_ntop (network, s_network),
+		         plen,
+		         metric,
+		         nm_utils_inet6_ntop (src, s_src),
+		         src_plen,
+		         c_exists,
+		         c);
 	}
+
+	return r;
+}
+
+const NMPlatformIP6Route *
+nmtstp_ip6_route_get (NMPlatform *platform,
+                      int ifindex,
+                      const struct in6_addr *network,
+                      guint plen,
+                      guint32 metric,
+                      const struct in6_addr *src,
+                      guint8 src_plen)
+{
+	return _ip6_route_get (platform,
+	                       ifindex,
+	                       network,
+	                       plen,
+	                       metric,
+	                       src,
+	                       src_plen,
+	                       NULL);
 }
 
 /*****************************************************************************/
@@ -369,7 +564,7 @@ _wait_for_signal_timeout (gpointer user_data)
 }
 
 guint
-nmtstp_wait_for_signal (NMPlatform *platform, guint timeout_ms)
+nmtstp_wait_for_signal (NMPlatform *platform, gint64 timeout_ms)
 {
 	WaitForSignalData data = { 0 };
 	gulong id_link, id_ip4_address, id_ip6_address, id_ip4_route, id_ip6_route;
@@ -384,8 +579,18 @@ nmtstp_wait_for_signal (NMPlatform *platform, guint timeout_ms)
 	id_ip4_route   = g_signal_connect (platform, NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED, G_CALLBACK (_wait_for_signal_cb), &data);
 	id_ip6_route   = g_signal_connect (platform, NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED, G_CALLBACK (_wait_for_signal_cb), &data);
 
-	if (timeout_ms != 0)
-		data.id = g_timeout_add (timeout_ms, _wait_for_signal_timeout, &data);
+	/* if timeout_ms is negative, it means the wait-time already expired.
+	 * Maybe, we should do nothing and return right away, without even
+	 * processing events from platform. However, that inconsistency (of not
+	 * processing events from mainloop) is inconvenient.
+	 *
+	 * It's better that on the return of nmtstp_wait_for_signal(), we always
+	 * have no events pending. So, a negative timeout is treated the same as
+	 * a zero timeout: we check whether there are any events pending in platform,
+	 * and quite the mainloop immediately afterwards. But we always check. */
+
+	data.id = g_timeout_add (CLAMP (timeout_ms, 0, G_MAXUINT32),
+	                         _wait_for_signal_timeout, &data);
 
 	g_main_loop_run (data.loop);
 
@@ -414,14 +619,14 @@ nmtstp_wait_for_signal_until (NMPlatform *platform, gint64 until_ms)
 		if (until_ms < now)
 			return 0;
 
-		signal_counts = nmtstp_wait_for_signal (platform, MAX (1, until_ms - now));
+		signal_counts = nmtstp_wait_for_signal (platform, until_ms - now);
 		if (signal_counts)
 			return signal_counts;
 	}
 }
 
 const NMPlatformLink *
-nmtstp_wait_for_link (NMPlatform *platform, const char *ifname, NMLinkType expected_link_type, guint timeout_ms)
+nmtstp_wait_for_link (NMPlatform *platform, const char *ifname, NMLinkType expected_link_type, gint64 timeout_ms)
 {
 	return nmtstp_wait_for_link_until (platform, ifname, expected_link_type, nm_utils_get_monotonic_timestamp_ms () + timeout_ms);
 }
@@ -445,7 +650,7 @@ nmtstp_wait_for_link_until (NMPlatform *platform, const char *ifname, NMLinkType
 		if (until_ms < now)
 			return NULL;
 
-		nmtstp_wait_for_signal (platform, MAX (1, until_ms - now));
+		nmtstp_wait_for_signal (platform, until_ms - now);
 	}
 }
 
@@ -717,7 +922,7 @@ _ip_address_add (NMPlatform *platform,
 			g_assert (label == NULL);
 			g_assert (flags == 0);
 
-			a = nm_platform_ip6_address_get (platform, ifindex, address->addr6, plen);
+			a = nm_platform_ip6_address_get (platform, ifindex, address->addr6);
 			if (   a
 			    && !memcmp (nm_platform_ip6_address_get_peer (a),
 			                (IN6_IS_ADDR_UNSPECIFIED (&peer_address->addr6) || IN6_ARE_ADDR_EQUAL (&address->addr6, &peer_address->addr6))
@@ -804,7 +1009,7 @@ void nmtstp_ip4_route_add (NMPlatform *platform,
 	route.metric = metric;
 	route.mss = mss;
 
-	g_assert (nm_platform_ip4_route_add (platform, &route));
+	g_assert_cmpint (nm_platform_ip4_route_add (platform, NMP_NLM_FLAG_REPLACE, &route), ==, NM_PLATFORM_ERROR_SUCCESS);
 }
 
 void nmtstp_ip6_route_add (NMPlatform *platform,
@@ -828,7 +1033,7 @@ void nmtstp_ip6_route_add (NMPlatform *platform,
 	route.metric = metric;
 	route.mss = mss;
 
-	g_assert (nm_platform_ip6_route_add (platform, &route));
+	g_assert_cmpint (nm_platform_ip6_route_add (platform, NMP_NLM_FLAG_REPLACE, &route), ==, NM_PLATFORM_ERROR_SUCCESS);
 }
 
 /*****************************************************************************/
@@ -861,7 +1066,7 @@ _ip_address_del (NMPlatform *platform,
 		if (is_v4)
 			had_address = !!nm_platform_ip4_address_get (platform, ifindex, address->addr4, plen, peer_address->addr4);
 		else
-			had_address = !!nm_platform_ip6_address_get (platform, ifindex, address->addr6, plen);
+			had_address = !!nm_platform_ip6_address_get (platform, ifindex, address->addr6);
 
 		if (is_v4) {
 			success = nmtstp_run_command ("ip address delete %s%s%s/%d dev %s",
@@ -913,7 +1118,7 @@ _ip_address_del (NMPlatform *platform,
 		} else {
 			const NMPlatformIP6Address *a;
 
-			a = nm_platform_ip6_address_get (platform, ifindex, address->addr6, plen);
+			a = nm_platform_ip6_address_get (platform, ifindex, address->addr6);
 			if (!a)
 				break;
 		}
@@ -974,6 +1179,36 @@ nmtstp_ip6_address_del (NMPlatform *platform,
 	} G_STMT_END
 
 const NMPlatformLink *
+nmtstp_link_veth_add (NMPlatform *platform,
+                      gboolean external_command,
+                      const char *name,
+                      const char *peer)
+{
+	const NMPlatformLink *pllink = NULL;
+	gboolean success;
+
+	g_assert (nm_utils_is_valid_iface_name (name, NULL));
+
+	external_command = nmtstp_run_command_check_external (external_command);
+
+	_init_platform (&platform, external_command);
+
+	if (external_command) {
+		success = !nmtstp_run_command ("ip link add dev %s type veth peer name %s",
+		                                name, peer);
+		if (success) {
+			pllink = nmtstp_assert_wait_for_link (platform, name, NM_LINK_TYPE_VETH, 100);
+			nmtstp_assert_wait_for_link (platform, peer, NM_LINK_TYPE_VETH, 10);
+		}
+	} else
+		success = nm_platform_link_veth_add (platform, name, peer, &pllink) == NM_PLATFORM_ERROR_SUCCESS;
+
+	g_assert (success);
+	_assert_pllink (platform, success, pllink, name, NM_LINK_TYPE_VETH);
+	return pllink;
+}
+
+const NMPlatformLink *
 nmtstp_link_dummy_add (NMPlatform *platform,
                        gboolean external_command,
                        const char *name)
diff --git a/src/platform/tests/test-common.h b/src/platform/tests/test-common.h
index a52a5db5..4010aa2f 100644
--- a/src/platform/tests/test-common.h
+++ b/src/platform/tests/test-common.h
@@ -5,6 +5,7 @@
 #include <arpa/inet.h>
 
 #include "platform/nm-platform.h"
+#include "platform/nmp-object.h"
 #include "platform/nm-fake-platform.h"
 #include "platform/nm-linux-platform.h"
 
@@ -93,9 +94,9 @@ int nmtstp_run_command (const char *format, ...) _nm_printf (1, 2);
 
 /*****************************************************************************/
 
-guint nmtstp_wait_for_signal (NMPlatform *platform, guint timeout_ms);
+guint nmtstp_wait_for_signal (NMPlatform *platform, gint64 timeout_ms);
 guint nmtstp_wait_for_signal_until (NMPlatform *platform, gint64 until_ms);
-const NMPlatformLink *nmtstp_wait_for_link (NMPlatform *platform, const char *ifname, NMLinkType expected_link_type, guint timeout_ms);
+const NMPlatformLink *nmtstp_wait_for_link (NMPlatform *platform, const char *ifname, NMLinkType expected_link_type, gint64 timeout_ms);
 const NMPlatformLink *nmtstp_wait_for_link_until (NMPlatform *platform, const char *ifname, NMLinkType expected_link_type, gint64 until_ms);
 
 #define nmtstp_assert_wait_for_signal(platform, timeout_ms) \
@@ -120,10 +121,45 @@ gboolean nmtstp_run_command_check_external (int external_command);
 
 /*****************************************************************************/
 
-gboolean nmtstp_ip4_route_exists (const char *ifname, guint32 network, int plen, guint32 metric);
-
-void _nmtstp_assert_ip4_route_exists (const char *file, guint line, const char *func, NMPlatform *platform, gboolean exists, const char *ifname, guint32 network, int plen, guint32 metric);
-#define nmtstp_assert_ip4_route_exists(platform, exists, ifname, network, plen, metric) _nmtstp_assert_ip4_route_exists (__FILE__, __LINE__, G_STRFUNC, platform, exists, ifname, network, plen, metric)
+const NMPlatformIP4Route *_nmtstp_assert_ip4_route_exists (const char *file,
+                                                           guint line,
+                                                           const char *func,
+                                                           NMPlatform *platform,
+                                                           int c_exists,
+                                                           const char *ifname,
+                                                           guint32 network,
+                                                           int plen,
+                                                           guint32 metric,
+                                                           guint8 tos);
+#define nmtstp_assert_ip4_route_exists(platform, c_exists, ifname, network, plen, metric, tos) _nmtstp_assert_ip4_route_exists (__FILE__, __LINE__, G_STRFUNC, platform, c_exists, ifname, network, plen, metric, tos)
+
+const NMPlatformIP4Route *nmtstp_ip4_route_get (NMPlatform *platform,
+                                                int ifindex,
+                                                guint32 network,
+                                                int plen,
+                                                guint32 metric,
+                                                guint8 tos);
+
+const NMPlatformIP6Route *_nmtstp_assert_ip6_route_exists (const char *file,
+                                                           guint line,
+                                                           const char *func,
+                                                           NMPlatform *platform,
+                                                           int c_exists,
+                                                           const char *ifname,
+                                                           const struct in6_addr *network,
+                                                           guint plen,
+                                                           guint32 metric,
+                                                           const struct in6_addr *src,
+                                                           guint8 src_plen);
+#define nmtstp_assert_ip6_route_exists(platform, c_exists, ifname, network, plen, metric, src, src_plen) _nmtstp_assert_ip6_route_exists (__FILE__, __LINE__, G_STRFUNC, platform, c_exists, ifname, network, plen, metric, src, src_plen)
+
+const NMPlatformIP6Route *nmtstp_ip6_route_get (NMPlatform *platform,
+                                                int ifindex,
+                                                const struct in6_addr *network,
+                                                guint plen,
+                                                guint32 metric,
+                                                const struct in6_addr *src,
+                                                guint8 src_plen);
 
 /*****************************************************************************/
 
@@ -187,8 +223,36 @@ void nmtstp_ip6_route_add (NMPlatform *platform,
                            guint32 metric,
                            guint32 mss);
 
+static inline GPtrArray *
+nmtstp_ip4_route_get_all (NMPlatform *platform,
+                          int ifindex)
+{
+	return nm_platform_lookup_addrroute_clone (platform,
+	                                           NMP_OBJECT_TYPE_IP4_ROUTE,
+	                                           ifindex,
+	                                           nm_platform_lookup_predicate_routes_main_skip_rtprot_kernel,
+	                                           NULL);
+}
+
+static inline GPtrArray *
+nmtstp_ip6_route_get_all (NMPlatform *platform,
+                          int ifindex)
+{
+	return nm_platform_lookup_addrroute_clone (platform,
+	                                           NMP_OBJECT_TYPE_IP6_ROUTE,
+	                                           ifindex,
+	                                           nm_platform_lookup_predicate_routes_main_skip_rtprot_kernel,
+	                                           NULL);
+}
+
 /*****************************************************************************/
 
+GArray *nmtstp_platform_ip4_address_get_all (NMPlatform *self, int ifindex);
+GArray *nmtstp_platform_ip6_address_get_all (NMPlatform *self, int ifindex);
+
+gboolean nmtstp_platform_ip4_route_delete (NMPlatform *platform, int ifindex, in_addr_t network, guint8 plen, guint32 metric);
+gboolean nmtstp_platform_ip6_route_delete (NMPlatform *platform, int ifindex, struct in6_addr network, guint8 plen, guint32 metric);
+
 const NMPlatformLink *nmtstp_link_get_typed (NMPlatform *platform, int ifindex, const char *name, NMLinkType link_type);
 const NMPlatformLink *nmtstp_link_get (NMPlatform *platform, int ifindex, const char *name);
 
@@ -197,6 +261,10 @@ void nmtstp_link_set_updown (NMPlatform *platform,
                              int ifindex,
                              gboolean up);
 
+const NMPlatformLink *nmtstp_link_veth_add (NMPlatform *platform,
+                                            gboolean external_command,
+                                            const char *name,
+                                            const char *peer);
 const NMPlatformLink *nmtstp_link_dummy_add (NMPlatform *platform,
                                              gboolean external_command,
                                              const char *name);
@@ -231,6 +299,100 @@ void nmtstp_link_del (NMPlatform *platform,
                       int ifindex,
                       const char *name);
 
+/*****************************************************************************/
+
+extern int NMTSTP_ENV1_IFINDEX;
+extern int NMTSTP_ENV1_EX;
+
+static inline void
+_nmtstp_env1_wrapper_setup (const NmtstTestData *test_data)
+{
+	int *p_ifindex;
+	gpointer p_ifup;
+
+	nmtst_test_data_unpack (test_data, &p_ifindex, NULL, NULL, NULL, &p_ifup);
+
+	g_assert (p_ifindex && *p_ifindex == -1);
+
+	_LOGT ("TEST[%s]: setup", test_data->testpath);
+
+	nm_platform_link_delete (NM_PLATFORM_GET, nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME));
+	g_assert (!nm_platform_link_get_by_ifname (NM_PLATFORM_GET, DEVICE_NAME));
+	g_assert_cmpint (nm_platform_link_dummy_add (NM_PLATFORM_GET, DEVICE_NAME, NULL), ==, NM_PLATFORM_ERROR_SUCCESS);
+
+	*p_ifindex = nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME);
+	g_assert_cmpint (*p_ifindex, >, 0);
+	g_assert_cmpint (NMTSTP_ENV1_IFINDEX, ==, -1);
+
+	if (GPOINTER_TO_INT (p_ifup))
+		g_assert (nm_platform_link_set_up (NM_PLATFORM_GET, *p_ifindex, NULL));
+
+	nm_platform_process_events (NM_PLATFORM_GET);
+
+	NMTSTP_ENV1_IFINDEX = *p_ifindex;
+	NMTSTP_ENV1_EX = nmtstp_run_command_check_external_global ();
+}
+
+static inline void
+_nmtstp_env1_wrapper_run (gconstpointer user_data)
+{
+	const NmtstTestData *test_data = user_data;
+	GTestDataFunc test_func_data;
+	GTestFunc test_func;
+	gconstpointer d;
+
+	nmtst_test_data_unpack (test_data, NULL, &test_func, &test_func_data, &d, NULL);
+
+	_LOGT ("TEST[%s]: run", test_data->testpath);
+	if (test_func)
+		test_func ();
+	else
+		test_func_data (d);
+}
+
+static inline void
+_nmtstp_env1_wrapper_teardown (const NmtstTestData *test_data)
+{
+	int *p_ifindex;
+
+	nmtst_test_data_unpack (test_data, &p_ifindex, NULL, NULL, NULL, NULL);
+
+	g_assert_cmpint (NMTSTP_ENV1_IFINDEX, ==, *p_ifindex);
+	NMTSTP_ENV1_IFINDEX = -1;
+
+	_LOGT ("TEST[%s]: teardown", test_data->testpath);
+
+	g_assert_cmpint (*p_ifindex, ==, nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME));
+	g_assert (nm_platform_link_delete (NM_PLATFORM_GET, *p_ifindex));
+
+	nm_platform_process_events (NM_PLATFORM_GET);
+
+	_LOGT ("TEST[%s]: finished", test_data->testpath);
+
+	*p_ifindex = -1;
+}
+
+/* add test function, that set's up a particular environment, consisting
+ * of a dummy device with ifindex NMTSTP_ENV1_IFINDEX. */
+#define _nmtstp_env1_add_test_func_full(testpath, test_func, test_data_func, arg, ifup) \
+	nmtst_add_test_func_full (testpath, \
+	                          _nmtstp_env1_wrapper_run, \
+	                          _nmtstp_env1_wrapper_setup, \
+	                          _nmtstp_env1_wrapper_teardown, \
+	                          ({ static int _ifindex = -1; &_ifindex; }), \
+	                          ({ GTestFunc _test_func = (test_func); _test_func; }), \
+	                          ({ GTestDataFunc _test_func = (test_data_func); _test_func; }), \
+	                          (arg), \
+	                          ({ gboolean _ifup = (ifup); GINT_TO_POINTER (_ifup);}))
+
+#define nmtstp_env1_add_test_func_data(testpath, test_func, arg, ifup) \
+	_nmtstp_env1_add_test_func_full(testpath, NULL, test_func, arg, ifup)
+
+#define nmtstp_env1_add_test_func(testpath, test_func, ifup) \
+	_nmtstp_env1_add_test_func_full(testpath, test_func, NULL, NULL, ifup)
+
+/*****************************************************************************/
+
 typedef void (*NMTstpSetupFunc) (void);
 extern NMTstpSetupFunc const _nmtstp_setup_platform_func;
 
diff --git a/src/platform/tests/test-general.c b/src/platform/tests/test-general.c
index e772662c..342aa0d6 100644
--- a/src/platform/tests/test-general.c
+++ b/src/platform/tests/test-general.c
@@ -44,7 +44,7 @@ static void
 test_link_get_all (void)
 {
 	gs_unref_object NMPlatform *platform = NULL;
-	gs_unref_array GArray *links = NULL;
+	gs_unref_ptrarray GPtrArray *links = NULL;
 
 	platform = nm_linux_platform_new (TRUE, NM_PLATFORM_NETNS_SUPPORT_DEFAULT);
 
diff --git a/src/platform/tests/test-link.c b/src/platform/tests/test-link.c
index ed435567..9c72371c 100644
--- a/src/platform/tests/test-link.c
+++ b/src/platform/tests/test-link.c
@@ -76,7 +76,7 @@ test_bogus(void)
 	g_assert (!addrlen);
 	g_assert (!nm_platform_link_get_address (NM_PLATFORM_GET, BOGUS_IFINDEX, NULL));
 
-	g_assert (!nm_platform_link_set_mtu (NM_PLATFORM_GET, BOGUS_IFINDEX, MTU));
+	g_assert (nm_platform_link_set_mtu (NM_PLATFORM_GET, BOGUS_IFINDEX, MTU) != NM_PLATFORM_ERROR_SUCCESS);
 
 	g_assert (!nm_platform_link_get_mtu (NM_PLATFORM_GET, BOGUS_IFINDEX));
 
@@ -264,7 +264,8 @@ test_slave (int master, int type, SignalData *master_changed)
 	}
 	g_assert (!nm_platform_link_is_up (NM_PLATFORM_GET, ifindex));
 	g_assert (!nm_platform_link_is_connected (NM_PLATFORM_GET, ifindex));
-	if (nm_platform_link_is_connected (NM_PLATFORM_GET, master)) {
+	if (   nmtstp_is_root_test ()
+	    && nm_platform_link_is_connected (NM_PLATFORM_GET, master)) {
 		if (nm_platform_link_get_type (NM_PLATFORM_GET, master) == NM_LINK_TYPE_TEAM) {
 			/* Older team versions (e.g. Fedora 17) have a bug that team master stays
 			 * IFF_LOWER_UP even if its slave is down. Double check it with iproute2 and if
@@ -285,7 +286,7 @@ test_slave (int master, int type, SignalData *master_changed)
 	g_assert (nm_platform_link_is_connected (NM_PLATFORM_GET, master));
 	accept_signals (link_changed, 1, 3);
 	/* NM running, can cause additional change of addrgenmode */
-	accept_signals (master_changed, 1, 2);
+	accept_signals (master_changed, 0, 2);
 
 	/* Enslave again
 	 *
@@ -294,7 +295,7 @@ test_slave (int master, int type, SignalData *master_changed)
 	ensure_no_signal (link_changed);
 	g_assert (nm_platform_link_enslave (NM_PLATFORM_GET, master, ifindex));
 	accept_signals (link_changed, 0, 2);
-	ensure_no_signal (master_changed);
+	accept_signals (master_changed, 0, 2);
 
 	/* Set slave option */
 	switch (type) {
@@ -327,7 +328,7 @@ test_slave (int master, int type, SignalData *master_changed)
 		ensure_no_signal (link_changed);
 		accept_signal (link_removed);
 	}
-	accept_signals (master_changed, 1, 2);
+	accept_signals (master_changed, 0, 2);
 
 	ensure_no_signal (master_changed);
 
@@ -511,7 +512,8 @@ test_bridge_addr (void)
 	plink = nm_platform_link_get (NM_PLATFORM_GET, link.ifindex);
 	g_assert (plink);
 
-	if (nm_platform_check_support_user_ipv6ll (NM_PLATFORM_GET)) {
+	if (nm_platform_check_kernel_support (NM_PLATFORM_GET,
+	                                      NM_PLATFORM_KERNEL_SUPPORT_USER_IPV6LL)) {
 		g_assert (!nm_platform_link_get_user_ipv6ll_enabled (NM_PLATFORM_GET, link.ifindex));
 		g_assert_cmpint (_nm_platform_uint8_inv (plink->inet6_addr_gen_mode_inv), ==, NM_IN6_ADDR_GEN_MODE_EUI64);
 
@@ -601,7 +603,7 @@ test_internal (void)
 	accept_signal (link_changed);
 
 	/* Set MTU */
-	g_assert (nm_platform_link_set_mtu (NM_PLATFORM_GET, ifindex, MTU));
+	g_assert (nm_platform_link_set_mtu (NM_PLATFORM_GET, ifindex, MTU) == NM_PLATFORM_ERROR_SUCCESS);
 	g_assert_cmpint (nm_platform_link_get_mtu (NM_PLATFORM_GET, ifindex), ==, MTU);
 	accept_signal (link_changed);
 
@@ -790,7 +792,7 @@ test_software_detect (gconstpointer user_data)
 		 * namespaced, the creation can fail if a macvtap in another namespace
 		 * has the same index. Try to detect this situation and skip already
 		 * used indexes.
-		 * http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=17af2bce88d31e65ed73d638bb752d2e13c66ced
+		 * The fix (17af2bce) is included kernel 4.7, dated 24 July, 2016.
 		 */
 		for (i = ifindex_parent + 1; i < ifindex_parent + 100; i++) {
 			snprintf (buf, sizeof (buf), "/sys/class/macvtap/tap%d", i);
@@ -1713,9 +1715,8 @@ test_nl_bugs_veth (void)
 	NMTstpNamespaceHandle *ns_handle = NULL;
 
 	/* create veth pair. */
-	nmtstp_run_command_check ("ip link add dev %s type veth peer name %s", IFACE_VETH0, IFACE_VETH1);
-	ifindex_veth0 = nmtstp_assert_wait_for_link (NM_PLATFORM_GET, IFACE_VETH0, NM_LINK_TYPE_VETH, 100)->ifindex;
-	ifindex_veth1 = nmtstp_assert_wait_for_link (NM_PLATFORM_GET, IFACE_VETH1, NM_LINK_TYPE_VETH, 100)->ifindex;
+	ifindex_veth0 = nmtstp_link_veth_add (NM_PLATFORM_GET, -1, IFACE_VETH0, IFACE_VETH1)->ifindex;
+	ifindex_veth1 = nmtstp_link_get_typed (NM_PLATFORM_GET, -1, IFACE_VETH1, NM_LINK_TYPE_VETH)->ifindex;
 
 	/* assert that nm_platform_link_veth_get_properties() returns the expected peer ifindexes. */
 	g_assert (nm_platform_link_veth_get_properties (NM_PLATFORM_GET, ifindex_veth0, &i));
@@ -1728,8 +1729,8 @@ test_nl_bugs_veth (void)
 	pllink_veth0 = nm_platform_link_get (NM_PLATFORM_GET, ifindex_veth0);
 	g_assert (pllink_veth0);
 	if (pllink_veth0->parent == 0) {
-		/* pre-4.1 kernels don't support exposing the veth peer as IFA_LINK. skip the remainder
-		 * of the test. */
+		/* Kernels prior to 4.1 dated 21 June, 2015 don't support exposing the veth peer
+		 * as IFA_LINK. skip the remainder of the test. */
 		goto out;
 	}
 	g_assert_cmpint (pllink_veth0->parent, ==, ifindex_veth1);
@@ -2023,8 +2024,8 @@ test_netns_general (gpointer fixture, gconstpointer test_data)
 	_sysctl_assert_eq (platform_1, "/proc/sys/net/ipv6/conf/dummy2b/disable_ipv6", NULL);
 	_sysctl_assert_eq (platform_2, "/proc/sys/net/ipv6/conf/dummy2a/disable_ipv6", NULL);
 
-	/* older kernels (Ubuntu 12.04) don't support ethtool -i for dummy devices. Work around that and
-	 * skip asserts that are known to fail. */
+	/* Kernels prior to 3.19 dated 8 February, 2015 don't support ethtool -i for dummy devices.
+	 * Work around that and skip asserts that are known to fail. */
 	ethtool_support = nmtstp_run_command ("ethtool -i dummy1_ > /dev/null") == 0;
 	if (ethtool_support) {
 		g_assert (nmp_utils_ethtool_get_driver_info (nmtstp_link_get_typed (platform_1, 0, "dummy1_", NM_LINK_TYPE_DUMMY)->ifindex, &driver_info));
diff --git a/src/platform/tests/test-nmp-object.c b/src/platform/tests/test-nmp-object.c
index 42dfc572..a02388d2 100644
--- a/src/platform/tests/test-nmp-object.c
+++ b/src/platform/tests/test-nmp-object.c
@@ -33,6 +33,60 @@ struct {
 
 /*****************************************************************************/
 
+static void
+test_obj_base (void)
+{
+	static const union {
+		GObject g;
+		NMPObject k;
+	} x = { };
+	static const union {
+		GTypeClass k;
+		NMPClass c;
+	} l = { };
+	static const GObject *g = &x.g;
+	static const GTypeClass *k = &l.k;
+	static const NMPObject *o = &x.k;
+	static const NMPClass *c = &l.c;
+
+	NMObjBaseInst *obj;
+	gs_unref_object GCancellable *obj_cancellable = g_cancellable_new ();
+	nm_auto_nmpobj NMPObject *obj_link = nmp_object_new_link (10);
+
+#define STATIC_ASSERT(cond) \
+	G_STMT_START { \
+		G_STATIC_ASSERT (cond); \
+		G_STATIC_ASSERT_EXPR (cond); \
+		g_assert (cond); \
+	} G_STMT_END
+
+	STATIC_ASSERT (&g->g_type_instance              == (void *) &o->_class);
+	STATIC_ASSERT (&g->g_type_instance.g_class      == (void *) &o->_class);
+
+	STATIC_ASSERT (sizeof (o->parent.parent)        == sizeof (GTypeInstance));
+
+	STATIC_ASSERT (&c->parent                       == (void *) c);
+	STATIC_ASSERT (&c->parent.parent.g_type_class   == (void *) c);
+	STATIC_ASSERT (&c->parent.parent.g_type         == (void *) c);
+	STATIC_ASSERT (&c->parent.parent.g_type         == &k->g_type);
+
+	STATIC_ASSERT (sizeof (c->parent.parent)        == sizeof (GTypeClass));
+
+	STATIC_ASSERT (&o->parent                       == (void *) o);
+	STATIC_ASSERT (&o->parent.klass                 == (void *) &o->_class);
+
+	obj = (NMObjBaseInst *) obj_cancellable;
+	g_assert (!NMP_CLASS_IS_VALID ((NMPClass *) obj->klass));
+	g_assert (G_TYPE_CHECK_INSTANCE_TYPE (obj, G_TYPE_CANCELLABLE));
+
+	obj = (NMObjBaseInst *) obj_link;
+	g_assert (NMP_CLASS_IS_VALID ((NMPClass *) obj->klass));
+	g_assert (!G_TYPE_CHECK_INSTANCE_TYPE (obj, G_TYPE_CANCELLABLE));
+
+}
+
+/*****************************************************************************/
+
 static gboolean
 _nmp_object_id_equal (const NMPObject *a, const NMPObject *b)
 {
@@ -56,150 +110,144 @@ _nmp_object_equal (const NMPObject *a, const NMPObject *b)
 /*****************************************************************************/
 
 static void
-_assert_cache_multi_lookup_contains (const NMPCache *cache, const NMPCacheId *cache_id, const NMPObject *obj, gboolean contains)
+_assert_cache_multi_lookup_contains (const NMPCache *cache, const NMDedupMultiHeadEntry *head_entry, const NMPObject *obj, gboolean visible_only, gboolean contains)
 {
-	const NMPlatformObject *const *objects;
-	guint i, len;
+	NMDedupMultiIter iter;
 	gboolean found;
+	guint i, len;
+	const NMPObject *o;
 
-	g_assert (cache_id);
 	g_assert (NMP_OBJECT_IS_VALID (obj));
 
 	g_assert (nmp_cache_lookup_obj (cache, obj) == obj);
+	g_assert (!head_entry || (head_entry->len > 0 && c_list_length (&head_entry->lst_entries_head) == head_entry->len));
 
-	objects = nmp_cache_lookup_multi (cache, cache_id, &len);
-
-	g_assert ((len == 0 && !objects) || (len > 0 && objects && !objects[len]));
+	len = head_entry ? head_entry->len : 0;
 
 	found = FALSE;
-	for (i = 0; i < len; i++) {
-		NMPObject *o;
-
-		g_assert (objects[i]);
-		o = NMP_OBJECT_UP_CAST (objects[i]);
+	i = 0;
+	nmp_cache_iter_for_each (&iter,
+	                         head_entry,
+	                         &o) {
 		g_assert (NMP_OBJECT_IS_VALID (o));
-
 		if (obj == o) {
-			g_assert (!found);
-			found = TRUE;
+			if (   !visible_only
+			    || nmp_object_is_visible (o)) {
+				g_assert (!found);
+				found = TRUE;
+			}
 		}
+		i++;
 	}
 
+	g_assert (len == i);
 	g_assert (!!contains == found);
 }
 
-/*****************************************************************************/
+static void
+_assert_cache_multi_lookup_contains_link (const NMPCache *cache,
+                                          gboolean visible_only,
+                                          const NMPObject *obj,
+                                          gboolean contains)
+{
+	const NMDedupMultiHeadEntry *head_entry;
+	NMPLookup lookup;
 
-typedef struct {
-	NMPCache *cache;
-	NMPCacheOpsType expected_ops_type;
-	const NMPObject *obj_clone;
-	NMPObject *new_clone;
-	gboolean was_visible;
-	gboolean called;
-} _NMPCacheUpdateData;
+	g_assert (cache);
+
+	nmp_lookup_init_obj_type (&lookup, NMP_OBJECT_TYPE_LINK);
+	head_entry = nmp_cache_lookup (cache, &lookup);
+	_assert_cache_multi_lookup_contains (cache, head_entry, obj, visible_only, contains);
+}
+
+/*****************************************************************************/
 
 static void
-_nmp_cache_update_hook (NMPCache *cache, const NMPObject *old, const NMPObject *new, NMPCacheOpsType ops_type, gpointer user_data)
+ops_post_check (NMPCache *cache,
+                NMPCacheOpsType ops_type,
+                const NMPObject *obj_old,
+                const NMPObject *obj_new,
+                const NMPObject *obj_new_expected,
+                NMPCacheOpsType expected_ops_type)
 {
-	_NMPCacheUpdateData *data = user_data;
-
-	g_assert (data);
-	g_assert (!data->called);
-	g_assert (data->cache == cache);
+	g_assert (cache);
 
-	g_assert_cmpint (data->expected_ops_type, ==, ops_type);
+	g_assert_cmpint (expected_ops_type, ==, ops_type);
 
 	switch (ops_type) {
 	case NMP_CACHE_OPS_ADDED:
-		g_assert (!old);
-		g_assert (NMP_OBJECT_IS_VALID (new));
-		g_assert (nmp_object_is_alive (new));
-		g_assert (nmp_object_id_equal (data->obj_clone, new));
-		g_assert (nmp_object_equal (data->obj_clone, new));
+		g_assert (!obj_old);
+		g_assert (NMP_OBJECT_IS_VALID (obj_new));
+		g_assert (nmp_object_is_alive (obj_new));
+		g_assert (nmp_object_id_equal (obj_new_expected, obj_new));
+		g_assert (nmp_object_equal (obj_new_expected, obj_new));
 		break;
 	case NMP_CACHE_OPS_UPDATED:
-		g_assert (NMP_OBJECT_IS_VALID (old));
-		g_assert (NMP_OBJECT_IS_VALID (new));
-		g_assert (nmp_object_is_alive (old));
-		g_assert (nmp_object_is_alive (new));
-		g_assert (nmp_object_id_equal (data->obj_clone, new));
-		g_assert (nmp_object_id_equal (data->obj_clone, old));
-		g_assert (nmp_object_id_equal (old, new));
-		g_assert (nmp_object_equal (data->obj_clone, new));
-		g_assert (!nmp_object_equal (data->obj_clone, old));
-		g_assert (!nmp_object_equal (old, new));
+		g_assert (obj_old != obj_new);
+		g_assert (NMP_OBJECT_IS_VALID (obj_old));
+		g_assert (NMP_OBJECT_IS_VALID (obj_new));
+		g_assert (nmp_object_is_alive (obj_old));
+		g_assert (nmp_object_is_alive (obj_new));
+		g_assert (nmp_object_id_equal (obj_new_expected, obj_new));
+		g_assert (nmp_object_id_equal (obj_new_expected, obj_old));
+		g_assert (nmp_object_id_equal (obj_old, obj_new));
+		g_assert (nmp_object_equal (obj_new_expected, obj_new));
+		g_assert (!nmp_object_equal (obj_new_expected, obj_old));
+		g_assert (!nmp_object_equal (obj_old, obj_new));
 		break;
 	case NMP_CACHE_OPS_REMOVED:
-		g_assert (!new);
-		g_assert (NMP_OBJECT_IS_VALID (old));
-		g_assert (nmp_object_is_alive (old));
-		g_assert (nmp_object_id_equal (data->obj_clone, old));
+		g_assert (!obj_new);
+		g_assert (NMP_OBJECT_IS_VALID (obj_old));
+		g_assert (nmp_object_is_alive (obj_old));
+		if (obj_new_expected)
+			g_assert (nmp_object_id_equal (obj_new_expected, obj_old));
+		break;
+	case NMP_CACHE_OPS_UNCHANGED:
+		g_assert (obj_old == obj_new);
+		if (obj_old) {
+			g_assert (NMP_OBJECT_IS_VALID (obj_old));
+			g_assert (nmp_object_is_alive (obj_old));
+			g_assert (nmp_object_equal (obj_old, obj_new));
+			g_assert (nmp_object_id_equal (obj_new_expected, obj_new));
+		} else
+			g_assert (!obj_new_expected);
 		break;
 	default:
 		g_assert_not_reached ();
 	}
-
-	data->was_visible = old ? nmp_object_is_visible (old) : FALSE;
-	data->new_clone = new ? nmp_object_clone (new, FALSE) : NULL;
-	data->called = TRUE;
 }
 
 static void
-_nmp_cache_update_netlink (NMPCache *cache, NMPObject *obj, NMPObject **out_obj, gboolean *out_was_visible, NMPCacheOpsType expected_ops_type)
+_nmp_cache_update_netlink (NMPCache *cache, NMPObject *obj, const NMPObject **out_obj_old, const NMPObject **out_obj_new, NMPCacheOpsType expected_ops_type)
 {
 	NMPCacheOpsType ops_type;
-	NMPObject *obj2;
-	gboolean was_visible;
-	nm_auto_nmpobj NMPObject *obj_clone = nmp_object_clone (obj, FALSE);
-	nm_auto_nmpobj NMPObject *new_clone = NULL;
+	const NMPObject *obj_prev;
 	const NMPObject *obj_old;
-	_NMPCacheUpdateData data = {
-		.cache = cache,
-		.expected_ops_type = expected_ops_type,
-		.obj_clone = obj_clone,
-	};
-
-	obj_old = nmp_cache_lookup_link (cache, obj->object.ifindex);
-	if (obj_old && obj_old->_link.udev.device)
-		obj_clone->_link.udev.device = udev_device_ref (obj_old->_link.udev.device);
-	_nmp_object_fixup_link_udev_fields (obj_clone, nmp_cache_use_udev_get (cache));
+	const NMPObject *obj_new;
+	nm_auto_nmpobj NMPObject *obj_new_expected = NULL;
 
 	g_assert (cache);
 	g_assert (NMP_OBJECT_IS_VALID (obj));
 
-	ops_type = nmp_cache_update_netlink (cache, obj, &obj2, &was_visible, _nmp_cache_update_hook, &data);
-
-	new_clone = data.new_clone;
-
-	g_assert_cmpint (ops_type, ==, expected_ops_type);
+	obj_prev = nmp_cache_lookup_link (cache, obj->object.ifindex);
+	obj_new_expected = nmp_object_clone (obj, FALSE);
+	if (obj_prev && obj_prev->_link.udev.device)
+		obj_new_expected->_link.udev.device = udev_device_ref (obj_prev->_link.udev.device);
+	_nmp_object_fixup_link_udev_fields (&obj_new_expected, NULL, nmp_cache_use_udev_get (cache));
 
-	if (ops_type != NMP_CACHE_OPS_UNCHANGED) {
-		g_assert (NMP_OBJECT_IS_VALID (obj2));
-		g_assert (data.called);
-		g_assert_cmpint (data.was_visible, ==, was_visible);
+	ops_type = nmp_cache_update_netlink (cache, obj, FALSE, &obj_old, &obj_new);
+	ops_post_check (cache, ops_type, obj_old, obj_new,
+	                nmp_object_is_alive (obj_new_expected) ? obj_new_expected : NULL,
+	                expected_ops_type);
 
-		if (ops_type == NMP_CACHE_OPS_REMOVED)
-			g_assert (!data.new_clone);
-		else {
-			g_assert (data.new_clone);
-			g_assert (nmp_object_equal (obj2, data.new_clone));
-		}
-	} else {
-		g_assert (!data.called);
-		g_assert (!obj2 || was_visible == nmp_object_is_visible (obj2));
-	}
-
-	g_assert (!obj2 || nmp_object_id_equal (obj, obj2));
-	if (ops_type != NMP_CACHE_OPS_REMOVED && obj2)
-		g_assert (nmp_object_equal (obj, obj2));
-
-	if (out_obj)
-		*out_obj = obj2;
+	if (out_obj_new)
+		*out_obj_new = obj_new;
 	else
-		nmp_object_unref (obj2);
-	if (out_was_visible)
-		*out_was_visible = was_visible;
+		nmp_object_unref (obj_new);
+	if (out_obj_old)
+		*out_obj_old = obj_old;
+	else
+		nmp_object_unref (obj_old);
 }
 
 static const NMPlatformLink pl_link_2 = {
@@ -218,168 +266,189 @@ static void
 test_cache_link (void)
 {
 	NMPCache *cache;
-	NMPObject *obj1, *obj2;
+	NMPObject *objm1;
+	const NMPObject *obj_old, *obj_new;
 	NMPObject objs1;
-	gboolean was_visible;
-	NMPCacheId cache_id_storage;
 	struct udev_device *udev_device_2 = g_list_nth_data (global.udev_devices, 0);
 	struct udev_device *udev_device_3 = g_list_nth_data (global.udev_devices, 0);
 	NMPCacheOpsType ops_type;
+	nm_auto_unref_dedup_multi_index NMDedupMultiIndex *multi_idx = NULL;
+
+	multi_idx = nm_dedup_multi_index_new ();
 
-	cache = nmp_cache_new (nmtst_get_rand_int () % 2);
+	cache = nmp_cache_new (multi_idx, nmtst_get_rand_int () % 2);
 
 	/* if we have a link, and don't set is_in_netlink, adding it has no effect. */
-	obj1 = nmp_object_new (NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
-	g_assert (NMP_OBJECT_UP_CAST (&obj1->object) == obj1);
-	g_assert (!nmp_object_is_alive (obj1));
-	_nmp_cache_update_netlink (cache, obj1, &obj2, &was_visible, NMP_CACHE_OPS_UNCHANGED);
+	objm1 = nmp_object_new (NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
+	g_assert (NMP_OBJECT_UP_CAST (&objm1->object) == objm1);
+	g_assert (!nmp_object_is_alive (objm1));
+	_nmp_cache_update_netlink (cache, objm1, &obj_old, &obj_new, NMP_CACHE_OPS_UNCHANGED);
 	ASSERT_nmp_cache_is_consistent (cache);
-	g_assert (!obj2);
-	g_assert (!was_visible);
-	g_assert (!nmp_cache_lookup_obj (cache, obj1));
+	g_assert (!obj_old);
+	g_assert (!obj_new);
+	g_assert (!nmp_cache_lookup_obj (cache, objm1));
 	g_assert (!nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_2.ifindex)));
-	nmp_object_unref (obj1);
+	nmp_object_unref (objm1);
 
 	/* Only when setting @is_in_netlink the link is added. */
-	obj1 = nmp_object_new (NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
-	obj1->_link.netlink.is_in_netlink = TRUE;
-	g_assert (nmp_object_is_alive (obj1));
-	_nmp_cache_update_netlink (cache, obj1, &obj2, &was_visible, NMP_CACHE_OPS_ADDED);
+	objm1 = nmp_object_new (NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
+	objm1->_link.netlink.is_in_netlink = TRUE;
+	g_assert (nmp_object_is_alive (objm1));
+	_nmp_cache_update_netlink (cache, objm1, &obj_old, &obj_new, NMP_CACHE_OPS_ADDED);
 	ASSERT_nmp_cache_is_consistent (cache);
-	g_assert (nmp_object_equal (obj1, obj2));
-	g_assert (!was_visible);
-	g_assert (nmp_cache_lookup_obj (cache, obj1) == obj2);
-	g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_2.ifindex)) == obj2);
-	g_assert (nmp_object_is_visible (obj2));
-	_assert_cache_multi_lookup_contains (cache, nmp_cache_id_init_object_type (&cache_id_storage, NMP_OBJECT_TYPE_LINK, TRUE), obj2, TRUE);
-	_assert_cache_multi_lookup_contains (cache, nmp_cache_id_init_object_type (&cache_id_storage, NMP_OBJECT_TYPE_LINK, FALSE), obj2, TRUE);
-	nmp_object_unref (obj1);
-	nmp_object_unref (obj2);
+	g_assert (!obj_old);
+	g_assert (obj_new);
+	g_assert (objm1 == obj_new);
+	g_assert (nmp_object_equal (objm1, obj_new));
+	g_assert (nmp_cache_lookup_obj (cache, objm1) == obj_new);
+	g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_2.ifindex)) == obj_new);
+	g_assert (nmp_object_is_visible (obj_new));
+	_assert_cache_multi_lookup_contains_link (cache, FALSE, obj_new, TRUE);
+	_assert_cache_multi_lookup_contains_link (cache, TRUE, obj_new, TRUE);
+	nmp_object_unref (objm1);
+	nmp_object_unref (obj_new);
 
 	/* updating the same link with identical value, has no effect. */
-	obj1 = nmp_object_new (NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
-	obj1->_link.netlink.is_in_netlink = TRUE;
-	g_assert (nmp_object_is_alive (obj1));
-	_nmp_cache_update_netlink (cache, obj1, &obj2, &was_visible, NMP_CACHE_OPS_UNCHANGED);
+	objm1 = nmp_object_new (NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
+	objm1->_link.netlink.is_in_netlink = TRUE;
+	g_assert (nmp_object_is_alive (objm1));
+	_nmp_cache_update_netlink (cache, objm1, &obj_old, &obj_new, NMP_CACHE_OPS_UNCHANGED);
 	ASSERT_nmp_cache_is_consistent (cache);
-	g_assert (obj2 != obj1);
-	g_assert (nmp_object_equal (obj1, obj2));
-	g_assert (was_visible);
-	g_assert (nmp_cache_lookup_obj (cache, obj1) == obj2);
-	g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_2.ifindex)) == obj2);
-	nmp_object_unref (obj1);
-	nmp_object_unref (obj2);
+	g_assert (obj_old);
+	g_assert (obj_new);
+	g_assert (obj_new != objm1);
+	g_assert (nmp_object_equal (objm1, obj_new));
+	g_assert (nmp_cache_lookup_obj (cache, objm1) == obj_new);
+	g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_2.ifindex)) == obj_new);
+	nmp_object_unref (objm1);
+	nmp_object_unref (obj_new);
+	nmp_object_unref (obj_new);
 
 	/* remove the link from netlink */
-	obj1 = nmp_object_new (NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
-	g_assert (!nmp_object_is_alive (obj1));
-	_nmp_cache_update_netlink (cache, obj1, &obj2, &was_visible, NMP_CACHE_OPS_REMOVED);
+	objm1 = nmp_object_new (NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
+	g_assert (!nmp_object_is_alive (objm1));
+	_nmp_cache_update_netlink (cache, objm1, &obj_old, &obj_new, NMP_CACHE_OPS_REMOVED);
 	ASSERT_nmp_cache_is_consistent (cache);
-	g_assert (obj2 != obj1);
-	g_assert (was_visible);
-	g_assert (!nmp_cache_lookup_obj (cache, obj1));
+	g_assert (obj_old);
+	g_assert (!obj_new);
+	g_assert (!nmp_cache_lookup_obj (cache, objm1));
 	g_assert (!nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_2.ifindex)));
-	nmp_object_unref (obj1);
-	nmp_object_unref (obj2);
+	nmp_object_unref (objm1);
+	nmp_object_unref (obj_old);
+	nmp_object_unref (obj_new);
 
 	if (udev_device_2) {
 		/* now add the link only with aspect UDEV. */
-		ops_type = nmp_cache_update_link_udev (cache, pl_link_2.ifindex, udev_device_2, &obj2, &was_visible, NULL, NULL);
+		ops_type = nmp_cache_update_link_udev (cache, pl_link_2.ifindex, udev_device_2, &obj_old, &obj_new);
 		ASSERT_nmp_cache_is_consistent (cache);
 		g_assert_cmpint (ops_type, ==, NMP_CACHE_OPS_ADDED);
-		g_assert (!was_visible);
-		g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_2.ifindex)) == obj2);
-		g_assert (!nmp_object_is_visible (obj2));
-		_assert_cache_multi_lookup_contains (cache, nmp_cache_id_init_object_type (&cache_id_storage, NMP_OBJECT_TYPE_LINK, TRUE), obj2, FALSE);
-		_assert_cache_multi_lookup_contains (cache, nmp_cache_id_init_object_type (&cache_id_storage, NMP_OBJECT_TYPE_LINK, FALSE), obj2, TRUE);
-		nmp_object_unref (obj2);
+		g_assert (!obj_old);
+		g_assert (obj_new);
+		g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_2.ifindex)) == obj_new);
+		g_assert (!nmp_object_is_visible (obj_new));
+		_assert_cache_multi_lookup_contains_link (cache, TRUE, obj_new, FALSE);
+		_assert_cache_multi_lookup_contains_link (cache, FALSE, obj_new, TRUE);
+		nmp_object_unref (obj_new);
 	}
 
 	/* add it in netlink too. */
-	obj1 = nmp_object_new (NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
-	obj1->_link.netlink.is_in_netlink = TRUE;
-	g_assert (nmp_object_is_alive (obj1));
-	_nmp_cache_update_netlink (cache, obj1, &obj2, &was_visible, udev_device_2 ? NMP_CACHE_OPS_UPDATED : NMP_CACHE_OPS_ADDED);
+	objm1 = nmp_object_new (NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
+	objm1->_link.netlink.is_in_netlink = TRUE;
+	g_assert (nmp_object_is_alive (objm1));
+	_nmp_cache_update_netlink (cache, objm1, &obj_old, &obj_new, udev_device_2 ? NMP_CACHE_OPS_UPDATED : NMP_CACHE_OPS_ADDED);
 	ASSERT_nmp_cache_is_consistent (cache);
-	g_assert (nmp_object_equal (obj1, obj2));
-	g_assert (!was_visible);
-	g_assert (nmp_cache_lookup_obj (cache, obj1) == obj2);
-	g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_2.ifindex)) == obj2);
-	g_assert (nmp_object_is_visible (obj2));
-	_assert_cache_multi_lookup_contains (cache, nmp_cache_id_init_object_type (&cache_id_storage, NMP_OBJECT_TYPE_LINK, TRUE), obj2, TRUE);
-	_assert_cache_multi_lookup_contains (cache, nmp_cache_id_init_object_type (&cache_id_storage, NMP_OBJECT_TYPE_LINK, FALSE), obj2, TRUE);
-	nmp_object_unref (obj1);
-	nmp_object_unref (obj2);
+	if (udev_device_2) {
+		g_assert (obj_old);
+		g_assert (!nmp_object_is_visible (obj_old));
+	} else
+		g_assert (!obj_old);
+	g_assert (nmp_object_equal (objm1, obj_new));
+	g_assert (nmp_cache_lookup_obj (cache, objm1) == obj_new);
+	g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_2.ifindex)) == obj_new);
+	g_assert (nmp_object_is_visible (obj_new));
+	_assert_cache_multi_lookup_contains_link (cache, TRUE, obj_new, TRUE);
+	_assert_cache_multi_lookup_contains_link (cache, FALSE, obj_new, TRUE);
+	nmp_object_unref (objm1);
+	nmp_object_unref (obj_old);
+	nmp_object_unref (obj_new);
 
 	/* remove again from netlink. */
-	obj1 = nmp_object_new (NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
-	obj1->_link.netlink.is_in_netlink = FALSE;
-	g_assert (!nmp_object_is_alive (obj1));
-	_nmp_cache_update_netlink (cache, obj1, &obj2, &was_visible, udev_device_2 ? NMP_CACHE_OPS_UPDATED : NMP_CACHE_OPS_REMOVED);
+	objm1 = nmp_object_new (NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
+	objm1->_link.netlink.is_in_netlink = FALSE;
+	g_assert (!nmp_object_is_alive (objm1));
+	_nmp_cache_update_netlink (cache, objm1, &obj_old, &obj_new, udev_device_2 ? NMP_CACHE_OPS_UPDATED : NMP_CACHE_OPS_REMOVED);
 	ASSERT_nmp_cache_is_consistent (cache);
-	g_assert (obj2 != obj1);
-	g_assert (was_visible);
+	if (udev_device_2)
+		g_assert (obj_new == objm1);
+	else
+		g_assert (!obj_new);
+	g_assert (obj_old);
+	g_assert (nmp_object_is_alive (obj_old));
 	if (udev_device_2) {
-		g_assert (nmp_cache_lookup_obj (cache, obj1) == obj2);
-		g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_2.ifindex)) == obj2);
-		g_assert (!nmp_object_is_visible (obj2));
-		_assert_cache_multi_lookup_contains (cache, nmp_cache_id_init_object_type (&cache_id_storage, NMP_OBJECT_TYPE_LINK, TRUE), obj2, FALSE);
-		_assert_cache_multi_lookup_contains (cache, nmp_cache_id_init_object_type (&cache_id_storage, NMP_OBJECT_TYPE_LINK, FALSE), obj2, TRUE);
+		g_assert (nmp_cache_lookup_obj (cache, objm1) == obj_new);
+		g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_2.ifindex)) == obj_new);
+		g_assert (!nmp_object_is_visible (obj_new));
+		_assert_cache_multi_lookup_contains_link (cache, TRUE, obj_new, FALSE);
+		_assert_cache_multi_lookup_contains_link (cache, FALSE, obj_new, TRUE);
 	} else {
-		g_assert (nmp_cache_lookup_obj (cache, obj1) == NULL);
+		g_assert (nmp_cache_lookup_obj (cache, objm1) == NULL);
 		g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_2.ifindex)) == NULL);
-		g_assert (nmp_object_is_visible (obj2));
+		g_assert (nmp_object_is_visible (obj_new));
 	}
-	nmp_object_unref (obj1);
-	nmp_object_unref (obj2);
+	nmp_object_unref (objm1);
+	nmp_object_unref (obj_old);
+	nmp_object_unref (obj_new);
 
 	/* now another link only with aspect UDEV. */
 	if (udev_device_3) {
 		/* now add the link only with aspect UDEV. */
-		ops_type = nmp_cache_update_link_udev (cache, pl_link_3.ifindex, udev_device_3, &obj2, &was_visible, NULL, NULL);
+		ops_type = nmp_cache_update_link_udev (cache, pl_link_3.ifindex, udev_device_3, &obj_old, &obj_new);
 		g_assert_cmpint (ops_type, ==, NMP_CACHE_OPS_ADDED);
 		ASSERT_nmp_cache_is_consistent (cache);
-		g_assert (NMP_OBJECT_IS_VALID (obj2));
-		g_assert (!was_visible);
-		g_assert (!nmp_object_is_visible (obj2));
-		g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_3.ifindex)) == obj2);
-		_assert_cache_multi_lookup_contains (cache, nmp_cache_id_init_object_type (&cache_id_storage, NMP_OBJECT_TYPE_LINK, TRUE), obj2, FALSE);
-		_assert_cache_multi_lookup_contains (cache, nmp_cache_id_init_object_type (&cache_id_storage, NMP_OBJECT_TYPE_LINK, FALSE), obj2, TRUE);
-		g_assert_cmpint (obj2->_link.netlink.is_in_netlink, ==, FALSE);
-		g_assert_cmpint (obj2->link.initialized, ==, FALSE);
-		nmp_object_unref (obj2);
+		g_assert (NMP_OBJECT_IS_VALID (obj_new));
+		g_assert (!obj_old);
+		g_assert (!nmp_object_is_visible (obj_new));
+		g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_3.ifindex)) == obj_new);
+		_assert_cache_multi_lookup_contains_link (cache, TRUE, obj_new, FALSE);
+		_assert_cache_multi_lookup_contains_link (cache, FALSE, obj_new, TRUE);
+		g_assert_cmpint (obj_new->_link.netlink.is_in_netlink, ==, FALSE);
+		g_assert_cmpint (obj_new->link.initialized, ==, FALSE);
+		nmp_object_unref (obj_new);
 
 		/* add it in netlink too. */
-		obj1 = nmp_object_new (NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_3);
-		obj1->_link.netlink.is_in_netlink = TRUE;
-		g_assert (nmp_object_is_alive (obj1));
-		_nmp_cache_update_netlink (cache, obj1, &obj2, &was_visible, NMP_CACHE_OPS_UPDATED);
+		objm1 = nmp_object_new (NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_3);
+		objm1->_link.netlink.is_in_netlink = TRUE;
+		g_assert (nmp_object_is_alive (objm1));
+		_nmp_cache_update_netlink (cache, objm1, &obj_old, &obj_new, NMP_CACHE_OPS_UPDATED);
 		ASSERT_nmp_cache_is_consistent (cache);
-		g_assert (obj2 != obj1);
-		g_assert (nmp_object_equal (obj1, obj2));
-		g_assert (!was_visible);
-		g_assert (nmp_cache_lookup_obj (cache, obj1) == obj2);
-		g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_3.ifindex)) == obj2);
-		g_assert (nmp_object_is_visible (obj2));
-		_assert_cache_multi_lookup_contains (cache, nmp_cache_id_init_object_type (&cache_id_storage, NMP_OBJECT_TYPE_LINK, TRUE), obj2, TRUE);
-		_assert_cache_multi_lookup_contains (cache, nmp_cache_id_init_object_type (&cache_id_storage, NMP_OBJECT_TYPE_LINK, FALSE), obj2, TRUE);
-		g_assert_cmpint (obj2->_link.netlink.is_in_netlink, ==, TRUE);
-		g_assert_cmpint (obj2->link.initialized, ==, TRUE);
-		nmp_object_unref (obj1);
-		nmp_object_unref (obj2);
+		g_assert (obj_old);
+		g_assert (obj_new == objm1);
+		g_assert (nmp_object_equal (objm1, obj_new));
+		g_assert (!obj_old || !nmp_object_is_visible (obj_old));
+		g_assert (nmp_cache_lookup_obj (cache, objm1) == obj_new);
+		g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_3.ifindex)) == obj_new);
+		g_assert (nmp_object_is_visible (obj_new));
+		_assert_cache_multi_lookup_contains_link (cache, TRUE, obj_new, TRUE);
+		_assert_cache_multi_lookup_contains_link (cache, FALSE, obj_new, TRUE);
+		g_assert_cmpint (obj_new->_link.netlink.is_in_netlink, ==, TRUE);
+		g_assert_cmpint (obj_new->link.initialized, ==, TRUE);
+		nmp_object_unref (objm1);
+		nmp_object_unref (obj_old);
+		nmp_object_unref (obj_new);
 
 		/* remove UDEV. */
-		ops_type = nmp_cache_update_link_udev (cache, pl_link_3.ifindex, NULL, &obj2, &was_visible, NULL, NULL);
+		ops_type = nmp_cache_update_link_udev (cache, pl_link_3.ifindex, NULL, &obj_old, &obj_new);
 		g_assert_cmpint (ops_type, ==, NMP_CACHE_OPS_UPDATED);
 		ASSERT_nmp_cache_is_consistent (cache);
-		g_assert (was_visible);
-		g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_3.ifindex)) == obj2);
-		g_assert (nmp_object_is_visible (obj2));
-		_assert_cache_multi_lookup_contains (cache, nmp_cache_id_init_object_type (&cache_id_storage, NMP_OBJECT_TYPE_LINK, TRUE), obj2, TRUE);
-		_assert_cache_multi_lookup_contains (cache, nmp_cache_id_init_object_type (&cache_id_storage, NMP_OBJECT_TYPE_LINK, FALSE), obj2, TRUE);
-		g_assert_cmpint (obj2->_link.netlink.is_in_netlink, ==, TRUE);
-		g_assert_cmpint (obj2->link.initialized, ==, !nmp_cache_use_udev_get (cache));
-		nmp_object_unref (obj2);
+		g_assert (obj_old && nmp_object_is_visible (obj_old));
+		g_assert (nmp_cache_lookup_obj (cache, nmp_object_stackinit_id_link (&objs1, pl_link_3.ifindex)) == obj_new);
+		g_assert (nmp_object_is_visible (obj_new));
+		_assert_cache_multi_lookup_contains_link (cache, TRUE, obj_new, TRUE);
+		_assert_cache_multi_lookup_contains_link (cache, FALSE, obj_new, TRUE);
+		g_assert_cmpint (obj_new->_link.netlink.is_in_netlink, ==, TRUE);
+		g_assert_cmpint (obj_new->link.initialized, ==, !nmp_cache_use_udev_get (cache));
+		nmp_object_unref (obj_new);
+		nmp_object_unref (obj_old);
 	}
 
 	nmp_cache_free (cache);
@@ -429,6 +498,7 @@ main (int argc, char **argv)
 		udev_enumerate_unref (enumerator);
 	}
 
+	g_test_add_func ("/nmp-object/obj-base", test_obj_base);
 	g_test_add_func ("/nmp-object/cache_link", test_cache_link);
 
 	result = g_test_run ();
diff --git a/src/platform/tests/test-route.c b/src/platform/tests/test-route.c
index 9960d867..2c00fada 100644
--- a/src/platform/tests/test-route.c
+++ b/src/platform/tests/test-route.c
@@ -27,13 +27,50 @@
 
 #include "test-common.h"
 
-#define DEVICE_NAME "nm-test-device"
+#define DEVICE_IFINDEX NMTSTP_ENV1_IFINDEX
+#define EX             NMTSTP_ENV1_EX
+
+static void
+_wait_for_ipv6_addr_non_tentative (NMPlatform *platform,
+                                   gint64 timeout_ms,
+                                   int ifindex,
+                                   guint addr_n,
+                                   const struct in6_addr *addrs)
+{
+	guint i;
+
+	/* Wait that the addresses become non-tentative.  Dummy interfaces are NOARP
+	 * and thus don't do DAD, but the kernel sets the address as tentative for a
+	 * small amount of time, which prevents the immediate addition of the route
+	 * with RTA_PREFSRC */
+
+	NMTST_WAIT_ASSERT (400, {
+		gboolean should_wait = FALSE;
+		const NMPlatformIP6Address *plt_addr;
+
+		for (i = 0; i < addr_n; i++) {
+			plt_addr = nm_platform_ip6_address_get (platform, ifindex, addrs[i]);
+			if (   !plt_addr
+			    || NM_FLAGS_HAS (plt_addr->n_ifa_flags, IFA_F_TENTATIVE)) {
+				should_wait = TRUE;
+				break;
+			}
+		}
+		if (!should_wait)
+			return;
+		nmtstp_assert_wait_for_signal (platform,
+		                               (nmtst_wait_end_us - g_get_monotonic_time ()) / 1000);
+	});
+}
+
 
 static void
 ip4_route_callback (NMPlatform *platform, int obj_type_i, int ifindex, const NMPlatformIP4Route *received, int change_type_i, SignalData *data)
 {
 	const NMPObjectType obj_type = obj_type_i;
 	const NMPlatformSignalChangeType change_type = change_type_i;
+	NMPObject o_id;
+	nm_auto_nmpobj NMPObject *o_id_p = nmp_object_new (NMP_OBJECT_TYPE_IP4_ROUTE, NULL);
 
 	g_assert_cmpint (obj_type, ==, NMP_OBJECT_TYPE_IP4_ROUTE);
 	g_assert (received);
@@ -41,6 +78,11 @@ ip4_route_callback (NMPlatform *platform, int obj_type_i, int ifindex, const NMP
 	g_assert (data && data->name);
 	g_assert_cmpstr (data->name, ==, NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED);
 
+	/* run code for initializing the ID only */
+	nmp_object_stackinit_id (&o_id, NMP_OBJECT_UP_CAST (received));
+	nmp_object_copy (o_id_p, NMP_OBJECT_UP_CAST (received), TRUE);
+	nmp_object_copy (o_id_p, NMP_OBJECT_UP_CAST (received), FALSE);
+
 	if (data->ifindex && data->ifindex != received->ifindex)
 		return;
 	if (data->change_type != change_type)
@@ -58,6 +100,8 @@ ip6_route_callback (NMPlatform *platform, int obj_type_i, int ifindex, const NMP
 {
 	const NMPObjectType obj_type = obj_type_i;
 	const NMPlatformSignalChangeType change_type = change_type_i;
+	NMPObject o_id;
+	nm_auto_nmpobj NMPObject *o_id_p = nmp_object_new (NMP_OBJECT_TYPE_IP6_ROUTE, NULL);
 
 	g_assert_cmpint (obj_type, ==, NMP_OBJECT_TYPE_IP6_ROUTE);
 	g_assert (received);
@@ -65,6 +109,11 @@ ip6_route_callback (NMPlatform *platform, int obj_type_i, int ifindex, const NMP
 	g_assert (data && data->name);
 	g_assert_cmpstr (data->name, ==, NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED);
 
+	/* run code for initializing the ID only */
+	nmp_object_stackinit_id (&o_id, NMP_OBJECT_UP_CAST (received));
+	nmp_object_copy (o_id_p, NMP_OBJECT_UP_CAST (received), TRUE);
+	nmp_object_copy (o_id_p, NMP_OBJECT_UP_CAST (received), FALSE);
+
 	if (data->ifindex && data->ifindex != received->ifindex)
 		return;
 	if (data->change_type != change_type)
@@ -90,50 +139,50 @@ test_ip4_route_metric0 (void)
 	int mss = 1000;
 
 	/* No routes initially */
-	nmtstp_assert_ip4_route_exists (NULL, FALSE, DEVICE_NAME, network, plen, 0);
-	nmtstp_assert_ip4_route_exists (NULL, FALSE, DEVICE_NAME, network, plen, metric);
+	nmtstp_assert_ip4_route_exists (NULL, 0, DEVICE_NAME, network, plen, 0, 0);
+	nmtstp_assert_ip4_route_exists (NULL, 0, DEVICE_NAME, network, plen, metric, 0);
 
 	/* add the first route */
 	nmtstp_ip4_route_add (NM_PLATFORM_GET, ifindex, NM_IP_CONFIG_SOURCE_USER, network, plen, INADDR_ANY, 0, metric, mss);
 	accept_signal (route_added);
 
-	nmtstp_assert_ip4_route_exists (NULL, FALSE, DEVICE_NAME, network, plen, 0);
-	nmtstp_assert_ip4_route_exists (NULL, TRUE,  DEVICE_NAME, network, plen, metric);
+	nmtstp_assert_ip4_route_exists (NULL, 0, DEVICE_NAME, network, plen, 0, 0);
+	nmtstp_assert_ip4_route_exists (NULL, 1,  DEVICE_NAME, network, plen, metric, 0);
 
 	/* Deleting route with metric 0 does nothing */
-	g_assert (nm_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, network, plen, 0));
+	g_assert (nmtstp_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, network, plen, 0));
 	ensure_no_signal (route_removed);
 
-	nmtstp_assert_ip4_route_exists (NULL, FALSE, DEVICE_NAME, network, plen, 0);
-	nmtstp_assert_ip4_route_exists (NULL, TRUE,  DEVICE_NAME, network, plen, metric);
+	nmtstp_assert_ip4_route_exists (NULL, 0, DEVICE_NAME, network, plen, 0, 0);
+	nmtstp_assert_ip4_route_exists (NULL, 1,  DEVICE_NAME, network, plen, metric, 0);
 
 	/* add the second route */
 	nmtstp_ip4_route_add (NM_PLATFORM_GET, ifindex, NM_IP_CONFIG_SOURCE_USER, network, plen, INADDR_ANY, 0, 0, mss);
 	accept_signal (route_added);
 
-	nmtstp_assert_ip4_route_exists (NULL, TRUE,  DEVICE_NAME, network, plen, 0);
-	nmtstp_assert_ip4_route_exists (NULL, TRUE,  DEVICE_NAME, network, plen, metric);
+	nmtstp_assert_ip4_route_exists (NULL, 1,  DEVICE_NAME, network, plen, 0, 0);
+	nmtstp_assert_ip4_route_exists (NULL, 1,  DEVICE_NAME, network, plen, metric, 0);
 
 	/* Delete route with metric 0 */
-	g_assert (nm_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, network, plen, 0));
+	g_assert (nmtstp_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, network, plen, 0));
 	accept_signal (route_removed);
 
-	nmtstp_assert_ip4_route_exists (NULL, FALSE, DEVICE_NAME, network, plen, 0);
-	nmtstp_assert_ip4_route_exists (NULL, TRUE,  DEVICE_NAME, network, plen, metric);
+	nmtstp_assert_ip4_route_exists (NULL, 0, DEVICE_NAME, network, plen, 0, 0);
+	nmtstp_assert_ip4_route_exists (NULL, 1,  DEVICE_NAME, network, plen, metric, 0);
 
 	/* Delete route with metric 0 again (we expect nothing to happen) */
-	g_assert (nm_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, network, plen, 0));
+	g_assert (nmtstp_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, network, plen, 0));
 	ensure_no_signal (route_removed);
 
-	nmtstp_assert_ip4_route_exists (NULL, FALSE, DEVICE_NAME, network, plen, 0);
-	nmtstp_assert_ip4_route_exists (NULL, TRUE,  DEVICE_NAME, network, plen, metric);
+	nmtstp_assert_ip4_route_exists (NULL, 0, DEVICE_NAME, network, plen, 0, 0);
+	nmtstp_assert_ip4_route_exists (NULL, 1,  DEVICE_NAME, network, plen, metric, 0);
 
 	/* Delete the other route */
-	g_assert (nm_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, network, plen, metric));
+	g_assert (nmtstp_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, network, plen, metric));
 	accept_signal (route_removed);
 
-	nmtstp_assert_ip4_route_exists (NULL, FALSE, DEVICE_NAME, network, plen, 0);
-	nmtstp_assert_ip4_route_exists (NULL, FALSE, DEVICE_NAME, network, plen, metric);
+	nmtstp_assert_ip4_route_exists (NULL, 0, DEVICE_NAME, network, plen, 0, 0);
+	nmtstp_assert_ip4_route_exists (NULL, 0, DEVICE_NAME, network, plen, metric, 0);
 
 	free_signal (route_added);
 	free_signal (route_changed);
@@ -147,7 +196,7 @@ test_ip4_route (void)
 	SignalData *route_added = add_signal (NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED, NM_PLATFORM_SIGNAL_ADDED, ip4_route_callback);
 	SignalData *route_changed = add_signal (NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED, NM_PLATFORM_SIGNAL_CHANGED, ip4_route_callback);
 	SignalData *route_removed = add_signal (NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED, NM_PLATFORM_SIGNAL_REMOVED, ip4_route_callback);
-	GArray *routes;
+	GPtrArray *routes;
 	NMPlatformIP4Route rts[3];
 	in_addr_t network;
 	guint8 plen = 24;
@@ -164,9 +213,9 @@ test_ip4_route (void)
 	accept_signal (route_added);
 
 	/* Add route */
-	nmtstp_assert_ip4_route_exists (NULL, FALSE, DEVICE_NAME, network, plen, metric);
+	nmtstp_assert_ip4_route_exists (NULL, 0, DEVICE_NAME, network, plen, metric, 0);
 	nmtstp_ip4_route_add (NM_PLATFORM_GET, ifindex, NM_IP_CONFIG_SOURCE_USER, network, plen, gateway, 0, metric, mss);
-	nmtstp_assert_ip4_route_exists (NULL, TRUE, DEVICE_NAME, network, plen, metric);
+	nmtstp_assert_ip4_route_exists (NULL, 1, DEVICE_NAME, network, plen, metric, 0);
 	accept_signal (route_added);
 
 	/* Add route again */
@@ -174,9 +223,9 @@ test_ip4_route (void)
 	accept_signals (route_changed, 0, 1);
 
 	/* Add default route */
-	nmtstp_assert_ip4_route_exists (NULL, FALSE, DEVICE_NAME, 0, 0, metric);
+	nmtstp_assert_ip4_route_exists (NULL, 0, DEVICE_NAME, 0, 0, metric, 0);
 	nmtstp_ip4_route_add (NM_PLATFORM_GET, ifindex, NM_IP_CONFIG_SOURCE_USER, 0, 0, gateway, 0, metric, mss);
-	nmtstp_assert_ip4_route_exists (NULL, TRUE, DEVICE_NAME, 0, 0, metric);
+	nmtstp_assert_ip4_route_exists (NULL, 1, DEVICE_NAME, 0, 0, metric, 0);
 	accept_signal (route_added);
 
 	/* Add default route again */
@@ -184,7 +233,7 @@ test_ip4_route (void)
 	accept_signals (route_changed, 0, 1);
 
 	/* Test route listing */
-	routes = nm_platform_ip4_route_get_all (NM_PLATFORM_GET, ifindex, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT | NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT);
+	routes = nmtstp_ip4_route_get_all (NM_PLATFORM_GET, ifindex);
 	memset (rts, 0, sizeof (rts));
 	rts[0].rt_source = nmp_utils_ip_config_source_round_trip_rtprot (NM_IP_CONFIG_SOURCE_USER);
 	rts[0].network = gateway;
@@ -211,23 +260,23 @@ test_ip4_route (void)
 	rts[2].mss = mss;
 	rts[2].scope_inv = nm_platform_route_scope_inv (RT_SCOPE_UNIVERSE);
 	g_assert_cmpint (routes->len, ==, 3);
-	nmtst_platform_ip4_routes_equal ((NMPlatformIP4Route *) routes->data, rts, routes->len, TRUE);
-	g_array_unref (routes);
+	nmtst_platform_ip4_routes_equal_aptr ((const NMPObject *const*) routes->pdata, rts, routes->len, TRUE);
+	g_ptr_array_unref (routes);
 
 	/* Remove route */
-	g_assert (nm_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, network, plen, metric));
-	nmtstp_assert_ip4_route_exists (NULL, FALSE, DEVICE_NAME, network, plen, metric);
+	g_assert (nmtstp_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, network, plen, metric));
+	nmtstp_assert_ip4_route_exists (NULL, 0, DEVICE_NAME, network, plen, metric, 0);
 	accept_signal (route_removed);
 
 	/* Remove route again */
-	g_assert (nm_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, network, plen, metric));
+	g_assert (nmtstp_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, network, plen, metric));
 
 	/* Remove default route */
-	g_assert (nm_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, 0, 0, metric));
+	g_assert (nmtstp_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, 0, 0, metric));
 	accept_signal (route_removed);
 
 	/* Remove route to gateway */
-	g_assert (nm_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, gateway, 32, metric));
+	g_assert (nmtstp_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, gateway, 32, metric));
 	accept_signal (route_removed);
 
 	free_signal (route_added);
@@ -242,7 +291,7 @@ test_ip6_route (void)
 	SignalData *route_added = add_signal (NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED, NM_PLATFORM_SIGNAL_ADDED, ip6_route_callback);
 	SignalData *route_changed = add_signal (NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED, NM_PLATFORM_SIGNAL_CHANGED, ip6_route_callback);
 	SignalData *route_removed = add_signal (NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED, NM_PLATFORM_SIGNAL_REMOVED, ip6_route_callback);
-	GArray *routes;
+	GPtrArray *routes;
 	NMPlatformIP6Route rts[3];
 	struct in6_addr network;
 	guint8 plen = 64;
@@ -259,28 +308,16 @@ test_ip6_route (void)
 	                                       NM_PLATFORM_LIFETIME_PERMANENT, NM_PLATFORM_LIFETIME_PERMANENT, 0));
 	accept_signals (route_added, 0, 1);
 
-	/* Wait that the address becomes non-tentative.  Dummy interfaces are NOARP
-	 * and thus don't do DAD, but the kernel sets the address as tentative for a
-	 * small amount of time, which prevents the immediate addition of the route
-	 * with RTA_PREFSRC */
-	NMTST_WAIT_ASSERT (200, {
-		const NMPlatformIP6Address *plt_addr;
-
-		nmtstp_wait_for_signal (NM_PLATFORM_GET, 50);
-		nm_platform_process_events (NM_PLATFORM_GET);
-		plt_addr = nm_platform_ip6_address_get (NM_PLATFORM_GET, ifindex, pref_src, 128);
-		if (plt_addr && !NM_FLAGS_HAS (plt_addr->n_ifa_flags, IFA_F_TENTATIVE))
-			break;
-	});
+	_wait_for_ipv6_addr_non_tentative (NM_PLATFORM_GET, 200, ifindex, 1, &pref_src);
 
 	/* Add route to gateway */
 	nmtstp_ip6_route_add (NM_PLATFORM_GET, ifindex, NM_IP_CONFIG_SOURCE_USER, gateway, 128, in6addr_any, in6addr_any, metric, mss);
 	accept_signal (route_added);
 
 	/* Add route */
-	g_assert (!nm_platform_ip6_route_get (NM_PLATFORM_GET, ifindex, network, plen, metric));
+	g_assert (!nmtstp_ip6_route_get (NM_PLATFORM_GET, ifindex, &network, plen, metric, NULL, 0));
 	nmtstp_ip6_route_add (NM_PLATFORM_GET, ifindex, NM_IP_CONFIG_SOURCE_USER, network, plen, gateway, pref_src, metric, mss);
-	g_assert (nm_platform_ip6_route_get (NM_PLATFORM_GET, ifindex, network, plen, metric));
+	g_assert (nmtstp_ip6_route_get (NM_PLATFORM_GET, ifindex, &network, plen, metric, NULL, 0));
 	accept_signal (route_added);
 
 	/* Add route again */
@@ -288,9 +325,9 @@ test_ip6_route (void)
 	accept_signals (route_changed, 0, 1);
 
 	/* Add default route */
-	g_assert (!nm_platform_ip6_route_get (NM_PLATFORM_GET, ifindex, in6addr_any, 0, metric));
+	g_assert (!nmtstp_ip6_route_get (NM_PLATFORM_GET, ifindex, &in6addr_any, 0, metric, NULL, 0));
 	nmtstp_ip6_route_add (NM_PLATFORM_GET, ifindex, NM_IP_CONFIG_SOURCE_USER, in6addr_any, 0, gateway, in6addr_any, metric, mss);
-	g_assert (nm_platform_ip6_route_get (NM_PLATFORM_GET, ifindex, in6addr_any, 0, metric));
+	g_assert (nmtstp_ip6_route_get (NM_PLATFORM_GET, ifindex, &in6addr_any, 0, metric, NULL, 0));
 	accept_signal (route_added);
 
 	/* Add default route again */
@@ -298,7 +335,7 @@ test_ip6_route (void)
 	accept_signals (route_changed, 0, 1);
 
 	/* Test route listing */
-	routes = nm_platform_ip6_route_get_all (NM_PLATFORM_GET, ifindex, NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT | NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT);
+	routes = nmtstp_ip6_route_get_all (NM_PLATFORM_GET, ifindex);
 	memset (rts, 0, sizeof (rts));
 	rts[0].rt_source = nmp_utils_ip_config_source_round_trip_rtprot (NM_IP_CONFIG_SOURCE_USER);
 	rts[0].network = gateway;
@@ -325,23 +362,23 @@ test_ip6_route (void)
 	rts[2].metric = nm_utils_ip6_route_metric_normalize (metric);
 	rts[2].mss = mss;
 	g_assert_cmpint (routes->len, ==, 3);
-	nmtst_platform_ip6_routes_equal ((NMPlatformIP6Route *) routes->data, rts, routes->len, TRUE);
-	g_array_unref (routes);
+	nmtst_platform_ip6_routes_equal_aptr ((const NMPObject *const*) routes->pdata, rts, routes->len, TRUE);
+	g_ptr_array_unref (routes);
 
 	/* Remove route */
-	g_assert (nm_platform_ip6_route_delete (NM_PLATFORM_GET, ifindex, network, plen, metric));
-	g_assert (!nm_platform_ip6_route_get (NM_PLATFORM_GET, ifindex, network, plen, metric));
+	g_assert (nmtstp_platform_ip6_route_delete (NM_PLATFORM_GET, ifindex, network, plen, metric));
+	g_assert (!nmtstp_ip6_route_get (NM_PLATFORM_GET, ifindex, &network, plen, metric, NULL, 0));
 	accept_signal (route_removed);
 
 	/* Remove route again */
-	g_assert (nm_platform_ip6_route_delete (NM_PLATFORM_GET, ifindex, network, plen, metric));
+	g_assert (nmtstp_platform_ip6_route_delete (NM_PLATFORM_GET, ifindex, network, plen, metric));
 
 	/* Remove default route */
-	g_assert (nm_platform_ip6_route_delete (NM_PLATFORM_GET, ifindex, in6addr_any, 0, metric));
+	g_assert (nmtstp_platform_ip6_route_delete (NM_PLATFORM_GET, ifindex, in6addr_any, 0, metric));
 	accept_signal (route_removed);
 
 	/* Remove route to gateway */
-	g_assert (nm_platform_ip6_route_delete (NM_PLATFORM_GET, ifindex, gateway, 128, metric));
+	g_assert (nmtstp_platform_ip6_route_delete (NM_PLATFORM_GET, ifindex, gateway, 128, metric));
 	accept_signal (route_removed);
 
 	free_signal (route_added);
@@ -352,6 +389,45 @@ test_ip6_route (void)
 /*****************************************************************************/
 
 static void
+test_ip_route_get (void)
+{
+	int ifindex = nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME);
+	in_addr_t a;
+	NMPlatformError result;
+	nm_auto_nmpobj NMPObject *route = NULL;
+	const NMPlatformIP4Route *r;
+
+	nmtstp_run_command_check ("ip route add 1.2.3.0/24 dev %s", DEVICE_NAME);
+
+	NMTST_WAIT_ASSERT (100, {
+		nmtstp_wait_for_signal (NM_PLATFORM_GET, 10);
+		if (nmtstp_ip4_route_get (NM_PLATFORM_GET, ifindex, nmtst_inet4_from_string ("1.2.3.0"), 24, 0, 0))
+			break;
+	});
+
+	a = nmtst_inet4_from_string ("1.2.3.1");
+	result = nm_platform_ip_route_get (NM_PLATFORM_GET,
+	                                   AF_INET,
+	                                   &a,
+	                                   nmtst_get_rand_int () % 2 ? 0 : ifindex,
+	                                   &route);
+
+	g_assert (result == NM_PLATFORM_ERROR_SUCCESS);
+	g_assert (NMP_OBJECT_GET_TYPE (route) == NMP_OBJECT_TYPE_IP4_ROUTE);
+	g_assert (!NMP_OBJECT_IS_STACKINIT (route));
+	g_assert (route->parent._ref_count == 1);
+	r = NMP_OBJECT_CAST_IP4_ROUTE (route);
+	g_assert (r->rt_cloned);
+	g_assert (r->ifindex == ifindex);
+	g_assert (r->network == a);
+	g_assert (r->plen == 32);
+
+	nmtstp_run_command_check ("ip route flush dev %s", DEVICE_NAME);
+
+	nmtstp_wait_for_signal (NM_PLATFORM_GET, 50);
+}
+
+static void
 test_ip4_zero_gateway (void)
 {
 	int ifindex = nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME);
@@ -361,15 +437,14 @@ test_ip4_zero_gateway (void)
 
 	NMTST_WAIT_ASSERT (100, {
 		nmtstp_wait_for_signal (NM_PLATFORM_GET, 10);
-		if (   nm_platform_ip4_route_get (NM_PLATFORM_GET, ifindex, nmtst_inet4_from_string ("1.2.3.1"), 32, 0)
-		    && nm_platform_ip4_route_get (NM_PLATFORM_GET, ifindex, nmtst_inet4_from_string ("1.2.3.2"), 32, 0))
+		if (   nmtstp_ip4_route_get (NM_PLATFORM_GET, ifindex, nmtst_inet4_from_string ("1.2.3.1"), 32, 0, 0)
+		    && nmtstp_ip4_route_get (NM_PLATFORM_GET, ifindex, nmtst_inet4_from_string ("1.2.3.2"), 32, 0, 0))
 			break;
 	});
 
 	nmtstp_run_command_check ("ip route flush dev %s", DEVICE_NAME);
 
 	nmtstp_wait_for_signal (NM_PLATFORM_GET, 50);
-	nm_platform_process_events (NM_PLATFORM_GET);
 }
 
 static void
@@ -378,7 +453,7 @@ test_ip4_route_options (void)
 	int ifindex = nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME);
 	NMPlatformIP4Route route = { };
 	in_addr_t network;
-	GArray *routes;
+	GPtrArray *routes;
 	NMPlatformIP4Route rts[1];
 
 	inet_pton (AF_INET, "172.16.1.0", &network);
@@ -396,12 +471,10 @@ test_ip4_route_options (void)
 	route.mtu = 1350;
 	route.lock_cwnd = TRUE;
 
-	g_assert (nm_platform_ip4_route_add (NM_PLATFORM_GET, &route));
+	g_assert (nm_platform_ip4_route_add (NM_PLATFORM_GET, NMP_NLM_FLAG_REPLACE, &route) == NM_PLATFORM_ERROR_SUCCESS);
 
 	/* Test route listing */
-	routes = nm_platform_ip4_route_get_all (NM_PLATFORM_GET, ifindex,
-	                                        NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT |
-	                                        NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT);
+	routes = nmtstp_ip4_route_get_all (NM_PLATFORM_GET, ifindex);
 	memset (rts, 0, sizeof (rts));
 	rts[0].rt_source = nmp_utils_ip_config_source_round_trip_rtprot (NM_IP_CONFIG_SOURCE_USER);
 	rts[0].scope_inv = nm_platform_route_scope_inv (RT_SCOPE_LINK);
@@ -416,69 +489,268 @@ test_ip4_route_options (void)
 	rts[0].initrwnd = 50;
 	rts[0].mtu = 1350;
 	rts[0].lock_cwnd = TRUE;
-
 	g_assert_cmpint (routes->len, ==, 1);
-	nmtst_platform_ip4_routes_equal ((NMPlatformIP4Route *) routes->data, rts, routes->len, TRUE);
+	nmtst_platform_ip4_routes_equal_aptr ((const NMPObject *const*) routes->pdata, rts, routes->len, TRUE);
 
 	/* Remove route */
-	/* FIXME. Due to a bug, we cannot delete routes with non-zero TOS. See bgo#785004. */
-	//g_assert (nm_platform_ip4_route_delete (NM_PLATFORM_GET, ifindex, network, 24, 20));
+	g_assert (nm_platform_ip_route_delete (NM_PLATFORM_GET, routes->pdata[0]));
 
-	g_array_unref (routes);
+	g_ptr_array_unref (routes);
 }
 
 
 static void
-test_ip6_route_options (void)
+test_ip6_route_options (gconstpointer test_data)
 {
-	int ifindex = nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME);
-	NMPlatformIP6Route route = { };
-	struct in6_addr network;
-	GArray *routes;
-	NMPlatformIP6Route rts[3];
-
-	inet_pton (AF_INET6, "2001:db8:a:b:0:0:0:0", &network);
-
-	route.ifindex = ifindex;
-	route.rt_source = NM_IP_CONFIG_SOURCE_USER;
-	route.network = network;
-	route.plen = 64;
-	route.gateway = in6addr_any;
-	route.metric = 1024;
-	route.window = 20000;
-	route.cwnd = 8;
-	route.initcwnd = 22;
-	route.initrwnd = 33;
-	route.mtu = 1300;
-	route.lock_mtu = TRUE;
-
-	g_assert (nm_platform_ip6_route_add (NM_PLATFORM_GET, &route));
-
-	/* Test route listing */
-	routes = nm_platform_ip6_route_get_all (NM_PLATFORM_GET, ifindex,
-	                                        NM_PLATFORM_GET_ROUTE_FLAGS_WITH_DEFAULT |
-	                                        NM_PLATFORM_GET_ROUTE_FLAGS_WITH_NON_DEFAULT);
-	memset (rts, 0, sizeof (rts));
-	rts[0].rt_source = nmp_utils_ip_config_source_round_trip_rtprot (NM_IP_CONFIG_SOURCE_USER);
-	rts[0].network = network;
-	rts[0].plen = 64;
-	rts[0].ifindex = ifindex;
-	rts[0].gateway = in6addr_any;
-	rts[0].metric = 1024;
-	rts[0].window = 20000;
-	rts[0].cwnd = 8;
-	rts[0].initcwnd = 22;
-	rts[0].initrwnd = 33;
-	rts[0].mtu = 1300;
-	rts[0].lock_mtu = TRUE;
-
-	g_assert_cmpint (routes->len, ==, 1);
-	nmtst_platform_ip6_routes_equal ((NMPlatformIP6Route *) routes->data, rts, routes->len, TRUE);
+	const int TEST_IDX = GPOINTER_TO_INT (test_data);
+	const int IFINDEX = nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME);
+	GPtrArray *routes;
+#define RTS_MAX 3
+	NMPlatformIP6Route rts_add[RTS_MAX] = { };
+	NMPlatformIP6Route rts_cmp[RTS_MAX] = { };
+	NMPlatformIP6Address addr[1] = { };
+	struct in6_addr addr_in6[G_N_ELEMENTS (addr)] = { };
+	guint rts_n = 0;
+	guint addr_n = 0;
+	guint i;
+
+	switch (TEST_IDX) {
+	case 1:
+		rts_add[rts_n++] = ((NMPlatformIP6Route) {
+			.ifindex = IFINDEX,
+			.rt_source = NM_IP_CONFIG_SOURCE_USER,
+			.network = *nmtst_inet6_from_string ("2001:db8:a:b:0:0:0:0"),
+			.plen = 64,
+			.gateway = in6addr_any,
+			.metric = 1024,
+			.window = 20000,
+			.cwnd = 8,
+			.initcwnd = 22,
+			.initrwnd = 33,
+			.mtu = 1300,
+			.lock_mtu = TRUE,
+		});
+		break;
+	case 2:
+		addr[addr_n++] = ((NMPlatformIP6Address) {
+			.ifindex = IFINDEX,
+			.address = *nmtst_inet6_from_string ("2000::2"),
+			.plen = 128,
+			.peer_address = in6addr_any,
+			.lifetime = NM_PLATFORM_LIFETIME_PERMANENT,
+			.preferred = NM_PLATFORM_LIFETIME_PERMANENT,
+			.n_ifa_flags = 0,
+		});
+		rts_add[rts_n++] = ((NMPlatformIP6Route) {
+			.ifindex = IFINDEX,
+			.rt_source = NM_IP_CONFIG_SOURCE_USER,
+			.network = *nmtst_inet6_from_string ("1010::1"),
+			.plen = 128,
+			.gateway = in6addr_any,
+			.metric = 256,
+			.pref_src = *nmtst_inet6_from_string ("2000::2"),
+		});
+		break;
+	case 3:
+		addr[addr_n++] = ((NMPlatformIP6Address) {
+			.ifindex = IFINDEX,
+			.address = *nmtst_inet6_from_string ("2001:db8:8086::5"),
+			.plen = 128,
+			.peer_address = in6addr_any,
+			.lifetime = NM_PLATFORM_LIFETIME_PERMANENT,
+			.preferred = NM_PLATFORM_LIFETIME_PERMANENT,
+			.n_ifa_flags = 0,
+		});
+		rts_add[rts_n++] = ((NMPlatformIP6Route) {
+			.ifindex = IFINDEX,
+			.rt_source = nmp_utils_ip_config_source_round_trip_rtprot (NM_IP_CONFIG_SOURCE_USER),
+			.network = *nmtst_inet6_from_string ("2001:db8:8086::"),
+			.plen = 110,
+			.metric = 10021,
+			.mss = 0,
+		});
+		rts_add[rts_n++] = ((NMPlatformIP6Route) {
+			.ifindex = IFINDEX,
+			.rt_source = nmp_utils_ip_config_source_round_trip_rtprot (NM_IP_CONFIG_SOURCE_USER),
+			.network = *nmtst_inet6_from_string ("2001:db8:abad:c0de::"),
+			.plen = 64,
+			.gateway = *nmtst_inet6_from_string ("2001:db8:8086::1"),
+			.metric = 21,
+			.mss = 0,
+		});
+		break;
+	default:
+		g_assert_not_reached ();
+	}
+
+	for (i = 0; i < addr_n; i++) {
+		g_assert (addr[i].ifindex == IFINDEX);
+		addr_in6[i] = addr[i].address;
+		g_assert (nm_platform_ip6_address_add (NM_PLATFORM_GET,
+		                                       IFINDEX,
+		                                       addr[i].address,
+		                                       addr[i].plen,
+		                                       addr[i].peer_address,
+		                                       addr[i].lifetime,
+		                                       addr[i].preferred,
+		                                       addr[i].n_ifa_flags));
+	}
+
+	_wait_for_ipv6_addr_non_tentative (NM_PLATFORM_GET, 400, IFINDEX, addr_n, addr_in6);
+
+	for (i = 0; i < rts_n; i++)
+		g_assert (nm_platform_ip6_route_add (NM_PLATFORM_GET, NMP_NLM_FLAG_REPLACE, &rts_add[i]) == NM_PLATFORM_ERROR_SUCCESS);
+
+	routes = nmtstp_ip6_route_get_all (NM_PLATFORM_GET, IFINDEX);
+	switch (TEST_IDX) {
+	case 1:
+	case 2:
+	case 3:
+		for (i = 0; i < rts_n; i++) {
+			rts_cmp[i] = rts_add[i];
+			rts_cmp[i].rt_source = nmp_utils_ip_config_source_round_trip_rtprot (NM_IP_CONFIG_SOURCE_USER);
+		}
+		break;
+	default:
+		g_assert_not_reached ();
+	}
+	g_assert_cmpint (routes->len, ==, rts_n);
+	nmtst_platform_ip6_routes_equal_aptr ((const NMPObject *const*) routes->pdata, rts_cmp, routes->len, TRUE);
+	g_ptr_array_unref (routes);
+
+	for (i = 0; i < rts_n; i++) {
+		g_assert (nmtstp_platform_ip6_route_delete (NM_PLATFORM_GET, IFINDEX,
+		                                            rts_add[i].network, rts_add[i].plen,
+		                                            rts_add[i].metric));
+	}
+
+	for (i = 0; i < addr_n; i++) {
+		nmtstp_ip6_address_del (NM_PLATFORM_GET,
+		                        EX,
+		                        IFINDEX,
+		                        rts_add[i].network,
+		                        rts_add[i].plen);
+	}
+}
 
-	/* Remove route */
-	g_assert (nm_platform_ip6_route_delete (NM_PLATFORM_GET, ifindex, network, 64, 1024));
+/*****************************************************************************/
 
-	g_array_unref (routes);
+static void
+test_ip (gconstpointer test_data)
+{
+	const int TEST_IDX = GPOINTER_TO_INT (test_data);
+	const int IFINDEX = nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME);
+	guint i, j, k;
+	const NMPlatformLink *l;
+	char ifname[IFNAMSIZ];
+	char ifname2[IFNAMSIZ];
+	char s1[NM_UTILS_INET_ADDRSTRLEN];
+	NMPlatform *platform = NM_PLATFORM_GET;
+	const int EX_ = -1;
+	struct {
+		int ifindex;
+	} iface_data[10] = { 0 };
+	int order_idx[G_N_ELEMENTS (iface_data)] = { 0 };
+	guint order_len;
+	guint try;
+
+	for (i = 0; i < G_N_ELEMENTS (iface_data); i++) {
+		nm_sprintf_buf (ifname, "v%02u", i);
+		nm_sprintf_buf (ifname2, "w%02u", i);
+
+		g_assert (!nm_platform_link_get_by_ifname (platform, ifname));
+		g_assert (!nm_platform_link_get_by_ifname (platform, ifname2));
+		l = nmtstp_link_veth_add (platform, EX_, ifname, ifname2);
+		iface_data[i].ifindex = l->ifindex;
+
+		nmtstp_link_set_updown (platform, EX_, iface_data[i].ifindex, TRUE);
+		nmtstp_link_set_updown (platform, EX_, nmtstp_link_get (platform, -1, ifname2)->ifindex, TRUE);
+
+		nm_sprintf_buf (s1, "192.168.7.%d", 100 + i);
+		nmtstp_ip4_address_add (platform,
+		                        EX_,
+		                        iface_data[i].ifindex,
+		                        nmtst_inet4_from_string (s1),
+		                        24,
+		                        nmtst_inet4_from_string (s1),
+		                        3600,
+		                        3600,
+		                        0,
+		                        NULL);
+	}
+
+	order_len = 0;
+	for (try = 0; try < 5 * G_N_ELEMENTS (order_idx); try++) {
+		NMPObject o;
+		NMPlatformIP4Route *r;
+		guint idx;
+		const NMDedupMultiHeadEntry *head_entry;
+		NMPLookup lookup;
+
+		nmp_object_stackinit (&o, NMP_OBJECT_TYPE_IP4_ROUTE, NULL);
+		r = NMP_OBJECT_CAST_IP4_ROUTE (&o);
+		r->network = nmtst_inet4_from_string ("192.168.9.0");
+		r->plen = 24;
+		r->metric = 109;
+
+		if (   order_len == 0
+		    || (   order_len < G_N_ELEMENTS (order_idx)
+		        && nmtst_get_rand_int () % 2)) {
+again_find_idx:
+			idx = nmtst_get_rand_int () % G_N_ELEMENTS (iface_data);
+			for (i = 0; i < order_len; i++) {
+				if (order_idx[i] == idx)
+					goto again_find_idx;
+			}
+			order_idx[order_len++] = idx;
+
+			r->ifindex = iface_data[idx].ifindex;
+			g_assert (nm_platform_ip4_route_add (platform, NMP_NLM_FLAG_APPEND, r) == NM_PLATFORM_ERROR_SUCCESS);
+		} else {
+			i = nmtst_get_rand_int () % order_len;
+			idx = order_idx[i];
+			for (i++; i < order_len; i++)
+				order_idx[i - 1] = order_idx[i];
+			order_len--;
+
+			r->ifindex = iface_data[idx].ifindex;
+			g_assert (nm_platform_ip_route_delete (platform, &o));
+		}
+
+		head_entry = nm_platform_lookup (platform,
+		                                 nmp_lookup_init_obj_type (&lookup, NMP_OBJECT_TYPE_IP4_ROUTE));
+		for (j = 0; j < G_N_ELEMENTS (iface_data); j++) {
+			gboolean has;
+			NMDedupMultiIter iter;
+			const NMPObject *o_cached;
+
+			has = FALSE;
+			for (k = 0; k < order_len; k++) {
+				if (order_idx[k] == j) {
+					g_assert (!has);
+					has = TRUE;
+				}
+			}
+
+			nmp_cache_iter_for_each (&iter, head_entry, &o_cached) {
+				const NMPlatformIP4Route *r_cached = NMP_OBJECT_CAST_IP4_ROUTE (o_cached);
+
+				if (   r_cached->ifindex != iface_data[j].ifindex
+				    || r_cached->metric != 109)
+					continue;
+
+				g_assert (has);
+				has = FALSE;
+			}
+			g_assert (!has);
+		}
+	}
+
+	for (i = 0; i < G_N_ELEMENTS (iface_data); i++)
+		g_assert (nm_platform_link_delete (platform, iface_data[i].ifindex));
+
+	(void) TEST_IDX;
+	(void) IFINDEX;
 }
 
 /*****************************************************************************/
@@ -494,22 +766,19 @@ _nmtstp_init_tests (int *argc, char ***argv)
 void
 _nmtstp_setup_tests (void)
 {
-	SignalData *link_added = add_signal_ifname (NM_PLATFORM_SIGNAL_LINK_CHANGED, NM_PLATFORM_SIGNAL_ADDED, link_callback, DEVICE_NAME);
-
-	nm_platform_link_delete (NM_PLATFORM_GET, nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME));
-	g_assert (!nm_platform_link_get_by_ifname (NM_PLATFORM_GET, DEVICE_NAME));
-	g_assert (nm_platform_link_dummy_add (NM_PLATFORM_GET, DEVICE_NAME, NULL) == NM_PLATFORM_ERROR_SUCCESS);
-	accept_signal (link_added);
-	free_signal (link_added);
-
-	g_assert (nm_platform_link_set_up (NM_PLATFORM_GET, nm_platform_link_get_ifindex (NM_PLATFORM_GET, DEVICE_NAME), NULL));
-
-	g_test_add_func ("/route/ip4", test_ip4_route);
-	g_test_add_func ("/route/ip6", test_ip6_route);
-	g_test_add_func ("/route/ip4_metric0", test_ip4_route_metric0);
-	g_test_add_func ("/route/ip4_options", test_ip4_route_options);
-	g_test_add_func ("/route/ip6_options", test_ip6_route_options);
-
-	if (nmtstp_is_root_test ())
-		g_test_add_func ("/route/ip4_zero_gateway", test_ip4_zero_gateway);
+#define add_test_func(testpath, test_func) nmtstp_env1_add_test_func(testpath, test_func, TRUE)
+#define add_test_func_data(testpath, test_func, arg) nmtstp_env1_add_test_func_data(testpath, test_func, arg, TRUE)
+	add_test_func ("/route/ip4", test_ip4_route);
+	add_test_func ("/route/ip6", test_ip6_route);
+	add_test_func ("/route/ip4_metric0", test_ip4_route_metric0);
+	add_test_func ("/route/ip4_options", test_ip4_route_options);
+	add_test_func_data ("/route/ip6_options/1", test_ip6_route_options, GINT_TO_POINTER (1));
+	add_test_func_data ("/route/ip6_options/2", test_ip6_route_options, GINT_TO_POINTER (2));
+	add_test_func_data ("/route/ip6_options/3", test_ip6_route_options, GINT_TO_POINTER (3));
+
+	if (nmtstp_is_root_test ()) {
+		add_test_func_data ("/route/ip/1", test_ip, GINT_TO_POINTER (1));
+		add_test_func ("/route/ip_route_get", test_ip_route_get);
+		add_test_func ("/route/ip4_zero_gateway", test_ip4_zero_gateway);
+	}
 }
diff --git a/src/platform/wifi/wifi-utils-nl80211.c b/src/platform/wifi/wifi-utils-nl80211.c
index 06eb7cb9..a5f25b02 100644
--- a/src/platform/wifi/wifi-utils-nl80211.c
+++ b/src/platform/wifi/wifi-utils-nl80211.c
@@ -46,6 +46,24 @@
 		        _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
 	} G_STMT_END
 
+/*****************************************************************************/
+
+static int
+_nl_nla_parse (struct nlattr *tb[], int maxtype, struct nlattr *head, int len,
+               const struct nla_policy *policy)
+{
+	return nla_parse (tb, maxtype, head, len, (struct nla_policy *) policy);
+}
+#define nla_parse(...) _nl_nla_parse(__VA_ARGS__)
+
+static int
+_nl_nla_parse_nested (struct nlattr *tb[], int maxtype, struct nlattr *nla,
+                      const struct nla_policy *policy)
+{
+	return nla_parse_nested (tb, maxtype, nla, (struct nla_policy *) policy);
+}
+#define nla_parse_nested(...) _nl_nla_parse_nested(__VA_ARGS__)
+
 /*****************************************************************************
  * Copied from libnl3/genl:
  *****************************************************************************/
@@ -131,7 +149,7 @@ genlmsg_valid_hdr (struct nlmsghdr *nlh, int hdrlen)
 
 static int
 genlmsg_parse (struct nlmsghdr *nlh, int hdrlen, struct nlattr *tb[],
-               int maxtype, struct nla_policy *policy)
+               int maxtype, const struct nla_policy *policy)
 {
 	struct genlmsghdr *ghdr;
 
@@ -150,7 +168,7 @@ genlmsg_parse (struct nlmsghdr *nlh, int hdrlen, struct nlattr *tb[],
 static int
 probe_response (struct nl_msg *msg, void *arg)
 {
-	static struct nla_policy ctrl_policy[CTRL_ATTR_MAX+1] = {
+	static const struct nla_policy ctrl_policy[CTRL_ATTR_MAX+1] = {
 		[CTRL_ATTR_FAMILY_ID]    = { .type = NLA_U16 },
 		[CTRL_ATTR_FAMILY_NAME]  = { .type = NLA_STRING,
 		                            .maxlen = GENL_NAMSIZ },
@@ -389,7 +407,7 @@ nl80211_iface_info_handler (struct nl_msg *msg, void *arg)
 	struct nlattr *tb[NL80211_ATTR_MAX + 1];
 
 	if (nla_parse (tb, NL80211_ATTR_MAX, genlmsg_attrdata (gnlh, 0),
-		       genlmsg_attrlen (gnlh, 0), NULL) < 0)
+	               genlmsg_attrlen (gnlh, 0), NULL) < 0)
 		return NL_SKIP;
 
 	if (!tb[NL80211_ATTR_IFTYPE])
@@ -529,7 +547,7 @@ nl80211_bss_dump_handler (struct nl_msg *msg, void *arg)
 	struct genlmsghdr *gnlh = nlmsg_data (nlmsg_hdr (msg));
 	struct nlattr *tb[NL80211_ATTR_MAX + 1];
 	struct nlattr *bss[NL80211_BSS_MAX + 1];
-	static struct nla_policy bss_policy[NL80211_BSS_MAX + 1] = {
+	static const struct nla_policy bss_policy[NL80211_BSS_MAX + 1] = {
 		[NL80211_BSS_TSF] = { .type = NLA_U64 },
 		[NL80211_BSS_FREQUENCY] = { .type = NLA_U32 },
 		[NL80211_BSS_BSSID] = { },
@@ -543,15 +561,15 @@ nl80211_bss_dump_handler (struct nl_msg *msg, void *arg)
 	guint32 status;
 
 	if (nla_parse (tb, NL80211_ATTR_MAX, genlmsg_attrdata (gnlh, 0),
-		       genlmsg_attrlen (gnlh, 0), NULL) < 0)
+	               genlmsg_attrlen (gnlh, 0), NULL) < 0)
 		return NL_SKIP;
 
 	if (tb[NL80211_ATTR_BSS] == NULL)
 		return NL_SKIP;
 
 	if (nla_parse_nested (bss, NL80211_BSS_MAX,
-			      tb[NL80211_ATTR_BSS],
-			      bss_policy))
+	                      tb[NL80211_ATTR_BSS],
+	                      bss_policy))
 		return NL_SKIP;
 
 	if (bss[NL80211_BSS_STATUS] == NULL)
@@ -665,7 +683,7 @@ nl80211_station_handler (struct nl_msg *msg, void *arg)
 	struct genlmsghdr *gnlh = nlmsg_data (nlmsg_hdr (msg));
 	struct nlattr *sinfo[NL80211_STA_INFO_MAX + 1];
 	struct nlattr *rinfo[NL80211_RATE_INFO_MAX + 1];
-	static struct nla_policy stats_policy[NL80211_STA_INFO_MAX + 1] = {
+	static const struct nla_policy stats_policy[NL80211_STA_INFO_MAX + 1] = {
 		[NL80211_STA_INFO_INACTIVE_TIME] = { .type = NLA_U32 },
 		[NL80211_STA_INFO_RX_BYTES] = { .type = NLA_U32 },
 		[NL80211_STA_INFO_TX_BYTES] = { .type = NLA_U32 },
@@ -678,7 +696,7 @@ nl80211_station_handler (struct nl_msg *msg, void *arg)
 		[NL80211_STA_INFO_PLINK_STATE] = { .type = NLA_U8 },
 	};
 
-	static struct nla_policy rate_policy[NL80211_RATE_INFO_MAX + 1] = {
+	static const struct nla_policy rate_policy[NL80211_RATE_INFO_MAX + 1] = {
 		[NL80211_RATE_INFO_BITRATE] = { .type = NLA_U16 },
 		[NL80211_RATE_INFO_MCS] = { .type = NLA_U8 },
 		[NL80211_RATE_INFO_40_MHZ_WIDTH] = { .type = NLA_FLAG },
@@ -871,7 +889,7 @@ static int nl80211_wiphy_info_handler (struct nl_msg *msg, void *arg)
 	int rem_freq;
 	int rem_band;
 	int freq_idx;
-	static struct nla_policy freq_policy[NL80211_FREQUENCY_ATTR_MAX + 1] = {
+	static const struct nla_policy freq_policy[NL80211_FREQUENCY_ATTR_MAX + 1] = {
 		[NL80211_FREQUENCY_ATTR_FREQ] = { .type = NLA_U32 },
 		[NL80211_FREQUENCY_ATTR_DISABLED] = { .type = NLA_FLAG },
 #ifdef NL80211_FREQUENCY_ATTR_NO_IR
diff --git a/src/platform/wifi/wifi-utils-wext.c b/src/platform/wifi/wifi-utils-wext.c
index 1bc29ae8..c4d3c999 100644
--- a/src/platform/wifi/wifi-utils-wext.c
+++ b/src/platform/wifi/wifi-utils-wext.c
@@ -97,8 +97,7 @@ wifi_wext_deinit (WifiData *parent)
 {
 	WifiDataWext *wext = (WifiDataWext *) parent;
 
-	if (wext->fd >= 0)
-		close (wext->fd);
+	nm_close (wext->fd);
 }
 
 static gboolean
@@ -757,7 +756,7 @@ wifi_wext_is_wifi (const char *iface)
 		nm_utils_ifname_cpy (iwr.ifr_ifrn.ifrn_name, iface);
 		if (ioctl (fd, SIOCGIWNAME, &iwr) == 0)
 			is_wifi = TRUE;
-		close (fd);
+		nm_close (fd);
 	}
 	return is_wifi;
 }