summary refs log tree commit diff
path: root/src/platform/nm-platform.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/platform/nm-platform.c')
-rw-r--r--src/platform/nm-platform.c2926
1 files changed, 2215 insertions, 711 deletions
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,