summary refs log tree commit diff
path: root/src/platform
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2019-04-21 21:09:51 +0200
committerMichael Biebl <biebl@debian.org>2019-04-21 21:09:51 +0200
commit85563b7fc7ec2cd21e38debb9b28db342e2e8e7c (patch)
treecce7b0b02d28fae2df9fdf2c1804cacd1500f2d7 /src/platform
parent9a6dcbf895f9da01768e64b73cec88c16157d91e (diff)
New upstream version 1.18.0 upstream/1.18.0
Diffstat (limited to 'src/platform')
-rw-r--r--src/platform/nm-fake-platform.c15
-rw-r--r--src/platform/nm-linux-platform.c1203
-rw-r--r--src/platform/nm-netlink.h2
-rw-r--r--src/platform/nm-platform-utils.c2
-rw-r--r--src/platform/nm-platform-utils.h2
-rw-r--r--src/platform/nm-platform.c774
-rw-r--r--src/platform/nm-platform.h182
-rw-r--r--src/platform/nmp-object.c156
-rw-r--r--src/platform/nmp-object.h148
-rw-r--r--src/platform/nmp-rules-manager.c737
-rw-r--r--src/platform/nmp-rules-manager.h67
-rw-r--r--src/platform/tests/monitor.c2
-rw-r--r--src/platform/tests/test-common.c2
-rw-r--r--src/platform/tests/test-common.h52
-rw-r--r--src/platform/tests/test-link.c36
-rw-r--r--src/platform/tests/test-nmp-object.c4
-rw-r--r--src/platform/tests/test-route.c864
17 files changed, 3694 insertions, 554 deletions
diff --git a/src/platform/nm-fake-platform.c b/src/platform/nm-fake-platform.c
index 30466159..40a85390 100644
--- a/src/platform/nm-fake-platform.c
+++ b/src/platform/nm-fake-platform.c
@@ -154,10 +154,17 @@ static char *
 sysctl_get (NMPlatform *platform, const char *pathid, int dirfd, const char *path)
 {
 	NMFakePlatformPrivate *priv = NM_FAKE_PLATFORM_GET_PRIVATE ((NMFakePlatform *) platform);
+	const char *v;
 
 	ASSERT_SYSCTL_ARGS (pathid, dirfd, path);
 
-	return g_strdup (g_hash_table_lookup (priv->options, path));
+	v = g_hash_table_lookup (priv->options, path);
+	if (!v) {
+		errno = ENOENT;
+		return NULL;
+	}
+
+	return g_strdup (v);
 }
 
 static NMFakePlatformLink *
@@ -1119,7 +1126,7 @@ ipx_route_delete (NMPlatform *platform,
 		g_assert (NM_IN_SET (NMP_OBJECT_GET_TYPE (obj), NMP_OBJECT_TYPE_IP4_ROUTE,
 		                                                NMP_OBJECT_TYPE_IP6_ROUTE));
 		g_assert (ifindex == -1);
-		ifindex = obj->object.ifindex;
+		ifindex = NMP_OBJECT_CAST_IP_ROUTE (obj)->ifindex;
 		obj_type = NMP_OBJECT_GET_TYPE (obj);
 	} else {
 		g_assert (NM_IN_SET (addr_family, AF_INET, AF_INET6));
@@ -1394,6 +1401,10 @@ nm_fake_platform_class_init (NMFakePlatformClass *klass)
 {
 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
 	NMPlatformClass *platform_class = NM_PLATFORM_CLASS (klass);
+	NMPlatformKernelSupportType kernel_support;
+
+	for (kernel_support = 0; kernel_support < _NM_PLATFORM_KERNEL_SUPPORT_NUM; kernel_support++)
+		_nm_platform_kernel_support_init (kernel_support, -1);
 
 	object_class->finalize = finalize;
 
diff --git a/src/platform/nm-linux-platform.c b/src/platform/nm-linux-platform.c
index 2f5c75b0..d4b01152 100644
--- a/src/platform/nm-linux-platform.c
+++ b/src/platform/nm-linux-platform.c
@@ -26,8 +26,10 @@
 #include <endian.h>
 #include <fcntl.h>
 #include <libudev.h>
+#include <linux/fib_rules.h>
 #include <linux/ip.h>
 #include <linux/if_arp.h>
+#include <linux/if_bridge.h>
 #include <linux/if_link.h>
 #include <linux/if_tun.h>
 #include <linux/if_tunnel.h>
@@ -39,12 +41,14 @@
 #include <sys/socket.h>
 #include <unistd.h>
 
+#include "nm-std-aux/unaligned.h"
+
 #include "nm-utils.h"
 #include "nm-core-internal.h"
 #include "nm-setting-vlan.h"
 
-#include "nm-utils/nm-secret-utils.h"
-#include "nm-utils/nm-c-list.h"
+#include "nm-glib-aux/nm-secret-utils.h"
+#include "nm-glib-aux/nm-c-list.h"
 #include "nm-netlink.h"
 #include "nm-core-utils.h"
 #include "nmp-object.h"
@@ -54,9 +58,8 @@
 #include "wifi/nm-wifi-utils.h"
 #include "wifi/nm-wifi-utils-wext.h"
 #include "wpan/nm-wpan-utils.h"
-#include "nm-utils/unaligned.h"
-#include "nm-utils/nm-io-utils.h"
-#include "nm-utils/nm-udev-utils.h"
+#include "nm-glib-aux/nm-io-utils.h"
+#include "nm-udev-aux/nm-udev-utils.h"
 
 /*****************************************************************************/
 
@@ -140,8 +143,6 @@ enum {
 #define __IFLA_TUN_MAX                  10
 #define IFLA_TUN_MAX (__IFLA_TUN_MAX - 1)
 
-static const gboolean RTA_PREF_SUPPORTED_AT_COMPILETIME = (RTA_MAX >= 20 /* RTA_PREF */);
-
 G_STATIC_ASSERT (RTA_MAX == (__RTA_MAX - 1));
 #define RTA_PREF                        20
 #undef  RTA_MAX
@@ -164,6 +165,19 @@ G_STATIC_ASSERT (RTA_MAX == (__RTA_MAX - 1));
 
 /*****************************************************************************/
 
+#define FRA_TUN_ID              12
+#define FRA_SUPPRESS_IFGROUP    13
+#define FRA_SUPPRESS_PREFIXLEN  14
+#define FRA_PAD                 18
+#define FRA_L3MDEV              19
+#define FRA_UID_RANGE           20
+#define FRA_PROTOCOL            21
+#define FRA_IP_PROTO            22
+#define FRA_SPORT_RANGE         23
+#define FRA_DPORT_RANGE         24
+
+/*****************************************************************************/
+
 #define IFLA_MACSEC_UNSPEC              0
 #define IFLA_MACSEC_SCI                 1
 #define IFLA_MACSEC_PORT                2
@@ -263,6 +277,14 @@ struct _ifla_vf_vlan_info {
 
 /*****************************************************************************/
 
+/* Appeared in in kernel 4.0 dated April 12, 2015 */
+#ifndef BRIDGE_VLAN_INFO_RANGE_BEGIN
+#define BRIDGE_VLAN_INFO_RANGE_BEGIN    (1 << 3) /* VLAN is start of vlan range */
+#define BRIDGE_VLAN_INFO_RANGE_END      (1 << 4) /* VLAN is end of vlan range */
+#endif
+
+/*****************************************************************************/
+
 typedef enum {
 	INFINIBAND_ACTION_CREATE_CHILD,
 	INFINIBAND_ACTION_DELETE_CHILD,
@@ -283,41 +305,64 @@ typedef struct {
 	};
 } ChangeLinkData;
 
-enum {
-	DELAYED_ACTION_IDX_REFRESH_ALL_LINKS,
-	DELAYED_ACTION_IDX_REFRESH_ALL_IP4_ADDRESSES,
-	DELAYED_ACTION_IDX_REFRESH_ALL_IP6_ADDRESSES,
-	DELAYED_ACTION_IDX_REFRESH_ALL_IP4_ROUTES,
-	DELAYED_ACTION_IDX_REFRESH_ALL_IP6_ROUTES,
-	DELAYED_ACTION_IDX_REFRESH_ALL_QDISCS,
-	DELAYED_ACTION_IDX_REFRESH_ALL_TFILTERS,
-	_DELAYED_ACTION_IDX_REFRESH_ALL_NUM,
-};
+typedef enum {
+	_REFRESH_ALL_TYPE_FIRST            = 0,
+
+	REFRESH_ALL_TYPE_LINKS             = 0,
+	REFRESH_ALL_TYPE_IP4_ADDRESSES     = 1,
+	REFRESH_ALL_TYPE_IP6_ADDRESSES     = 2,
+	REFRESH_ALL_TYPE_IP4_ROUTES        = 3,
+	REFRESH_ALL_TYPE_IP6_ROUTES        = 4,
+	REFRESH_ALL_TYPE_ROUTING_RULES_IP4 = 5,
+	REFRESH_ALL_TYPE_ROUTING_RULES_IP6 = 6,
+	REFRESH_ALL_TYPE_QDISCS            = 7,
+	REFRESH_ALL_TYPE_TFILTERS          = 8,
+
+	_REFRESH_ALL_TYPE_NUM,
+} RefreshAllType;
+
+typedef struct {
+	NMPObjectType obj_type;
+
+	/* for NLM_F_DUMP, which address family to request. */
+	int addr_family;
+} RefreshAllInfo;
 
 typedef enum {
-	DELAYED_ACTION_TYPE_NONE                        = 0,
-	DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS           = (1LL << /* 0 */ DELAYED_ACTION_IDX_REFRESH_ALL_LINKS),
-	DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES   = (1LL << /* 1 */ DELAYED_ACTION_IDX_REFRESH_ALL_IP4_ADDRESSES),
-	DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES   = (1LL << /* 2 */ DELAYED_ACTION_IDX_REFRESH_ALL_IP6_ADDRESSES),
-	DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES      = (1LL << /* 3 */ DELAYED_ACTION_IDX_REFRESH_ALL_IP4_ROUTES),
-	DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES      = (1LL << /* 4 */ DELAYED_ACTION_IDX_REFRESH_ALL_IP6_ROUTES),
-	DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS          = (1LL << /* 5 */ DELAYED_ACTION_IDX_REFRESH_ALL_QDISCS),
-	DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS        = (1LL << /* 6 */ DELAYED_ACTION_IDX_REFRESH_ALL_TFILTERS),
-	DELAYED_ACTION_TYPE_REFRESH_LINK                = (1LL <<    7),
-	DELAYED_ACTION_TYPE_MASTER_CONNECTED            = (1LL <<   11),
-	DELAYED_ACTION_TYPE_READ_NETLINK                = (1LL <<   12),
-	DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE        = (1LL <<   13),
+	DELAYED_ACTION_TYPE_NONE                          = 0,
+
+#define F(val, name) ((sizeof (char[(((val)) == (name)) ? 1 : -1]) * 0) + (val))
+	DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS             = 1 << F (0, REFRESH_ALL_TYPE_LINKS),
+	DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES     = 1 << F (1, REFRESH_ALL_TYPE_IP4_ADDRESSES),
+	DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES     = 1 << F (2, REFRESH_ALL_TYPE_IP6_ADDRESSES),
+	DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES        = 1 << F (3, REFRESH_ALL_TYPE_IP4_ROUTES),
+	DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES        = 1 << F (4, REFRESH_ALL_TYPE_IP6_ROUTES),
+	DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP4 = 1 << F (5, REFRESH_ALL_TYPE_ROUTING_RULES_IP4),
+	DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP6 = 1 << F (6, REFRESH_ALL_TYPE_ROUTING_RULES_IP6),
+	DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS            = 1 << F (7, REFRESH_ALL_TYPE_QDISCS),
+	DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS          = 1 << F (8, REFRESH_ALL_TYPE_TFILTERS),
+#undef F
+
+	DELAYED_ACTION_TYPE_REFRESH_LINK                  = 1 <<  9,
+	DELAYED_ACTION_TYPE_MASTER_CONNECTED              = 1 << 10,
+	DELAYED_ACTION_TYPE_READ_NETLINK                  = 1 << 11,
+	DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE          = 1 << 12,
+
 	__DELAYED_ACTION_TYPE_MAX,
 
-	DELAYED_ACTION_TYPE_REFRESH_ALL                 = DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS |
-	                                                  DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES |
-	                                                  DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES |
-	                                                  DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES |
-	                                                  DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES |
-	                                                  DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS |
-	                                                  DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS,
+	DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL = DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP4 |
+	                                                    DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP6,
 
-	DELAYED_ACTION_TYPE_MAX                         = __DELAYED_ACTION_TYPE_MAX -1,
+	DELAYED_ACTION_TYPE_REFRESH_ALL                   = DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS |
+	                                                    DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES |
+	                                                    DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES |
+	                                                    DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES |
+	                                                    DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES |
+	                                                    DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL |
+	                                                    DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS |
+	                                                    DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS,
+
+	DELAYED_ACTION_TYPE_MAX                           = __DELAYED_ACTION_TYPE_MAX -1,
 } DelayedActionType;
 
 #define FOR_EACH_DELAYED_ACTION(iflags, flags_all) \
@@ -387,7 +432,7 @@ typedef struct {
 	GIOChannel *event_channel;
 	guint event_id;
 
-	bool pruning[_DELAYED_ACTION_IDX_REFRESH_ALL_NUM];
+	guint32 pruning[_REFRESH_ALL_TYPE_NUM];
 
 	GHashTable *sysctl_get_prev_values;
 	CList sysctl_list;
@@ -401,7 +446,7 @@ typedef struct {
 
 		/* counter that a refresh all action is in progress, separated
 		 * by type. */
-		int refresh_all_in_progress[_DELAYED_ACTION_IDX_REFRESH_ALL_NUM];
+		int refresh_all_in_progress[_REFRESH_ALL_TYPE_NUM];
 
 		GPtrArray *list_master_connected;
 		GPtrArray *list_refresh_link;
@@ -536,119 +581,6 @@ wait_for_nl_response_to_string (WaitForNlResponseResult seq_result,
 	return buf0;
 }
 
-/*****************************************************************************
- * Support IFLA_INET6_ADDR_GEN_MODE
- *****************************************************************************/
-
-static int _support_user_ipv6ll = 0;
-#define _support_user_ipv6ll_still_undecided() (G_UNLIKELY (_support_user_ipv6ll == 0))
-
-static void
-_support_user_ipv6ll_detect (struct nlattr **tb)
-{
-	gboolean supported;
-
-	nm_assert (_support_user_ipv6ll_still_undecided ());
-
-	/* IFLA_INET6_ADDR_GEN_MODE was added in kernel 3.17, dated 5 October, 2014. */
-	supported = !!tb[IFLA_INET6_ADDR_GEN_MODE];
-	_support_user_ipv6ll = supported ? 1 : -1;
-	_LOG2D ("kernel-support: IFLA_INET6_ADDR_GEN_MODE: %s",
-	        supported ? "detected" : "not detected");
-}
-
-static gboolean
-_support_user_ipv6ll_get (void)
-{
-	if (_support_user_ipv6ll_still_undecided ()) {
-		_support_user_ipv6ll = 1;
-		_LOG2D ("kernel-support: IFLA_INET6_ADDR_GEN_MODE: %s", "failed to detect; assume support");
-	}
-	return _support_user_ipv6ll >= 0;
-}
-
-/*****************************************************************************
- * extended IFA_FLAGS support
- *****************************************************************************/
-
-static int _support_kernel_extended_ifa_flags = 0;
-
-#define _support_kernel_extended_ifa_flags_still_undecided() (G_UNLIKELY (_support_kernel_extended_ifa_flags == 0))
-
-static void
-_support_kernel_extended_ifa_flags_detect (struct nl_msg *msg)
-{
-	struct nlmsghdr *msg_hdr;
-	gboolean support;
-
-	nm_assert (_support_kernel_extended_ifa_flags_still_undecided ());
-	nm_assert (msg);
-
-	msg_hdr = nlmsg_hdr (msg);
-
-	nm_assert (msg_hdr && msg_hdr->nlmsg_type == RTM_NEWADDR);
-
-	/* IFA_FLAGS is set for IPv4 and IPv6 addresses. It was added first to IPv6,
-	 * but if we encounter an IPv4 address with IFA_FLAGS, we surely have support. */
-	if (!NM_IN_SET (((struct ifaddrmsg *) nlmsg_data (msg_hdr))->ifa_family, AF_INET, AF_INET6))
-		return;
-
-	/* see if the nl_msg contains the IFA_FLAGS attribute. If it does,
-	 * we assume, that the kernel supports extended flags, IFA_F_MANAGETEMPADDR
-	 * and IFA_F_NOPREFIXROUTE for IPv6. They were added together in kernel 3.14,
-	 * dated 30 March, 2014.
-	 *
-	 * For IPv4, IFA_F_NOPREFIXROUTE was added later, but there is no easy
-	 * way to detect kernel support. */
-	support = !!nlmsg_find_attr (msg_hdr, sizeof (struct ifaddrmsg), IFA_FLAGS);
-	_support_kernel_extended_ifa_flags = support ? 1 : -1;
-	_LOG2D ("kernel-support: extended-ifa-flags: %s", support ? "detected" : "not detected");
-}
-
-static gboolean
-_support_kernel_extended_ifa_flags_get (void)
-{
-	if (_support_kernel_extended_ifa_flags_still_undecided ()) {
-		_LOG2D ("kernel-support: extended-ifa-flags: %s", "unable to detect kernel support for handling IPv6 temporary addresses. Assume support");
-		_support_kernel_extended_ifa_flags = 1;
-	}
-	return _support_kernel_extended_ifa_flags >= 0;
-}
-
-/*****************************************************************************
- * Support RTA_PREF
- *****************************************************************************/
-
-static int _support_rta_pref = 0;
-#define _support_rta_pref_still_undecided() (G_UNLIKELY (_support_rta_pref == 0))
-
-static void
-_support_rta_pref_detect (struct nlattr **tb)
-{
-	gboolean supported;
-
-	nm_assert (_support_rta_pref_still_undecided ());
-
-	/* RTA_PREF was added in kernel 4.1, dated 21 June, 2015. */
-	supported = !!tb[RTA_PREF];
-	_support_rta_pref = supported ? 1 : -1;
-	_LOG2D ("kernel-support: RTA_PREF: ability to set router preference for IPv6 routes: %s",
-	        supported ? "detected" : "not detected");
-}
-
-static gboolean
-_support_rta_pref_get (void)
-{
-	if (_support_rta_pref_still_undecided ()) {
-		/* if we couldn't detect support, we fallback on compile-time check, whether
-		 * RTA_PREF is present in the kernel headers. */
-		_support_rta_pref = RTA_PREF_SUPPORTED_AT_COMPILETIME ? 1 : -1;
-		_LOG2D ("kernel-support: RTA_PREF: ability to set router preference for IPv6 routes: %s",
-		        RTA_PREF_SUPPORTED_AT_COMPILETIME ? "assume support" : "assume no support");
-	}
-	return _support_rta_pref >= 0;
-}
-
 /******************************************************************
  * Various utilities
  ******************************************************************/
@@ -1200,8 +1132,11 @@ _parse_af_inet6 (NMPlatform *platform,
 	/* Hack to detect support addrgenmode of the kernel. We only parse
 	 * netlink messages that we receive from kernel, hence this check
 	 * is valid. */
-	if (_support_user_ipv6ll_still_undecided ())
-		_support_user_ipv6ll_detect (tb);
+	if (!_nm_platform_kernel_support_detected (NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL)) {
+		/* IFLA_INET6_ADDR_GEN_MODE was added in kernel 3.17, dated 5 October, 2014. */
+		_nm_platform_kernel_support_init (NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL,
+		                                  tb[IFLA_INET6_ADDR_GEN_MODE] ? 1 : -1);
+	}
 
 	if (tb[IFLA_INET6_ADDR_GEN_MODE]) {
 		i6_addr_gen_mode_inv = _nm_platform_uint8_inv (nla_get_u8 (tb[IFLA_INET6_ADDR_GEN_MODE]));
@@ -3325,9 +3260,13 @@ rta_multipath_done:
 	obj->ip_route.lock_mtu      = NM_FLAGS_HAS (lock, 1 << RTAX_MTU);
 
 	if (!is_v4) {
-		/* Detect support for RTA_PREF by inspecting the netlink message. */
-		if (_support_rta_pref_still_undecided ())
-			_support_rta_pref_detect (tb);
+
+		if (!_nm_platform_kernel_support_detected (NM_PLATFORM_KERNEL_SUPPORT_TYPE_RTA_PREF)) {
+			/* Detect support for RTA_PREF by inspecting the netlink message.
+			 * RTA_PREF was added in kernel 4.1, dated 21 June, 2015. */
+			_nm_platform_kernel_support_init (NM_PLATFORM_KERNEL_SUPPORT_TYPE_RTA_PREF,
+			                                  tb[RTA_PREF] ? 1 : -1);
+		}
 
 		if (tb[RTA_PREF])
 			obj->ip6_route.rt_pref = nla_get_u8 (tb[RTA_PREF]);
@@ -3340,6 +3279,204 @@ rta_multipath_done:
 }
 
 static NMPObject *
+_new_from_nl_routing_rule (struct nlmsghdr *nlh, gboolean id_only)
+{
+	static const struct nla_policy policy[] = {
+		[FRA_UNSPEC]             = { },
+		[FRA_DST]                = { /* struct in_addr, struct in6_addr */ },
+		[FRA_SRC]                = { /* struct in_addr, struct in6_addr */ },
+		[FRA_IIFNAME]            = { .type   = NLA_STRING,
+		                             .maxlen = IFNAMSIZ, },
+		[FRA_GOTO]               = { .type   = NLA_U32, },
+		[FRA_UNUSED2]            = { },
+		[FRA_PRIORITY]           = { .type   = NLA_U32, },
+		[FRA_UNUSED3]            = { },
+		[FRA_UNUSED4]            = { },
+		[FRA_UNUSED5]            = { },
+		[FRA_FWMARK]             = { .type   = NLA_U32, },
+		[FRA_FLOW]               = { .type   = NLA_U32, },
+		[FRA_TUN_ID]             = { .type   = NLA_U64, },
+		[FRA_SUPPRESS_IFGROUP]   = { .type   = NLA_U32, },
+		[FRA_SUPPRESS_PREFIXLEN] = { .type   = NLA_U32, },
+		[FRA_TABLE]              = { .type   = NLA_U32, },
+		[FRA_FWMASK]             = { .type   = NLA_U32, },
+		[FRA_OIFNAME]            = { .type   = NLA_STRING,
+		                             .maxlen = IFNAMSIZ, },
+		[FRA_PAD]                = { .type   = NLA_U32, },
+		[FRA_L3MDEV]             = { .type   = NLA_U8, },
+		[FRA_UID_RANGE]          = { .minlen = sizeof(NMFibRuleUidRange),
+		                             .maxlen = sizeof(NMFibRuleUidRange), },
+		[FRA_PROTOCOL]           = { .type   = NLA_U8, },
+		[FRA_IP_PROTO]           = { .type   = NLA_U8, },
+		[FRA_SPORT_RANGE]        = { .minlen = sizeof(NMFibRulePortRange),
+		                             .maxlen = sizeof(NMFibRulePortRange), },
+		[FRA_DPORT_RANGE]        = { .minlen = sizeof(NMFibRulePortRange),
+		                             .maxlen = sizeof(NMFibRulePortRange), },
+	};
+	struct nlattr *tb[G_N_ELEMENTS (policy)];
+	const struct fib_rule_hdr *frh;
+	NMPlatformRoutingRule *props;
+	nm_auto_nmpobj NMPObject *obj = NULL;
+	int addr_family;
+	guint8 addr_size;
+
+	if (nlmsg_parse_arr (nlh, sizeof (*frh), tb, policy) < 0)
+		return NULL;
+
+	frh = nlmsg_data (nlh);
+
+	addr_family = frh->family;
+
+	if (!NM_IN_SET (addr_family, AF_INET, AF_INET6)) {
+		/* we don't care about other address families. */
+		return NULL;
+	}
+
+	addr_size = nm_utils_addr_family_to_size (addr_family);
+
+	obj = nmp_object_new (NMP_OBJECT_TYPE_ROUTING_RULE, NULL);
+	props = &obj->routing_rule;
+
+	props->addr_family = addr_family;
+	props->action      = frh->action;
+	props->flags       = frh->flags;
+	props->tos         = frh->tos;
+
+	props->table =   tb[FRA_TABLE]
+	               ? nla_get_u32 (tb[FRA_TABLE])
+	               : frh->table;
+
+	if (tb[FRA_SUPPRESS_PREFIXLEN])
+		props->suppress_prefixlen_inverse = ~nla_get_u32 (tb[FRA_SUPPRESS_PREFIXLEN]);
+
+	if (tb[FRA_SUPPRESS_IFGROUP])
+		props->suppress_ifgroup_inverse = ~nla_get_u32 (tb[FRA_SUPPRESS_IFGROUP]);
+
+	if (tb[FRA_IIFNAME])
+		nla_strlcpy (props->iifname, tb[FRA_IIFNAME], sizeof (props->iifname));
+
+	if (tb[FRA_OIFNAME])
+		nla_strlcpy (props->oifname, tb[FRA_OIFNAME], sizeof (props->oifname));
+
+	if (tb[FRA_PRIORITY])
+		props->priority = nla_get_u32 (tb[FRA_PRIORITY]);
+
+	if (tb[FRA_FWMARK])
+		props->fwmark = nla_get_u32 (tb[FRA_FWMARK]);
+
+	if (tb[FRA_FWMASK])
+		props->fwmask = nla_get_u32 (tb[FRA_FWMASK]);
+
+	if (tb[FRA_GOTO])
+		props->goto_target = nla_get_u32 (tb[FRA_GOTO]);
+
+	props->src_len = frh->src_len;
+	if (props->src_len > addr_size * 8)
+		return NULL;
+	if (!tb[FRA_SRC]) {
+		if (props->src_len > 0)
+			return NULL;
+	} else if (!nm_ip_addr_set_from_untrusted (addr_family,
+	                                           &props->src,
+	                                           nla_data (tb[FRA_SRC]),
+	                                           nla_len (tb[FRA_SRC]),
+	                                           NULL))
+		return NULL;
+
+	props->dst_len = frh->dst_len;
+	if (props->dst_len > addr_size * 8)
+		return NULL;
+	if (!tb[FRA_DST]) {
+		if (props->dst_len > 0)
+			return NULL;
+	} else if (!nm_ip_addr_set_from_untrusted (addr_family,
+	                                           &props->dst,
+	                                           nla_data (tb[FRA_DST]),
+	                                           nla_len (tb[FRA_DST]),
+	                                           NULL))
+		return NULL;
+
+	if (tb[FRA_FLOW])
+		props->flow = nla_get_u32 (tb[FRA_FLOW]);
+
+	if (tb[FRA_TUN_ID])
+		props->tun_id = nla_get_be64 (tb[FRA_TUN_ID]);
+
+	if (tb[FRA_L3MDEV]) {
+
+		if (!_nm_platform_kernel_support_detected (NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_L3MDEV)) {
+			/* support for FRA_L3MDEV was added in 96c63fa7393d0a346acfe5a91e0c7d4c7782641b,
+			 * kernel 4.8, 3 October 2017.
+			 *
+			 * We can only detect support if the attribute is present. A missing attribute
+			 * is not conclusive. */
+			_nm_platform_kernel_support_init (NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_L3MDEV, 1);
+		}
+
+		/* actually, kernel only allows this attribute to be missing or
+		 * "1". Still, encode it as full uint8.
+		 *
+		 * Note that FRA_L3MDEV and FRA_TABLE are mutally exclusive. */
+		props->l3mdev = nla_get_u8 (tb[FRA_L3MDEV]);
+	}
+
+	if (tb[FRA_PROTOCOL])
+		props->protocol = nla_get_u8 (tb[FRA_PROTOCOL]);
+	else
+		nm_assert (props->protocol == RTPROT_UNSPEC);
+
+	if (!_nm_platform_kernel_support_detected (NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_PROTOCOL)) {
+		/* FRA_PROTOCOL was added in kernel 4.17, dated 3 June, 2018.
+		 * See commit 1b71af6053af1bd2f849e9fda4f71c1e3f145dcf. */
+		_nm_platform_kernel_support_init (NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_PROTOCOL,
+		                                  tb[FRA_PROTOCOL] ? 1 : -1);
+	}
+
+	if (tb[FRA_IP_PROTO])
+		props->ip_proto = nla_get_u8 (tb[FRA_IP_PROTO]);
+
+	G_STATIC_ASSERT_EXPR (sizeof (NMFibRulePortRange) == 4);
+	G_STATIC_ASSERT_EXPR (G_STRUCT_OFFSET (NMFibRulePortRange, start) == 0);
+	G_STATIC_ASSERT_EXPR (G_STRUCT_OFFSET (NMFibRulePortRange, end)   == 2);
+
+	nla_memcpy_checked_size (&props->sport_range, tb[FRA_SPORT_RANGE], sizeof (props->sport_range));
+	nla_memcpy_checked_size (&props->dport_range, tb[FRA_DPORT_RANGE], sizeof (props->dport_range));
+
+	if (!_nm_platform_kernel_support_detected (NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_IP_PROTO)) {
+		/* support for FRA_IP_PROTO, FRA_SPORT_RANGE, and FRA_DPORT_RANGE was added together
+		 * by bfff4862653bb96001ab57c1edd6d03f48e5f035, kernel 4.17, 4 June 2018.
+		 *
+		 * Unfortunately, a missing attribute does not tell us anything about support.
+		 * We can only tell for sure when we have support, but not when we don't have.  */
+		if (   tb[FRA_IP_PROTO]
+		    || tb[FRA_SPORT_RANGE]
+		    || tb[FRA_DPORT_RANGE])
+			_nm_platform_kernel_support_init (NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_IP_PROTO, 1);
+	}
+
+	G_STATIC_ASSERT_EXPR (sizeof (NMFibRuleUidRange) == 8);
+	G_STATIC_ASSERT_EXPR (G_STRUCT_OFFSET (NMFibRuleUidRange, start) == 0);
+	G_STATIC_ASSERT_EXPR (G_STRUCT_OFFSET (NMFibRuleUidRange, end) == 4);
+
+	if (tb[FRA_UID_RANGE]) {
+
+		if (!_nm_platform_kernel_support_detected (NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_UID_RANGE)) {
+			/* support for FRA_UID_RANGE was added in 622ec2c9d52405973c9f1ca5116eb1c393adfc7d,
+			 * kernel 4.10, 19 February 2017.
+			 *
+			 * We can only detect support if the attribute is present. A missing attribute
+			 * is not conclusive. */
+			_nm_platform_kernel_support_init (NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_UID_RANGE, 1);
+		}
+
+		nla_memcpy_checked_size (&props->uid_range, tb[FRA_UID_RANGE], sizeof (props->uid_range));
+		props->uid_range_has = TRUE;
+	}
+
+	return g_steal_pointer (&obj);
+}
+
+static NMPObject *
 _new_from_nl_qdisc (struct nlmsghdr *nlh, gboolean id_only)
 {
 	static const struct nla_policy policy[] = {
@@ -3438,6 +3575,10 @@ nmp_object_new_from_nl (NMPlatform *platform, const NMPCache *cache, struct nl_m
 	case RTM_DELROUTE:
 	case RTM_GETROUTE:
 		return _new_from_nl_route (msghdr, id_only);
+	case RTM_NEWRULE:
+	case RTM_DELRULE:
+	case RTM_GETRULE:
+		return _new_from_nl_routing_rule (msghdr, id_only);
 	case RTM_NEWQDISC:
 	case RTM_DELQDISC:
 	case RTM_GETQDISC:
@@ -3642,21 +3783,23 @@ nla_put_failure:
 }
 
 static struct nl_msg *
-_nl_msg_new_link (int nlmsg_type,
-                  int nlmsg_flags,
-                  int ifindex,
-                  const char *ifname,
-                  unsigned flags_mask,
-                  unsigned flags_set)
+_nl_msg_new_link_full (int nlmsg_type,
+                       int nlmsg_flags,
+                       int ifindex,
+                       const char *ifname,
+                       guint8 family,
+                       unsigned flags_mask,
+                       unsigned flags_set)
 {
 	nm_auto_nlmsg struct nl_msg *msg = NULL;
 	const struct ifinfomsg ifi = {
+		.ifi_family = family,
 		.ifi_change = flags_mask,
 		.ifi_flags = flags_set,
 		.ifi_index = ifindex,
 	};
 
-	nm_assert (NM_IN_SET (nlmsg_type, RTM_DELLINK, RTM_NEWLINK, RTM_GETLINK));
+	nm_assert (NM_IN_SET (nlmsg_type, RTM_DELLINK, RTM_NEWLINK, RTM_GETLINK, RTM_SETLINK));
 
 	msg = nlmsg_alloc_simple (nlmsg_type, nlmsg_flags);
 
@@ -3672,6 +3815,15 @@ nla_put_failure:
 	g_return_val_if_reached (NULL);
 }
 
+static struct nl_msg *
+_nl_msg_new_link (int nlmsg_type,
+                  int nlmsg_flags,
+                  int ifindex,
+                  const char *ifname)
+{
+	return _nl_msg_new_link_full (nlmsg_type, nlmsg_flags, ifindex, ifname, AF_UNSPEC, 0, 0);
+}
+
 /* Copied and modified from libnl3's build_addr_msg(). */
 static struct nl_msg *
 _nl_msg_new_address (int nlmsg_type,
@@ -3796,9 +3948,7 @@ _nl_msg_new_route (int nlmsg_type,
 		             ? nm_platform_route_scope_inv (obj->ip4_route.scope_inv)
 		             : RT_SCOPE_NOWHERE,
 		.rtm_type = RTN_UNICAST,
-		.rtm_flags = obj->ip_route.r_rtm_flags & (is_v4
-		                                          ? (unsigned) (RTNH_F_ONLINK)
-		                                          : (unsigned) 0),
+		.rtm_flags = obj->ip_route.r_rtm_flags & ((unsigned) (RTNH_F_ONLINK)),
 		.rtm_dst_len = obj->ip_route.plen,
 		.rtm_src_len = is_v4
 		               ? 0
@@ -3893,6 +4043,119 @@ nla_put_failure:
 }
 
 static struct nl_msg *
+_nl_msg_new_routing_rule (int nlmsg_type,
+                          int nlmsg_flags,
+                          const NMPlatformRoutingRule *routing_rule)
+{
+	nm_auto_nlmsg struct nl_msg *msg = NULL;
+	const guint8 addr_size = nm_utils_addr_family_to_size (routing_rule->addr_family);
+	guint32 table;
+
+	msg = nlmsg_alloc_simple (nlmsg_type, nlmsg_flags);
+
+	table = routing_rule->table;
+
+	if (   NM_IN_SET (routing_rule->addr_family, AF_INET, AF_INET6)
+	    && routing_rule->action == FR_ACT_TO_TBL
+	    && routing_rule->l3mdev == 0
+	    && table == RT_TABLE_UNSPEC) {
+		/* for IPv6, this setting is invalid and rejected by kernel. That's fine.
+		 *
+		 * for IPv4, kernel will automatically assign an unused table. That's not
+		 * fine, because we don't know what we will get.
+		 *
+		 * The caller must not allow that to happen. */
+		nm_assert_not_reached ();
+	}
+
+	{
+		const struct fib_rule_hdr frh = {
+			.family  = routing_rule->addr_family,
+			.src_len = routing_rule->src_len,
+			.dst_len = routing_rule->dst_len,
+			.tos     = routing_rule->tos,
+			.table   = table < 0x100u ? (guint8) table : (guint8) RT_TABLE_UNSPEC,
+			.action  = routing_rule->action,
+
+			/* we only allow setting the "not" flag. */
+			.flags   = routing_rule->flags & ((guint32) FIB_RULE_INVERT),
+		};
+
+		if (nlmsg_append_struct (msg, &frh) < 0)
+			goto nla_put_failure;
+	}
+
+	if (table > G_MAXINT8)
+		NLA_PUT_U32 (msg, FRA_TABLE, table);
+
+	if (routing_rule->suppress_prefixlen_inverse != 0)
+		NLA_PUT_U32 (msg, FRA_SUPPRESS_PREFIXLEN, ~routing_rule->suppress_prefixlen_inverse);
+
+	if (routing_rule->suppress_ifgroup_inverse != 0)
+		NLA_PUT_U32 (msg, FRA_SUPPRESS_IFGROUP, ~routing_rule->suppress_ifgroup_inverse);
+
+	if (routing_rule->iifname[0] != '\0')
+		NLA_PUT_STRING (msg, FRA_IIFNAME, routing_rule->iifname);
+
+	if (routing_rule->oifname[0] != '\0')
+		NLA_PUT_STRING (msg, FRA_OIFNAME, routing_rule->oifname);
+
+	/* we always set the priority and don't support letting kernel pick one. */
+	NLA_PUT_U32 (msg, FRA_PRIORITY, routing_rule->priority);
+
+	if (   routing_rule->fwmark != 0
+	    || routing_rule->fwmask != 0) {
+		NLA_PUT_U32 (msg, FRA_FWMARK, routing_rule->fwmark);
+		NLA_PUT_U32 (msg, FRA_FWMASK, routing_rule->fwmask);
+	}
+
+	if (routing_rule->src_len > 0)
+		NLA_PUT (msg, FRA_SRC, addr_size, &routing_rule->src);
+
+	if (routing_rule->dst_len > 0)
+		NLA_PUT (msg, FRA_DST, addr_size, &routing_rule->dst);
+
+	if (routing_rule->flow != 0) {
+		/* only relevant for IPv4. */
+		NLA_PUT_U32 (msg, FRA_FLOW, routing_rule->flow);
+	}
+
+	if (routing_rule->tun_id != 0)
+		NLA_PUT_U64 (msg, FRA_TUN_ID, htobe64 (routing_rule->tun_id));
+
+	if (routing_rule->l3mdev)
+		NLA_PUT_U8 (msg, FRA_L3MDEV, routing_rule->l3mdev);
+
+	if (routing_rule->protocol != RTPROT_UNSPEC)
+		NLA_PUT_U8 (msg, FRA_PROTOCOL, routing_rule->protocol);
+
+	if (routing_rule->ip_proto != 0)
+		NLA_PUT_U8 (msg, FRA_IP_PROTO, routing_rule->ip_proto);
+
+	if (   routing_rule->sport_range.start
+	    || routing_rule->sport_range.end)
+		NLA_PUT (msg, FRA_SPORT_RANGE, sizeof (routing_rule->sport_range), &routing_rule->sport_range);
+
+	if (   routing_rule->dport_range.start
+	    || routing_rule->dport_range.end)
+		NLA_PUT (msg, FRA_DPORT_RANGE, sizeof (routing_rule->dport_range), &routing_rule->dport_range);
+
+	if (routing_rule->uid_range_has)
+		NLA_PUT (msg, FRA_UID_RANGE, sizeof (routing_rule->uid_range), &routing_rule->uid_range);
+
+	switch (routing_rule->action) {
+	case FR_ACT_GOTO:
+		NLA_PUT_U32 (msg, FRA_GOTO, routing_rule->goto_target);
+		break;
+	}
+
+	return g_steal_pointer (&msg);
+
+nla_put_failure:
+	g_return_val_if_reached (NULL);
+}
+
+static struct nl_msg *
 _nl_msg_new_qdisc (int nlmsg_type,
                    int nlmsg_flags,
                    const NMPlatformQdisc *qdisc)
@@ -4285,22 +4548,31 @@ sysctl_get (NMPlatform *platform, const char *pathid, int dirfd, const char *pat
 	ASSERT_SYSCTL_ARGS (pathid, dirfd, path);
 
 	if (dirfd < 0) {
-		if (!nm_platform_netns_push (platform, &netns))
+		if (!nm_platform_netns_push (platform, &netns)) {
+			errno = EBUSY;
 			return NULL;
+		}
 		pathid = path;
 	}
 
 	if (nm_utils_file_get_contents (dirfd, path, 1*1024*1024,
 	                                NM_UTILS_FILE_GET_CONTENTS_FLAG_NONE,
 	                                &contents, NULL, &error) < 0) {
-		/* We assume FAILED means EOPNOTSUP */
-		if (   g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT)
-		    || g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NODEV)
-		    || g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_FAILED))
-			_LOGD ("error reading %s: %s", pathid, error->message);
-		else
-			_LOGE ("error reading %s: %s", pathid, error->message);
+		NMLogLevel log_level = LOGL_ERR;
+		int errsv = EBUSY;
+
+		if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) {
+			errsv = ENOENT;
+			log_level = LOGL_DEBUG;
+		} else if (   g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NODEV)
+		           || g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_FAILED)) {
+			/* We assume FAILED means EOPNOTSUP and don't log a error message. */
+			log_level = LOGL_DEBUG;
+		}
+
+		_NMLOG (log_level, "error reading %s: %s", pathid, error->message);
 		g_clear_error (&error);
+		errno = errsv;
 		return NULL;
 	}
 
@@ -4308,96 +4580,145 @@ sysctl_get (NMPlatform *platform, const char *pathid, int dirfd, const char *pat
 
 	_log_dbg_sysctl_get (platform, pathid, contents);
 
+	/* errno is left undefined (as we don't return NULL). */
 	return contents;
 }
 
 /*****************************************************************************/
 
-static NMPlatformKernelSupportFlags
-check_kernel_support (NMPlatform *platform,
-                      NMPlatformKernelSupportFlags request_flags)
+static void
+process_events (NMPlatform *platform)
 {
-	NMPlatformKernelSupportFlags response = 0;
+	delayed_action_handle_all (platform, TRUE);
+}
 
-	nm_assert (NM_IS_LINUX_PLATFORM (platform));
+/*****************************************************************************/
 
-	if (NM_FLAGS_HAS (request_flags, NM_PLATFORM_KERNEL_SUPPORT_EXTENDED_IFA_FLAGS)) {
-		if (_support_kernel_extended_ifa_flags_get ())
-			response |= NM_PLATFORM_KERNEL_SUPPORT_EXTENDED_IFA_FLAGS;
-	}
+static const RefreshAllInfo *
+refresh_all_type_get_info (RefreshAllType refresh_all_type)
+{
+	static const RefreshAllInfo infos[] = {
+#define R(_refresh_all_type, _obj_type, _addr_family) [_refresh_all_type] = { .obj_type = _obj_type, .addr_family = _addr_family, }
+		R (REFRESH_ALL_TYPE_LINKS,             NMP_OBJECT_TYPE_LINK,         AF_UNSPEC),
+		R (REFRESH_ALL_TYPE_IP4_ADDRESSES,     NMP_OBJECT_TYPE_IP4_ADDRESS,  AF_UNSPEC),
+		R (REFRESH_ALL_TYPE_IP6_ADDRESSES,     NMP_OBJECT_TYPE_IP6_ADDRESS,  AF_UNSPEC),
+		R (REFRESH_ALL_TYPE_IP4_ROUTES,        NMP_OBJECT_TYPE_IP4_ROUTE,    AF_UNSPEC),
+		R (REFRESH_ALL_TYPE_IP6_ROUTES,        NMP_OBJECT_TYPE_IP6_ROUTE,    AF_UNSPEC),
+		R (REFRESH_ALL_TYPE_ROUTING_RULES_IP4, NMP_OBJECT_TYPE_ROUTING_RULE, AF_INET),
+		R (REFRESH_ALL_TYPE_ROUTING_RULES_IP6, NMP_OBJECT_TYPE_ROUTING_RULE, AF_INET6),
+		R (REFRESH_ALL_TYPE_QDISCS,            NMP_OBJECT_TYPE_QDISC,        AF_UNSPEC),
+		R (REFRESH_ALL_TYPE_TFILTERS,          NMP_OBJECT_TYPE_TFILTER,      AF_UNSPEC),
+#undef R
+	};
 
-	if (NM_FLAGS_HAS (request_flags, NM_PLATFORM_KERNEL_SUPPORT_USER_IPV6LL)) {
-		if (_support_user_ipv6ll_get ())
-			response |= NM_PLATFORM_KERNEL_SUPPORT_USER_IPV6LL;
-	}
+	nm_assert (_NM_INT_NOT_NEGATIVE (refresh_all_type));
+	nm_assert (refresh_all_type < G_N_ELEMENTS (infos));
+	nm_assert (nmp_class_from_type (infos[refresh_all_type].obj_type));
 
-	if (NM_FLAGS_HAS (request_flags, NM_PLATFORM_KERNEL_SUPPORT_RTA_PREF)) {
-		if (_support_rta_pref_get ())
-			response |= NM_PLATFORM_KERNEL_SUPPORT_RTA_PREF;
-	}
+	return &infos[refresh_all_type];
+}
+
+_NM_UTILS_LOOKUP_DEFINE (static, delayed_action_type_to_refresh_all_type, DelayedActionType, RefreshAllType,
+	NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT (0),
+	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS,             REFRESH_ALL_TYPE_LINKS),
+	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES,     REFRESH_ALL_TYPE_IP4_ADDRESSES),
+	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES,     REFRESH_ALL_TYPE_IP6_ADDRESSES),
+	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES,        REFRESH_ALL_TYPE_IP4_ROUTES),
+	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES,        REFRESH_ALL_TYPE_IP6_ROUTES),
+	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP4, REFRESH_ALL_TYPE_ROUTING_RULES_IP4),
+	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP6, REFRESH_ALL_TYPE_ROUTING_RULES_IP6),
+	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS,            REFRESH_ALL_TYPE_QDISCS),
+	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS,          REFRESH_ALL_TYPE_TFILTERS),
+	NM_UTILS_LOOKUP_ITEM_IGNORE_OTHER (),
+);
+
+static DelayedActionType
+delayed_action_type_from_refresh_all_type (RefreshAllType refresh_all_type)
+{
+	DelayedActionType t;
+
+	nm_assert (refresh_all_type_get_info (refresh_all_type));
 
-	return response;
+	t = (((DelayedActionType) 1) << refresh_all_type);
+
+	nm_assert (refresh_all_type == delayed_action_type_to_refresh_all_type (t));
+
+	return t;
 }
 
-static void
-process_events (NMPlatform *platform)
+static RefreshAllType
+refresh_all_type_from_needle_object (const NMPObject *obj_needle)
 {
-	delayed_action_handle_all (platform, TRUE);
+	switch (NMP_OBJECT_GET_TYPE (obj_needle)) {
+	case NMP_OBJECT_TYPE_LINK:        return REFRESH_ALL_TYPE_LINKS;
+	case NMP_OBJECT_TYPE_IP4_ADDRESS: return REFRESH_ALL_TYPE_IP4_ADDRESSES;
+	case NMP_OBJECT_TYPE_IP6_ADDRESS: return REFRESH_ALL_TYPE_IP6_ADDRESSES;
+	case NMP_OBJECT_TYPE_IP4_ROUTE:   return REFRESH_ALL_TYPE_IP4_ROUTES;
+	case NMP_OBJECT_TYPE_IP6_ROUTE:   return REFRESH_ALL_TYPE_IP6_ROUTES;
+	case NMP_OBJECT_TYPE_QDISC:       return REFRESH_ALL_TYPE_QDISCS;
+	case NMP_OBJECT_TYPE_TFILTER:     return REFRESH_ALL_TYPE_TFILTERS;
+	case NMP_OBJECT_TYPE_ROUTING_RULE:
+		switch (NMP_OBJECT_CAST_ROUTING_RULE (obj_needle)->addr_family) {
+		case AF_INET:  return REFRESH_ALL_TYPE_ROUTING_RULES_IP4;
+		case AF_INET6: return REFRESH_ALL_TYPE_ROUTING_RULES_IP6;
+		}
+		nm_assert_not_reached ();
+		return 0;
+	default:
+		nm_assert_not_reached ();
+		return 0;
+	}
 }
 
-/*****************************************************************************/
+static const NMPLookup *
+refresh_all_type_init_lookup (RefreshAllType refresh_all_type,
+                              NMPLookup *lookup)
+{
+	const RefreshAllInfo *refresh_all_info;
 
-_NM_UTILS_LOOKUP_DEFINE (static, delayed_action_refresh_from_object_type, NMPObjectType, DelayedActionType,
-	NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT (DELAYED_ACTION_TYPE_NONE),
-	NM_UTILS_LOOKUP_ITEM (NMP_OBJECT_TYPE_LINK,        DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS),
-	NM_UTILS_LOOKUP_ITEM (NMP_OBJECT_TYPE_IP4_ADDRESS, DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES),
-	NM_UTILS_LOOKUP_ITEM (NMP_OBJECT_TYPE_IP6_ADDRESS, DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES),
-	NM_UTILS_LOOKUP_ITEM (NMP_OBJECT_TYPE_IP4_ROUTE,   DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES),
-	NM_UTILS_LOOKUP_ITEM (NMP_OBJECT_TYPE_IP6_ROUTE,   DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES),
-	NM_UTILS_LOOKUP_ITEM (NMP_OBJECT_TYPE_QDISC,       DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS),
-	NM_UTILS_LOOKUP_ITEM (NMP_OBJECT_TYPE_TFILTER,     DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS),
-	NM_UTILS_LOOKUP_ITEM_IGNORE_OTHER (),
-);
+	nm_assert (lookup);
 
-_NM_UTILS_LOOKUP_DEFINE (static, delayed_action_refresh_to_object_type, DelayedActionType, NMPObjectType,
-	NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT (NMP_OBJECT_TYPE_UNKNOWN),
-	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS,         NMP_OBJECT_TYPE_LINK),
-	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES, NMP_OBJECT_TYPE_IP4_ADDRESS),
-	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES, NMP_OBJECT_TYPE_IP6_ADDRESS),
-	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES,    NMP_OBJECT_TYPE_IP4_ROUTE),
-	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES,    NMP_OBJECT_TYPE_IP6_ROUTE),
-	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS,        NMP_OBJECT_TYPE_QDISC),
-	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS,      NMP_OBJECT_TYPE_TFILTER),
-	NM_UTILS_LOOKUP_ITEM_IGNORE_OTHER (),
-);
+	refresh_all_info = refresh_all_type_get_info (refresh_all_type);
 
-_NM_UTILS_LOOKUP_DEFINE (static, delayed_action_refresh_all_to_idx, DelayedActionType, guint,
-	NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT (0),
-	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS,         DELAYED_ACTION_IDX_REFRESH_ALL_LINKS),
-	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES, DELAYED_ACTION_IDX_REFRESH_ALL_IP4_ADDRESSES),
-	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES, DELAYED_ACTION_IDX_REFRESH_ALL_IP6_ADDRESSES),
-	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES,    DELAYED_ACTION_IDX_REFRESH_ALL_IP4_ROUTES),
-	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES,    DELAYED_ACTION_IDX_REFRESH_ALL_IP6_ROUTES),
-	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS,        DELAYED_ACTION_IDX_REFRESH_ALL_QDISCS),
-	NM_UTILS_LOOKUP_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS,      DELAYED_ACTION_IDX_REFRESH_ALL_TFILTERS),
-	NM_UTILS_LOOKUP_ITEM_IGNORE_OTHER (),
-);
+	nm_assert (refresh_all_info);
+
+	if (NM_IN_SET (refresh_all_info->obj_type, NMP_OBJECT_TYPE_ROUTING_RULE)) {
+		return nmp_lookup_init_object_by_addr_family (lookup,
+		                                              refresh_all_info->obj_type,
+		                                              refresh_all_info->addr_family);
+	}
+
+	/* not yet implemented. */
+	nm_assert (refresh_all_info->addr_family == AF_UNSPEC);
+
+	return nmp_lookup_init_obj_type (lookup,
+	                                 refresh_all_info->obj_type);
+}
+
+static DelayedActionType
+delayed_action_refresh_from_needle_object (const NMPObject *obj_needle)
+{
+	return delayed_action_type_from_refresh_all_type (refresh_all_type_from_needle_object (obj_needle));
+}
 
 NM_UTILS_LOOKUP_STR_DEFINE_STATIC (delayed_action_to_string, DelayedActionType,
 	NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT ("unknown"),
-	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS,         "refresh-all-links"),
-	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES, "refresh-all-ip4-addresses"),
-	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES, "refresh-all-ip6-addresses"),
-	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES,    "refresh-all-ip4-routes"),
-	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES,    "refresh-all-ip6-routes"),
-	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS,        "refresh-all-qdiscs"),
-	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS,      "refresh-all-tfilters"),
-	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_LINK,              "refresh-link"),
-	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_MASTER_CONNECTED,          "master-connected"),
-	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_READ_NETLINK,              "read-netlink"),
-	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE,      "wait-for-nl-response"),
+	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_LINKS,             "refresh-all-links"),
+	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ADDRESSES,     "refresh-all-ip4-addresses"),
+	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES,     "refresh-all-ip6-addresses"),
+	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES,        "refresh-all-ip4-routes"),
+	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES,        "refresh-all-ip6-routes"),
+	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP4, "refresh-all-routing-rules-ip4"),
+	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_IP6, "refresh-all-routing-rules-ip6"),
+	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS,            "refresh-all-qdiscs"),
+	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS,          "refresh-all-tfilters"),
+	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_REFRESH_LINK,                  "refresh-link"),
+	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_MASTER_CONNECTED,              "master-connected"),
+	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_READ_NETLINK,                  "read-netlink"),
+	NM_UTILS_LOOKUP_STR_ITEM (DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE,          "wait-for-nl-response"),
 	NM_UTILS_LOOKUP_ITEM_IGNORE (DELAYED_ACTION_TYPE_NONE),
 	NM_UTILS_LOOKUP_ITEM_IGNORE (DELAYED_ACTION_TYPE_REFRESH_ALL),
+	NM_UTILS_LOOKUP_ITEM_IGNORE (DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL),
 	NM_UTILS_LOOKUP_ITEM_IGNORE (__DELAYED_ACTION_TYPE_MAX),
 );
 
@@ -4455,6 +4776,7 @@ static gboolean
 delayed_action_refresh_all_in_progress (NMPlatform *platform, DelayedActionType action_type)
 {
 	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
+	RefreshAllType refresh_all_type;
 
 	nm_assert (nm_utils_is_power_of_two (action_type));
 	nm_assert (NM_FLAGS_ANY (action_type, DELAYED_ACTION_TYPE_REFRESH_ALL));
@@ -4463,10 +4785,8 @@ delayed_action_refresh_all_in_progress (NMPlatform *platform, DelayedActionType
 	if (NM_FLAGS_ANY (priv->delayed_action.flags, action_type))
 		return TRUE;
 
-	if (priv->delayed_action.refresh_all_in_progress[delayed_action_refresh_all_to_idx (action_type)] > 0)
-		return TRUE;
-
-	return FALSE;
+	refresh_all_type = delayed_action_type_to_refresh_all_type (action_type);
+	return (priv->delayed_action.refresh_all_in_progress[refresh_all_type] > 0);
 }
 
 static void
@@ -4764,25 +5084,33 @@ delayed_action_schedule_WAIT_FOR_NL_RESPONSE (NMPlatform *platform,
 /*****************************************************************************/
 
 static void
-cache_prune_one_type (NMPlatform *platform, NMPObjectType obj_type)
+cache_prune_one_type (NMPlatform *platform,
+                      const NMPLookup *lookup)
 {
 	NMDedupMultiIter iter;
 	const NMPObject *obj;
 	NMPCacheOpsType cache_op;
-	NMPLookup lookup;
 	NMPCache *cache = nm_platform_get_cache (platform);
 
-	nmp_lookup_init_obj_type (&lookup,
-	                          obj_type);
 	nm_dedup_multi_iter_init (&iter,
 	                          nmp_cache_lookup (cache,
-	                                            &lookup));
+	                                            lookup));
 	while (nm_dedup_multi_iter_next (&iter)) {
-		if (iter.current->dirty) {
+		const NMDedupMultiEntry *main_entry;
+
+		/* we only track the dirty flag for the OBJECT-TYPE index. That means,
+		 * for other lookup types we need to check the dirty flag of the main-entry. */
+		main_entry = nmp_cache_reresolve_main_entry (cache, iter.current, lookup);
+		if (!main_entry->dirty)
+			continue;
+
+		obj = main_entry->obj;
+
+		_LOGt ("cache-prune: prune %s", nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_ALL, NULL, 0));
+
+		{
 			nm_auto_nmpobj const NMPObject *obj_old = NULL;
 
-			obj = iter.current->obj;
-			_LOGt ("cache-prune: prune %s", nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_ALL, NULL, 0));
 			cache_op = nmp_cache_remove (cache, obj, TRUE, TRUE, &obj_old);
 			nm_assert (cache_op == NMP_CACHE_OPS_REMOVED);
 			cache_on_change (platform, cache_op, obj_old, NULL);
@@ -4795,16 +5123,19 @@ static void
 cache_prune_all (NMPlatform *platform)
 {
 	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
-	DelayedActionType iflags, action_type;
+	RefreshAllType refresh_all_type;
 
-	action_type = DELAYED_ACTION_TYPE_REFRESH_ALL;
-	FOR_EACH_DELAYED_ACTION (iflags, action_type) {
-		bool *p = &priv->pruning[delayed_action_refresh_all_to_idx (iflags)];
+	for (refresh_all_type = _REFRESH_ALL_TYPE_FIRST; refresh_all_type < _REFRESH_ALL_TYPE_NUM; refresh_all_type++) {
+		NMPLookup lookup;
 
-		if (*p) {
-			*p = FALSE;
-			cache_prune_one_type (platform, delayed_action_refresh_to_object_type (iflags));
-		}
+		if (priv->pruning[refresh_all_type] == 0)
+			continue;
+		priv->pruning[refresh_all_type] -= 1;
+		if (priv->pruning[refresh_all_type] > 0)
+			continue;
+		refresh_all_type_init_lookup (refresh_all_type,
+		                              &lookup);
+		cache_prune_one_type (platform, &lookup);
 	}
 }
 
@@ -4876,6 +5207,7 @@ cache_on_change (NMPlatform *platform,
 				                         DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES |
 				                         DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES |
 				                         DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES |
+				                         DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL |
 				                         DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS |
 				                         DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS,
 				                         NULL);
@@ -5166,7 +5498,7 @@ do_request_link_no_delayed_actions (NMPlatform *platform, int ifindex, const cha
 
 		entry = nmp_cache_lookup_entry_link (nm_platform_get_cache (platform), ifindex);
 		if (entry) {
-			priv->pruning[DELAYED_ACTION_IDX_REFRESH_ALL_LINKS] = TRUE;
+			priv->pruning[REFRESH_ALL_TYPE_LINKS] += 1;
 			nm_dedup_multi_entry_set_dirty (entry, TRUE);
 		}
 	}
@@ -5176,9 +5508,7 @@ do_request_link_no_delayed_actions (NMPlatform *platform, int ifindex, const cha
 	nlmsg = _nl_msg_new_link (RTM_GETLINK,
 	                          0,
 	                          ifindex,
-	                          name,
-	                          0,
-	                          0);
+	                          name);
 	if (nlmsg) {
 		nle = _nl_send_nlmsg (platform, nlmsg, NULL, NULL, DELAYED_ACTION_RESPONSE_TYPE_VOID, NULL);
 		if (nle < 0) {
@@ -5234,6 +5564,7 @@ _nl_msg_new_dump (NMPObjectType obj_type,
 	case NMP_OBJECT_TYPE_IP6_ADDRESS:
 	case NMP_OBJECT_TYPE_IP4_ROUTE:
 	case NMP_OBJECT_TYPE_IP6_ROUTE:
+	case NMP_OBJECT_TYPE_ROUTING_RULE:
 		{
 			const struct rtgenmsg gmsg = {
 				.rtgen_family = preferred_addr_family,
@@ -5254,42 +5585,68 @@ static void
 do_request_all_no_delayed_actions (NMPlatform *platform, DelayedActionType action_type)
 {
 	NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE (platform);
+	DelayedActionType action_type_prune;
 	DelayedActionType iflags;
 
 	nm_assert (!NM_FLAGS_ANY (action_type, ~DELAYED_ACTION_TYPE_REFRESH_ALL));
 	action_type &= DELAYED_ACTION_TYPE_REFRESH_ALL;
 
-	FOR_EACH_DELAYED_ACTION (iflags, action_type) {
+	action_type_prune = action_type;
+
+	/* calling nmp_cache_dirty_set_all_main() with a non-main lookup-index requires an extra
+	 * cache lookup for every entry.
+	 *
+	 * Avoid that, by special casing routing-rules here. */
+	if (NM_FLAGS_ALL (action_type_prune, DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL)) {
+		NMPLookup lookup;
+
+		priv->pruning[REFRESH_ALL_TYPE_ROUTING_RULES_IP4] += 1;
+		priv->pruning[REFRESH_ALL_TYPE_ROUTING_RULES_IP6] += 1;
+		nmp_lookup_init_obj_type (&lookup, NMP_OBJECT_TYPE_ROUTING_RULE);
+		nmp_cache_dirty_set_all_main (nm_platform_get_cache (platform),
+		                              &lookup);
+		action_type_prune &= ~DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL;
+	}
+
+	FOR_EACH_DELAYED_ACTION (iflags, action_type_prune) {
+		RefreshAllType refresh_all_type = delayed_action_type_to_refresh_all_type (iflags);
 		NMPLookup lookup;
 
-		priv->pruning[delayed_action_refresh_all_to_idx (iflags)] = TRUE;
-		nmp_lookup_init_obj_type (&lookup,
-		                          delayed_action_refresh_to_object_type (iflags));
-		nmp_cache_dirty_set_all (nm_platform_get_cache (platform),
-		                         &lookup);
+		priv->pruning[refresh_all_type] += 1;
+		refresh_all_type_init_lookup (refresh_all_type,
+		                              &lookup);
+		nmp_cache_dirty_set_all_main (nm_platform_get_cache (platform),
+		                              &lookup);
 	}
 
 	FOR_EACH_DELAYED_ACTION (iflags, action_type) {
-		NMPObjectType obj_type = delayed_action_refresh_to_object_type (iflags);
+		RefreshAllType refresh_all_type = delayed_action_type_to_refresh_all_type (iflags);
+		const RefreshAllInfo *refresh_all_info = refresh_all_type_get_info (refresh_all_type);
 		nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
 		int *out_refresh_all_in_progress;
 
-		out_refresh_all_in_progress = &priv->delayed_action.refresh_all_in_progress[delayed_action_refresh_all_to_idx (iflags)];
+		out_refresh_all_in_progress = &priv->delayed_action.refresh_all_in_progress[refresh_all_type];
 		nm_assert (*out_refresh_all_in_progress >= 0);
 		*out_refresh_all_in_progress += 1;
 
 		/* clear any delayed action that request a refresh of this object type. */
 		priv->delayed_action.flags &= ~iflags;
 		_LOGt_delayed_action (iflags, NULL, "handle (do-request-all)");
-		if (obj_type == NMP_OBJECT_TYPE_LINK) {
-			priv->delayed_action.flags &= ~DELAYED_ACTION_TYPE_REFRESH_LINK;
-			g_ptr_array_set_size (priv->delayed_action.list_refresh_link, 0);
-			_LOGt_delayed_action (DELAYED_ACTION_TYPE_REFRESH_LINK, NULL, "clear (do-request-all)");
+
+		if (refresh_all_type == REFRESH_ALL_TYPE_LINKS) {
+			nm_assert (   (priv->delayed_action.list_refresh_link->len > 0)
+			           == NM_FLAGS_HAS (priv->delayed_action.flags, DELAYED_ACTION_TYPE_REFRESH_LINK));
+			if (NM_FLAGS_HAS (priv->delayed_action.flags, DELAYED_ACTION_TYPE_REFRESH_LINK)) {
+				_LOGt_delayed_action (DELAYED_ACTION_TYPE_REFRESH_LINK, NULL, "clear (do-request-all)");
+				priv->delayed_action.flags &= ~DELAYED_ACTION_TYPE_REFRESH_LINK;
+				g_ptr_array_set_size (priv->delayed_action.list_refresh_link, 0);
+			}
 		}
 
 		event_handler_read_netlink (platform, FALSE);
 
-		nlmsg = _nl_msg_new_dump (obj_type, AF_UNSPEC);
+		nlmsg = _nl_msg_new_dump (refresh_all_info->obj_type,
+		                          refresh_all_info->addr_family);
 		if (!nlmsg)
 			goto next_after_fail;
 
@@ -5310,9 +5667,9 @@ next_after_fail:
 }
 
 static void
-do_request_one_type (NMPlatform *platform, NMPObjectType obj_type)
+do_request_one_type_by_needle_object (NMPlatform *platform, const NMPObject *obj_needle)
 {
-	do_request_all_no_delayed_actions (platform, delayed_action_refresh_from_object_type (obj_type));
+	do_request_all_no_delayed_actions (platform, delayed_action_refresh_from_needle_object (obj_needle));
 	delayed_action_handle_all (platform, FALSE);
 }
 
@@ -5398,9 +5755,27 @@ event_valid_msg (NMPlatform *platform, struct nl_msg *msg, gboolean handle_event
 
 	msghdr = nlmsg_hdr (msg);
 
-	if (   _support_kernel_extended_ifa_flags_still_undecided ()
-	    && msghdr->nlmsg_type == RTM_NEWADDR)
-		_support_kernel_extended_ifa_flags_detect (msg);
+	if (   !_nm_platform_kernel_support_detected (NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS)
+	    && msghdr->nlmsg_type == RTM_NEWADDR) {
+		/* IFA_FLAGS is set for IPv4 and IPv6 addresses. It was added first to IPv6,
+		 * but if we encounter an IPv4 address with IFA_FLAGS, we surely have support. */
+		if (   nlmsg_valid_hdr (msghdr, sizeof (struct ifaddrmsg))
+		    && NM_IN_SET (((struct ifaddrmsg *) nlmsg_data (msghdr))->ifa_family,
+		                  AF_INET,
+		                  AF_INET6)) {
+			/* see if the nl_msg contains the IFA_FLAGS attribute. If it does,
+			 * we assume, that the kernel supports extended flags, IFA_F_MANAGETEMPADDR
+			 * and IFA_F_NOPREFIXROUTE for IPv6. They were added together in kernel 3.14,
+			 * dated 30 March, 2014.
+			 *
+			 * For IPv4, IFA_F_NOPREFIXROUTE was added later, but there is no easy
+			 * way to detect kernel support. */
+			_nm_platform_kernel_support_init (NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS,
+			                                    !!nlmsg_find_attr (msghdr, sizeof (struct ifaddrmsg), IFA_FLAGS)
+			                                  ? 1
+			                                  : -1);
+		}
+	}
 
 	if (!handle_events)
 		return;
@@ -5408,6 +5783,7 @@ event_valid_msg (NMPlatform *platform, struct nl_msg *msg, gboolean handle_event
 	if (NM_IN_SET (msghdr->nlmsg_type, RTM_DELLINK,
 	                                   RTM_DELADDR,
 	                                   RTM_DELROUTE,
+	                                   RTM_DELRULE,
 	                                   RTM_DELQDISC,
 	                                   RTM_DELTFILTER)) {
 		/* The event notifies about a deleted object. We don't need to initialize all
@@ -5426,10 +5802,11 @@ event_valid_msg (NMPlatform *platform, struct nl_msg *msg, gboolean handle_event
 	    && NM_IN_SET (msghdr->nlmsg_type, RTM_NEWADDR,
 	                                      RTM_NEWLINK,
 	                                      RTM_NEWROUTE,
+	                                      RTM_NEWRULE,
 	                                      RTM_NEWQDISC,
 	                                      RTM_NEWTFILTER)) {
 		is_dump = delayed_action_refresh_all_in_progress (platform,
-		                                                  delayed_action_refresh_from_object_type (NMP_OBJECT_GET_TYPE (obj)));
+		                                                  delayed_action_refresh_from_needle_object (obj));
 	}
 
 	_LOGT ("event-notification: %s%s: %s",
@@ -5445,10 +5822,11 @@ event_valid_msg (NMPlatform *platform, struct nl_msg *msg, gboolean handle_event
 
 		switch (msghdr->nlmsg_type) {
 
-		case RTM_NEWLINK:
-		case RTM_NEWADDR:
 		case RTM_GETLINK:
+		case RTM_NEWADDR:
+		case RTM_NEWLINK:
 		case RTM_NEWQDISC:
+		case RTM_NEWRULE:
 		case RTM_NEWTFILTER:
 			cache_op = nmp_cache_update_netlink (cache, obj, is_dump, &obj_old, &obj_new);
 			if (cache_op != NMP_CACHE_OPS_UNCHANGED) {
@@ -5537,16 +5915,17 @@ event_valid_msg (NMPlatform *platform, struct nl_msg *msg, gboolean handle_event
 				 * netlink events. This needs investigation. */
 				_LOGT ("schedule resync of routes after RTM_NEWROUTE");
 				delayed_action_schedule (platform,
-				                         delayed_action_refresh_from_object_type (NMP_OBJECT_GET_TYPE (obj)),
+				                         delayed_action_refresh_from_needle_object (obj),
 				                         NULL);
 			}
 			break;
 		}
 
-		case RTM_DELLINK:
 		case RTM_DELADDR:
-		case RTM_DELROUTE:
+		case RTM_DELLINK:
 		case RTM_DELQDISC:
+		case RTM_DELROUTE:
+		case RTM_DELRULE:
 		case RTM_DELTFILTER:
 			cache_op = nmp_cache_remove_netlink (cache, obj, &obj_old, &obj_new);
 			if (cache_op != NMP_CACHE_OPS_UNCHANGED) {
@@ -5657,7 +6036,7 @@ do_add_addrroute (NMPlatform *platform,
 		 *
 		 * rh#1484434 */
 		if (!nmp_cache_lookup_obj (nm_platform_get_cache (platform), obj_id))
-			do_request_one_type (platform, NMP_OBJECT_GET_TYPE (obj_id));
+			do_request_one_type_by_needle_object (platform, obj_id);
 	}
 
 	return wait_for_nl_response_to_nmerr (seq_result);
@@ -5722,7 +6101,7 @@ do_delete_object (NMPlatform *platform, const NMPObject *obj_id, struct nl_msg *
 		 *
 		 * rh#1484434 */
 		if (nmp_cache_lookup_obj (nm_platform_get_cache (platform), obj_id))
-			do_request_one_type (platform, NMP_OBJECT_GET_TYPE (obj_id));
+			do_request_one_type_by_needle_object (platform, obj_id);
 	}
 
 	return success;
@@ -5845,9 +6224,7 @@ link_add (NMPlatform *platform,
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          NLM_F_CREATE | NLM_F_EXCL,
 	                          0,
-	                          name,
-	                          0,
-	                          0);
+	                          name);
 	if (!nlmsg)
 		return -NME_UNSPEC;
 
@@ -5876,9 +6253,7 @@ link_delete (NMPlatform *platform, int ifindex)
 	nlmsg = _nl_msg_new_link (RTM_DELLINK,
 	                          0,
 	                          ifindex,
-	                          NULL,
-	                          0,
-	                          0);
+	                          NULL);
 
 	nmp_object_stackinit_id_link (&obj_id, ifindex);
 	return do_delete_object (platform, &obj_id, nlmsg);
@@ -5891,12 +6266,6 @@ link_refresh (NMPlatform *platform, int ifindex)
 	return !!nm_platform_link_get_obj (platform, ifindex, TRUE);
 }
 
-static void
-refresh_all (NMPlatform *platform, NMPObjectType obj_type)
-{
-	do_request_one_type (platform, obj_type);
-}
-
 static gboolean
 link_set_netns (NMPlatform *platform,
                 int ifindex,
@@ -5907,9 +6276,7 @@ link_set_netns (NMPlatform *platform,
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          0,
 	                          ifindex,
-	                          NULL,
-	                          0,
-	                          0);
+	                          NULL);
 	if (!nlmsg)
 		return FALSE;
 
@@ -5936,12 +6303,13 @@ link_change_flags (NMPlatform *platform,
 	       nm_platform_link_flags2str (flags_set, s_flags, sizeof (s_flags)),
 	       nm_platform_link_flags2str (flags_mask, NULL, 0));
 
-	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
-	                          0,
-	                          ifindex,
-	                          NULL,
-	                          flags_mask,
-	                          flags_set);
+	nlmsg = _nl_msg_new_link_full (RTM_NEWLINK,
+	                               0,
+	                               ifindex,
+	                               NULL,
+	                               AF_UNSPEC,
+	                               flags_mask,
+	                               flags_set);
 	if (!nlmsg)
 		return -NME_UNSPEC;
 	return do_change_link (platform, CHANGE_LINK_TYPE_UNSPEC, ifindex, nlmsg, NULL);
@@ -5997,7 +6365,7 @@ link_set_user_ipv6ll_enabled (NMPlatform *platform, int ifindex, gboolean enable
 	       ifindex,
 	       nm_platform_link_inet6_addrgenmode2str (mode, NULL, 0));
 
-	if (!_support_user_ipv6ll_get ()) {
+	if (!nm_platform_kernel_support_get (NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL)) {
 		_LOGD ("link: change %d: user-ipv6ll: not supported", ifindex);
 		return -NME_PL_OPNOTSUPP;
 	}
@@ -6005,9 +6373,7 @@ link_set_user_ipv6ll_enabled (NMPlatform *platform, int ifindex, gboolean enable
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          0,
 	                          ifindex,
-	                          NULL,
-	                          0,
-	                          0);
+	                          NULL);
 	if (   !nlmsg
 	    || !_nl_msg_new_link_set_afspec (nlmsg, mode, NULL))
 		g_return_val_if_reached (-NME_BUG);
@@ -6024,7 +6390,7 @@ link_set_token (NMPlatform *platform, int ifindex, NMUtilsIPv6IfaceId iid)
 	_LOGD ("link: change %d: token: set IPv6 address generation token to %s",
 	       ifindex, nm_utils_inet6_interface_identifier_to_token (iid, sbuf));
 
-	nlmsg = _nl_msg_new_link (RTM_NEWLINK, 0, ifindex, NULL, 0, 0);
+	nlmsg = _nl_msg_new_link (RTM_NEWLINK, 0, ifindex, NULL);
 	if (!nlmsg || !_nl_msg_new_link_set_afspec (nlmsg, -1, &iid))
 		g_return_val_if_reached (FALSE);
 
@@ -6105,9 +6471,7 @@ link_set_address (NMPlatform *platform, int ifindex, gconstpointer address, size
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          0,
 	                          ifindex,
-	                          NULL,
-	                          0,
-	                          0);
+	                          NULL);
 	if (!nlmsg)
 		g_return_val_if_reached (-NME_BUG);
 
@@ -6126,9 +6490,7 @@ link_set_name (NMPlatform *platform, int ifindex, const char *name)
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          0,
 	                          ifindex,
-	                          NULL,
-	                          0,
-	                          0);
+	                          NULL);
 	if (!nlmsg)
 		g_return_val_if_reached (-NME_BUG);
 
@@ -6161,9 +6523,7 @@ link_set_mtu (NMPlatform *platform, int ifindex, guint32 mtu)
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          0,
 	                          ifindex,
-	                          NULL,
-	                          0,
-	                          0);
+	                          NULL);
 	if (!nlmsg)
 		return FALSE;
 
@@ -6224,6 +6584,14 @@ link_set_sriov_params (NMPlatform *platform,
 	                                                                                  ifname,
 	                                                                                  "device/sriov_drivers_autoprobe"),
 	                                                        10, 0, 1, -1);
+
+	if (   current_autoprobe == -1
+	    && errno == ENOENT) {
+		/* older kernel versions don't have this sysctl. Assume the value is
+		 * "1". */
+		current_autoprobe = 1;
+	}
+
 	if (   current_num == num_vfs
 	    && (autoprobe == NM_TERNARY_DEFAULT || current_autoprobe == autoprobe))
 		return TRUE;
@@ -6279,9 +6647,7 @@ link_set_sriov_vfs (NMPlatform *platform, int ifindex, const NMPlatformVF *const
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          0,
 	                          ifindex,
-	                          NULL,
-	                          0,
-	                          0);
+	                          NULL);
 	if (!nlmsg)
 		g_return_val_if_reached (-NME_BUG);
 
@@ -6364,6 +6730,74 @@ nla_put_failure:
 	g_return_val_if_reached (FALSE);
 }
 
+static gboolean
+link_set_bridge_vlans (NMPlatform *platform,
+                       int ifindex,
+                       gboolean on_master,
+                       const NMPlatformBridgeVlan *const *vlans)
+{
+	nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
+	struct nlattr *list;
+	struct bridge_vlan_info vinfo = { };
+	guint i;
+
+	nlmsg = _nl_msg_new_link_full (vlans ? RTM_SETLINK : RTM_DELLINK,
+	                               0,
+	                               ifindex,
+	                               NULL,
+	                               AF_BRIDGE,
+	                               0,
+	                               0);
+	if (!nlmsg)
+		g_return_val_if_reached (-NME_BUG);
+
+	if (!(list = nla_nest_start (nlmsg, IFLA_AF_SPEC)))
+		goto nla_put_failure;
+
+	NLA_PUT_U16 (nlmsg,
+	             IFLA_BRIDGE_FLAGS,
+	             on_master ? BRIDGE_FLAGS_MASTER : BRIDGE_FLAGS_SELF);
+
+	if (vlans) {
+		/* Add VLANs */
+		for (i = 0; vlans[i]; i++) {
+			const NMPlatformBridgeVlan *vlan = vlans[i];
+			gboolean is_range = vlan->vid_start != vlan->vid_end;
+
+			vinfo.vid = vlan->vid_start;
+			vinfo.flags = is_range ? BRIDGE_VLAN_INFO_RANGE_BEGIN : 0;
+
+			if (vlan->untagged)
+				vinfo.flags |= BRIDGE_VLAN_INFO_UNTAGGED;
+			if (vlan->pvid)
+				vinfo.flags |= BRIDGE_VLAN_INFO_PVID;
+
+			NLA_PUT (nlmsg, IFLA_BRIDGE_VLAN_INFO, sizeof (vinfo), &vinfo);
+
+			if (is_range) {
+				vinfo.vid = vlan->vid_end;
+				vinfo.flags = BRIDGE_VLAN_INFO_RANGE_END;
+				NLA_PUT (nlmsg, IFLA_BRIDGE_VLAN_INFO, sizeof (vinfo), &vinfo);
+			}
+		}
+	} else {
+		/* Flush existing VLANs */
+		vinfo.vid = 1;
+		vinfo.flags = BRIDGE_VLAN_INFO_RANGE_BEGIN;
+		NLA_PUT (nlmsg, IFLA_BRIDGE_VLAN_INFO, sizeof (vinfo), &vinfo);
+
+		vinfo.vid = 4094;
+		vinfo.flags = BRIDGE_VLAN_INFO_RANGE_END;
+		NLA_PUT (nlmsg, IFLA_BRIDGE_VLAN_INFO, sizeof (vinfo), &vinfo);
+	}
+
+	nla_nest_end (nlmsg, list);
+
+	return (do_change_link (platform, CHANGE_LINK_TYPE_UNSPEC, ifindex, nlmsg, NULL) >= 0);
+nla_put_failure:
+	g_return_val_if_reached (FALSE);
+}
+
 static char *
 link_get_physical_port_id (NMPlatform *platform, int ifindex)
 {
@@ -6409,9 +6843,7 @@ vlan_add (NMPlatform *platform,
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          NLM_F_CREATE | NLM_F_EXCL,
 	                          0,
-	                          name,
-	                          0,
-	                          0);
+	                          name);
 	if (!nlmsg)
 		return FALSE;
 
@@ -6445,9 +6877,7 @@ link_gre_add (NMPlatform *platform,
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          NLM_F_CREATE | NLM_F_EXCL,
 	                          0,
-	                          name,
-	                          0,
-	                          0);
+	                          name);
 	if (!nlmsg)
 		return FALSE;
 
@@ -6497,9 +6927,7 @@ link_ip6tnl_add (NMPlatform *platform,
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          NLM_F_CREATE | NLM_F_EXCL,
 	                          0,
-	                          name,
-	                          0,
-	                          0);
+	                          name);
 	if (!nlmsg)
 		return FALSE;
 
@@ -6553,9 +6981,7 @@ link_ip6gre_add (NMPlatform *platform,
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          NLM_F_CREATE | NLM_F_EXCL,
 	                          0,
-	                          name,
-	                          0,
-	                          0);
+	                          name);
 	if (!nlmsg)
 		return FALSE;
 
@@ -6612,9 +7038,7 @@ link_ipip_add (NMPlatform *platform,
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          NLM_F_CREATE | NLM_F_EXCL,
 	                          0,
-	                          name,
-	                          0,
-	                          0);
+	                          name);
 	if (!nlmsg)
 		return FALSE;
 
@@ -6656,9 +7080,7 @@ link_macsec_add (NMPlatform *platform,
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          NLM_F_CREATE | NLM_F_EXCL,
 	                          0,
-	                          name,
-	                          0,
-	                          0);
+	                          name);
 	if (!nlmsg)
 		return FALSE;
 
@@ -6713,9 +7135,7 @@ link_macvlan_add (NMPlatform *platform,
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          NLM_F_CREATE | NLM_F_EXCL,
 	                          0,
-	                          name,
-	                          0,
-	                          0);
+	                          name);
 	if (!nlmsg)
 		return FALSE;
 
@@ -6755,9 +7175,7 @@ link_sit_add (NMPlatform *platform,
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          NLM_F_CREATE | NLM_F_EXCL,
 	                          0,
-	                          name,
-	                          0,
-	                          0);
+	                          name);
 	if (!nlmsg)
 		return FALSE;
 
@@ -6857,9 +7275,7 @@ link_vxlan_add (NMPlatform *platform,
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          NLM_F_CREATE | NLM_F_EXCL,
 	                          0,
-	                          name,
-	                          0,
-	                          0);
+	                          name);
 	if (!nlmsg)
 		return FALSE;
 
@@ -6923,9 +7339,7 @@ link_6lowpan_add (NMPlatform *platform,
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          NLM_F_CREATE | NLM_F_EXCL,
 	                          0,
-	                          name,
-	                          0,
-	                          0);
+	                          name);
 	if (!nlmsg)
 		return FALSE;
 
@@ -7073,9 +7487,7 @@ link_vlan_change (NMPlatform *platform,
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          0,
 	                          ifindex,
-	                          NULL,
-	                          0,
-	                          0);
+	                          NULL);
 	if (   !nlmsg
 	    || !_nl_msg_new_link_set_linkinfo_vlan (nlmsg,
 	                                            -1,
@@ -7099,9 +7511,7 @@ link_enslave (NMPlatform *platform, int master, int slave)
 	nlmsg = _nl_msg_new_link (RTM_NEWLINK,
 	                          0,
 	                          ifindex,
-	                          NULL,
-	                          0,
-	                          0);
+	                          NULL);
 	if (!nlmsg)
 		return FALSE;
 
@@ -7606,6 +8016,9 @@ object_delete (NMPlatform *platform,
 	case NMP_OBJECT_TYPE_IP6_ROUTE:
 		nlmsg = _nl_msg_new_route (RTM_DELROUTE, 0, obj);
 		break;
+	case NMP_OBJECT_TYPE_ROUTING_RULE:
+		nlmsg = _nl_msg_new_routing_rule (RTM_DELRULE, 0, NMP_OBJECT_CAST_ROUTING_RULE (obj));
+		break;
 	case NMP_OBJECT_TYPE_QDISC:
 		nlmsg = _nl_msg_new_qdisc (RTM_DELQDISC, 0, NMP_OBJECT_CAST_QDISC (obj));
 		break;
@@ -7703,6 +8116,47 @@ ip_route_get (NMPlatform *platform,
 /*****************************************************************************/
 
 static int
+routing_rule_add (NMPlatform *platform,
+                  NMPNlmFlags flags,
+                  const NMPlatformRoutingRule *routing_rule)
+{
+	WaitForNlResponseResult seq_result = WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN;
+	nm_auto_nlmsg struct nl_msg *msg = NULL;
+	gs_free char *errmsg = NULL;
+	char s_buf[256];
+	int nle;
+
+	msg = _nl_msg_new_routing_rule (RTM_NEWRULE, flags, routing_rule);
+
+	event_handler_read_netlink (platform, FALSE);
+
+	nle = _nl_send_nlmsg (platform, msg, &seq_result, &errmsg, DELAYED_ACTION_RESPONSE_TYPE_VOID, NULL);
+	if (nle < 0) {
+		_LOGE ("do-add-rule: failed sending netlink request \"%s\" (%d)",
+		      nm_strerror (nle), -nle);
+		return -NME_PL_NETLINK;
+	}
+
+	delayed_action_handle_all (platform, FALSE);
+
+	nm_assert (seq_result);
+
+	_NMLOG (seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK
+	            ? LOGL_DEBUG
+	            : LOGL_WARN,
+	        "do-add-rule: %s",
+	        wait_for_nl_response_to_string (seq_result, errmsg, s_buf, sizeof (s_buf)));
+
+	if (seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK)
+		return 0;
+	if (seq_result < 0)
+		return seq_result;
+	return -NME_UNSPEC;
+}
+
+/*****************************************************************************/
+
+static int
 qdisc_add (NMPlatform *platform,
            NMPNlmFlags flags,
            const NMPlatformQdisc *qdisc)
@@ -7736,7 +8190,8 @@ qdisc_add (NMPlatform *platform,
 
 	if (seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK)
 		return 0;
-
+	if (seq_result < 0)
+		return seq_result;
 	return -NME_UNSPEC;
 }
 
@@ -8053,6 +8508,7 @@ event_handler_read_netlink (NMPlatform *platform, gboolean wait_for_acks)
 					                         DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES |
 					                         DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES |
 					                         DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES |
+					                         DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL |
 					                         DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS |
 					                         DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS,
 					                         NULL);
@@ -8318,9 +8774,13 @@ constructed (GObject *_object)
 	g_assert (!nle);
 
 	nle = nl_socket_add_memberships (priv->nlh,
+	                                 RTNLGRP_IPV4_IFADDR,
+	                                 RTNLGRP_IPV4_ROUTE,
+	                                 RTNLGRP_IPV4_RULE,
+	                                 RTNLGRP_IPV6_RULE,
+	                                 RTNLGRP_IPV6_IFADDR,
+	                                 RTNLGRP_IPV6_ROUTE,
 	                                 RTNLGRP_LINK,
-	                                 RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV6_IFADDR,
-	                                 RTNLGRP_IPV4_ROUTE,  RTNLGRP_IPV6_ROUTE,
 	                                 RTNLGRP_TC,
 	                                 0);
 	g_assert (!nle);
@@ -8347,6 +8807,7 @@ constructed (GObject *_object)
 	                         DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ADDRESSES |
 	                         DELAYED_ACTION_TYPE_REFRESH_ALL_IP4_ROUTES |
 	                         DELAYED_ACTION_TYPE_REFRESH_ALL_IP6_ROUTES |
+	                         DELAYED_ACTION_TYPE_REFRESH_ALL_ROUTING_RULES_ALL |
 	                         DELAYED_ACTION_TYPE_REFRESH_ALL_QDISCS |
 	                         DELAYED_ACTION_TYPE_REFRESH_ALL_TFILTERS,
 	                         NULL);
@@ -8457,7 +8918,6 @@ nm_linux_platform_class_init (NMLinuxPlatformClass *klass)
 	platform_class->link_add = link_add;
 	platform_class->link_delete = link_delete;
 
-	platform_class->refresh_all = refresh_all;
 	platform_class->link_refresh = link_refresh;
 
 	platform_class->link_set_netns = link_set_netns;
@@ -8478,6 +8938,7 @@ nm_linux_platform_class_init (NMLinuxPlatformClass *klass)
 	platform_class->link_set_name = link_set_name;
 	platform_class->link_set_sriov_params = link_set_sriov_params;
 	platform_class->link_set_sriov_vfs = link_set_sriov_vfs;
+	platform_class->link_set_bridge_vlans = link_set_bridge_vlans;
 
 	platform_class->link_get_physical_port_id = link_get_physical_port_id;
 	platform_class->link_get_dev_id = link_get_dev_id;
@@ -8543,11 +9004,11 @@ nm_linux_platform_class_init (NMLinuxPlatformClass *klass)
 	platform_class->ip_route_add = ip_route_add;
 	platform_class->ip_route_get = ip_route_get;
 
+	platform_class->routing_rule_add = routing_rule_add;
+
 	platform_class->qdisc_add = qdisc_add;
 	platform_class->tfilter_add = tfilter_add;
 
-	platform_class->check_kernel_support = check_kernel_support;
-
 	platform_class->process_events = process_events;
 }
 
diff --git a/src/platform/nm-netlink.h b/src/platform/nm-netlink.h
index 094a3c6f..b12fd243 100644
--- a/src/platform/nm-netlink.h
+++ b/src/platform/nm-netlink.h
@@ -25,7 +25,7 @@
 #include <linux/rtnetlink.h>
 #include <linux/genetlink.h>
 
-#include "nm-utils/unaligned.h"
+#include "nm-std-aux/unaligned.h"
 
 /*****************************************************************************/
 
diff --git a/src/platform/nm-platform-utils.c b/src/platform/nm-platform-utils.c
index 93cd0b3c..456871a7 100644
--- a/src/platform/nm-platform-utils.c
+++ b/src/platform/nm-platform-utils.c
@@ -35,7 +35,7 @@
 
 #include "nm-utils.h"
 #include "nm-setting-wired.h"
-#include "nm-ethtool-utils.h"
+#include "nm-libnm-core-intern/nm-ethtool-utils.h"
 
 #include "nm-core-utils.h"
 
diff --git a/src/platform/nm-platform-utils.h b/src/platform/nm-platform-utils.h
index bad77aee..f5491679 100644
--- a/src/platform/nm-platform-utils.h
+++ b/src/platform/nm-platform-utils.h
@@ -23,7 +23,7 @@
 
 #include "nm-platform.h"
 #include "nm-setting-wired.h"
-#include "nm-ethtool-utils.h"
+#include "nm-libnm-core-intern/nm-ethtool-utils.h"
 
 /*****************************************************************************/
 
diff --git a/src/platform/nm-platform.c b/src/platform/nm-platform.c
index fe0cb662..1fc0ccb7 100644
--- a/src/platform/nm-platform.c
+++ b/src/platform/nm-platform.c
@@ -28,6 +28,7 @@
 #include <arpa/inet.h>
 #include <sys/socket.h>
 #include <netdb.h>
+#include <linux/fib_rules.h>
 #include <linux/ip.h>
 #include <linux/if.h>
 #include <linux/if_tun.h>
@@ -37,9 +38,9 @@
 
 #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-utils/nm-secret-utils.h"
+#include "nm-glib-aux/nm-dedup-multi.h"
+#include "nm-udev-aux/nm-udev-utils.h"
+#include "nm-glib-aux/nm-secret-utils.h"
 
 #include "nm-core-utils.h"
 #include "nm-platform-utils.h"
@@ -118,9 +119,6 @@ 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;
@@ -276,40 +274,99 @@ NM_UTILS_LOOKUP_STR_DEFINE_STATIC (_nmp_nlm_flag_to_string_lookup, NMPNlmFlags,
 
 /*****************************************************************************/
 
-NMPlatformKernelSupportFlags
-nm_platform_check_kernel_support (NMPlatform *self,
-                                  NMPlatformKernelSupportFlags request_flags)
+volatile int _nm_platform_kernel_support_state[_NM_PLATFORM_KERNEL_SUPPORT_NUM] = { };
+
+static const struct {
+	bool compile_time_default;
+	const char *name;
+	const char *desc;
+} _nm_platform_kernel_support_info[_NM_PLATFORM_KERNEL_SUPPORT_NUM] = {
+	[NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS] = {
+		.compile_time_default = TRUE,
+		.name                 = "EXTENDED_IFA_FLAGS",
+		.desc                 = "IPv6 temporary addresses support",
+	},
+	[NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL] = {
+		.compile_time_default = TRUE,
+		.name                 = "USER_IPV6LL",
+		.desc                 = "IFLA_INET6_ADDR_GEN_MODE support",
+	},
+	[NM_PLATFORM_KERNEL_SUPPORT_TYPE_RTA_PREF] = {
+		.compile_time_default = (RTA_MAX >= 20 /* RTA_PREF */),
+		.name                 = "RTA_PREF",
+		.desc                 = "ability to set router preference for IPv6 routes",
+	},
+	[NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_L3MDEV] = {
+		.compile_time_default = (FRA_MAX >= 19 /* FRA_L3MDEV */),
+		.name                 = "FRA_L3MDEV",
+		.desc                 = "FRA_L3MDEV attribute for policy routing rules",
+	},
+	[NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_UID_RANGE] = {
+		.compile_time_default = (FRA_MAX >= 20 /* FRA_UID_RANGE */),
+		.name                 = "FRA_UID_RANGE",
+		.desc                 = "FRA_UID_RANGE attribute for policy routing rules",
+	},
+	[NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_PROTOCOL] = {
+		.compile_time_default = (FRA_MAX >= 21 /* FRA_PROTOCOL */),
+		.name                 = "FRA_PROTOCOL",
+		.desc                 = "FRA_PROTOCOL attribute for policy routing rules",
+	},
+	[NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_IP_PROTO] = {
+		.compile_time_default = (FRA_MAX >= 22 /* FRA_IP_PROTO */),
+		.name                 = "FRA_IP_PROTO",
+		.desc                 = "FRA_IP_PROTO, FRA_SPORT_RANGE, FRA_DPORT_RANGE attributes for policy routing rules",
+	},
+};
+
+int
+_nm_platform_kernel_support_init (NMPlatformKernelSupportType type,
+                                  int value)
 {
-	NMPlatformPrivate *priv;
+	volatile int *p_state;
+	gboolean set_default = FALSE;
 
-	_CHECK_SELF (self, klass, TRUE);
+	nm_assert (_NM_INT_NOT_NEGATIVE (type) && type < G_N_ELEMENTS (_nm_platform_kernel_support_state));
 
-	priv = NM_PLATFORM_GET_PRIVATE (self);
+	p_state = &_nm_platform_kernel_support_state[type];
 
-	/* 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 (value == 0) {
+		set_default = TRUE;
+		value =   _nm_platform_kernel_support_info[type].compile_time_default
+		        ? 1
+		        : -1;
+	}
 
-		checked = request_flags & ~priv->support_checked;
-		nm_assert (checked);
+	nm_assert (NM_IN_SET (value, -1, 1));
 
-		if (klass->check_kernel_support)
-			response = klass->check_kernel_support (self, checked);
-		else {
-			/* fake platform. Pretend no support for anything. */
-			response = 0;
-		}
+	if (!g_atomic_int_compare_and_exchange (p_state, 0, value)) {
+		value = g_atomic_int_get (p_state);
+		nm_assert (NM_IN_SET (value, -1, 1));
+		return value;
+	}
 
-		priv->support_checked |= checked;
-		priv->support_present = (priv->support_present & ~checked) | (response & checked);
+#undef NM_THREAD_SAFE_ON_MAIN_THREAD
+#define NM_THREAD_SAFE_ON_MAIN_THREAD 0
+
+	if (set_default) {
+		nm_log_dbg (LOGD_PLATFORM, "platform: kernel-support for %s (%s) not detected: assume %ssupported",
+		             _nm_platform_kernel_support_info[type].name,
+		             _nm_platform_kernel_support_info[type].desc,
+		             value >= 0 ? "" : "not ");
+	} else {
+		nm_log_dbg (LOGD_PLATFORM, "platform: kernel-support for %s (%s) detected: %ssupported",
+		            _nm_platform_kernel_support_info[type].name,
+		            _nm_platform_kernel_support_info[type].desc,
+		            value >= 0 ? "" : "not ");
 	}
 
-	return priv->support_present & request_flags;
+#undef NM_THREAD_SAFE_ON_MAIN_THREAD
+#define NM_THREAD_SAFE_ON_MAIN_THREAD 1
+
+	return value;
 }
 
+/*****************************************************************************/
+
 /**
  * nm_platform_process_events:
  * @self: platform instance
@@ -468,6 +525,8 @@ nm_platform_sysctl_ip_conf_set_ipv6_hop_limit_safe (NMPlatform *self,
  * @path: Absolute path to sysctl
  *
  * Returns: (transfer full): Contents of the virtual sysctl file.
+ *
+ * If the path does not exist, %NULL is returned and %errno set to %ENOENT.
  */
 char *
 nm_platform_sysctl_get (NMPlatform *self, const char *pathid, int dirfd, const char *path)
@@ -517,6 +576,8 @@ nm_platform_sysctl_get_int32 (NMPlatform *self, const char *pathid, int dirfd, c
  * value. On success, %errno will be set to zero. The returned value
  * will always be in the range between @min and @max
  * (inclusive) or @fallback.
+ * If the file does not exist, the fallback is returned and %errno
+ * is set to ENOENT.
  */
 gint64
 nm_platform_sysctl_get_int_checked (NMPlatform *self,
@@ -536,11 +597,17 @@ nm_platform_sysctl_get_int_checked (NMPlatform *self,
 
 	g_return_val_if_fail (path, fallback);
 
-	if (path)
-		value = nm_platform_sysctl_get (self, pathid, dirfd, path);
+	if (!path) {
+		errno = EINVAL;
+		return fallback;
+	}
 
+	value = nm_platform_sysctl_get (self, pathid, dirfd, path);
 	if (!value) {
-		errno = EINVAL;
+		/* nm_platform_sysctl_get() set errno to ENOENT if the file does not exist.
+		 * Propagate/preserve that. */
+		if (errno != ENOENT)
+			errno = EINVAL;
 		return fallback;
 	}
 
@@ -1189,21 +1256,6 @@ nm_platform_link_supports_slaves (NMPlatform *self, int ifindex)
 }
 
 /**
- * nm_platform_refresh_all:
- * @self: platform instance
- * @obj_type: The object type to request.
- *
- * Resync and re-request all objects from kernel of a certain @obj_type.
- */
-void
-nm_platform_refresh_all (NMPlatform *self, NMPObjectType obj_type)
-{
-	_CHECK_SELF_VOID (self, klass);
-
-	klass->refresh_all (self, obj_type);
-}
-
-/**
  * nm_platform_link_refresh:
  * @self: platform instance
  * @ifindex: Interface index
@@ -1532,6 +1584,28 @@ nm_platform_link_set_sriov_vfs (NMPlatform *self, int ifindex, const NMPlatformV
 	return klass->link_set_sriov_vfs (self, ifindex, vfs);
 }
 
+gboolean
+nm_platform_link_set_bridge_vlans (NMPlatform *self, int ifindex, gboolean on_master, const NMPlatformBridgeVlan *const *vlans)
+{
+	guint i;
+	_CHECK_SELF (self, klass, FALSE);
+
+	g_return_val_if_fail (ifindex > 0, FALSE);
+
+	_LOG3D ("link: %s bridge VLANs on %s",
+	        vlans ? "setting" : "clearing",
+	        on_master ? "master" : "self");
+	if (vlans) {
+		for (i = 0; vlans[i]; i++) {
+			const NMPlatformBridgeVlan *vlan = vlans[i];
+
+			_LOG3D ("link:   bridge VLAN %s", nm_platform_bridge_vlan_to_string (vlan, NULL, 0));
+		}
+	}
+
+	return klass->link_set_bridge_vlans (self, ifindex, on_master, vlans);
+}
+
 /**
  * nm_platform_link_set_up:
  * @self: platform instance
@@ -3894,7 +3968,7 @@ nm_platform_ip4_address_sync (NMPlatform *self,
 	if (!known_addresses)
 		return TRUE;
 
-	ifa_flags =   nm_platform_check_kernel_support (self, NM_PLATFORM_KERNEL_SUPPORT_EXTENDED_IFA_FLAGS)
+	ifa_flags =   nm_platform_kernel_support_get (NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS)
 	            ? IFA_F_NOPREFIXROUTE
 	            : 0;
 
@@ -4094,7 +4168,7 @@ next_plat:
 	if (!known_addresses)
 		return TRUE;
 
-	ifa_flags =   nm_platform_check_kernel_support (self, NM_PLATFORM_KERNEL_SUPPORT_EXTENDED_IFA_FLAGS)
+	ifa_flags =   nm_platform_kernel_support_get (NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS)
 	            ? IFA_F_NOPREFIXROUTE
 	            : 0;
 
@@ -4264,14 +4338,13 @@ nm_platform_ip_route_sync (NMPlatform *self,
 	gboolean success = TRUE;
 	char sbuf1[sizeof (_nm_utils_to_string_buffer)];
 	char sbuf2[sizeof (_nm_utils_to_string_buffer)];
+	const gboolean IS_IPv4 = (addr_family == AF_INET);
 
 	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;
+	vt = &nm_platform_vtable_route.vx[IS_IPv4];
 
 	for (i_type = 0; routes && i_type < 2; i_type++) {
 		for (i = 0; i < routes->len; i++) {
@@ -4578,7 +4651,7 @@ _ip_route_add (NMPlatform *self,
 	nm_assert (route);
 	nm_assert (NM_IN_SET (addr_family, AF_INET, AF_INET6));
 
-	ifindex = ((NMPlatformObject *)route)->ifindex;
+	ifindex = ((const NMPlatformIPRoute *) route)->ifindex;
 	_LOG3D ("route: %-10s IPv%c route: %s",
 	        _nmp_nlm_flag_to_string (flags & NMP_NLM_FLAG_FMASK),
 	        nm_utils_addr_family_to_char (addr_family),
@@ -4630,18 +4703,28 @@ gboolean
 nm_platform_object_delete (NMPlatform *self,
                            const NMPObject *obj)
 {
-	int ifindex = obj->object.ifindex;
+	int ifindex;
+
 	_CHECK_SELF (self, klass, FALSE);
 
-	if (!NM_IN_SET (NMP_OBJECT_GET_TYPE (obj), NMP_OBJECT_TYPE_IP4_ROUTE,
-	                                           NMP_OBJECT_TYPE_IP6_ROUTE,
-	                                           NMP_OBJECT_TYPE_QDISC,
-	                                           NMP_OBJECT_TYPE_TFILTER))
+	switch (NMP_OBJECT_GET_TYPE (obj)) {
+	case NMP_OBJECT_TYPE_ROUTING_RULE:
+		_LOGD ("%s: delete %s",
+		       NMP_OBJECT_GET_CLASS (obj)->obj_type_name,
+		       nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+		break;
+	case NMP_OBJECT_TYPE_IP4_ROUTE:
+	case NMP_OBJECT_TYPE_IP6_ROUTE:
+	case NMP_OBJECT_TYPE_QDISC:
+	case NMP_OBJECT_TYPE_TFILTER:
+		ifindex = NMP_OBJECT_CAST_OBJ_WITH_IFINDEX (obj)->ifindex;
+		_LOG3D ("%s: delete %s",
+		        NMP_OBJECT_GET_CLASS (obj)->obj_type_name,
+		        nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+		break;
+	default:
 		g_return_val_if_reached (FALSE);
-
-	_LOG3D ("%s: delete %s",
-	        NMP_OBJECT_GET_CLASS (obj)->obj_type_name,
-	        nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+	}
 
 	return klass->object_delete (self, obj);
 }
@@ -4971,6 +5054,21 @@ nm_platform_ip4_dev_route_blacklist_set (NMPlatform *self,
 /*****************************************************************************/
 
 int
+nm_platform_routing_rule_add (NMPlatform *self,
+                              NMPNlmFlags flags,
+                              const NMPlatformRoutingRule *routing_rule)
+{
+	_CHECK_SELF (self, klass, -NME_BUG);
+
+	g_return_val_if_fail (routing_rule, -NME_BUG);
+
+	_LOGD ("routing-rule: adding or updating: %s", nm_platform_routing_rule_to_string (routing_rule, NULL, 0));
+	return klass->routing_rule_add (self, flags, routing_rule);
+}
+
+/*****************************************************************************/
+
+int
 nm_platform_qdisc_add (NMPlatform *self,
                        NMPNlmFlags flags,
                        const NMPlatformQdisc *qdisc)
@@ -5276,7 +5374,7 @@ nm_platform_lnk_gre_to_string (const NMPlatformLnkGre *lnk, char *buf, gsize len
 		return buf;
 
 	g_snprintf (buf, len,
-	            lnk->is_tap ? "gretap" : "gre"
+	            "gre%s" /* is_tap */
 	            "%s" /* remote */
 	            "%s" /* local */
 	            "%s" /* parent_ifindex */
@@ -5288,6 +5386,7 @@ nm_platform_lnk_gre_to_string (const NMPlatformLnkGre *lnk, char *buf, gsize len
 	            "%s" /* ikey */
 	            "%s" /* okey */
 	            "",
+	            lnk->is_tap ? "tap" : "",
 	            lnk->remote ? nm_sprintf_buf (str_remote, " remote %s", nm_utils_inet4_ntop (lnk->remote, str_remote1)) : "",
 	            lnk->local ? nm_sprintf_buf (str_local, " local %s", nm_utils_inet4_ntop (lnk->local, str_local1)) : "",
 	            lnk->parent_ifindex ? nm_sprintf_buf (str_parent_ifindex, " dev %d", lnk->parent_ifindex) : "",
@@ -6076,6 +6175,253 @@ nm_platform_ip6_route_to_string (const NMPlatformIP6Route *route, char *buf, gsi
 	return buf;
 }
 
+static void
+_routing_rule_addr_to_string (char **buf,
+                              gsize *len,
+                              int addr_family,
+                              const NMIPAddr *addr,
+                              guint8 plen,
+                              gboolean is_src)
+{
+	char s_addr[NM_UTILS_INET_ADDRSTRLEN];
+	gboolean is_zero;
+	gsize addr_size;
+
+	nm_assert_addr_family (addr_family);
+	nm_assert (addr);
+
+	addr_size = nm_utils_addr_family_to_size (addr_family);
+
+	is_zero = nm_utils_memeqzero (addr, addr_size);
+
+	if (   plen == 0
+	    && is_zero) {
+		if (is_src)
+			nm_utils_strbuf_append_str (buf, len, " from all");
+		else
+			nm_utils_strbuf_append_str (buf, len, "");
+		return;
+	}
+
+	nm_utils_strbuf_append_str (buf, len, is_src ? " from " : " to ");
+
+	nm_utils_strbuf_append_str (buf, len, nm_utils_inet_ntop (addr_family, addr, s_addr));
+
+	if (plen != (addr_size * 8))
+		nm_utils_strbuf_append (buf, len, "/%u", plen);
+}
+
+static void
+_routing_rule_port_range_to_string (char **buf,
+                                    gsize *len,
+                                    const NMFibRulePortRange *port_range,
+                                    const char *name)
+{
+	if (   port_range->start == 0
+	    && port_range->end == 0)
+		nm_utils_strbuf_append_str (buf, len, "");
+	else {
+		nm_utils_strbuf_append (buf, len, " %s %u", name, port_range->start);
+		if (port_range->start != port_range->end)
+			nm_utils_strbuf_append (buf, len, "-%u", port_range->end);
+	}
+}
+
+const char *
+nm_platform_routing_rule_to_string (const NMPlatformRoutingRule *routing_rule, char *buf, gsize len)
+{
+	const char *buf0;
+	guint32 rr_flags;
+
+	if (!nm_utils_to_string_buffer_init_null (routing_rule, &buf, &len))
+		return buf;
+
+	if (!NM_IN_SET (routing_rule->addr_family, AF_INET, AF_INET6)) {
+		/* invalid addr-family. The other fields are undefined. */
+		if (routing_rule->addr_family == AF_UNSPEC)
+			g_snprintf (buf, len, "[routing-rule]");
+		else
+			g_snprintf (buf, len, "[routing-rule family:%u]", routing_rule->addr_family);
+		return buf;
+	}
+
+	buf0 = buf;
+
+	rr_flags = routing_rule->flags;
+
+	rr_flags = NM_FLAGS_UNSET (rr_flags, FIB_RULE_INVERT);
+	nm_utils_strbuf_append (&buf, &len,
+	                        "[%c] " /* addr-family */
+	                        "%u:"  /* priority */
+	                        "%s",   /* not/FIB_RULE_INVERT */
+	                        nm_utils_addr_family_to_char (routing_rule->addr_family),
+	                        routing_rule->priority,
+	                        (  NM_FLAGS_HAS (routing_rule->flags, FIB_RULE_INVERT)
+	                         ? " not"
+	                         : ""));
+
+	_routing_rule_addr_to_string (&buf, &len,
+	                              routing_rule->addr_family,
+	                              &routing_rule->src,
+	                              routing_rule->src_len,
+	                              TRUE);
+
+	_routing_rule_addr_to_string (&buf, &len,
+	                              routing_rule->addr_family,
+	                              &routing_rule->dst,
+	                              routing_rule->dst_len,
+	                              FALSE);
+
+	if (routing_rule->tos)
+		nm_utils_strbuf_append (&buf, &len, " tos 0x%02x", routing_rule->tos);
+
+	if (   routing_rule->fwmark != 0
+	    || routing_rule->fwmask != 0) {
+		nm_utils_strbuf_append (&buf, &len, " fwmark %#x", (unsigned) routing_rule->fwmark);
+		if (routing_rule->fwmark != 0xFFFFFFFFu)
+			nm_utils_strbuf_append (&buf, &len, "/%#x", (unsigned) routing_rule->fwmask);
+	}
+
+	if (routing_rule->iifname[0]) {
+		nm_utils_strbuf_append (&buf, &len, " iif %s", routing_rule->iifname);
+		rr_flags = NM_FLAGS_UNSET (rr_flags, FIB_RULE_IIF_DETACHED);
+		if (NM_FLAGS_HAS (routing_rule->flags, FIB_RULE_IIF_DETACHED))
+			nm_utils_strbuf_append_str (&buf, &len, " [detached]");
+	}
+
+	if (routing_rule->oifname[0]) {
+		nm_utils_strbuf_append (&buf, &len, " oif %s", routing_rule->oifname);
+		rr_flags = NM_FLAGS_UNSET (rr_flags, FIB_RULE_OIF_DETACHED);
+		if (NM_FLAGS_HAS (routing_rule->flags, FIB_RULE_OIF_DETACHED))
+			nm_utils_strbuf_append_str (&buf, &len, " [detached]");
+	}
+
+	if (routing_rule->l3mdev != 0) {
+		if (routing_rule->l3mdev == 1)
+			nm_utils_strbuf_append_str (&buf, &len, " lookup [l3mdev-table]");
+		else {
+			nm_utils_strbuf_append (&buf, &len, " lookup [l3mdev-table/%u]", (unsigned) routing_rule->l3mdev);
+		}
+	}
+
+	if (   routing_rule->uid_range_has
+	    || routing_rule->uid_range.start
+	    || routing_rule->uid_range.end) {
+		nm_utils_strbuf_append (&buf, &len,
+		                        " uidrange %u-%u%s",
+		                        routing_rule->uid_range.start,
+		                        routing_rule->uid_range.end,
+		                        routing_rule->uid_range_has ? "" : "(?)");
+	}
+
+	if (routing_rule->ip_proto != 0) {
+		/* we don't call getprotobynumber(), just print the numeric value.
+		 * This differs from what ip-rule prints. */
+		nm_utils_strbuf_append (&buf, &len,
+		                        " ipproto %u",
+		                        routing_rule->ip_proto);
+	}
+
+	_routing_rule_port_range_to_string (&buf, &len,
+	                                    &routing_rule->sport_range,
+	                                    "sport");
+
+	_routing_rule_port_range_to_string (&buf, &len,
+	                                    &routing_rule->dport_range,
+	                                    "dport");
+
+	if (routing_rule->tun_id != 0) {
+		nm_utils_strbuf_append (&buf, &len,
+		                        " tun_id %"G_GUINT64_FORMAT,
+		                        routing_rule->tun_id);
+	}
+
+	if (routing_rule->table != 0) {
+		nm_utils_strbuf_append (&buf, &len,
+		                        " lookup %u",
+		                        routing_rule->table);
+	}
+
+	if (routing_rule->suppress_prefixlen_inverse != 0) {
+		nm_utils_strbuf_append (&buf, &len,
+		                        " suppress_prefixlen %d",
+		                        (int) (~routing_rule->suppress_prefixlen_inverse));
+	}
+
+	if (routing_rule->suppress_ifgroup_inverse != 0) {
+		nm_utils_strbuf_append (&buf, &len,
+		                        " suppress_ifgroup %d",
+		                        (int) (~routing_rule->suppress_ifgroup_inverse));
+	}
+
+	if (routing_rule->flow) {
+		/* FRA_FLOW is only for IPv4, but we want to print the value for all address-families,
+		 * to see when it is set. In practice, this should not be set except for IPv4.
+		 *
+		 * We don't follow the style how ip-rule prints flow/realms. It's confusing. Just
+		 * print the value hex. */
+		nm_utils_strbuf_append (&buf, &len,
+		                        " realms 0x%08x",
+		                        routing_rule->flow);
+	}
+
+	if (routing_rule->action == RTN_NAT) {
+		G_STATIC_ASSERT_EXPR (RTN_NAT == 10);
+
+		/* NAT is deprecated for many years. We don't support RTA_GATEWAY/FRA_UNUSED2
+		 * for the gateway, and so do recent kernels ignore that parameter. */
+		nm_utils_strbuf_append_str (&buf, &len, " masquerade");
+	} else if (routing_rule->action == FR_ACT_GOTO) {
+		if (routing_rule->goto_target != 0)
+			nm_utils_strbuf_append (&buf, &len, " goto %u", routing_rule->goto_target);
+		else
+			nm_utils_strbuf_append_str (&buf, &len, " goto none");
+		rr_flags = NM_FLAGS_UNSET (rr_flags, FIB_RULE_UNRESOLVED);
+		if (NM_FLAGS_HAS (routing_rule->flags, FIB_RULE_UNRESOLVED))
+			nm_utils_strbuf_append_str (&buf, &len, " unresolved");
+	} else if (routing_rule->action != FR_ACT_TO_TBL) {
+		const char *ss;
+		char ss_buf[60];
+
+#define _V(v1, v2) ((sizeof (char[(((int) (v1)) == ((int) (v2))) ? 1 : -1]) * 0) + (v1))
+		switch (routing_rule->action) {
+		case _V (FR_ACT_UNSPEC,      RTN_UNSPEC)      : ss = "none";        break;
+		case _V (FR_ACT_TO_TBL,      RTN_UNICAST)     : ss = "unicast";     break;
+		case _V (FR_ACT_GOTO,        RTN_LOCAL)       : ss = "local";       break;
+		case _V (FR_ACT_NOP,         RTN_BROADCAST)   : ss = "nop";         break;
+		case _V (FR_ACT_RES3,        RTN_ANYCAST)     : ss = "anycast";     break;
+		case _V (FR_ACT_RES4,        RTN_MULTICAST)   : ss = "multicast";   break;
+		case _V (FR_ACT_BLACKHOLE,   RTN_BLACKHOLE)   : ss = "blackhole";   break;
+		case _V (FR_ACT_UNREACHABLE, RTN_UNREACHABLE) : ss = "unreachable"; break;
+		case _V (FR_ACT_PROHIBIT,    RTN_PROHIBIT)    : ss = "prohibit";    break;
+		case RTN_THROW                                : ss = "throw";       break;
+		case RTN_NAT                                  : ss = "nat";         break;
+		case RTN_XRESOLVE                             : ss = "xresolve";    break;
+		default:
+			ss = nm_sprintf_buf (ss_buf, "action-%u", routing_rule->action);
+			break;
+		}
+#undef _V
+		nm_utils_strbuf_append (&buf, &len, " %s", ss);
+	}
+
+	if (routing_rule->protocol != RTPROT_UNSPEC)
+		nm_utils_strbuf_append (&buf, &len, " protocol %u", routing_rule->protocol);
+
+	if (   routing_rule->goto_target != 0
+	    && routing_rule->action != FR_ACT_GOTO) {
+		/* a trailing target is set for an unexpected action. Print it. */
+		nm_utils_strbuf_append (&buf, &len, " goto-target %u", routing_rule->goto_target);
+	}
+
+	if (rr_flags != 0) {
+		/* we have some flags we didn't print about yet. */
+		nm_utils_strbuf_append (&buf, &len, " remaining-flags %x", rr_flags);
+	}
+
+	return buf0;
+}
+
 const char *
 nm_platform_qdisc_to_string (const NMPlatformQdisc *qdisc, char *buf, gsize len)
 {
@@ -6249,6 +6595,28 @@ nm_platform_vf_to_string (const NMPlatformVF *vf, char *buf, gsize len)
 	return buf;
 }
 
+
+const char *
+nm_platform_bridge_vlan_to_string (const NMPlatformBridgeVlan *vlan, char *buf, gsize len)
+{
+	char str_vid_end[64];
+
+	if (!nm_utils_to_string_buffer_init_null (vlan, &buf, &len))
+		return buf;
+
+	g_snprintf (buf, len,
+	            "%u"
+	            "%s"
+	            "%s"
+	            "%s",
+	            vlan->vid_start,
+	            vlan->vid_start != vlan->vid_end ? nm_sprintf_buf (str_vid_end, "-%u", vlan->vid_end) : "",
+	            vlan->pvid ? " PVID" : "",
+	            vlan->untagged ? " untagged" : "");
+
+	return buf;
+}
+
 void
 nm_platform_link_hash_update (const NMPlatformLink *obj, NMHashState *h)
 {
@@ -7034,6 +7402,216 @@ nm_platform_ip6_route_cmp (const NMPlatformIP6Route *a, const NMPlatformIP6Route
 	return 0;
 }
 
+#define _ROUTING_RULE_FLAGS_IGNORE (  FIB_RULE_UNRESOLVED \
+                                    | FIB_RULE_IIF_DETACHED \
+                                    | FIB_RULE_OIF_DETACHED)
+
+#define _routing_rule_compare(cmp_type, kernel_support_type) \
+	(   (cmp_type) == NM_PLATFORM_ROUTING_RULE_CMP_TYPE_FULL \
+	 || nm_platform_kernel_support_get (kernel_support_type))
+
+void
+nm_platform_routing_rule_hash_update (const NMPlatformRoutingRule *obj,
+                                      NMPlatformRoutingRuleCmpType cmp_type,
+                                      NMHashState *h)
+{
+	gboolean cmp_full = TRUE;
+	gsize addr_size;
+	guint32 flags_mask = G_MAXUINT32;
+
+	if (G_UNLIKELY (!NM_IN_SET (obj->addr_family, AF_INET, AF_INET6))) {
+		/* the address family is not one of the supported ones. That means, the
+		 * instance will only compare equal to itself (pointer-equality). */
+		nm_hash_update_val (h, (gconstpointer) obj);
+		return;
+	}
+
+	switch (cmp_type) {
+
+	case NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID:
+
+		flags_mask &= ~_ROUTING_RULE_FLAGS_IGNORE;
+
+		/* fall-through */
+	case NM_PLATFORM_ROUTING_RULE_CMP_TYPE_SEMANTICALLY:
+
+		cmp_full = FALSE;
+
+		/* fall-through */
+	case NM_PLATFORM_ROUTING_RULE_CMP_TYPE_FULL:
+
+		nm_hash_update_vals (h,
+		                     obj->addr_family,
+		                     obj->tun_id,
+		                     obj->table,
+		                     obj->flags & flags_mask,
+		                     obj->priority,
+		                     obj->fwmark,
+		                     obj->fwmask,
+		                     (  (   cmp_full
+		                         || (   cmp_type == NM_PLATFORM_ROUTING_RULE_CMP_TYPE_SEMANTICALLY
+		                             && obj->action == FR_ACT_GOTO))
+		                      ? obj->goto_target
+		                      : (guint32) 0u),
+		                     (  (   cmp_full
+		                         || obj->addr_family == AF_INET)
+		                      ? obj->flow
+		                      : (guint32) 0u),
+		                     NM_HASH_COMBINE_BOOLS (guint8,
+		                                            (  _routing_rule_compare (cmp_type,
+		                                                                      NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_UID_RANGE)
+		                                             ? obj->uid_range_has
+		                                             : FALSE)),
+		                     obj->suppress_prefixlen_inverse,
+		                     obj->suppress_ifgroup_inverse,
+		                     (  _routing_rule_compare (cmp_type,
+		                                               NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_L3MDEV)
+		                      ? (  cmp_full
+		                         ? (guint16) obj->l3mdev
+		                         : (guint16) !!obj->l3mdev)
+		                      : G_MAXUINT16),
+		                     obj->action,
+		                     obj->tos,
+		                     obj->src_len,
+		                     obj->dst_len,
+		                     (  _routing_rule_compare (cmp_type,
+		                                               NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_PROTOCOL)
+		                      ? (guint16) obj->protocol
+		                      : G_MAXUINT16));
+		addr_size = nm_utils_addr_family_to_size (obj->addr_family);
+		if (cmp_full || obj->src_len > 0)
+			nm_hash_update (h, &obj->src, addr_size);
+		if (cmp_full || obj->dst_len > 0)
+			nm_hash_update (h, &obj->dst, addr_size);
+		if (_routing_rule_compare (cmp_type,
+		                           NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_UID_RANGE)) {
+			if (cmp_full || obj->uid_range_has)
+				nm_hash_update_valp (h, &obj->uid_range);
+		}
+		if (_routing_rule_compare (cmp_type,
+		                           NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_IP_PROTO)) {
+			nm_hash_update_val (h, obj->ip_proto);
+			nm_hash_update_valp (h, &obj->sport_range);
+			nm_hash_update_valp (h, &obj->dport_range);
+		}
+		nm_hash_update_str (h, obj->iifname);
+		nm_hash_update_str (h, obj->oifname);
+		return;
+	}
+
+	nm_assert_not_reached ();
+}
+
+int
+nm_platform_routing_rule_cmp (const NMPlatformRoutingRule *a,
+                              const NMPlatformRoutingRule *b,
+                              NMPlatformRoutingRuleCmpType cmp_type)
+{
+	gboolean cmp_full = TRUE;
+	gsize addr_size;
+	bool valid;
+	guint32 flags_mask = G_MAXUINT32;
+
+	NM_CMP_SELF (a, b);
+
+	valid = NM_IN_SET (a->addr_family, AF_INET, AF_INET6);
+	NM_CMP_DIRECT (valid,
+	               (bool) NM_IN_SET (b->addr_family, AF_INET, AF_INET6));
+
+	if (G_UNLIKELY (!valid)) {
+		/* the address family is not one of the supported ones. That means, the
+		 * instance will only compare equal to itself. */
+		NM_CMP_DIRECT ((uintptr_t) a, (uintptr_t) b);
+		nm_assert_not_reached ();
+		return 0;
+	}
+
+	switch (cmp_type) {
+
+	case NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID:
+
+		flags_mask &= ~_ROUTING_RULE_FLAGS_IGNORE;
+
+		/* fall-through */
+	case NM_PLATFORM_ROUTING_RULE_CMP_TYPE_SEMANTICALLY:
+
+		cmp_full = FALSE;
+
+		/* fall-through */
+	case NM_PLATFORM_ROUTING_RULE_CMP_TYPE_FULL:
+		NM_CMP_FIELD (a, b, addr_family);
+		NM_CMP_FIELD (a, b, action);
+		NM_CMP_FIELD (a, b, priority);
+		NM_CMP_FIELD (a, b, tun_id);
+
+		if (_routing_rule_compare (cmp_type,
+		                           NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_L3MDEV)) {
+			if (cmp_full)
+				NM_CMP_FIELD (a, b, l3mdev);
+			else
+				NM_CMP_FIELD_BOOL (a, b, l3mdev);
+		}
+
+		NM_CMP_FIELD (a, b, table);
+
+		NM_CMP_DIRECT (a->flags & flags_mask, b->flags & flags_mask);
+
+		NM_CMP_FIELD (a, b, fwmark);
+		NM_CMP_FIELD (a, b, fwmask);
+
+		if (   cmp_full
+		    || (   cmp_type == NM_PLATFORM_ROUTING_RULE_CMP_TYPE_SEMANTICALLY
+		        && a->action == FR_ACT_GOTO))
+			NM_CMP_FIELD (a, b, goto_target);
+
+		NM_CMP_FIELD (a, b, suppress_prefixlen_inverse);
+		NM_CMP_FIELD (a, b, suppress_ifgroup_inverse);
+		NM_CMP_FIELD (a, b, tos);
+
+		if (cmp_full || a->addr_family == AF_INET)
+			NM_CMP_FIELD (a, b, flow);
+
+		if (_routing_rule_compare (cmp_type,
+		                           NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_PROTOCOL))
+			NM_CMP_FIELD (a, b, protocol);
+
+		if (_routing_rule_compare (cmp_type,
+		                           NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_IP_PROTO)) {
+			NM_CMP_FIELD (a, b, ip_proto);
+			NM_CMP_FIELD (a, b, sport_range.start);
+			NM_CMP_FIELD (a, b, sport_range.end);
+			NM_CMP_FIELD (a, b, dport_range.start);
+			NM_CMP_FIELD (a, b, dport_range.end);
+		}
+
+		addr_size = nm_utils_addr_family_to_size (a->addr_family);
+
+		NM_CMP_FIELD (a, b, src_len);
+		if (cmp_full || a->src_len > 0)
+			NM_CMP_FIELD_MEMCMP_LEN (a, b, src, addr_size);
+
+		NM_CMP_FIELD (a, b, dst_len);
+		if (cmp_full || a->dst_len > 0)
+			NM_CMP_FIELD_MEMCMP_LEN (a, b, dst, addr_size);
+
+		if (_routing_rule_compare (cmp_type,
+		                           NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_UID_RANGE)) {
+			NM_CMP_FIELD_UNSAFE (a, b, uid_range_has);
+			if (cmp_full || a->uid_range_has) {
+				NM_CMP_FIELD (a, b, uid_range.start);
+				NM_CMP_FIELD (a, b, uid_range.end);
+			}
+		}
+
+		NM_CMP_FIELD_STR (a, b, iifname);
+		NM_CMP_FIELD_STR (a, b, oifname);
+		return 0;
+	}
+
+	nm_assert_not_reached ();
+	return 0;
+}
+
 /**
  * nm_platform_ip_address_cmp_expiry:
  * @a: a NMPlatformIPAddress to compare
@@ -7131,6 +7709,13 @@ log_ip6_route (NMPlatform *self, NMPObjectType obj_type, int ifindex, NMPlatform
 }
 
 static void
+log_routing_rule (NMPlatform *self, NMPObjectType obj_type, int ifindex, NMPlatformRoutingRule *routing_rule, NMPlatformSignalChangeType change_type, gpointer user_data)
+{
+	/* routing rules don't have an ifindex. We probably should refactor the signals that are emitted for platform changes. */
+	_LOG3D ("signal: rt-rule %7s: %s", nm_platform_signal_change_type_to_string (change_type), nm_platform_routing_rule_to_string (routing_rule, NULL, 0));
+}
+
+static void
 log_qdisc (NMPlatform *self, NMPObjectType obj_type, int ifindex, NMPlatformQdisc *qdisc, NMPlatformSignalChangeType change_type, gpointer user_data)
 {
 	_LOG3D ("signal: qdisc %7s: %s", nm_platform_signal_change_type_to_string (change_type), nm_platform_qdisc_to_string (qdisc, NULL, 0));
@@ -7196,9 +7781,13 @@ nm_platform_cache_update_emit_signal (NMPlatform *self,
 		return;
 	}
 
-	ifindex = o->object.ifindex;
 	klass = NMP_OBJECT_GET_CLASS (o);
 
+	if (klass->obj_type == NMP_OBJECT_TYPE_ROUTING_RULE)
+		ifindex = 0;
+	else
+		ifindex = NMP_OBJECT_CAST_OBJ_WITH_IFINDEX (o)->ifindex;
+
 	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))
@@ -7214,7 +7803,7 @@ nm_platform_cache_update_emit_signal (NMPlatform *self,
 	               _nm_platform_signal_id_get (klass->signal_type_id),
 	               0,
 	               (int) klass->obj_type,
-	               o->object.ifindex,
+	               ifindex,
 	               &o->object,
 	               (int) cache_op);
 	nmp_object_unref (o);
@@ -7261,24 +7850,25 @@ _vtr_v4_metric_normalize (guint32 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, 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,
-	.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, 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,
-	.metric_normalize               = nm_utils_ip6_route_metric_normalize,
+const _NMPlatformVTableRouteUnion 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, 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,
+		.metric_normalize               = _vtr_v4_metric_normalize,
+	},
+	.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, 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,
+		.metric_normalize               = nm_utils_ip6_route_metric_normalize,
+	},
 };
 
 /*****************************************************************************/
@@ -7338,8 +7928,9 @@ constructor (GType type,
 
 	priv->multi_idx = nm_dedup_multi_index_new ();
 
-	priv->cache = nmp_cache_new (nm_platform_get_multi_idx (self),
+	priv->cache = nmp_cache_new (priv->multi_idx,
 	                             priv->use_udev);
+
 	return object;
 }
 
@@ -7411,11 +8002,12 @@ nm_platform_class_init (NMPlatformClass *platform_class)
 	} G_STMT_END
 
 	/* Signals */
-	SIGNAL (NM_PLATFORM_SIGNAL_ID_LINK,        NM_PLATFORM_SIGNAL_LINK_CHANGED,        log_link);
-	SIGNAL (NM_PLATFORM_SIGNAL_ID_IP4_ADDRESS, NM_PLATFORM_SIGNAL_IP4_ADDRESS_CHANGED, log_ip4_address);
-	SIGNAL (NM_PLATFORM_SIGNAL_ID_IP6_ADDRESS, NM_PLATFORM_SIGNAL_IP6_ADDRESS_CHANGED, log_ip6_address);
-	SIGNAL (NM_PLATFORM_SIGNAL_ID_IP4_ROUTE,   NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED,   log_ip4_route);
-	SIGNAL (NM_PLATFORM_SIGNAL_ID_IP6_ROUTE,   NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED,   log_ip6_route);
-	SIGNAL (NM_PLATFORM_SIGNAL_ID_QDISC,       NM_PLATFORM_SIGNAL_QDISC_CHANGED,       log_qdisc);
-	SIGNAL (NM_PLATFORM_SIGNAL_ID_TFILTER,     NM_PLATFORM_SIGNAL_TFILTER_CHANGED,     log_tfilter);
+	SIGNAL (NM_PLATFORM_SIGNAL_ID_LINK,         NM_PLATFORM_SIGNAL_LINK_CHANGED,         log_link);
+	SIGNAL (NM_PLATFORM_SIGNAL_ID_IP4_ADDRESS,  NM_PLATFORM_SIGNAL_IP4_ADDRESS_CHANGED,  log_ip4_address);
+	SIGNAL (NM_PLATFORM_SIGNAL_ID_IP6_ADDRESS,  NM_PLATFORM_SIGNAL_IP6_ADDRESS_CHANGED,  log_ip6_address);
+	SIGNAL (NM_PLATFORM_SIGNAL_ID_IP4_ROUTE,    NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED,    log_ip4_route);
+	SIGNAL (NM_PLATFORM_SIGNAL_ID_IP6_ROUTE,    NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED,    log_ip6_route);
+	SIGNAL (NM_PLATFORM_SIGNAL_ID_ROUTING_RULE, NM_PLATFORM_SIGNAL_ROUTING_RULE_CHANGED, log_routing_rule);
+	SIGNAL (NM_PLATFORM_SIGNAL_ID_QDISC,        NM_PLATFORM_SIGNAL_QDISC_CHANGED,        log_qdisc);
+	SIGNAL (NM_PLATFORM_SIGNAL_ID_TFILTER,      NM_PLATFORM_SIGNAL_TFILTER_CHANGED,      log_tfilter);
 }
diff --git a/src/platform/nm-platform.h b/src/platform/nm-platform.h
index 37aa58fd..97be8832 100644
--- a/src/platform/nm-platform.h
+++ b/src/platform/nm-platform.h
@@ -152,6 +152,14 @@ typedef enum {
 } NMPlatformIPRouteCmpType;
 
 typedef enum {
+	NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID,
+
+	NM_PLATFORM_ROUTING_RULE_CMP_TYPE_SEMANTICALLY,
+
+	NM_PLATFORM_ROUTING_RULE_CMP_TYPE_FULL,
+} NMPlatformRoutingRuleCmpType;
+
+typedef enum {
 
 	/* match-flags are strictly inclusive. That means,
 	 * by default nothing is matched, but if you enable a particular
@@ -181,12 +189,22 @@ typedef enum {
 
 #define NM_PLATFORM_LINK_OTHER_NETNS    (-1)
 
-#define __NMPlatformObject_COMMON \
+struct _NMPlatformObject {
+	/* the object type has no fields of its own, it is only used to having
+	 * a special pointer type that can be used to indicate "any" type. */
+	char _dummy_don_t_use_me;
+};
+
+#define __NMPlatformObjWithIfindex_COMMON \
 	int ifindex; \
 	;
 
+struct _NMPlatformObjWithIfindex {
+	__NMPlatformObjWithIfindex_COMMON;
+};
+
 struct _NMPlatformLink {
-	__NMPlatformObject_COMMON;
+	__NMPlatformObjWithIfindex_COMMON;
 	char name[NMP_IFNAMSIZ];
 	NMLinkType type;
 
@@ -246,6 +264,7 @@ typedef enum { /*< skip >*/
 	NM_PLATFORM_SIGNAL_ID_IP6_ADDRESS,
 	NM_PLATFORM_SIGNAL_ID_IP4_ROUTE,
 	NM_PLATFORM_SIGNAL_ID_IP6_ROUTE,
+	NM_PLATFORM_SIGNAL_ID_ROUTING_RULE,
 	NM_PLATFORM_SIGNAL_ID_QDISC,
 	NM_PLATFORM_SIGNAL_ID_TFILTER,
 	_NM_PLATFORM_SIGNAL_ID_LAST,
@@ -260,15 +279,11 @@ typedef enum {
 	NM_PLATFORM_SIGNAL_REMOVED,
 } NMPlatformSignalChangeType;
 
-struct _NMPlatformObject {
-	__NMPlatformObject_COMMON;
-};
-
 #define NM_PLATFORM_IP_ADDRESS_CAST(address) \
 	NM_CONSTCAST (NMPlatformIPAddress, (address), NMPlatformIPXAddress, NMPlatformIP4Address, NMPlatformIP6Address)
 
 #define __NMPlatformIPAddress_COMMON \
-	__NMPlatformObject_COMMON; \
+	__NMPlatformObjWithIfindex_COMMON; \
 	NMIPConfigSource addr_source; \
 	\
 	/* Timestamp in seconds in the reference system of nm_utils_get_monotonic_timestamp_*().
@@ -370,7 +385,7 @@ typedef union {
 #define NM_PLATFORM_ROUTE_METRIC_IP4_DEVICE_ROUTE 0
 
 #define __NMPlatformIPRoute_COMMON \
-	__NMPlatformObject_COMMON; \
+	__NMPlatformObjWithIfindex_COMMON; \
 	\
 	/* The NMIPConfigSource. For routes that we receive from cache this corresponds
 	 * to the rtm_protocol field (and is one of the NM_IP_CONFIG_SOURCE_RTPROT_* values).
@@ -414,7 +429,7 @@ typedef union {
 	 * Such a route is not alive, according to nmp_object_is_alive().
 	 *
 	 * NOTE: currently we ignore all flags except RTM_F_CLONED
-	 * and RTNH_F_ONLINK for IPv4.
+	 * and RTNH_F_ONLINK.
 	 * We also may not properly consider the flags as part of the ID
 	 * in route-cmp. */ \
 	unsigned r_rtm_flags; \
@@ -540,7 +555,49 @@ typedef union {
 #undef __NMPlatformIPRoute_COMMON
 
 typedef struct {
-	__NMPlatformObject_COMMON;
+	/* struct fib_rule_uid_range */
+	guint32 start;
+	guint32 end;
+} NMFibRuleUidRange;
+
+typedef struct {
+	/* struct fib_rule_port_range */
+	guint16 start;
+	guint16 end;
+} NMFibRulePortRange;
+
+typedef struct {
+	NMIPAddr src;                        /* FRA_SRC */
+	NMIPAddr dst;                        /* FRA_DST */
+	guint64  tun_id;                     /* betoh64(FRA_TUN_ID) */
+	guint32  table;                      /* (struct fib_rule_hdr).table, FRA_TABLE */
+	guint32  flags;                      /* (struct fib_rule_hdr).flags */
+	guint32  priority;                   /* RA_PRIORITY */
+	guint32  fwmark;                     /* FRA_FWMARK */
+	guint32  fwmask;                     /* FRA_FWMASK */
+	guint32  goto_target;                /* FRA_GOTO */
+	guint32  flow;                       /* FRA_FLOW */
+	guint32  suppress_prefixlen_inverse; /* ~(FRA_SUPPRESS_PREFIXLEN) */
+	guint32  suppress_ifgroup_inverse;   /* ~(FRA_SUPPRESS_IFGROUP) */
+	NMFibRuleUidRange uid_range;         /* FRA_UID_RANGE */
+	NMFibRulePortRange sport_range;      /* FRA_SPORT_RANGE */
+	NMFibRulePortRange dport_range;      /* FRA_DPORT_RANGE */
+	char     iifname[NMP_IFNAMSIZ];      /* FRA_IIFNAME */
+	char     oifname[NMP_IFNAMSIZ];      /* FRA_OIFNAME */
+	guint8   addr_family;                /* (struct fib_rule_hdr).family */
+	guint8   action;                     /* (struct fib_rule_hdr).action */
+	guint8   tos;                        /* (struct fib_rule_hdr).tos */
+	guint8   src_len;                    /* (struct fib_rule_hdr).src_len */
+	guint8   dst_len;                    /* (struct fib_rule_hdr).dst_len */
+	guint8   l3mdev;                     /* FRA_L3MDEV */
+	guint8   protocol;                   /* FRA_PROTOCOL */
+	guint8   ip_proto;                   /* FRA_IP_PROTO */
+
+	bool     uid_range_has:1;            /* has(FRA_UID_RANGE) */
+} NMPlatformRoutingRule;
+
+typedef struct {
+	__NMPlatformObjWithIfindex_COMMON;
 	const char *kind;
 	int addr_family;
 	guint32 handle;
@@ -562,7 +619,7 @@ typedef struct {
 #define NM_PLATFORM_ACTION_KIND_SIMPLE "simple"
 
 typedef struct {
-	__NMPlatformObject_COMMON;
+	__NMPlatformObjWithIfindex_COMMON;
 	const char *kind;
 	int addr_family;
 	guint32 handle;
@@ -571,7 +628,7 @@ typedef struct {
 	NMPlatformAction action;
 } NMPlatformTfilter;
 
-#undef __NMPlatformObject_COMMON
+#undef __NMPlatformObjWithIfindex_COMMON
 
 typedef struct {
 	gboolean is_ip4;
@@ -583,8 +640,15 @@ typedef struct {
 	guint32 (*metric_normalize) (guint32 metric);
 } NMPlatformVTableRoute;
 
-extern const NMPlatformVTableRoute nm_platform_vtable_route_v4;
-extern const NMPlatformVTableRoute nm_platform_vtable_route_v6;
+typedef union {
+	struct {
+		NMPlatformVTableRoute v6;
+		NMPlatformVTableRoute v4;
+	};
+	NMPlatformVTableRoute vx[2];
+} _NMPlatformVTableRouteUnion;
+
+extern const _NMPlatformVTableRouteUnion nm_platform_vtable_route;
 
 typedef struct {
 	guint16 id;
@@ -607,6 +671,13 @@ typedef struct {
 } NMPlatformVF;
 
 typedef struct {
+	guint16 vid_start;
+	guint16 vid_end;
+	bool untagged:1;
+	bool pvid:1;
+} NMPlatformBridgeVlan;
+
+typedef struct {
 	in_addr_t local;
 	in_addr_t remote;
 	int parent_ifindex;
@@ -748,12 +819,6 @@ typedef enum {
 } NMPlatformLinkDuplexType;
 
 typedef enum {
-	NM_PLATFORM_KERNEL_SUPPORT_EXTENDED_IFA_FLAGS                 = (1LL <<  0),
-	NM_PLATFORM_KERNEL_SUPPORT_USER_IPV6LL                        = (1LL <<  1),
-	NM_PLATFORM_KERNEL_SUPPORT_RTA_PREF                           = (1LL <<  2),
-} NMPlatformKernelSupportFlags;
-
-typedef enum {
 	NM_PLATFORM_WIREGUARD_CHANGE_FLAG_NONE                        = 0,
 	NM_PLATFORM_WIREGUARD_CHANGE_FLAG_REPLACE_PEERS               = (1LL << 0),
 	NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_PRIVATE_KEY             = (1LL << 1),
@@ -779,6 +844,51 @@ typedef enum {
 
 /*****************************************************************************/
 
+typedef enum {
+	NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS,
+	NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL,
+	NM_PLATFORM_KERNEL_SUPPORT_TYPE_RTA_PREF,
+	NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_L3MDEV,
+	NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_UID_RANGE,
+	NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_PROTOCOL,
+
+	/* this also includes FRA_SPORT_RANGE and FRA_DPORT_RANGE which
+	 * were added at the same time. */
+	NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_IP_PROTO,
+
+	_NM_PLATFORM_KERNEL_SUPPORT_NUM,
+} NMPlatformKernelSupportType;
+
+extern volatile int _nm_platform_kernel_support_state[_NM_PLATFORM_KERNEL_SUPPORT_NUM];
+
+int _nm_platform_kernel_support_init (NMPlatformKernelSupportType type,
+                                      int value);
+
+#define _nm_platform_kernel_support_detected(type) \
+	G_LIKELY (({ \
+		const NMPlatformKernelSupportType _type = (type); \
+		\
+		nm_assert (_NM_INT_NOT_NEGATIVE (_type) && _type < G_N_ELEMENTS (_nm_platform_kernel_support_state)); \
+		\
+		(_nm_platform_kernel_support_state[_type] != 0); \
+	}))
+
+#define nm_platform_kernel_support_get(type) \
+	({ \
+		const NMPlatformKernelSupportType _type = (type); \
+		int _v; \
+		\
+		nm_assert (_NM_INT_NOT_NEGATIVE (_type) && _type < G_N_ELEMENTS (_nm_platform_kernel_support_state)); \
+		\
+		_v = _nm_platform_kernel_support_state[_type]; \
+		if (G_UNLIKELY (_v == 0)) \
+			_v = _nm_platform_kernel_support_init (_type, 0); \
+		\
+		(_v >= 0); \
+	})
+
+/*****************************************************************************/
+
 struct _NMPlatformPrivate;
 
 struct _NMPlatform {
@@ -793,8 +903,6 @@ typedef struct {
 	gboolean (*sysctl_set) (NMPlatform *, const char *pathid, int dirfd, const char *path, const char *value);
 	char * (*sysctl_get) (NMPlatform *, const char *pathid, int dirfd, const char *path);
 
-	void (*refresh_all) (NMPlatform *self, NMPObjectType obj_type);
-
 	int (*link_add) (NMPlatform *,
 	                 const char *name,
 	                 NMLinkType type,
@@ -831,6 +939,7 @@ typedef struct {
 	gboolean (*link_set_name) (NMPlatform *, int ifindex, const char *name);
 	gboolean (*link_set_sriov_params) (NMPlatform *, int ifindex, guint num_vfs, int autoprobe);
 	gboolean (*link_set_sriov_vfs) (NMPlatform *self, int ifindex, const NMPlatformVF *const *vfs);
+	gboolean (*link_set_bridge_vlans) (NMPlatform *self, int ifindex, gboolean on_master, const NMPlatformBridgeVlan *const *vlans);
 
 	char *   (*link_get_physical_port_id) (NMPlatform *, int ifindex);
 	guint    (*link_get_dev_id) (NMPlatform *, int ifindex);
@@ -973,6 +1082,10 @@ typedef struct {
 	                     int oif_ifindex,
 	                     NMPObject **out_route);
 
+	int (*routing_rule_add) (NMPlatform *self,
+	                         NMPNlmFlags flags,
+	                         const NMPlatformRoutingRule *routing_rule);
+
 	int (*qdisc_add)   (NMPlatform *self,
 	                    NMPNlmFlags flags,
 	                    const NMPlatformQdisc *qdisc);
@@ -980,9 +1093,6 @@ typedef struct {
 	int (*tfilter_add)   (NMPlatform *self,
 	                      NMPNlmFlags flags,
 	                      const NMPlatformTfilter *tfilter);
-
-	NMPlatformKernelSupportFlags (*check_kernel_support) (NMPlatform * self,
-	                                                      NMPlatformKernelSupportFlags request_flags);
 } NMPlatformClass;
 
 /* NMPlatform signals
@@ -1001,6 +1111,7 @@ typedef struct {
 #define NM_PLATFORM_SIGNAL_IP6_ADDRESS_CHANGED "ip6-address-changed"
 #define NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED "ip4-route-changed"
 #define NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED "ip6-route-changed"
+#define NM_PLATFORM_SIGNAL_ROUTING_RULE_CHANGED "routing-rule-changed"
 #define NM_PLATFORM_SIGNAL_QDISC_CHANGED "qdisc-changed"
 #define NM_PLATFORM_SIGNAL_TFILTER_CHANGED "tfilter-changed"
 
@@ -1167,8 +1278,6 @@ gboolean nm_platform_sysctl_ip_conf_set_ipv6_hop_limit_safe (NMPlatform *self,
 const char *nm_platform_if_indextoname (NMPlatform *self, int ifindex, char *out_ifname/* of size IFNAMSIZ */);
 int nm_platform_if_nametoindex (NMPlatform *self, const char *ifname);
 
-void nm_platform_refresh_all (NMPlatform *self, NMPObjectType obj_type);
-
 const NMPObject *nm_platform_link_get_obj (NMPlatform *self,
                                            int ifindex,
                                            gboolean visible_only);
@@ -1261,6 +1370,7 @@ int nm_platform_link_set_mtu (NMPlatform *self, int ifindex, guint32 mtu);
 gboolean nm_platform_link_set_name (NMPlatform *self, int ifindex, const char *name);
 gboolean nm_platform_link_set_sriov_params (NMPlatform *self, int ifindex, guint num_vfs, int autoprobe);
 gboolean nm_platform_link_set_sriov_vfs (NMPlatform *self, int ifindex, const NMPlatformVF *const *vfs);
+gboolean nm_platform_link_set_bridge_vlans (NMPlatform *self, int ifindex, gboolean on_master, const NMPlatformBridgeVlan *const *vlans);
 
 char    *nm_platform_link_get_physical_port_id (NMPlatform *self, int ifindex);
 guint    nm_platform_link_get_dev_id (NMPlatform *self, int ifindex);
@@ -1482,6 +1592,10 @@ int nm_platform_ip_route_get (NMPlatform *self,
                               int oif_ifindex,
                               NMPObject **out_route);
 
+int nm_platform_routing_rule_add (NMPlatform *self,
+                                  NMPNlmFlags flags,
+                                  const NMPlatformRoutingRule *routing_rule);
+
 int nm_platform_qdisc_add   (NMPlatform *self,
                              NMPNlmFlags flags,
                              const NMPlatformQdisc *qdisc);
@@ -1512,9 +1626,11 @@ const char *nm_platform_ip4_address_to_string (const NMPlatformIP4Address *addre
 const char *nm_platform_ip6_address_to_string (const NMPlatformIP6Address *address, char *buf, gsize len);
 const char *nm_platform_ip4_route_to_string (const NMPlatformIP4Route *route, char *buf, gsize len);
 const char *nm_platform_ip6_route_to_string (const NMPlatformIP6Route *route, char *buf, gsize len);
+const char *nm_platform_routing_rule_to_string (const NMPlatformRoutingRule *routing_rule, char *buf, gsize len);
 const char *nm_platform_qdisc_to_string (const NMPlatformQdisc *qdisc, char *buf, gsize len);
 const char *nm_platform_tfilter_to_string (const NMPlatformTfilter *tfilter, char *buf, gsize len);
 const char *nm_platform_vf_to_string (const NMPlatformVF *vf, char *buf, gsize len);
+const char *nm_platform_bridge_vlan_to_string (const NMPlatformBridgeVlan *vlan, char *buf, gsize len);
 
 const char *nm_platform_vlan_qos_mapping_to_string (const char *name,
                                                     const NMVlanQosMapping *map,
@@ -1556,6 +1672,14 @@ nm_platform_ip6_route_cmp_full (const NMPlatformIP6Route *a, const NMPlatformIP6
 	return nm_platform_ip6_route_cmp (a, b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL);
 }
 
+int nm_platform_routing_rule_cmp (const NMPlatformRoutingRule *a, const NMPlatformRoutingRule *b, NMPlatformRoutingRuleCmpType cmp_type);
+
+static inline int
+nm_platform_routing_rule_cmp_full (const NMPlatformRoutingRule *a, const NMPlatformRoutingRule *b)
+{
+	return nm_platform_routing_rule_cmp (a, b, NM_PLATFORM_ROUTING_RULE_CMP_TYPE_FULL);
+}
+
 int nm_platform_qdisc_cmp (const NMPlatformQdisc *a, const NMPlatformQdisc *b);
 int nm_platform_tfilter_cmp (const NMPlatformTfilter *a, const NMPlatformTfilter *b);
 
@@ -1564,6 +1688,7 @@ void nm_platform_ip4_address_hash_update (const NMPlatformIP4Address *obj, NMHas
 void nm_platform_ip6_address_hash_update (const NMPlatformIP6Address *obj, NMHashState *h);
 void nm_platform_ip4_route_hash_update (const NMPlatformIP4Route *obj, NMPlatformIPRouteCmpType cmp_type, NMHashState *h);
 void nm_platform_ip6_route_hash_update (const NMPlatformIP6Route *obj, NMPlatformIPRouteCmpType cmp_type, NMHashState *h);
+void nm_platform_routing_rule_hash_update (const NMPlatformRoutingRule *obj, NMPlatformRoutingRuleCmpType cmp_type, NMHashState *h);
 void nm_platform_lnk_gre_hash_update (const NMPlatformLnkGre *obj, NMHashState *h);
 void nm_platform_lnk_infiniband_hash_update (const NMPlatformLnkInfiniband *obj, NMHashState *h);
 void nm_platform_lnk_ip6tnl_hash_update (const NMPlatformLnkIp6Tnl *obj, NMHashState *h);
@@ -1579,9 +1704,6 @@ void nm_platform_lnk_wireguard_hash_update (const NMPlatformLnkWireGuard *obj, N
 void nm_platform_qdisc_hash_update (const NMPlatformQdisc *obj, NMHashState *h);
 void nm_platform_tfilter_hash_update (const NMPlatformTfilter *obj, NMHashState *h);
 
-NMPlatformKernelSupportFlags nm_platform_check_kernel_support (NMPlatform *self,
-                                                               NMPlatformKernelSupportFlags request_flags);
-
 const char *nm_platform_link_flags2str (unsigned flags, char *buf, gsize len);
 const char *nm_platform_link_inet6_addrgenmode2str (guint8 mode, char *buf, gsize len);
 const char *nm_platform_addr_flags2str (unsigned flags, char *buf, gsize len);
diff --git a/src/platform/nmp-object.c b/src/platform/nmp-object.c
index 6ec6fea1..0d20100c 100644
--- a/src/platform/nmp-object.c
+++ b/src/platform/nmp-object.c
@@ -28,7 +28,7 @@
 #include <libudev.h>
 
 #include "nm-utils.h"
-#include "nm-utils/nm-secret-utils.h"
+#include "nm-glib-aux/nm-secret-utils.h"
 
 #include "nm-core-utils.h"
 #include "nm-platform-utils.h"
@@ -389,16 +389,16 @@ _idx_obj_part (const DedupMultiIdxType *idx_type,
 				nm_hash_update_val (h, obj_a);
 			return 0;
 		}
-		nm_assert (obj_a->object.ifindex > 0);
+		nm_assert (NMP_OBJECT_CAST_OBJ_WITH_IFINDEX (obj_a)->ifindex > 0);
 		if (obj_b) {
 			return    NMP_OBJECT_GET_TYPE (obj_a) == NMP_OBJECT_GET_TYPE (obj_b)
-			       && obj_a->object.ifindex == obj_b->object.ifindex
+			       && NMP_OBJECT_CAST_OBJ_WITH_IFINDEX (obj_a)->ifindex == NMP_OBJECT_CAST_OBJ_WITH_IFINDEX (obj_b)->ifindex
 			       && nmp_object_is_visible (obj_b);
 		}
 		if (h) {
 			nm_hash_update_vals (h,
 			                     idx_type->cache_id_type,
-			                     obj_a->object.ifindex);
+			                     NMP_OBJECT_CAST_OBJ_WITH_IFINDEX (obj_a)->ifindex);
 		}
 		return 1;
 
@@ -406,14 +406,14 @@ _idx_obj_part (const DedupMultiIdxType *idx_type,
 		obj_type = NMP_OBJECT_GET_TYPE (obj_a);
 		if (   !NM_IN_SET (obj_type, NMP_OBJECT_TYPE_IP4_ROUTE,
 		                             NMP_OBJECT_TYPE_IP6_ROUTE)
-		    || obj_a->object.ifindex <= 0) {
+		    || NMP_OBJECT_CAST_IP_ROUTE (obj_a)->ifindex <= 0) {
 			if (h)
 				nm_hash_update_val (h, obj_a);
 			return 0;
 		}
 		if (obj_b) {
 			return    obj_type == NMP_OBJECT_GET_TYPE (obj_b)
-			       && obj_b->object.ifindex > 0
+			       && NMP_OBJECT_CAST_IP_ROUTE (obj_b)->ifindex > 0
 			       && (obj_type == NMP_OBJECT_TYPE_IP4_ROUTE
 			           ? (nm_platform_ip4_route_cmp (&obj_a->ip4_route, &obj_b->ip4_route, NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID) == 0)
 			           : (nm_platform_ip6_route_cmp (&obj_a->ip6_route, &obj_b->ip6_route, NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID) == 0));
@@ -427,6 +427,25 @@ _idx_obj_part (const DedupMultiIdxType *idx_type,
 		}
 		return 1;
 
+	case NMP_CACHE_ID_TYPE_OBJECT_BY_ADDR_FAMILY:
+		obj_type = NMP_OBJECT_GET_TYPE (obj_a);
+		/* currently, only routing rules are supported for this cache-id-type. */
+		if (   obj_type != NMP_OBJECT_TYPE_ROUTING_RULE
+		    || !NM_IN_SET (obj_a->routing_rule.addr_family, AF_INET, AF_INET6)) {
+			if (h)
+				nm_hash_update_val (h, obj_a);
+			return 0;
+		}
+		if (obj_b) {
+			return    NMP_OBJECT_GET_TYPE (obj_b) == NMP_OBJECT_TYPE_ROUTING_RULE
+			       && obj_a->routing_rule.addr_family == obj_b->routing_rule.addr_family;
+		}
+		if (h) {
+			nm_hash_update_vals (h, idx_type->cache_id_type,
+			                        obj_a->routing_rule.addr_family);
+		}
+		return 1;
+
 	case NMP_CACHE_ID_TYPE_NONE:
 	case __NMP_CACHE_ID_TYPE_MAX:
 		break;
@@ -712,16 +731,6 @@ _nmp_object_fixup_link_master_connected (NMPObject **obj_new, NMPObject *obj_ori
 
 /*****************************************************************************/
 
-const NMPClass *
-nmp_class_from_type (NMPObjectType obj_type)
-{
-	g_return_val_if_fail (obj_type > NMP_OBJECT_TYPE_UNKNOWN && obj_type <= NMP_OBJECT_TYPE_MAX, NULL);
-
-	return &_nmp_classes[obj_type - 1];
-}
-
-/*****************************************************************************/
-
 static void
 _vt_cmd_obj_dispose_link (NMPObject *obj)
 {
@@ -1369,6 +1378,10 @@ _vt_cmd_plobj_id_copy (ip6_route, NMPlatformIP6Route, {
 	*dst = *src;
 	nm_assert (nm_platform_ip6_route_cmp (dst, src, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) == 0);
 });
+_vt_cmd_plobj_id_copy (routing_rule, NMPlatformRoutingRule, {
+	*dst = *src;
+	nm_assert (nm_platform_routing_rule_cmp (dst, src, NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID) == 0);
+});
 
 /* Uses internally nmp_object_copy(), hence it also violates the const
  * promise for @obj.
@@ -1469,6 +1482,12 @@ _vt_cmd_plobj_id_cmp_ip6_route (const NMPlatformObject *obj1, const NMPlatformOb
 	return nm_platform_ip6_route_cmp ((NMPlatformIP6Route *) obj1, (NMPlatformIP6Route *) obj2, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID);
 }
 
+static int
+_vt_cmd_plobj_id_cmp_routing_rule (const NMPlatformObject *obj1, const NMPlatformObject *obj2)
+{
+	return nm_platform_routing_rule_cmp ((NMPlatformRoutingRule *) obj1, (NMPlatformRoutingRule *) obj2, NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID);
+}
+
 void
 nmp_object_id_hash_update (const NMPObject *obj, NMHashState *h)
 {
@@ -1534,6 +1553,9 @@ _vt_cmd_plobj_id_hash_update (ip4_route, NMPlatformIP4Route, {
 _vt_cmd_plobj_id_hash_update (ip6_route, NMPlatformIP6Route, {
 	nm_platform_ip6_route_hash_update (obj, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID, h);
 })
+_vt_cmd_plobj_id_hash_update (routing_rule, NMPlatformRoutingRule, {
+	nm_platform_routing_rule_hash_update (obj, NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID, h);
+})
 _vt_cmd_plobj_id_hash_update (qdisc, NMPlatformQdisc, {
 	nm_hash_update_vals (h,
 	                     obj->ifindex,
@@ -1557,6 +1579,12 @@ _vt_cmd_plobj_hash_update_ip6_route (const NMPlatformObject *obj, NMHashState *h
 	return nm_platform_ip6_route_hash_update ((const NMPlatformIP6Route *) obj, NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL, h);
 }
 
+static void
+_vt_cmd_plobj_hash_update_routing_rule (const NMPlatformObject *obj, NMHashState *h)
+{
+	return nm_platform_routing_rule_hash_update ((const NMPlatformRoutingRule *) obj, NM_PLATFORM_ROUTING_RULE_CMP_TYPE_FULL, h);
+}
+
 gboolean
 nmp_object_is_alive (const NMPObject *obj)
 {
@@ -1574,13 +1602,14 @@ nmp_object_is_alive (const NMPObject *obj)
 static gboolean
 _vt_cmd_obj_is_alive_link (const NMPObject *obj)
 {
-	return obj->object.ifindex > 0 && (obj->_link.netlink.is_in_netlink || obj->_link.udev.device);
+	return    NMP_OBJECT_CAST_LINK (obj)->ifindex > 0
+	       && (obj->_link.netlink.is_in_netlink || obj->_link.udev.device);
 }
 
 static gboolean
 _vt_cmd_obj_is_alive_ipx_address (const NMPObject *obj)
 {
-	return obj->object.ifindex > 0;
+	return NMP_OBJECT_CAST_IP_ADDRESS (obj)->ifindex > 0;
 }
 
 static gboolean
@@ -1601,20 +1630,26 @@ _vt_cmd_obj_is_alive_ipx_route (const NMPObject *obj)
 	 * Instead we create a dead object, and nmp_cache_update_netlink()
 	 * will remove the old version of the update.
 	 **/
-	return    obj->object.ifindex > 0
+	return    NMP_OBJECT_CAST_IP_ROUTE (obj)->ifindex > 0
 	       && !NM_FLAGS_HAS (obj->ip_route.r_rtm_flags, RTM_F_CLONED);
 }
 
 static gboolean
+_vt_cmd_obj_is_alive_routing_rule (const NMPObject *obj)
+{
+	return NM_IN_SET (obj->routing_rule.addr_family, AF_INET, AF_INET6);
+}
+
+static gboolean
 _vt_cmd_obj_is_alive_qdisc (const NMPObject *obj)
 {
-	return obj->object.ifindex > 0;
+	return NMP_OBJECT_CAST_QDISC (obj)->ifindex > 0;
 }
 
 static gboolean
 _vt_cmd_obj_is_alive_tfilter (const NMPObject *obj)
 {
-	return obj->object.ifindex > 0;
+	return NMP_OBJECT_CAST_TFILTER (obj)->ifindex > 0;
 }
 
 gboolean
@@ -1672,6 +1707,12 @@ static const guint8 _supported_cache_ids_ipx_route[] = {
 	0,
 };
 
+static const guint8 _supported_cache_ids_routing_rules[] = {
+	NMP_CACHE_ID_TYPE_OBJECT_TYPE,
+	NMP_CACHE_ID_TYPE_OBJECT_BY_ADDR_FAMILY,
+	0,
+};
+
 /*****************************************************************************/
 
 static void
@@ -1949,6 +1990,7 @@ nmp_lookup_init_obj_type (NMPLookup *lookup,
 	case NMP_OBJECT_TYPE_IP6_ADDRESS:
 	case NMP_OBJECT_TYPE_IP4_ROUTE:
 	case NMP_OBJECT_TYPE_IP6_ROUTE:
+	case NMP_OBJECT_TYPE_ROUTING_RULE:
 	case NMP_OBJECT_TYPE_QDISC:
 	case NMP_OBJECT_TYPE_TFILTER:
 		_nmp_object_stackinit_from_type (&lookup->selector_obj, obj_type);
@@ -1998,7 +2040,7 @@ nmp_lookup_init_object (NMPLookup *lookup,
 	}
 
 	o = _nmp_object_stackinit_from_type (&lookup->selector_obj, obj_type);
-	o->object.ifindex = ifindex;
+	o->obj_with_ifindex.ifindex = ifindex;
 	lookup->cache_id_type = NMP_CACHE_ID_TYPE_OBJECT_BY_IFINDEX;
 	return _L (lookup);
 }
@@ -2014,7 +2056,7 @@ nmp_lookup_init_route_default (NMPLookup *lookup,
 	                                NMP_OBJECT_TYPE_IP6_ROUTE));
 
 	o = _nmp_object_stackinit_from_type (&lookup->selector_obj, obj_type);
-	o->object.ifindex = 1;
+	o->ip_route.ifindex = 1;
 	lookup->cache_id_type = NMP_CACHE_ID_TYPE_DEFAULT_ROUTES;
 	return _L (lookup);
 }
@@ -2062,9 +2104,9 @@ nmp_lookup_init_ip4_route_by_weak_id (NMPLookup *lookup,
 	nm_assert (lookup);
 
 	o = _nmp_object_stackinit_from_type (&lookup->selector_obj, NMP_OBJECT_TYPE_IP4_ROUTE);
-	o->object.ifindex = 1;
-	o->ip_route.plen = plen;
-	o->ip_route.metric = metric;
+	o->ip4_route.ifindex = 1;
+	o->ip4_route.plen = plen;
+	o->ip4_route.metric = metric;
 	if (network)
 		o->ip4_route.network = network;
 	o->ip4_route.tos = tos;
@@ -2085,9 +2127,9 @@ nmp_lookup_init_ip6_route_by_weak_id (NMPLookup *lookup,
 	nm_assert (lookup);
 
 	o = _nmp_object_stackinit_from_type (&lookup->selector_obj, NMP_OBJECT_TYPE_IP6_ROUTE);
-	o->object.ifindex = 1;
-	o->ip_route.plen = plen;
-	o->ip_route.metric = metric;
+	o->ip6_route.ifindex = 1;
+	o->ip6_route.plen = plen;
+	o->ip6_route.metric = metric;
 	if (network)
 		o->ip6_route.network = *network;
 	if (src)
@@ -2097,6 +2139,23 @@ nmp_lookup_init_ip6_route_by_weak_id (NMPLookup *lookup,
 	return _L (lookup);
 }
 
+const NMPLookup *
+nmp_lookup_init_object_by_addr_family (NMPLookup *lookup,
+                                       NMPObjectType obj_type,
+                                       int addr_family)
+{
+	NMPObject *o;
+
+	nm_assert (lookup);
+	nm_assert_addr_family (addr_family);
+	nm_assert (NM_IN_SET (obj_type, NMP_OBJECT_TYPE_ROUTING_RULE));
+
+	o = _nmp_object_stackinit_from_type (&lookup->selector_obj, obj_type);
+	NMP_OBJECT_CAST_ROUTING_RULE (o)->addr_family = addr_family;
+	lookup->cache_id_type = NMP_CACHE_ID_TYPE_OBJECT_BY_ADDR_FAMILY;
+	return _L (lookup);
+}
+
 /*****************************************************************************/
 
 GArray *
@@ -2920,15 +2979,25 @@ nmp_cache_update_link_master_connected (NMPCache *cache,
 /*****************************************************************************/
 
 void
-nmp_cache_dirty_set_all (NMPCache *cache,
-                         const NMPLookup *lookup)
+nmp_cache_dirty_set_all_main (NMPCache *cache,
+                              const NMPLookup *lookup)
 {
+	const NMDedupMultiHeadEntry *head_entry;
+	NMDedupMultiIter iter;
+
 	nm_assert (cache);
 	nm_assert (lookup);
 
-	nm_dedup_multi_index_dirty_set_head (cache->multi_idx,
-	                                     _idx_type_get (cache, lookup->cache_id_type),
-	                                     &lookup->selector_obj);
+	head_entry = nmp_cache_lookup (cache, lookup);
+
+	nm_dedup_multi_iter_init (&iter, head_entry);
+	while (nm_dedup_multi_iter_next (&iter)) {
+		const NMDedupMultiEntry *main_entry;
+
+		main_entry = nmp_cache_reresolve_main_entry (cache, iter.current, lookup);
+
+		nm_dedup_multi_entry_set_dirty (main_entry, TRUE);
+	}
 }
 
 /*****************************************************************************/
@@ -3076,6 +3145,25 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
 		.cmd_plobj_hash_update              = _vt_cmd_plobj_hash_update_ip6_route,
 		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_ip6_route_cmp_full,
 	},
+	[NMP_OBJECT_TYPE_ROUTING_RULE - 1] = {
+		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
+		.obj_type                           = NMP_OBJECT_TYPE_ROUTING_RULE,
+		.sizeof_data                        = sizeof (NMPObjectRoutingRule),
+		.sizeof_public                      = sizeof (NMPlatformRoutingRule),
+		.obj_type_name                      = "routing-rule",
+		.rtm_gettype                        = RTM_GETRULE,
+		.signal_type_id                     = NM_PLATFORM_SIGNAL_ID_ROUTING_RULE,
+		.signal_type                        = NM_PLATFORM_SIGNAL_ROUTING_RULE_CHANGED,
+		.supported_cache_ids                = _supported_cache_ids_routing_rules,
+		.cmd_obj_is_alive                   = _vt_cmd_obj_is_alive_routing_rule,
+		.cmd_plobj_id_copy                  = _vt_cmd_plobj_id_copy_routing_rule,
+		.cmd_plobj_id_cmp                   = _vt_cmd_plobj_id_cmp_routing_rule,
+		.cmd_plobj_id_hash_update           = _vt_cmd_plobj_id_hash_update_routing_rule,
+		.cmd_plobj_to_string_id             = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_routing_rule_to_string,
+		.cmd_plobj_to_string                = (const char *(*) (const NMPlatformObject *obj, char *buf, gsize len)) nm_platform_routing_rule_to_string,
+		.cmd_plobj_hash_update              = _vt_cmd_plobj_hash_update_routing_rule,
+		.cmd_plobj_cmp                      = (int (*) (const NMPlatformObject *obj1, const NMPlatformObject *obj2)) nm_platform_routing_rule_cmp_full,
+	},
 	[NMP_OBJECT_TYPE_QDISC - 1] = {
 		.parent                             = DEDUP_MULTI_OBJ_CLASS_INIT(),
 		.obj_type                           = NMP_OBJECT_TYPE_QDISC,
diff --git a/src/platform/nmp-object.h b/src/platform/nmp-object.h
index 525e1440..3ce9db36 100644
--- a/src/platform/nmp-object.h
+++ b/src/platform/nmp-object.h
@@ -23,8 +23,8 @@
 
 #include <netinet/in.h>
 
-#include "nm-utils/nm-obj.h"
-#include "nm-utils/nm-dedup-multi.h"
+#include "nm-glib-aux/nm-obj.h"
+#include "nm-glib-aux/nm-dedup-multi.h"
 #include "nm-platform.h"
 
 struct udev_device;
@@ -174,6 +174,11 @@ typedef enum { /*< skip >*/
 	 * cache-resync. */
 	NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID,
 
+	/* a filter for objects that track an explicit address family.
+	 *
+	 * Note that currently on NMPObjectRoutingRule is indexed by this filter. */
+	NMP_CACHE_ID_TYPE_OBJECT_BY_ADDR_FAMILY,
+
 	__NMP_CACHE_ID_TYPE_MAX,
 	NMP_CACHE_ID_TYPE_MAX = __NMP_CACHE_ID_TYPE_MAX - 1,
 } NMPCacheIdType;
@@ -325,6 +330,10 @@ typedef struct {
 } NMPObjectIP6Route;
 
 typedef struct {
+	NMPlatformRoutingRule _public;
+} NMPObjectRoutingRule;
+
+typedef struct {
 	NMPlatformQdisc _public;
 } NMPObjectQdisc;
 
@@ -340,6 +349,8 @@ struct _NMPObject {
 	union {
 		NMPlatformObject        object;
 
+		NMPlatformObjWithIfindex obj_with_ifindex;
+
 		NMPlatformLink          link;
 		NMPObjectLink           _link;
 
@@ -390,6 +401,9 @@ struct _NMPObject {
 		NMPObjectIP4Route       _ip4_route;
 		NMPObjectIP6Route       _ip6_route;
 
+		NMPlatformRoutingRule   routing_rule;
+		NMPObjectRoutingRule    _routing_rule;
+
 		NMPlatformQdisc         qdisc;
 		NMPObjectQdisc          _qdisc;
 		NMPlatformTfilter       tfilter;
@@ -397,6 +411,8 @@ struct _NMPObject {
 	};
 };
 
+/*****************************************************************************/
+
 static inline gboolean
 NMP_CLASS_IS_VALID (const NMPClass *klass)
 {
@@ -405,6 +421,17 @@ NMP_CLASS_IS_VALID (const NMPClass *klass)
 	    && ((((char *) klass) - ((char *) _nmp_classes)) % (sizeof (_nmp_classes[0]))) == 0;
 }
 
+static inline const NMPClass *
+nmp_class_from_type (NMPObjectType obj_type)
+{
+	nm_assert (obj_type > 0);
+	nm_assert (obj_type <= G_N_ELEMENTS (_nmp_classes));
+	nm_assert (_nmp_classes[obj_type - 1].obj_type == obj_type);
+	nm_assert (NMP_CLASS_IS_VALID (&_nmp_classes[obj_type - 1]));
+
+	return &_nmp_classes[obj_type - 1];
+}
+
 static inline NMPObject *
 NMP_OBJECT_UP_CAST(const NMPlatformObject *plobj)
 {
@@ -454,11 +481,65 @@ NMP_OBJECT_GET_TYPE (const NMPObject *obj)
 	return obj ? obj->_class->obj_type : NMP_OBJECT_TYPE_UNKNOWN;
 }
 
+static inline gboolean
+_NMP_OBJECT_TYPE_IS_OBJ_WITH_IFINDEX (NMPObjectType obj_type)
+{
+	switch (obj_type) {
+	case NMP_OBJECT_TYPE_LINK:
+	case NMP_OBJECT_TYPE_IP4_ADDRESS:
+	case NMP_OBJECT_TYPE_IP6_ADDRESS:
+	case NMP_OBJECT_TYPE_IP4_ROUTE:
+	case NMP_OBJECT_TYPE_IP6_ROUTE:
+
+	case NMP_OBJECT_TYPE_QDISC:
+
+	case NMP_OBJECT_TYPE_TFILTER:
+
+	case NMP_OBJECT_TYPE_LNK_GRE:
+	case NMP_OBJECT_TYPE_LNK_GRETAP:
+	case NMP_OBJECT_TYPE_LNK_INFINIBAND:
+	case NMP_OBJECT_TYPE_LNK_IP6TNL:
+	case NMP_OBJECT_TYPE_LNK_IP6GRE:
+	case NMP_OBJECT_TYPE_LNK_IP6GRETAP:
+	case NMP_OBJECT_TYPE_LNK_IPIP:
+	case NMP_OBJECT_TYPE_LNK_MACSEC:
+	case NMP_OBJECT_TYPE_LNK_MACVLAN:
+	case NMP_OBJECT_TYPE_LNK_MACVTAP:
+	case NMP_OBJECT_TYPE_LNK_SIT:
+	case NMP_OBJECT_TYPE_LNK_TUN:
+	case NMP_OBJECT_TYPE_LNK_VLAN:
+	case NMP_OBJECT_TYPE_LNK_VXLAN:
+	case NMP_OBJECT_TYPE_LNK_WIREGUARD:
+		return TRUE;
+	default:
+		nm_assert (nmp_class_from_type (obj_type));
+		return FALSE;
+	}
+}
+
+#define NMP_OBJECT_CAST_OBJECT(obj) \
+	({ \
+		typeof (obj) _obj = (obj); \
+		\
+		nm_assert (   !_obj \
+		           || nmp_class_from_type (NMP_OBJECT_GET_TYPE (_obj)))); \
+		_obj ? &NM_CONSTCAST (NMPObject, _obj)->object : NULL; \
+	})
+
+#define NMP_OBJECT_CAST_OBJ_WITH_IFINDEX(obj) \
+	({ \
+		typeof (obj) _obj = (obj); \
+		\
+		nm_assert (   !_obj \
+		           || _NMP_OBJECT_TYPE_IS_OBJ_WITH_IFINDEX (NMP_OBJECT_GET_TYPE (_obj))); \
+		_obj ? &NM_CONSTCAST (NMPObject, _obj)->obj_with_ifindex : NULL; \
+	})
+
 #define NMP_OBJECT_CAST_LINK(obj) \
 	({ \
 		typeof (obj) _obj = (obj); \
 		\
-		nm_assert (!_obj || NMP_OBJECT_GET_TYPE ((const NMPObject *) _obj) == NMP_OBJECT_TYPE_LINK); \
+		nm_assert (!_obj || NMP_OBJECT_GET_TYPE (_obj) == NMP_OBJECT_TYPE_LINK); \
 		_obj ? &NM_CONSTCAST (NMPObject, _obj)->link : NULL; \
 	})
 
@@ -482,7 +563,7 @@ NMP_OBJECT_GET_TYPE (const NMPObject *obj)
 	({ \
 		typeof (obj) _obj = (obj); \
 		\
-		nm_assert (!_obj || NMP_OBJECT_GET_TYPE ((const NMPObject *) _obj) == NMP_OBJECT_TYPE_IP4_ADDRESS); \
+		nm_assert (!_obj || NMP_OBJECT_GET_TYPE (_obj) == NMP_OBJECT_TYPE_IP4_ADDRESS); \
 		_obj ? &NM_CONSTCAST (NMPObject, _obj)->ip4_address : NULL; \
 	})
 
@@ -490,7 +571,7 @@ NMP_OBJECT_GET_TYPE (const NMPObject *obj)
 	({ \
 		typeof (obj) _obj = (obj); \
 		\
-		nm_assert (!_obj || NMP_OBJECT_GET_TYPE ((const NMPObject *) _obj) == NMP_OBJECT_TYPE_IP6_ADDRESS); \
+		nm_assert (!_obj || NMP_OBJECT_GET_TYPE (_obj) == NMP_OBJECT_TYPE_IP6_ADDRESS); \
 		_obj ? &NM_CONSTCAST (NMPObject, _obj)->ip6_address : NULL; \
 	})
 
@@ -514,7 +595,7 @@ NMP_OBJECT_GET_TYPE (const NMPObject *obj)
 	({ \
 		typeof (obj) _obj = (obj); \
 		\
-		nm_assert (!_obj || NMP_OBJECT_GET_TYPE ((const NMPObject *) _obj) == NMP_OBJECT_TYPE_IP4_ROUTE); \
+		nm_assert (!_obj || NMP_OBJECT_GET_TYPE (_obj) == NMP_OBJECT_TYPE_IP4_ROUTE); \
 		_obj ? &NM_CONSTCAST (NMPObject, _obj)->ip4_route : NULL; \
 	})
 
@@ -522,15 +603,23 @@ NMP_OBJECT_GET_TYPE (const NMPObject *obj)
 	({ \
 		typeof (obj) _obj = (obj); \
 		\
-		nm_assert (!_obj || NMP_OBJECT_GET_TYPE ((const NMPObject *) _obj) == NMP_OBJECT_TYPE_IP6_ROUTE); \
+		nm_assert (!_obj || NMP_OBJECT_GET_TYPE (_obj) == NMP_OBJECT_TYPE_IP6_ROUTE); \
 		_obj ? &NM_CONSTCAST (NMPObject, _obj)->ip6_route : NULL; \
 	})
 
+#define NMP_OBJECT_CAST_ROUTING_RULE(obj) \
+	({ \
+		typeof (obj) _obj = (obj); \
+		\
+		nm_assert (!_obj || NMP_OBJECT_GET_TYPE (_obj) == NMP_OBJECT_TYPE_ROUTING_RULE); \
+		_obj ? &NM_CONSTCAST (NMPObject, _obj)->routing_rule : NULL; \
+	})
+
 #define NMP_OBJECT_CAST_QDISC(obj) \
 	({ \
 		typeof (obj) _obj = (obj); \
 		\
-		nm_assert (!_obj || NMP_OBJECT_GET_TYPE ((const NMPObject *) _obj) == NMP_OBJECT_TYPE_QDISC); \
+		nm_assert (!_obj || NMP_OBJECT_GET_TYPE (_obj) == NMP_OBJECT_TYPE_QDISC); \
 		_obj ? &NM_CONSTCAST (NMPObject, _obj)->qdisc : NULL; \
 	})
 
@@ -538,12 +627,10 @@ NMP_OBJECT_GET_TYPE (const NMPObject *obj)
 	({ \
 		typeof (obj) _obj = (obj); \
 		\
-		nm_assert (!_obj || NMP_OBJECT_GET_TYPE ((const NMPObject *) _obj) == NMP_OBJECT_TYPE_TFILTER); \
+		nm_assert (!_obj || NMP_OBJECT_GET_TYPE (_obj) == NMP_OBJECT_TYPE_TFILTER); \
 		_obj ? &NM_CONSTCAST (NMPObject, _obj)->tfilter : NULL; \
 	})
 
-const NMPClass *nmp_class_from_type (NMPObjectType obj_type);
-
 static inline const NMPObject *
 nmp_object_ref (const NMPObject *obj)
 {
@@ -565,9 +652,11 @@ nmp_object_ref (const NMPObject *obj)
 static inline void
 nmp_object_unref (const NMPObject *obj)
 {
-	nm_assert (!obj || NMP_OBJECT_IS_VALID (obj));
+	if (obj) {
+		nm_assert (NMP_OBJECT_IS_VALID (obj));
 
-	nm_dedup_multi_obj_unref ((const NMDedupMultiObj *) obj);
+		nm_dedup_multi_obj_unref ((const NMDedupMultiObj *) obj);
+	}
 }
 
 #define nm_clear_nmp_object(ptr) \
@@ -695,6 +784,9 @@ const NMPLookup *nmp_lookup_init_ip6_route_by_weak_id (NMPLookup *lookup,
                                                        guint32 metric,
                                                        const struct in6_addr *src,
                                                        guint8 src_plen);
+const NMPLookup *nmp_lookup_init_object_by_addr_family (NMPLookup *lookup,
+                                                        NMPObjectType obj_type,
+                                                        int addr_family);
 
 GArray *nmp_cache_lookup_to_array (const NMDedupMultiHeadEntry *head_entry,
                                    NMPObjectType obj_type,
@@ -784,8 +876,34 @@ NMPCacheOpsType nmp_cache_update_link_master_connected (NMPCache *cache,
                                                         const NMPObject **out_obj_old,
                                                         const NMPObject **out_obj_new);
 
-void nmp_cache_dirty_set_all (NMPCache *cache,
-                              const NMPLookup *lookup);
+static inline const NMDedupMultiEntry *
+nmp_cache_reresolve_main_entry (NMPCache *cache,
+                                const NMDedupMultiEntry *entry,
+                                const NMPLookup *lookup)
+{
+	const NMDedupMultiEntry *main_entry;
+
+	nm_assert (cache);
+	nm_assert (entry);
+	nm_assert (lookup);
+
+	if (lookup->cache_id_type == NMP_CACHE_ID_TYPE_OBJECT_TYPE) {
+		nm_assert (entry == nmp_cache_lookup_entry (cache, entry->obj));
+		return entry;
+	}
+
+	/* we only track the dirty flag for the OBJECT-TYPE index. That means,
+	 * for other lookup types we need to check the dirty flag of the main-entry. */
+	main_entry = nmp_cache_lookup_entry (cache, entry->obj);
+
+	nm_assert (main_entry);
+	nm_assert (main_entry->obj == entry->obj);
+
+	return main_entry;
+}
+
+void nmp_cache_dirty_set_all_main (NMPCache *cache,
+                                   const NMPLookup *lookup);
 
 NMPCache *nmp_cache_new (NMDedupMultiIndex *multi_idx, gboolean use_udev);
 void nmp_cache_free (NMPCache *cache);
diff --git a/src/platform/nmp-rules-manager.c b/src/platform/nmp-rules-manager.c
new file mode 100644
index 00000000..970afcde
--- /dev/null
+++ b/src/platform/nmp-rules-manager.c
@@ -0,0 +1,737 @@
+/*
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ */
+
+#include "nm-default.h"
+
+#include "nmp-rules-manager.h"
+
+#include <linux/fib_rules.h>
+#include <linux/rtnetlink.h>
+
+#include "nm-std-aux/c-list-util.h"
+#include "nmp-object.h"
+
+/*****************************************************************************/
+
+struct _NMPRulesManager {
+	NMPlatform *platform;
+	GHashTable *by_obj;
+	GHashTable *by_user_tag;
+	GHashTable *by_data;
+	guint ref_count;
+};
+
+/*****************************************************************************/
+
+static void _rules_init (NMPRulesManager *self);
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN           LOGD_PLATFORM
+#define _NMLOG_PREFIX_NAME      "rules-manager"
+
+#define _NMLOG(level, ...) \
+    G_STMT_START { \
+        const NMLogLevel __level = (level); \
+        \
+        if (nm_logging_enabled (__level, _NMLOG_DOMAIN)) { \
+            _nm_log (__level, _NMLOG_DOMAIN, 0, NULL, NULL, \
+                     "%s: " _NM_UTILS_MACRO_FIRST (__VA_ARGS__), \
+                     _NMLOG_PREFIX_NAME \
+                     _NM_UTILS_MACRO_REST (__VA_ARGS__)); \
+        } \
+    } G_STMT_END
+
+/*****************************************************************************/
+
+static gboolean
+NMP_IS_RULES_MANAGER (gpointer self)
+{
+	return    self
+	       && ((NMPRulesManager *) self)->ref_count > 0
+	       && NM_IS_PLATFORM (((NMPRulesManager *) self)->platform);
+}
+
+#define _USER_TAG_LOG(user_tag) nm_hash_obfuscate_ptr (1240261787u, (user_tag))
+
+/*****************************************************************************/
+
+typedef struct {
+	const NMPObject *obj;
+	gconstpointer user_tag;
+	CList obj_lst;
+	CList user_tag_lst;
+
+	/* track_priority_val zero is special: those are weakly tracked rules.
+	 * That means: NetworkManager will restore them only if it removed them earlier.
+	 * But it will not remove or add them otherwise.
+	 *
+	 * Otherwise, the track_priority_val goes together with track_priority_present.
+	 * In case of one rule being tracked multile times (with different priorities),
+	 * the one with higher priority wins. See _rules_obj_get_best_data().
+	 * Then, the winning present state either enforces that the rule is present
+	 * or absent.
+	 *
+	 * If a rules is not tracked at all, it is ignored by NetworkManager. Assuming
+	 * that it was added externally by the user. But unlike weakly tracked rules,
+	 * NM will *not* restore such rules if NetworkManager themself removed them. */
+	guint32 track_priority_val;
+	bool track_priority_present:1;
+
+	bool dirty:1;
+} RulesData;
+
+typedef enum {
+	CONFIG_STATE_NONE          = 0,
+	CONFIG_STATE_ADDED_BY_US   = 1,
+	CONFIG_STATE_REMOVED_BY_US = 2,
+} ConfigState;
+
+typedef struct {
+	const NMPObject *obj;
+	CList obj_lst_head;
+
+	/* indicates whether we configured/removed the rule (during sync()). We need that, so
+	 * if the rule gets untracked, that we know to remove/restore it.
+	 *
+	 * This makes NMPRulesManager stateful (beyond the configuration that indicates
+	 * which rules are tracked).
+	 * After a restart, NetworkManager would no longer remember which rules were added
+	 * by us. That would need to be fixed by persisting the state and reloading it after
+	 * restart. */
+	ConfigState config_state;
+} RulesObjData;
+
+typedef struct {
+	gconstpointer user_tag;
+	CList user_tag_lst_head;
+} RulesUserTagData;
+
+static void
+_rules_data_assert (const RulesData *rules_data, gboolean linked)
+{
+	nm_assert (rules_data);
+	nm_assert (NMP_OBJECT_GET_TYPE (rules_data->obj) == NMP_OBJECT_TYPE_ROUTING_RULE);
+	nm_assert (nmp_object_is_visible (rules_data->obj));
+	nm_assert (rules_data->user_tag);
+	nm_assert (!linked || !c_list_is_empty (&rules_data->obj_lst));
+	nm_assert (!linked || !c_list_is_empty (&rules_data->user_tag_lst));
+}
+
+static guint
+_rules_data_hash (gconstpointer data)
+{
+	const RulesData *rules_data = data;
+	NMHashState h;
+
+	_rules_data_assert (rules_data, FALSE);
+
+	nm_hash_init (&h, 269297543u);
+	nm_platform_routing_rule_hash_update (NMP_OBJECT_CAST_ROUTING_RULE (rules_data->obj),
+	                                      NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID,
+	                                      &h);
+	nm_hash_update_val (&h, rules_data->user_tag);
+	return nm_hash_complete (&h);
+}
+
+static gboolean
+_rules_data_equal (gconstpointer data_a, gconstpointer data_b)
+{
+	const RulesData *rules_data_a = data_a;
+	const RulesData *rules_data_b = data_b;
+
+	_rules_data_assert (rules_data_a, FALSE);
+	_rules_data_assert (rules_data_b, FALSE);
+
+	return    rules_data_a->user_tag == rules_data_b->user_tag
+	       && (nm_platform_routing_rule_cmp (NMP_OBJECT_CAST_ROUTING_RULE (rules_data_a->obj),
+	                                         NMP_OBJECT_CAST_ROUTING_RULE (rules_data_b->obj),
+	                                         NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID) == 0);
+}
+
+static void
+_rules_data_destroy (gpointer data)
+{
+	RulesData *rules_data = data;
+
+	_rules_data_assert (rules_data, FALSE);
+
+	c_list_unlink_stale (&rules_data->obj_lst);
+	c_list_unlink_stale (&rules_data->user_tag_lst);
+	nmp_object_unref (rules_data->obj);
+	g_slice_free (RulesData, rules_data);
+}
+
+static const RulesData *
+_rules_obj_get_best_data (RulesObjData *obj_data)
+{
+	RulesData *rules_data;
+	const RulesData *rd_best = NULL;
+
+	c_list_for_each_entry (rules_data, &obj_data->obj_lst_head, obj_lst) {
+
+		_rules_data_assert (rules_data, TRUE);
+
+		if (rd_best) {
+			if (rd_best->track_priority_val > rules_data->track_priority_val)
+				continue;
+			if (rd_best->track_priority_val == rules_data->track_priority_val) {
+				if (   rd_best->track_priority_present
+				    || !rules_data->track_priority_present) {
+					/* if the priorities are identical, then "present" wins over
+					 * "!present" (absent). */
+					continue;
+				}
+			}
+		}
+
+		rd_best = rules_data;
+	}
+
+	return rd_best;
+}
+
+static guint
+_rules_obj_hash (gconstpointer data)
+{
+	const RulesObjData *obj_data = data;
+	NMHashState h;
+
+	nm_hash_init (&h, 432817559u);
+	nm_platform_routing_rule_hash_update (NMP_OBJECT_CAST_ROUTING_RULE (obj_data->obj),
+	                                      NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID,
+	                                      &h);
+	return nm_hash_complete (&h);
+}
+
+static gboolean
+_rules_obj_equal (gconstpointer data_a, gconstpointer data_b)
+{
+	const RulesObjData *obj_data_a = data_a;
+	const RulesObjData *obj_data_b = data_b;
+
+	return (nm_platform_routing_rule_cmp (NMP_OBJECT_CAST_ROUTING_RULE (obj_data_a->obj),
+	                                      NMP_OBJECT_CAST_ROUTING_RULE (obj_data_b->obj),
+	                                      NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID) == 0);
+}
+
+static void
+_rules_obj_destroy (gpointer data)
+{
+	RulesObjData *obj_data = data;
+
+	c_list_unlink_stale (&obj_data->obj_lst_head);
+	nmp_object_unref (obj_data->obj);
+	g_slice_free (RulesObjData, obj_data);
+}
+
+static guint
+_rules_user_tag_hash (gconstpointer data)
+{
+	const RulesUserTagData *user_tag_data = data;
+
+	return nm_hash_val (644693447u, user_tag_data->user_tag);
+}
+
+static gboolean
+_rules_user_tag_equal (gconstpointer data_a, gconstpointer data_b)
+{
+	const RulesUserTagData *user_tag_data_a = data_a;
+	const RulesUserTagData *user_tag_data_b = data_b;
+
+	return user_tag_data_a->user_tag == user_tag_data_b->user_tag;
+}
+
+static void
+_rules_user_tag_destroy (gpointer data)
+{
+	RulesUserTagData *user_tag_data = data;
+
+	c_list_unlink_stale (&user_tag_data->user_tag_lst_head);
+	g_slice_free (RulesUserTagData, user_tag_data);
+}
+
+static RulesData *
+_rules_data_lookup (GHashTable *by_data,
+                    const NMPObject *obj,
+                    gconstpointer user_tag)
+{
+	RulesData rules_data_needle = {
+		.obj      = obj,
+		.user_tag = user_tag,
+	};
+
+	return g_hash_table_lookup (by_data, &rules_data_needle);
+}
+
+void
+nmp_rules_manager_track (NMPRulesManager *self,
+                         const NMPlatformRoutingRule *routing_rule,
+                         gint32 track_priority,
+                         gconstpointer user_tag)
+{
+	NMPObject obj_stack;
+	const NMPObject *p_obj_stack;
+	RulesData *rules_data;
+	RulesObjData *obj_data;
+	RulesUserTagData *user_tag_data;
+	gboolean changed = FALSE;
+	guint32 track_priority_val;
+	gboolean track_priority_present;
+
+	g_return_if_fail (NMP_IS_RULES_MANAGER (self));
+	g_return_if_fail (routing_rule);
+	g_return_if_fail (user_tag);
+	nm_assert (track_priority != G_MININT32);
+
+	_rules_init (self);
+
+	p_obj_stack = nmp_object_stackinit (&obj_stack, NMP_OBJECT_TYPE_ROUTING_RULE, routing_rule);
+
+	nm_assert (nmp_object_is_visible (p_obj_stack));
+
+	if (track_priority >= 0) {
+		track_priority_val = track_priority;
+		track_priority_present = TRUE;
+	} else {
+		track_priority_val = -track_priority;
+		track_priority_present = FALSE;
+	}
+
+	rules_data = _rules_data_lookup (self->by_data, p_obj_stack, user_tag);
+
+	if (!rules_data) {
+		rules_data = g_slice_new (RulesData);
+		*rules_data = (RulesData) {
+			.obj                    = nm_dedup_multi_index_obj_intern (nm_platform_get_multi_idx (self->platform),
+			                                                           p_obj_stack),
+			.user_tag               = user_tag,
+			.track_priority_val     = track_priority_val,
+			.track_priority_present = track_priority_present,
+			.dirty                  = FALSE,
+		};
+		g_hash_table_add (self->by_data, rules_data);
+
+		obj_data = g_hash_table_lookup (self->by_obj, &rules_data->obj);
+		if (!obj_data) {
+			obj_data = g_slice_new (RulesObjData);
+			*obj_data = (RulesObjData) {
+				.obj          = nmp_object_ref (rules_data->obj),
+				.obj_lst_head = C_LIST_INIT (obj_data->obj_lst_head),
+				.config_state = CONFIG_STATE_NONE,
+			};
+			g_hash_table_add (self->by_obj, obj_data);
+		}
+		c_list_link_tail (&obj_data->obj_lst_head, &rules_data->obj_lst);
+
+		user_tag_data = g_hash_table_lookup (self->by_user_tag, &rules_data->user_tag);
+		if (!user_tag_data) {
+			user_tag_data = g_slice_new (RulesUserTagData);
+			*user_tag_data = (RulesUserTagData) {
+				.user_tag          = user_tag,
+				.user_tag_lst_head = C_LIST_INIT (user_tag_data->user_tag_lst_head),
+			};
+			g_hash_table_add (self->by_user_tag, user_tag_data);
+		}
+		c_list_link_tail (&user_tag_data->user_tag_lst_head, &rules_data->user_tag_lst);
+		changed = TRUE;
+	} else {
+		rules_data->dirty = FALSE;
+		if (   rules_data->track_priority_val != track_priority_val
+		    || rules_data->track_priority_present != track_priority_present) {
+			rules_data->track_priority_val = track_priority_val;
+			rules_data->track_priority_present = track_priority_present;
+			changed = TRUE;
+		}
+	}
+
+	_rules_data_assert (rules_data, TRUE);
+
+	if (changed) {
+		_LOGD ("routing-rule: track ["NM_HASH_OBFUSCATE_PTR_FMT",%s%u] \"%s\")",
+		       _USER_TAG_LOG (rules_data->user_tag),
+		       ( rules_data->track_priority_val == 0
+		        ? ""
+		        : (  rules_data->track_priority_present
+		           ? "+"
+		           : "-")),
+		       (guint) rules_data->track_priority_val,
+		       nmp_object_to_string (rules_data->obj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+	}
+}
+
+static void
+_rules_data_untrack (NMPRulesManager *self,
+                     RulesData *rules_data,
+                     gboolean remove_user_tag_data)
+{
+	RulesObjData *obj_data;
+
+	nm_assert (NMP_IS_RULES_MANAGER (self));
+	_rules_data_assert (rules_data, TRUE);
+	nm_assert (self->by_data);
+	nm_assert (g_hash_table_lookup (self->by_data, rules_data) == rules_data);
+
+	_LOGD ("routing-rule: untrack ["NM_HASH_OBFUSCATE_PTR_FMT"] \"%s\"",
+	       _USER_TAG_LOG (rules_data->user_tag),
+	       nmp_object_to_string (rules_data->obj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+
+#if NM_MORE_ASSERTS
+	{
+		RulesUserTagData *user_tag_data;
+
+		user_tag_data = g_hash_table_lookup (self->by_user_tag, &rules_data->user_tag);
+		nm_assert (user_tag_data);
+		nm_assert (c_list_contains (&user_tag_data->user_tag_lst_head, &rules_data->user_tag_lst));
+	}
+#endif
+
+	nm_assert (!c_list_is_empty (&rules_data->user_tag_lst));
+	if (   remove_user_tag_data
+	    && c_list_length_is (&rules_data->user_tag_lst, 1))
+		g_hash_table_remove (self->by_user_tag, &rules_data->user_tag);
+
+	obj_data = g_hash_table_lookup (self->by_obj, &rules_data->obj);
+	nm_assert (obj_data);
+	nm_assert (c_list_contains (&obj_data->obj_lst_head, &rules_data->obj_lst));
+	nm_assert (obj_data == g_hash_table_lookup (self->by_obj, &rules_data->obj));
+
+	/* if obj_data is marked to be "added_by_us" or "removed_by_us", we need to keep this entry
+	 * around for the next sync -- so that we can undo what we did earlier. */
+	if (   obj_data->config_state == CONFIG_STATE_NONE
+	    && c_list_length_is (&rules_data->obj_lst, 1))
+		g_hash_table_remove (self->by_obj, &rules_data->obj);
+
+	g_hash_table_remove (self->by_data, rules_data);
+}
+
+void
+nmp_rules_manager_untrack (NMPRulesManager *self,
+                           const NMPlatformRoutingRule *routing_rule,
+                           gconstpointer user_tag)
+{
+	NMPObject obj_stack;
+	const NMPObject *p_obj_stack;
+	RulesData *rules_data;
+
+	g_return_if_fail (NMP_IS_RULES_MANAGER (self));
+	g_return_if_fail (routing_rule);
+	g_return_if_fail (user_tag);
+
+	_rules_init (self);
+
+	p_obj_stack = nmp_object_stackinit (&obj_stack, NMP_OBJECT_TYPE_ROUTING_RULE, routing_rule);
+
+	nm_assert (nmp_object_is_visible (p_obj_stack));
+
+	rules_data = _rules_data_lookup (self->by_data, p_obj_stack, user_tag);
+	if (rules_data)
+		_rules_data_untrack (self, rules_data, TRUE);
+}
+
+void
+nmp_rules_manager_set_dirty (NMPRulesManager *self,
+                             gconstpointer user_tag)
+{
+	RulesData *rules_data;
+	RulesUserTagData *user_tag_data;
+
+	g_return_if_fail (NMP_IS_RULES_MANAGER (self));
+	g_return_if_fail (user_tag);
+
+	if (!self->by_data)
+		return;
+
+	user_tag_data = g_hash_table_lookup (self->by_user_tag, &user_tag);
+	if (!user_tag_data)
+		return;
+
+	c_list_for_each_entry (rules_data, &user_tag_data->user_tag_lst_head, user_tag_lst)
+		rules_data->dirty = TRUE;
+}
+
+void
+nmp_rules_manager_untrack_all (NMPRulesManager *self,
+                               gconstpointer user_tag,
+                               gboolean all /* or only dirty */)
+{
+	RulesData *rules_data;
+	RulesData *rules_data_safe;
+	RulesUserTagData *user_tag_data;
+
+	g_return_if_fail (NMP_IS_RULES_MANAGER (self));
+	g_return_if_fail (user_tag);
+
+	if (!self->by_data)
+		return;
+
+	user_tag_data = g_hash_table_lookup (self->by_user_tag, &user_tag);
+	if (!user_tag_data)
+		return;
+
+	c_list_for_each_entry_safe (rules_data, rules_data_safe, &user_tag_data->user_tag_lst_head, user_tag_lst) {
+		if (   all
+		    || rules_data->dirty)
+			_rules_data_untrack (self, rules_data, FALSE);
+	}
+	if (c_list_is_empty (&user_tag_data->user_tag_lst_head))
+		g_hash_table_remove (self->by_user_tag, user_tag_data);
+}
+
+void
+nmp_rules_manager_sync (NMPRulesManager *self,
+                        gboolean keep_deleted_rules)
+{
+	const NMDedupMultiHeadEntry *pl_head_entry;
+	NMDedupMultiIter pl_iter;
+	const NMPObject *plobj;
+	gs_unref_ptrarray GPtrArray *rules_to_delete = NULL;
+	RulesObjData *obj_data;
+	GHashTableIter h_iter;
+	guint i;
+	const RulesData *rd_best;
+
+	g_return_if_fail (NMP_IS_RULES_MANAGER (self));
+
+	if (!self->by_data)
+		return;
+
+	_LOGD ("sync%s", keep_deleted_rules ? " (don't remove any rules)" : "");
+
+	pl_head_entry = nm_platform_lookup_obj_type (self->platform, NMP_OBJECT_TYPE_ROUTING_RULE);
+	if (pl_head_entry) {
+		nmp_cache_iter_for_each (&pl_iter, pl_head_entry, &plobj) {
+			obj_data = g_hash_table_lookup (self->by_obj, &plobj);
+
+			if (!obj_data) {
+				/* this rule is not tracked. It was externally added, hence we
+				 * ignore it. */
+				continue;
+			}
+
+			rd_best = _rules_obj_get_best_data (obj_data);
+			if (rd_best) {
+				if (rd_best->track_priority_present)
+					continue;
+				if (rd_best->track_priority_val == 0) {
+					if (obj_data->config_state != CONFIG_STATE_ADDED_BY_US)
+						continue;
+					obj_data->config_state = CONFIG_STATE_NONE;
+				}
+			}
+
+			if (keep_deleted_rules) {
+				_LOGD ("forget/leak rule added by us: %s", nmp_object_to_string (plobj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+				continue;
+			}
+
+			if (!rules_to_delete)
+				rules_to_delete = g_ptr_array_new_with_free_func ((GDestroyNotify) nmp_object_unref);
+
+			g_ptr_array_add (rules_to_delete, (gpointer) nmp_object_ref (plobj));
+
+			obj_data->config_state = CONFIG_STATE_REMOVED_BY_US;
+		}
+	}
+
+	if (rules_to_delete) {
+		for (i = 0; i < rules_to_delete->len; i++)
+			nm_platform_object_delete (self->platform, rules_to_delete->pdata[i]);
+	}
+
+	g_hash_table_iter_init (&h_iter, self->by_obj);
+	while (g_hash_table_iter_next (&h_iter, (gpointer *) &obj_data, NULL)) {
+
+		rd_best = _rules_obj_get_best_data (obj_data);
+
+		if (!rd_best) {
+			g_hash_table_iter_remove (&h_iter);
+			continue;
+		}
+
+		if (!rd_best->track_priority_present)
+			continue;
+		if (rd_best->track_priority_val == 0) {
+			if (obj_data->config_state != CONFIG_STATE_REMOVED_BY_US)
+				continue;
+			obj_data->config_state = CONFIG_STATE_NONE;
+		}
+
+		plobj = nm_platform_lookup_obj (self->platform, NMP_CACHE_ID_TYPE_OBJECT_TYPE, obj_data->obj);
+		if (plobj)
+			continue;
+
+		obj_data->config_state = CONFIG_STATE_ADDED_BY_US;
+		nm_platform_routing_rule_add (self->platform, NMP_NLM_FLAG_ADD, NMP_OBJECT_CAST_ROUTING_RULE (obj_data->obj));
+	}
+}
+
+void
+nmp_rules_manager_track_from_platform (NMPRulesManager *self,
+                                       NMPlatform *platform,
+                                       int addr_family,
+                                       gint32 tracking_priority,
+                                       gconstpointer user_tag)
+{
+	NMPLookup lookup;
+	const NMDedupMultiHeadEntry *head_entry;
+	NMDedupMultiIter iter;
+	const NMPObject *o;
+
+	g_return_if_fail (NMP_IS_RULES_MANAGER (self));
+
+	if (!platform)
+		platform = self->platform;
+	else
+		g_return_if_fail (NM_IS_PLATFORM (platform));
+
+	nm_assert (NM_IN_SET (addr_family, AF_UNSPEC, AF_INET, AF_INET6));
+
+	nmp_lookup_init_obj_type (&lookup, NMP_OBJECT_TYPE_ROUTING_RULE);
+	head_entry = nm_platform_lookup (platform, &lookup);
+	nmp_cache_iter_for_each (&iter, head_entry, &o) {
+		const NMPlatformRoutingRule *rr = NMP_OBJECT_CAST_ROUTING_RULE (o);
+
+		if (   addr_family != AF_UNSPEC
+		    && rr->addr_family != addr_family)
+			continue;
+
+		nmp_rules_manager_track (self, rr, tracking_priority, user_tag);
+	}
+}
+
+/*****************************************************************************/
+
+void
+nmp_rules_manager_track_default (NMPRulesManager *self,
+                                 int addr_family,
+                                 gint32 track_priority,
+                                 gconstpointer user_tag)
+{
+	g_return_if_fail (NMP_IS_RULES_MANAGER (self));
+
+	nm_assert (NM_IN_SET (addr_family, AF_UNSPEC, AF_INET, AF_INET6));
+
+	/* track the default rules. See also `man ip-rule`. */
+
+	if (NM_IN_SET (addr_family, AF_UNSPEC, AF_INET)) {
+		nmp_rules_manager_track (self,
+		                         &((NMPlatformRoutingRule) {
+		                             .addr_family = AF_INET,
+		                             .priority    = 0,
+		                             .table       = RT_TABLE_LOCAL,
+		                             .action      = FR_ACT_TO_TBL,
+		                             .protocol    = RTPROT_KERNEL,
+		                         }),
+		                         track_priority,
+		                         user_tag);
+		nmp_rules_manager_track (self,
+		                         &((NMPlatformRoutingRule) {
+		                             .addr_family = AF_INET,
+		                             .priority    = 32766,
+		                             .table       = RT_TABLE_MAIN,
+		                             .action      = FR_ACT_TO_TBL,
+		                             .protocol    = RTPROT_KERNEL,
+		                         }),
+		                         track_priority,
+		                         user_tag);
+		nmp_rules_manager_track (self,
+		                         &((NMPlatformRoutingRule) {
+		                             .addr_family = AF_INET,
+		                             .priority    = 32767,
+		                             .table       = RT_TABLE_DEFAULT,
+		                             .action      = FR_ACT_TO_TBL,
+		                             .protocol    = RTPROT_KERNEL,
+		                         }),
+		                         track_priority,
+		                         user_tag);
+	}
+	if (NM_IN_SET (addr_family, AF_UNSPEC, AF_INET6)) {
+		nmp_rules_manager_track (self,
+		                         &((NMPlatformRoutingRule) {
+		                             .addr_family = AF_INET6,
+		                             .priority    = 0,
+		                             .table       = RT_TABLE_LOCAL,
+		                             .action      = FR_ACT_TO_TBL,
+		                             .protocol    = RTPROT_KERNEL,
+		                         }),
+		                         track_priority,
+		                         user_tag);
+		nmp_rules_manager_track (self,
+		                         &((NMPlatformRoutingRule) {
+		                             .addr_family = AF_INET6,
+		                             .priority    = 32766,
+		                             .table       = RT_TABLE_MAIN,
+		                             .action      = FR_ACT_TO_TBL,
+		                             .protocol    = RTPROT_KERNEL,
+		                         }),
+		                         track_priority,
+		                         user_tag);
+	}
+}
+
+static void
+_rules_init (NMPRulesManager *self)
+{
+	if (self->by_data)
+		return;
+
+	self->by_data      = g_hash_table_new_full (_rules_data_hash,      _rules_data_equal,      NULL, _rules_data_destroy);
+	self->by_obj       = g_hash_table_new_full (_rules_obj_hash,       _rules_obj_equal,       NULL, _rules_obj_destroy);
+	self->by_user_tag  = g_hash_table_new_full (_rules_user_tag_hash,  _rules_user_tag_equal,  NULL, _rules_user_tag_destroy);
+}
+
+/*****************************************************************************/
+
+NMPRulesManager *
+nmp_rules_manager_new (NMPlatform *platform)
+{
+	NMPRulesManager *self;
+
+	g_return_val_if_fail (NM_IS_PLATFORM (platform), NULL);
+
+	self = g_slice_new (NMPRulesManager);
+	*self = (NMPRulesManager) {
+		.ref_count     = 1,
+		.platform      = g_object_ref (platform),
+	};
+	return self;
+}
+
+void
+nmp_rules_manager_ref (NMPRulesManager *self)
+{
+	g_return_if_fail (NMP_IS_RULES_MANAGER (self));
+
+	self->ref_count++;
+}
+
+void nmp_rules_manager_unref (NMPRulesManager *self)
+{
+	g_return_if_fail (NMP_IS_RULES_MANAGER (self));
+
+	if (--self->ref_count > 0)
+		return;
+
+	if (self->by_data) {
+		g_hash_table_destroy (self->by_user_tag);
+		g_hash_table_destroy (self->by_obj);
+		g_hash_table_destroy (self->by_data);
+	}
+	g_object_unref (self->platform);
+	g_slice_free (NMPRulesManager, self);
+}
diff --git a/src/platform/nmp-rules-manager.h b/src/platform/nmp-rules-manager.h
new file mode 100644
index 00000000..310c7971
--- /dev/null
+++ b/src/platform/nmp-rules-manager.h
@@ -0,0 +1,67 @@
+/*
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __NMP_RULES_MANAGER_H__
+#define __NMP_RULES_MANAGER_H__
+
+#include "nm-platform.h"
+
+/*****************************************************************************/
+
+typedef struct _NMPRulesManager NMPRulesManager;
+
+NMPRulesManager *nmp_rules_manager_new (NMPlatform *platform);
+
+void nmp_rules_manager_ref (NMPRulesManager *self);
+void nmp_rules_manager_unref (NMPRulesManager *self);
+
+#define nm_auto_unref_rules_manager nm_auto (_nmp_rules_manager_unref)
+NM_AUTO_DEFINE_FCN0 (NMPRulesManager *, _nmp_rules_manager_unref, nmp_rules_manager_unref)
+
+void nmp_rules_manager_track (NMPRulesManager *self,
+                              const NMPlatformRoutingRule *routing_rule,
+                              gint32 track_priority,
+                              gconstpointer user_tag);
+
+void nmp_rules_manager_track_default (NMPRulesManager *self,
+                                      int addr_family,
+                                      gint32 track_priority,
+                                      gconstpointer user_tag);
+
+void nmp_rules_manager_track_from_platform (NMPRulesManager *self,
+                                            NMPlatform *platform,
+                                            int addr_family,
+                                            gint32 tracking_priority,
+                                            gconstpointer user_tag);
+
+void nmp_rules_manager_untrack (NMPRulesManager *self,
+                                const NMPlatformRoutingRule *routing_rule,
+                                gconstpointer user_tag);
+
+void nmp_rules_manager_set_dirty (NMPRulesManager *self,
+                                  gconstpointer user_tag);
+
+void nmp_rules_manager_untrack_all (NMPRulesManager *self,
+                                    gconstpointer user_tag,
+                                    gboolean all /* or only dirty */);
+
+void nmp_rules_manager_sync (NMPRulesManager *self,
+                             gboolean keep_deleted_rules);
+
+/*****************************************************************************/
+
+#endif /* __NMP_RULES_MANAGER_H__ */
diff --git a/src/platform/tests/monitor.c b/src/platform/tests/monitor.c
index f0e3e6cf..fff5967b 100644
--- a/src/platform/tests/monitor.c
+++ b/src/platform/tests/monitor.c
@@ -77,8 +77,6 @@ main (int argc, char **argv)
 
 	nm_linux_platform_setup ();
 
-	nm_platform_check_kernel_support (NM_PLATFORM_GET, ~((NMPlatformKernelSupportFlags) 0));
-
 	if (global_opt.persist)
 		g_main_loop_run (loop);
 
diff --git a/src/platform/tests/test-common.c b/src/platform/tests/test-common.c
index c510710b..7ebda9ff 100644
--- a/src/platform/tests/test-common.c
+++ b/src/platform/tests/test-common.c
@@ -900,7 +900,7 @@ _ip_address_add (NMPlatform *platform,
 	}
 
 	/* Let's wait until we see the address. */
-	end_time = nm_utils_get_monotonic_timestamp_ms () + 250;
+	end_time = nm_utils_get_monotonic_timestamp_ms () + 500;
 	do {
 
 		if (external_command)
diff --git a/src/platform/tests/test-common.h b/src/platform/tests/test-common.h
index aa1f5460..e4eaec29 100644
--- a/src/platform/tests/test-common.h
+++ b/src/platform/tests/test-common.h
@@ -273,6 +273,58 @@ nmtstp_ip6_route_get_all (NMPlatform *platform,
 GArray *nmtstp_platform_ip4_address_get_all (NMPlatform *self, int ifindex);
 GArray *nmtstp_platform_ip6_address_get_all (NMPlatform *self, int ifindex);
 
+/*****************************************************************************/
+
+static inline gboolean
+_nmtstp_platform_routing_rules_get_all_predicate (const NMPObject *obj,
+                                                  gpointer user_data)
+{
+	int addr_family = GPOINTER_TO_INT (user_data);
+
+	g_assert (NMP_OBJECT_GET_TYPE (obj) == NMP_OBJECT_TYPE_ROUTING_RULE);
+
+	return    addr_family == AF_UNSPEC
+	       || NMP_OBJECT_CAST_ROUTING_RULE (obj)->addr_family == addr_family;
+}
+
+static inline GPtrArray *
+nmtstp_platform_routing_rules_get_all (NMPlatform *platform, int addr_family)
+{
+	NMPLookup lookup;
+
+	g_assert (NM_IS_PLATFORM (platform));
+	g_assert (NM_IN_SET (addr_family, AF_UNSPEC, AF_INET, AF_INET6));
+
+	nmp_lookup_init_obj_type (&lookup, NMP_OBJECT_TYPE_ROUTING_RULE);
+	return nm_platform_lookup_clone (platform,
+	                                 &lookup,
+	                                 _nmtstp_platform_routing_rules_get_all_predicate,
+	                                 GINT_TO_POINTER (addr_family));
+}
+
+static inline guint
+nmtstp_platform_routing_rules_get_count (NMPlatform *platform, int addr_family)
+{
+	const NMDedupMultiHeadEntry *head_entry;
+	NMDedupMultiIter iter;
+	const NMPObject *obj;
+	NMPLookup lookup;
+	guint n;
+
+	g_assert (NM_IS_PLATFORM (platform));
+	g_assert (NM_IN_SET (addr_family, AF_UNSPEC, AF_INET, AF_INET6));
+
+	nmp_lookup_init_obj_type (&lookup, NMP_OBJECT_TYPE_ROUTING_RULE);
+	head_entry = nm_platform_lookup (platform, &lookup);
+
+	n = 0;
+	nmp_cache_iter_for_each (&iter, head_entry, &obj) {
+		if (_nmtstp_platform_routing_rules_get_all_predicate (obj, GINT_TO_POINTER (addr_family)))
+			n++;
+	}
+	return n;
+}
+
 gboolean nmtstp_platform_ip4_route_delete (NMPlatform *platform, int ifindex, in_addr_t network, guint8 plen, guint32 metric);
 gboolean nmtstp_platform_ip6_route_delete (NMPlatform *platform, int ifindex, struct in6_addr network, guint8 plen, guint32 metric);
 
diff --git a/src/platform/tests/test-link.c b/src/platform/tests/test-link.c
index 71324301..a0650cc1 100644
--- a/src/platform/tests/test-link.c
+++ b/src/platform/tests/test-link.c
@@ -25,7 +25,7 @@
 #include <sys/types.h>
 #include <linux/if_tun.h>
 
-#include "nm-utils/nm-io-utils.h"
+#include "nm-glib-aux/nm-io-utils.h"
 #include "platform/nmp-object.h"
 #include "platform/nmp-netns.h"
 #include "platform/nm-platform-utils.h"
@@ -521,8 +521,7 @@ test_bridge_addr (void)
 	plink = nm_platform_link_get (NM_PLATFORM_GET, link.ifindex);
 	g_assert (plink);
 
-	if (nm_platform_check_kernel_support (NM_PLATFORM_GET,
-	                                      NM_PLATFORM_KERNEL_SUPPORT_USER_IPV6LL)) {
+	if (nm_platform_kernel_support_get (NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL)) {
 		g_assert (!nm_platform_link_get_user_ipv6ll_enabled (NM_PLATFORM_GET, link.ifindex));
 		g_assert_cmpint (_nm_platform_uint8_inv (plink->inet6_addr_gen_mode_inv), ==, NM_IN6_ADDR_GEN_MODE_EUI64);
 
@@ -2963,6 +2962,37 @@ test_sysctl_netns_switch (void)
 	g_assert_cmpint (ifindex, ==, (gint32) nm_platform_sysctl_get_int32 (PL, NMP_SYSCTL_PATHID_NETDIR (dirfd, s ?: "<unknown>", "ifindex"), -1));
 	g_assert_cmpint (ifindex, ==, (gint32) nm_platform_sysctl_get_int32 (PL, NMP_SYSCTL_PATHID_ABSOLUTE (nm_sprintf_bufa (100, "/sys/class/net/%s/ifindex", IFNAME)), -1));
 
+	/* also test that nm_platform_sysctl_get() sets errno to ENOENT for non-existing paths. */
+	{
+		gint64 i64;
+		int errsv;
+		char *v;
+
+		errno = ESRCH;
+		v = nm_platform_sysctl_get (PL, NMP_SYSCTL_PATHID_ABSOLUTE ("/sys/devices/virtual/net/not-existing/ifindex"));
+		errsv = errno;
+		g_assert (!v);
+		g_assert_cmpint (errsv, ==, ENOENT);
+
+		errno = ESRCH;
+		i64 = nm_platform_sysctl_get_int_checked (PL, NMP_SYSCTL_PATHID_ABSOLUTE ("/sys/devices/virtual/net/not-existing/ifindex"), 10, 1, G_MAXINT, -1);
+		errsv = errno;
+		g_assert_cmpint (i64, ==, -1);
+		g_assert_cmpint (errsv, ==, ENOENT);
+
+		errno = ESRCH;
+		v = nm_platform_sysctl_get (PL, NMP_SYSCTL_PATHID_ABSOLUTE ("/sys/devices/virtual/net/lo/not-existing"));
+		errsv = errno;
+		g_assert (!v);
+		g_assert_cmpint (errsv, ==, ENOENT);
+
+		errno = ESRCH;
+		i64 = nm_platform_sysctl_get_int_checked (PL, NMP_SYSCTL_PATHID_ABSOLUTE ("/sys/devices/virtual/net/lo/not-existing"), 10, 1, G_MAXINT, -1);
+		errsv = errno;
+		g_assert_cmpint (i64, ==, -1);
+		g_assert_cmpint (errsv, ==, ENOENT);
+	}
+
 	/* accessing the path directly, only succeeds iff the current namespace happens to be the namespace
 	 * in which we created the link. */
 	{
diff --git a/src/platform/tests/test-nmp-object.c b/src/platform/tests/test-nmp-object.c
index 12acf4a5..e2019e96 100644
--- a/src/platform/tests/test-nmp-object.c
+++ b/src/platform/tests/test-nmp-object.c
@@ -24,7 +24,7 @@
 #include <linux/pkt_sched.h>
 
 #include "platform/nmp-object.h"
-#include "nm-utils/nm-udev-utils.h"
+#include "nm-udev-aux/nm-udev-utils.h"
 
 #include "nm-test-utils-core.h"
 
@@ -223,7 +223,7 @@ _nmp_cache_update_netlink (NMPCache *cache, NMPObject *obj, const NMPObject **ou
 	g_assert (cache);
 	g_assert (NMP_OBJECT_IS_VALID (obj));
 
-	obj_prev = nmp_cache_lookup_link (cache, obj->object.ifindex);
+	obj_prev = nmp_cache_lookup_link (cache, NMP_OBJECT_CAST_LINK (obj)->ifindex);
 	obj_new_expected = nmp_object_clone (obj, FALSE);
 	if (obj_prev && obj_prev->_link.udev.device)
 		obj_new_expected->_link.udev.device = udev_device_ref (obj_prev->_link.udev.device);
diff --git a/src/platform/tests/test-route.c b/src/platform/tests/test-route.c
index 2619ec52..e6ffe1ae 100644
--- a/src/platform/tests/test-route.c
+++ b/src/platform/tests/test-route.c
@@ -20,9 +20,11 @@
 #include "nm-default.h"
 
 #include <linux/rtnetlink.h>
+#include <linux/fib_rules.h>
 
 #include "nm-core-utils.h"
 #include "platform/nm-platform-utils.h"
+#include "platform/nmp-rules-manager.h"
 
 #include "test-common.h"
 
@@ -873,6 +875,861 @@ again_find_idx:
 
 /*****************************************************************************/
 
+#define FRA_SUPPRESS_IFGROUP         13
+#define FRA_SUPPRESS_PREFIXLEN       14
+#define FRA_L3MDEV                   19
+#define FRA_UID_RANGE                20
+#define FRA_PROTOCOL                 21
+#define FRA_IP_PROTO                 22
+#define FRA_SPORT_RANGE              23
+#define FRA_DPORT_RANGE              24
+
+static const NMPObject *
+_rule_find_by_priority (NMPlatform *platform,
+                        guint32 priority)
+{
+	const NMDedupMultiHeadEntry *head_entry;
+	NMDedupMultiIter iter;
+	const NMPObject *o;
+	const NMPObject *obj = NULL;
+	NMPLookup lookup;
+
+	nmp_lookup_init_obj_type (&lookup, NMP_OBJECT_TYPE_ROUTING_RULE);
+	head_entry = nm_platform_lookup (platform, &lookup);
+	nmp_cache_iter_for_each (&iter, head_entry, &o) {
+		if (NMP_OBJECT_CAST_ROUTING_RULE (o)->priority != priority)
+			continue;
+		g_assert (!obj);
+		obj = o;
+	}
+	return obj;
+}
+
+static const NMPObject *
+_rule_check_kernel_support_one (NMPlatform *platform,
+                                const NMPlatformRoutingRule *rr)
+{
+	nm_auto_nmpobj const NMPObject *obj = NULL;
+	int r;
+
+	g_assert (!_rule_find_by_priority (platform, rr->priority));
+
+	r = nm_platform_routing_rule_add (platform, NMP_NLM_FLAG_ADD, rr);
+	g_assert_cmpint (r, ==, 0);
+
+	obj = nmp_object_ref (_rule_find_by_priority (platform, rr->priority));
+	g_assert (obj);
+
+	r = nm_platform_object_delete (platform, obj);
+	g_assert_cmpint (r, ==, TRUE);
+
+	g_assert (!_rule_find_by_priority (platform, rr->priority));
+
+	return g_steal_pointer (&obj);
+}
+
+static gboolean
+_rule_check_kernel_support (NMPlatform *platform,
+                            int attribute)
+{
+	static int support[] = {
+		[FRA_SUPPRESS_IFGROUP]   = 0,
+		[FRA_SUPPRESS_PREFIXLEN] = 0,
+		[FRA_L3MDEV]             = 0,
+		[FRA_UID_RANGE]          = 0,
+		[FRA_PROTOCOL]           = 0,
+		[FRA_IP_PROTO]           = 0,
+		[FRA_SPORT_RANGE]        = 0,
+		[FRA_DPORT_RANGE]        = 0,
+	};
+	const guint32 PROBE_PRORITY = 12033;
+	gboolean sup;
+	int i;
+
+	g_assert (NM_IS_PLATFORM (platform));
+
+	if (attribute == -1) {
+		for (i = 0; i < G_N_ELEMENTS (support); i++) {
+			if (support[i] < 0) {
+				/* indicate that some test was skipped. */
+				return FALSE;
+			}
+		}
+		return TRUE;
+	}
+
+	g_assert (attribute >= 0 && attribute < G_N_ELEMENTS (support));
+
+	if (support[attribute] != 0)
+		return support[attribute] >= 0;
+
+	switch (attribute) {
+	case FRA_SUPPRESS_IFGROUP: {
+		nm_auto_nmpobj const NMPObject *obj = NULL;
+		const NMPlatformRoutingRule rr = {
+			.addr_family              = AF_INET,
+			.priority                 = PROBE_PRORITY,
+			.suppress_ifgroup_inverse = ~((guint32) 1245),
+		};
+
+		obj = _rule_check_kernel_support_one (platform, &rr);
+
+		sup = NMP_OBJECT_CAST_ROUTING_RULE (obj)->suppress_prefixlen_inverse == rr.suppress_ifgroup_inverse;
+		break;
+	}
+	case FRA_SUPPRESS_PREFIXLEN: {
+		nm_auto_nmpobj const NMPObject *obj = NULL;
+		const NMPlatformRoutingRule rr = {
+			.addr_family                = AF_INET,
+			.priority                   = PROBE_PRORITY,
+			.suppress_prefixlen_inverse = ~((guint32) 1245),
+		};
+
+		obj = _rule_check_kernel_support_one (platform, &rr);
+
+		sup = NMP_OBJECT_CAST_ROUTING_RULE (obj)->suppress_prefixlen_inverse == rr.suppress_prefixlen_inverse;
+		break;
+	}
+	case FRA_L3MDEV: {
+		nm_auto_nmpobj const NMPObject *obj = NULL;
+		const NMPlatformRoutingRule rr = {
+			.addr_family = AF_INET,
+			.priority    = PROBE_PRORITY,
+			.l3mdev      = TRUE,
+		};
+
+		obj = _rule_check_kernel_support_one (platform, &rr);
+
+		sup = NMP_OBJECT_CAST_ROUTING_RULE (obj)->l3mdev != 0;
+		break;
+	}
+	case FRA_UID_RANGE: {
+		nm_auto_nmpobj const NMPObject *obj = NULL;
+		const NMPlatformRoutingRule rr = {
+			.addr_family  = AF_INET,
+			.priority     = PROBE_PRORITY,
+			.uid_range    = {
+				.start = 0,
+				.end   = 0,
+			},
+			.uid_range_has = TRUE,
+		};
+
+		obj = _rule_check_kernel_support_one (platform, &rr);
+
+		sup = NMP_OBJECT_CAST_ROUTING_RULE (obj)->uid_range_has;
+		break;
+	}
+	case FRA_PROTOCOL: {
+		nm_auto_nmpobj const NMPObject *obj = NULL;
+		const NMPlatformRoutingRule rr = {
+			.addr_family = AF_INET,
+			.priority    = PROBE_PRORITY,
+			.protocol    = 30,
+		};
+
+		obj = _rule_check_kernel_support_one (platform, &rr);
+
+		sup = NMP_OBJECT_CAST_ROUTING_RULE (obj)->protocol == 30;
+		break;
+	}
+	case FRA_IP_PROTO: {
+		nm_auto_nmpobj const NMPObject *obj = NULL;
+		const NMPlatformRoutingRule rr = {
+			.addr_family = AF_INET,
+			.priority    = PROBE_PRORITY,
+			.ip_proto    = 30,
+		};
+
+		obj = _rule_check_kernel_support_one (platform, &rr);
+
+		sup = NMP_OBJECT_CAST_ROUTING_RULE (obj)->ip_proto == 30;
+		break;
+	}
+	case FRA_SPORT_RANGE:
+	case FRA_DPORT_RANGE:
+		/* these were added at the same time as FRA_IP_PROTO. */
+		sup = _rule_check_kernel_support (platform, FRA_IP_PROTO);
+		break;
+	default:
+		g_assert_not_reached ();
+		return FALSE;
+	}
+
+	support[attribute] = sup ? 1 : -1;
+
+	_LOGD ("kernel support for routing rule attribute #%d %s", attribute, sup ? "detected" : "not detected");
+	return sup;
+}
+
+static const NMPObject *
+_platform_has_routing_rule (NMPlatform *platform,
+                            const NMPObject *obj)
+{
+	const NMPObject *o;
+
+	g_assert (NM_IS_PLATFORM (platform));
+	g_assert (NMP_OBJECT_IS_VALID (obj));
+	g_assert (NMP_OBJECT_GET_TYPE (obj) == NMP_OBJECT_TYPE_ROUTING_RULE);
+
+	o = nm_platform_lookup_obj (platform, NMP_CACHE_ID_TYPE_OBJECT_TYPE, obj);
+	if (o)
+		g_assert (nm_platform_routing_rule_cmp (NMP_OBJECT_CAST_ROUTING_RULE (obj), NMP_OBJECT_CAST_ROUTING_RULE (o), NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID) == 0);
+
+	return o;
+}
+
+static guint32
+_rr_rand_choose_u32 (guint32 p)
+{
+	/* mostly, we just return zero. We want that each rule only has few
+	 * fields set -- having most fields at zero. */
+	if ((p % 10000u) < 7500u)
+		return 0;
+
+	/* give 0xFFFFFFFFu extra probability. */
+	if ((p % 10000u) < 8250u)
+		return 0xFFFFFFFFu;
+
+	/* choose a small number. */
+	if ((p % 10000u) < 9125u)
+		return (~p) % 10;
+
+	/* finally, full random number. */
+	return ~p;
+}
+
+#define _rr_rand_choose_u8(p)  ((guint8) _rr_rand_choose_u32 ((p)))
+
+static const NMPObject *
+_rule_create_random (NMPlatform *platform)
+{
+	NMPObject *obj;
+	NMPlatformRoutingRule *rr;
+	guint32 p;
+	int addr_size;
+	guint i;
+	char saddr[NM_UTILS_INET_ADDRSTRLEN];
+	static struct {
+		guint32 uid;
+		guint32 euid;
+		bool initialized;
+	} uids;
+
+	if (G_UNLIKELY (!uids.initialized)) {
+		uids.uid = getuid ();
+		uids.euid = geteuid ();
+		uids.initialized = TRUE;
+	}
+
+	obj = nmp_object_new (NMP_OBJECT_TYPE_ROUTING_RULE, NULL);
+	rr = NMP_OBJECT_CAST_ROUTING_RULE (obj);
+
+	rr->addr_family = nmtst_rand_select (AF_INET, AF_INET6);
+
+	addr_size = nm_utils_addr_family_to_size (rr->addr_family);
+
+	p = nmtst_get_rand_int ();
+	if ((p % 1000u) < 50)
+		rr->priority = 10000 + ((~p) % 20u);
+
+	p = nmtst_get_rand_int ();
+	if ((p % 1000u) < 40)
+		nm_sprintf_buf (rr->iifname, "t-iif-%u", (~p) % 20);
+	else if ((p % 1000u) < 80)
+		nm_sprintf_buf (rr->iifname, "%s", DEVICE_NAME);
+
+	p = nmtst_get_rand_int ();
+	if ((p % 1000u) < 40)
+		nm_sprintf_buf (rr->oifname, "t-oif-%d", (~p) % 20);
+	else if ((p % 1000u) < 80)
+		nm_sprintf_buf (rr->oifname, "%s", DEVICE_NAME);
+
+	for (i = 0; i < 2; i++) {
+		NMIPAddr *p_addr = i ? &rr->src     : &rr->dst;
+		guint8 *p_len    = i ? &rr->src_len : &rr->dst_len;
+
+		p = nmtst_get_rand_int ();
+		if ((p % 1000u) < 100) {
+			/* if we set src_len/dst_len to zero, the src/dst is actually ignored.
+			 *
+			 * For fuzzying, still set the address. It shall have no further effect.
+			 * */
+			*p_len = (~p) % (addr_size * 8 + 1);
+			p = nmtst_get_rand_int ();
+			if ((p % 3u) == 0) {
+				if (rr->addr_family == AF_INET)
+					p_addr->addr4 = nmtst_inet4_from_string (nm_sprintf_buf (saddr, "192.192.5.%u", (~p) % 256u));
+				else
+					p_addr->addr6 = *nmtst_inet6_from_string (nm_sprintf_buf (saddr, "1:2:3:4::f:%02x", (~p) % 256u));
+			} else if ((p % 3u) == 1)
+				nmtst_rand_buf (NULL, p_addr, addr_size);
+		}
+	}
+
+	p = nmtst_get_rand_int ();
+	if ((p % 1000u) < 50)
+		rr->tun_id = 10000 + ((~p) % 20);
+
+again_action:
+	p = nmtst_get_rand_int ();
+	if ((p % 1000u) < 500)
+		rr->action = FR_ACT_UNSPEC;
+	else if ((p % 1000u) < 750)
+		rr->action = (~p) % 12u;
+	else
+		rr->action = (~p) % 0x100u;
+
+	rr->priority = _rr_rand_choose_u32 (nmtst_get_rand_int ());
+
+	if (   rr->action == FR_ACT_GOTO
+	    && rr->priority == G_MAXINT32)
+		goto again_action;
+
+	p = nmtst_get_rand_int ();
+	if ((p % 10000u) < 100)
+		rr->goto_target = rr->priority + 1;
+	else
+		rr->goto_target = _rr_rand_choose_u32 (nmtst_get_rand_int ());
+	if (   rr->action == FR_ACT_GOTO
+	    && rr->goto_target <= rr->priority)
+		goto again_action;
+
+	p = nmtst_get_rand_int ();
+	if ((p % 1000u) < 25) {
+		if (_rule_check_kernel_support (platform, FRA_L3MDEV)) {
+			rr->l3mdev = TRUE;
+			rr->table = RT_TABLE_UNSPEC;
+		}
+	}
+
+again_table:
+	if (!rr->l3mdev) {
+		p = nmtst_get_rand_int ();
+		if ((p % 1000u) < 700)
+			rr->table = RT_TABLE_UNSPEC;
+		else if ((p % 1000u) < 850)
+			rr->table = RT_TABLE_MAIN;
+		else
+			rr->table = 10000 + ((~p) % 10);
+		if (   rr->action == FR_ACT_TO_TBL
+		    && rr->table == RT_TABLE_UNSPEC)
+			goto again_table;
+	}
+
+	rr->fwmark = _rr_rand_choose_u32 (nmtst_get_rand_int ());
+	rr->fwmask = _rr_rand_choose_u32 (nmtst_get_rand_int ());
+
+	rr->flow = _rr_rand_choose_u32 (nmtst_get_rand_int ());
+
+	if (_rule_check_kernel_support (platform, FRA_PROTOCOL))
+		rr->protocol = _rr_rand_choose_u8 (nmtst_get_rand_int ());
+
+#define IPTOS_TOS_MASK 0x1E
+
+again_tos:
+	rr->tos = _rr_rand_choose_u8 (nmtst_get_rand_int ());
+	if (   rr->addr_family == AF_INET
+	    && rr->tos & ~IPTOS_TOS_MASK)
+		goto again_tos;
+
+	if (_rule_check_kernel_support (platform, FRA_IP_PROTO))
+		rr->ip_proto = _rr_rand_choose_u8 (nmtst_get_rand_int ());
+
+	if (_rule_check_kernel_support (platform, FRA_SUPPRESS_PREFIXLEN))
+		rr->suppress_prefixlen_inverse = ~_rr_rand_choose_u32 (nmtst_get_rand_int ());
+
+	if (_rule_check_kernel_support (platform, FRA_SUPPRESS_IFGROUP))
+		rr->suppress_ifgroup_inverse = ~_rr_rand_choose_u32 (nmtst_get_rand_int ());
+
+	if (_rule_check_kernel_support (platform, FRA_UID_RANGE)) {
+		p = nmtst_get_rand_int ();
+		rr->uid_range_has = (p % 10000u) < 200;
+	}
+
+again_uid_range:
+	rr->uid_range.start = nmtst_rand_select (0u, uids.uid, uids.euid);
+	rr->uid_range.end   = nmtst_rand_select (0u, uids.uid, uids.euid);
+	if (rr->uid_range_has) {
+		if (rr->uid_range.end < rr->uid_range.start)
+			NMTST_SWAP (rr->uid_range.start, rr->uid_range.end);
+		if (   rr->uid_range.start == ((guint32) -1)
+		    || rr->uid_range.end   == ((guint32) -1))
+			goto again_uid_range;
+	}
+
+	for (i = 0; i < 2; i++) {
+		NMFibRulePortRange *range = i ? &rr->sport_range : &rr->dport_range;
+		int attribute             = i ? FRA_SPORT_RANGE  : FRA_DPORT_RANGE;
+
+		if (!_rule_check_kernel_support (platform, attribute))
+			continue;
+
+		p = nmtst_get_rand_int ();
+		if ((p % 10000u) < 300) {
+			while (range->start == 0) {
+				p = p ^ nmtst_get_rand_int ();
+				range->start = nmtst_rand_select (1u, 0xFFFEu, ((p      ) % 0xFFFEu) + 1);
+				range->end   = nmtst_rand_select (1u, 0xFFFEu, ((p >> 16) % 0xFFFEu) + 1, range->start);
+				if (range->end < range->start)
+					NMTST_SWAP (range->start, range->end);
+			}
+		}
+	}
+
+	p = nmtst_get_rand_int () % 1000u;
+	if (p < 100)
+		rr->flags |= FIB_RULE_INVERT;
+
+	return obj;
+}
+
+static gboolean
+_rule_fuzzy_equal (const NMPObject *obj,
+                   const NMPObject *obj_comp,
+                   int op_type)
+{
+	const NMPlatformRoutingRule *rr = NMP_OBJECT_CAST_ROUTING_RULE (obj);
+	NMPlatformRoutingRule rr_co = *NMP_OBJECT_CAST_ROUTING_RULE (obj_comp);
+
+	switch (op_type) {
+	case RTM_NEWRULE:
+		/* when adding rules with RTM_NEWRULE, kernel checks whether an existing
+		 * rule already exists and may fail with EEXIST. This check has issues
+		 * and reject legitimate rules (rh#1686075).
+		 *
+		 * Work around that. */
+		if (rr->src_len == 0)
+			rr_co.src_len = 0;
+		if (rr->dst_len == 0)
+			rr_co.dst_len = 0;
+		if (rr->flow == 0)
+			rr_co.flow = 0;
+		if (rr->tos == 0)
+			rr_co.tos = 0;
+		rr_co.suppress_prefixlen_inverse = rr->suppress_prefixlen_inverse;
+		rr_co.suppress_ifgroup_inverse = rr->suppress_ifgroup_inverse;
+		if (!NM_FLAGS_HAS (rr->flags, FIB_RULE_INVERT))
+			rr_co.flags &= ~((guint32) FIB_RULE_INVERT);
+		else
+			rr_co.flags |= ((guint32) FIB_RULE_INVERT);
+		break;
+	case RTM_DELRULE:
+		/* when deleting a rule with RTM_DELRULE, kernel tries to find the
+		 * cadidate to delete. It might delete the wrong rule (rh#1685816). */
+		if (rr->action == FR_ACT_UNSPEC)
+			rr_co.action = FR_ACT_UNSPEC;
+		if (rr->iifname[0] == '\0')
+			rr_co.iifname[0] = '\0';
+		if (rr->oifname[0] == '\0')
+			rr_co.oifname[0] = '\0';
+		if (rr->src_len == 0)
+			rr_co.src_len = 0;
+		if (rr->dst_len == 0)
+			rr_co.dst_len = 0;
+		if (rr->tun_id == 0)
+			rr_co.tun_id = 0;
+		if (rr->fwmark == 0)
+			rr_co.fwmark = 0;
+		if (rr->fwmask == 0)
+			rr_co.fwmask = 0;
+		if (rr->flow == 0)
+			rr_co.flow = 0;
+		if (rr->protocol == 0)
+			rr_co.protocol = 0;
+		if (rr->table == RT_TABLE_UNSPEC)
+			rr_co.table = RT_TABLE_UNSPEC;
+		if (rr->l3mdev == 0)
+			rr_co.l3mdev = 0;
+		if (rr->tos == 0)
+			rr_co.tos = 0;
+		if (rr->ip_proto == 0)
+			rr_co.ip_proto = 0;
+		if (rr->suppress_prefixlen_inverse == 0)
+			rr_co.suppress_prefixlen_inverse = 0;
+		if (rr->suppress_ifgroup_inverse == 0)
+			rr_co.suppress_ifgroup_inverse = 0;
+		if (!rr->uid_range_has)
+			rr_co.uid_range_has = FALSE;
+		if (rr->sport_range.start == 0 && rr->sport_range.end == 0) {
+			rr_co.sport_range.start = 0;
+			rr_co.sport_range.end   = 0;
+		}
+		if (rr->dport_range.start == 0 && rr->dport_range.end == 0) {
+			rr_co.dport_range.start = 0;
+			rr_co.dport_range.end   = 0;
+		}
+		if (!NM_FLAGS_HAS (rr->flags, FIB_RULE_INVERT))
+			rr_co.flags &= ~((guint32) FIB_RULE_INVERT);
+		else
+			rr_co.flags |= ((guint32) FIB_RULE_INVERT);
+		break;
+	default:
+		g_assert_not_reached ();
+		break;
+	}
+
+	return nm_platform_routing_rule_cmp (rr, &rr_co, NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID) == 0;
+}
+
+static void
+test_rule (gconstpointer test_data)
+{
+	const int TEST_IDX = GPOINTER_TO_INT (test_data);
+	const gboolean TEST_SYNC = (TEST_IDX == 4);
+	gs_unref_ptrarray GPtrArray *objs = NULL;
+	gs_unref_ptrarray GPtrArray *objs_initial = NULL;
+	NMPlatform *platform = NM_PLATFORM_GET;
+	guint i, j, n;
+	int r;
+	gboolean had_an_issue_exist = FALSE;
+
+	nm_platform_process_events (platform);
+
+	objs_initial = nmtstp_platform_routing_rules_get_all (platform, AF_UNSPEC);
+	g_assert (objs_initial);
+	g_assert_cmpint (objs_initial->len, ==, 5);
+
+	nmtstp_run_command_check ("ip rule add table 766");
+	nm_platform_process_events (platform);
+
+	for (i = 6; i > 0; i--) {
+		gs_unref_ptrarray GPtrArray *objs_extern = NULL;
+		const NMPObject *obj;
+
+		objs_extern = nmtstp_platform_routing_rules_get_all (platform, AF_UNSPEC);
+
+		g_assert (objs_extern);
+		g_assert_cmpint (objs_extern->len, ==, i);
+
+		if (TEST_IDX != 1)
+			nmtst_rand_perm (NULL, objs_extern->pdata, NULL, sizeof (gpointer), objs_extern->len);
+
+		obj = objs_extern->pdata[0];
+
+		r = nm_platform_object_delete (platform, obj);
+		g_assert_cmpint (r, ==, TRUE);
+
+		g_assert (!_platform_has_routing_rule (platform, obj));
+	}
+
+	g_assert_cmpint (nmtstp_platform_routing_rules_get_count (platform, AF_UNSPEC), ==, 0);
+
+#define RR(...) \
+	nmp_object_new (NMP_OBJECT_TYPE_ROUTING_RULE, \
+	                (const NMPlatformObject *) &((NMPlatformRoutingRule) { \
+	                  __VA_ARGS__ \
+	                }))
+
+	objs = g_ptr_array_new_with_free_func ((GDestroyNotify) nmp_object_unref);
+
+	g_ptr_array_add (objs, RR (
+		.addr_family = AF_INET,
+		.priority    = 10,
+	));
+
+	g_ptr_array_add (objs, RR (
+		.addr_family = AF_INET,
+		.priority    = 400,
+		.action      = FR_ACT_GOTO,
+		.goto_target = 10000,
+	));
+
+	g_ptr_array_add (objs, RR (
+		.addr_family = AF_INET6,
+	));
+
+	g_ptr_array_add (objs, RR (
+		.addr_family = AF_INET6,
+		.action      = FR_ACT_TO_TBL,
+		.table       = RT_TABLE_MAIN,
+	));
+
+	g_ptr_array_add (objs, RR (
+		.addr_family = AF_INET6,
+		.priority    = 30,
+	));
+
+	g_ptr_array_add (objs, RR (
+		.addr_family = AF_INET6,
+		.priority    = 50,
+		.iifname     = "t-iif-1",
+	));
+
+	g_ptr_array_add (objs, RR (
+		.addr_family = AF_INET6,
+		.priority    = 50,
+		.iifname     = "t-oif-1",
+	));
+
+	g_ptr_array_add (objs, RR (
+		.addr_family = AF_INET,
+		.priority    = 50,
+		.iifname     = "t-oif-2",
+	));
+
+	g_ptr_array_add (objs, RR (
+		.addr_family = AF_INET,
+		.priority    = 51,
+		.iifname     = DEVICE_NAME,
+	));
+
+	if (TEST_IDX == 1) {
+		g_ptr_array_add (objs, RR (
+			.addr_family = AF_INET,
+			.table       = 10000,
+		));
+	}
+
+	if (TEST_IDX != 1) {
+		nmtst_rand_perm (NULL, objs->pdata, NULL, sizeof (gpointer), objs->len);
+		g_ptr_array_set_size (objs, nmtst_get_rand_int () % (objs->len + 1));
+	}
+
+	n = (TEST_IDX != 1) ? nmtst_get_rand_int () % 50u : 0u;
+	for (i = 0; i < n; i++) {
+		nm_auto_nmpobj const NMPObject *o = NULL;
+		guint try = 0;
+
+again:
+		o = _rule_create_random (platform);
+		for (j = 0; j < objs->len; j++) {
+			if (nm_platform_routing_rule_cmp (NMP_OBJECT_CAST_ROUTING_RULE (o),
+			                                  NMP_OBJECT_CAST_ROUTING_RULE (objs->pdata[j]),
+			                                  NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID) == 0)  {
+				try++;
+				g_assert (try < 200);
+				nm_clear_pointer (&o, nmp_object_unref);
+				goto again;
+			}
+		}
+		g_ptr_array_add (objs, (gpointer) g_steal_pointer (&o));
+	}
+
+	if (TEST_IDX != 1)
+		nmtst_rand_perm (NULL, objs->pdata, NULL, sizeof (gpointer), objs->len);
+
+	if (TEST_SYNC) {
+		gs_unref_hashtable GHashTable *unique_priorities = g_hash_table_new (NULL, NULL);
+		nm_auto_unref_rules_manager NMPRulesManager *rules_manager = nmp_rules_manager_new (platform);
+		gs_unref_ptrarray GPtrArray *objs_sync = NULL;
+		gconstpointer USER_TAG_1 = &platform;
+		gconstpointer USER_TAG_2 = &unique_priorities;
+
+		objs_sync = g_ptr_array_new_with_free_func ((GDestroyNotify) nmp_object_unref);
+
+		/* ensure that priorities are unique. Otherwise it confuses the test, because
+		 * kernel may wrongly be unable to add/delete routes based on a wrong match
+		 * (rh#1685816, rh#1685816). */
+		for (i = 0; i < objs->len; i++) {
+			const NMPObject *obj = objs->pdata[i];
+			guint32 prio = NMP_OBJECT_CAST_ROUTING_RULE (obj)->priority;
+
+			if (   !NM_IN_SET (prio, 0, 32766, 32767)
+			    && !g_hash_table_contains (unique_priorities, GUINT_TO_POINTER (prio))) {
+				g_hash_table_add (unique_priorities, GUINT_TO_POINTER (prio));
+				g_ptr_array_add (objs_sync, (gpointer) nmp_object_ref (obj));
+			}
+		}
+
+		for (i = 0; i < objs_sync->len; i++) {
+			nmp_rules_manager_track (rules_manager,
+			                         NMP_OBJECT_CAST_ROUTING_RULE (objs_sync->pdata[i]),
+			                         1,
+			                         USER_TAG_1);
+			if (nmtst_get_rand_bool ()) {
+				/* this has no effect, because a negative priority (of same absolute value)
+				 * has lower priority than the positive priority above. */
+				nmp_rules_manager_track (rules_manager,
+				                         NMP_OBJECT_CAST_ROUTING_RULE (objs_sync->pdata[i]),
+				                         -1,
+				                         USER_TAG_2);
+			}
+			if (nmtst_get_rand_int () % objs_sync->len == 0) {
+				nmp_rules_manager_sync (rules_manager, FALSE);
+				g_assert_cmpint (nmtstp_platform_routing_rules_get_count (platform, AF_UNSPEC), ==, i + 1);
+			}
+		}
+
+		nmp_rules_manager_sync (rules_manager, FALSE);
+		g_assert_cmpint (nmtstp_platform_routing_rules_get_count (platform, AF_UNSPEC), ==, objs_sync->len);
+
+		for (i = 0; i < objs_sync->len; i++) {
+			switch (nmtst_get_rand_int () % 3) {
+			case 0:
+				nmp_rules_manager_untrack (rules_manager,
+				                           NMP_OBJECT_CAST_ROUTING_RULE (objs_sync->pdata[i]),
+				                           USER_TAG_1);
+				nmp_rules_manager_untrack (rules_manager,
+				                           NMP_OBJECT_CAST_ROUTING_RULE (objs_sync->pdata[i]),
+				                           USER_TAG_1);
+				break;
+			case 1:
+				nmp_rules_manager_track (rules_manager,
+				                         NMP_OBJECT_CAST_ROUTING_RULE (objs_sync->pdata[i]),
+				                         -1,
+				                         USER_TAG_1);
+				break;
+			case 2:
+				nmp_rules_manager_track (rules_manager,
+				                         NMP_OBJECT_CAST_ROUTING_RULE (objs_sync->pdata[i]),
+				                         -2,
+				                         USER_TAG_2);
+				break;
+			}
+			if (nmtst_get_rand_int () % objs_sync->len == 0) {
+				nmp_rules_manager_sync (rules_manager, FALSE);
+				g_assert_cmpint (nmtstp_platform_routing_rules_get_count (platform, AF_UNSPEC), ==, objs_sync->len - i - 1);
+			}
+		}
+
+		nmp_rules_manager_sync (rules_manager, FALSE);
+
+	} else {
+		for (i = 0; i < objs->len;) {
+			const NMPObject *obj = objs->pdata[i];
+
+			for (j = 0; j < objs->len; j++)
+				g_assert ((j < i) == (!!_platform_has_routing_rule (platform, objs->pdata[j])));
+
+			r = nm_platform_routing_rule_add (platform, NMP_NLM_FLAG_ADD, NMP_OBJECT_CAST_ROUTING_RULE (obj));
+
+			if (r == -EEXIST) {
+				g_assert (!_platform_has_routing_rule (platform, obj));
+				/* this should not happen, but there are bugs in kernel (rh#1686075). */
+				for (j = 0; j < i; j++) {
+					const NMPObject *obj2 = objs->pdata[j];
+
+					g_assert (_platform_has_routing_rule (platform, obj2));
+
+					if (_rule_fuzzy_equal (obj, obj2, RTM_NEWRULE)) {
+						r = 0;
+						break;
+					}
+				}
+				if (r == 0) {
+					/* OK, the rule is shadowed by another rule, and kernel does not allow
+					 * us to add this one (rh#1686075). Drop this from the test. */
+					g_ptr_array_remove_index (objs, i);
+					had_an_issue_exist = TRUE;
+					continue;
+				}
+			}
+
+			if (r != 0) {
+				NMPLookup lookup;
+				const NMDedupMultiHeadEntry *head_entry;
+				NMDedupMultiIter iter;
+				const NMPObject *o;
+
+				g_print (">>> failing... errno=%d, rule=%s\n", r, nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_ALL, NULL, 0));
+
+				nmp_lookup_init_obj_type (&lookup, NMP_OBJECT_TYPE_ROUTING_RULE);
+				head_entry = nm_platform_lookup (platform, &lookup);
+				nmp_cache_iter_for_each (&iter, head_entry, &o) {
+					char ch = ' ';
+
+					if (   NMP_OBJECT_CAST_ROUTING_RULE (o)->addr_family == NMP_OBJECT_CAST_ROUTING_RULE (obj)->addr_family
+					    && NMP_OBJECT_CAST_ROUTING_RULE (o)->priority == NMP_OBJECT_CAST_ROUTING_RULE (obj)->priority)
+						ch = '*';
+					g_print (">>> existing rule: %c %s\n", ch, nmp_object_to_string (o, NMP_OBJECT_TO_STRING_ALL, NULL, 0));
+				}
+
+				nmtstp_run_command_check ("ip rule");
+				nmtstp_run_command_check ("ip -6 rule");
+				g_assert_cmpint (r, ==, 0);
+			}
+
+			g_assert (_platform_has_routing_rule (platform, obj));
+
+			g_assert_cmpint (nmtstp_platform_routing_rules_get_count (platform, AF_UNSPEC), ==, i + 1);
+
+			i++;
+		}
+
+		if (TEST_IDX != 1)
+			nmtst_rand_perm (NULL, objs->pdata, NULL, sizeof (gpointer), objs->len);
+
+		if (_LOGD_ENABLED ()) {
+			nmtstp_run_command_check ("ip rule");
+			nmtstp_run_command_check ("ip -6 rule");
+		}
+
+		for (i = 0; i < objs->len; i++) {
+			const NMPObject *obj = objs->pdata[i];
+			const NMPObject *obj2;
+
+			for (j = 0; j < objs->len; j++)
+				g_assert ((j < i) == (!_platform_has_routing_rule (platform, objs->pdata[j])));
+
+			g_assert (_platform_has_routing_rule (platform, obj));
+
+			r = nm_platform_object_delete (platform, obj);
+			g_assert_cmpint (r, ==, TRUE);
+
+			obj2 = _platform_has_routing_rule (platform, obj);
+
+			if (obj2) {
+				guint k;
+
+				/* When deleting a rule, kernel does a fuzzy match, ignoring for example:
+				 *  - action, if it is FR_ACT_UNSPEC
+				 *  - iifname,oifname if it is unspecified
+				 * rh#1685816
+				 *
+				 * That means, we may have deleted the wrong rule. Which one? */
+				k = i;
+				for (j = i + 1; j < objs->len; j++) {
+					if (!_platform_has_routing_rule (platform, objs->pdata[j])) {
+						g_assert_cmpint (k, ==, i);
+						k = j;
+					}
+				}
+				g_assert_cmpint (k, >, i);
+
+				if (!_rule_fuzzy_equal (obj, objs->pdata[k], RTM_DELRULE)) {
+					g_print (">>> failing...\n");
+					g_print (">>> no fuzzy match between: %s\n", nmp_object_to_string (obj, NMP_OBJECT_TO_STRING_ALL, NULL, 0));
+					g_print (">>>                    and: %s\n", nmp_object_to_string (objs->pdata[k], NMP_OBJECT_TO_STRING_ALL, NULL, 0));
+					g_assert_not_reached ();
+				}
+
+				objs->pdata[i] = objs->pdata[k];
+				objs->pdata[k] = (gpointer) obj;
+				obj2 = NULL;
+			}
+
+			g_assert (!obj2);
+
+			g_assert_cmpint (nmtstp_platform_routing_rules_get_count (platform, AF_UNSPEC), ==, objs->len -i - 1);
+		}
+	}
+
+	g_assert_cmpint (nmtstp_platform_routing_rules_get_count (platform, AF_UNSPEC), ==, 0);
+
+	for (i = 0; i < objs_initial->len; i++) {
+		const NMPObject *obj = objs_initial->pdata[i];
+
+		for (j = 0; j < objs_initial->len; j++)
+			g_assert ((j < i) == (!!_platform_has_routing_rule (platform, objs_initial->pdata[j])));
+
+		r = nm_platform_routing_rule_add (platform, NMP_NLM_FLAG_ADD, NMP_OBJECT_CAST_ROUTING_RULE (obj));
+		g_assert_cmpint (r, ==, 0);
+	}
+	for (j = 0; j < objs_initial->len; j++)
+		g_assert (_platform_has_routing_rule (platform, objs_initial->pdata[j]));
+	g_assert_cmpint (nmtstp_platform_routing_rules_get_count (platform, AF_UNSPEC), ==, objs_initial->len);
+
+	/* the tests passed as good as we could (as good as we implemented workarounds for them).
+	 * Still, with this kernel, not all features were fully tested. Mark the test as skipped. */
+	if (had_an_issue_exist)
+		g_test_skip ("adding a rule failed with EEXIST although it should not (rh#1686075)");
+	else if (!_rule_check_kernel_support (platform, -1))
+		g_test_skip ("some kernel features were not available and skipped for the test");
+}
+
+/*****************************************************************************/
+
 NMTstpSetupFunc const _nmtstp_setup_platform_func = SETUP;
 
 void
@@ -902,4 +1759,11 @@ _nmtstp_setup_tests (void)
 		add_test_func ("/route/ip6_route_get", test_ip6_route_get);
 		add_test_func ("/route/ip4_zero_gateway", test_ip4_zero_gateway);
 	}
+
+	if (nmtstp_is_root_test ()) {
+		add_test_func_data ("/route/rule/1", test_rule, GINT_TO_POINTER (1));
+		add_test_func_data ("/route/rule/2", test_rule, GINT_TO_POINTER (2));
+		add_test_func_data ("/route/rule/3", test_rule, GINT_TO_POINTER (3));
+		add_test_func_data ("/route/rule/4", test_rule, GINT_TO_POINTER (4));
+	}
 }