about summary refs log tree commit diff
path: root/src/libnm-platform
diff options
context:
space:
mode:
authorJeremy Bicha <jeremy.bicha@canonical.com>2022-02-22 16:51:45 -0500
committerJeremy Bicha <jeremy.bicha@canonical.com>2022-02-22 16:51:45 -0500
commit9d53567404b8f256e36619ee9345cef6dfb59f76 (patch)
tree8c760223d10c83710d45d5c2819f39f121e3d9f0 /src/libnm-platform
parentda4d2b990df5b0119d1dffd76ff68141517d6136 (diff)
parent2f94dba7385fd0e0ef19a06eb4a2fcf6c43d7946 (diff)
Merge tag 'upstream/1.35.91' into ubuntu/master
Upstream version 1.35.91
Diffstat (limited to 'src/libnm-platform')
-rw-r--r--src/libnm-platform/meson.build2
-rw-r--r--src/libnm-platform/nm-linux-platform.c127
-rw-r--r--src/libnm-platform/nm-platform.c84
-rw-r--r--src/libnm-platform/nm-platform.h6
-rw-r--r--src/libnm-platform/nmp-object.c23
-rw-r--r--src/libnm-platform/nmp-route-manager.c839
-rw-r--r--src/libnm-platform/nmp-route-manager.h79
-rw-r--r--src/libnm-platform/nmp-rules-manager.c809
-rw-r--r--src/libnm-platform/nmp-rules-manager.h53
9 files changed, 1077 insertions, 945 deletions
diff --git a/src/libnm-platform/meson.build b/src/libnm-platform/meson.build
index 3e4f41a9..d0a7c6c2 100644
--- a/src/libnm-platform/meson.build
+++ b/src/libnm-platform/meson.build
@@ -9,7 +9,7 @@ libnm_platform = static_library(
     'nm-platform.c',
     'nmp-netns.c',
     'nmp-object.c',
-    'nmp-rules-manager.c',
+    'nmp-route-manager.c',
     'wifi/nm-wifi-utils-nl80211.c',
     'wifi/nm-wifi-utils.c',
     'wpan/nm-wpan-utils.c',
diff --git a/src/libnm-platform/nm-linux-platform.c b/src/libnm-platform/nm-linux-platform.c
index 83f2207d..93935b98 100644
--- a/src/libnm-platform/nm-linux-platform.c
+++ b/src/libnm-platform/nm-linux-platform.c
@@ -3268,7 +3268,7 @@ _new_from_nl_addr(struct nlmsghdr *nlh, gboolean id_only)
     };
     struct nlattr            *tb[G_N_ELEMENTS(policy)];
     const struct ifaddrmsg   *ifa;
-    gboolean                  is_v4;
+    gboolean                  IS_IPv4;
     nm_auto_nmpobj NMPObject *obj = NULL;
     int                       addr_len;
     guint32                   lifetime, preferred, timestamp;
@@ -3278,29 +3278,31 @@ _new_from_nl_addr(struct nlmsghdr *nlh, gboolean id_only)
 
     ifa = nlmsg_data(nlh);
 
-    if (!NM_IN_SET(ifa->ifa_family, AF_INET, AF_INET6))
+    if (ifa->ifa_family == AF_INET)
+        IS_IPv4 = TRUE;
+    else if (ifa->ifa_family == AF_INET6)
+        IS_IPv4 = FALSE;
+    else
         return NULL;
 
-    is_v4 = ifa->ifa_family == AF_INET;
-
     if (nlmsg_parse_arr(nlh, sizeof(*ifa), tb, policy) < 0)
         return NULL;
 
-    addr_len = is_v4 ? sizeof(in_addr_t) : sizeof(struct in6_addr);
+    addr_len = IS_IPv4 ? sizeof(in_addr_t) : sizeof(struct in6_addr);
 
-    if (ifa->ifa_prefixlen > (is_v4 ? 32 : 128))
+    if (ifa->ifa_prefixlen > (IS_IPv4 ? 32 : 128))
         return NULL;
 
     /*****************************************************************/
 
-    obj = nmp_object_new(is_v4 ? NMP_OBJECT_TYPE_IP4_ADDRESS : NMP_OBJECT_TYPE_IP6_ADDRESS, NULL);
+    obj = nmp_object_new(IS_IPv4 ? NMP_OBJECT_TYPE_IP4_ADDRESS : NMP_OBJECT_TYPE_IP6_ADDRESS, NULL);
 
     obj->ip_address.ifindex = ifa->ifa_index;
     obj->ip_address.plen    = ifa->ifa_prefixlen;
 
     _check_addr_or_return_null(tb, IFA_ADDRESS, addr_len);
     _check_addr_or_return_null(tb, IFA_LOCAL, addr_len);
-    if (is_v4) {
+    if (IS_IPv4) {
         /* For IPv4, kernel omits IFA_LOCAL/IFA_ADDRESS if (and only if) they
          * are effectively 0.0.0.0 (all-zero). */
         if (tb[IFA_LOCAL])
@@ -3336,7 +3338,7 @@ _new_from_nl_addr(struct nlmsghdr *nlh, gboolean id_only)
 
     obj->ip_address.n_ifa_flags = tb[IFA_FLAGS] ? nla_get_u32(tb[IFA_FLAGS]) : ifa->ifa_flags;
 
-    if (is_v4) {
+    if (IS_IPv4) {
         if (tb[IFA_LABEL]) {
             char label[IFNAMSIZ];
 
@@ -3387,7 +3389,7 @@ _new_from_nl_route(struct nlmsghdr *nlh, gboolean id_only)
     };
     const struct rtmsg       *rtm;
     struct nlattr            *tb[G_N_ELEMENTS(policy)];
-    gboolean                  is_v4;
+    gboolean                  IS_IPv4;
     nm_auto_nmpobj NMPObject *obj = NULL;
     int                       addr_len;
     struct {
@@ -3414,10 +3416,19 @@ _new_from_nl_route(struct nlmsghdr *nlh, gboolean id_only)
      * only handle ~supported~ routes.
      *****************************************************************/
 
-    if (!NM_IN_SET(rtm->rtm_family, AF_INET, AF_INET6))
+    if (rtm->rtm_family == AF_INET)
+        IS_IPv4 = TRUE;
+    else if (rtm->rtm_family == AF_INET6)
+        IS_IPv4 = FALSE;
+    else
         return NULL;
 
-    if (!NM_IN_SET(rtm->rtm_type, RTN_UNICAST, RTN_LOCAL))
+    if (!NM_IN_SET(rtm->rtm_type,
+                   RTN_UNICAST,
+                   RTN_LOCAL,
+                   RTN_BLACKHOLE,
+                   RTN_UNREACHABLE,
+                   RTN_PROHIBIT))
         return NULL;
 
     if (nlmsg_parse_arr(nlh, sizeof(struct rtmsg), tb, policy) < 0)
@@ -3425,10 +3436,9 @@ _new_from_nl_route(struct nlmsghdr *nlh, gboolean id_only)
 
     /*****************************************************************/
 
-    is_v4    = rtm->rtm_family == AF_INET;
-    addr_len = is_v4 ? sizeof(in_addr_t) : sizeof(struct in6_addr);
+    addr_len = IS_IPv4 ? sizeof(in_addr_t) : sizeof(struct in6_addr);
 
-    if (rtm->rtm_dst_len > (is_v4 ? 32 : 128))
+    if (rtm->rtm_dst_len > (IS_IPv4 ? 32 : 128))
         return NULL;
 
     /*****************************************************************
@@ -3492,16 +3502,45 @@ rta_multipath_done:;
             /* If no nexthops have been provided via RTA_MULTIPATH
              * we add it as regular nexthop to maintain backwards
              * compatibility */
-            nh.ifindex = ifindex;
-            nh.gateway = gateway;
+            nh.ifindex    = ifindex;
+            nh.gateway    = gateway;
+            nh.is_present = TRUE;
         } else {
             /* Kernel supports new style nexthop configuration,
              * verify that it is a duplicate and ignore old-style nexthop. */
             if (nh.ifindex != ifindex || memcmp(&nh.gateway, &gateway, addr_len) != 0)
                 return NULL;
         }
-    } else if (!nh.is_present)
-        return NULL;
+    }
+
+    if (nm_platform_route_type_is_nodev(rtm->rtm_type)) {
+        /* These routes are special. They don't have an device/ifindex.
+         *
+         * Well, actually, for IPv6 kernel will always say that the device is
+         * 1 (lo). Of course it does!! */
+        if (nh.is_present) {
+            if (IS_IPv4) {
+                if (nh.ifindex != 0 || nh.gateway.addr4 != 0) {
+                    /* we only accept kernel to notify about the ifindex/gateway, if it
+                     * is zero. This is only to be a bit forgiving, but we really don't
+                     * know how to handle such routes that have an ifindex. */
+                    return NULL;
+                }
+            } else {
+                if (!NM_IN_SET(nh.ifindex, 0, 1) || !IN6_IS_ADDR_UNSPECIFIED(&nh.gateway.addr6)) {
+                    /* We allow an ifindex of 1 (will be normalized to zero). Otherwise,
+                     * we don't expect a device/next hop. */
+                    return NULL;
+                }
+                nh.ifindex = 0;
+            }
+        }
+    } else {
+        if (!nh.is_present) {
+            /* a "normal" route needs a device. This is not the route we are looking for. */
+            return NULL;
+        }
+    }
 
     /*****************************************************************/
 
@@ -3539,7 +3578,7 @@ rta_multipath_done:;
 
     /*****************************************************************/
 
-    obj = nmp_object_new(is_v4 ? NMP_OBJECT_TYPE_IP4_ROUTE : NMP_OBJECT_TYPE_IP6_ROUTE, NULL);
+    obj = nmp_object_new(IS_IPv4 ? NMP_OBJECT_TYPE_IP4_ROUTE : NMP_OBJECT_TYPE_IP6_ROUTE, NULL);
 
     obj->ip_route.type_coerced  = nm_platform_route_type_coerce(rtm->rtm_type);
     obj->ip_route.table_coerced = nm_platform_route_table_coerce(
@@ -3555,22 +3594,22 @@ rta_multipath_done:;
     if (tb[RTA_PRIORITY])
         obj->ip_route.metric = nla_get_u32(tb[RTA_PRIORITY]);
 
-    if (is_v4)
+    if (IS_IPv4)
         obj->ip4_route.gateway = nh.gateway.addr4;
     else
         obj->ip6_route.gateway = nh.gateway.addr6;
 
-    if (is_v4)
+    if (IS_IPv4)
         obj->ip4_route.scope_inv = nm_platform_route_scope_inv(rtm->rtm_scope);
 
     if (_check_addr_or_return_null(tb, RTA_PREFSRC, addr_len)) {
-        if (is_v4)
+        if (IS_IPv4)
             memcpy(&obj->ip4_route.pref_src, nla_data(tb[RTA_PREFSRC]), addr_len);
         else
             memcpy(&obj->ip6_route.pref_src, nla_data(tb[RTA_PREFSRC]), addr_len);
     }
 
-    if (is_v4)
+    if (IS_IPv4)
         obj->ip4_route.tos = rtm->rtm_tos;
     else {
         if (tb[RTA_SRC]) {
@@ -3592,7 +3631,7 @@ rta_multipath_done:;
     obj->ip_route.lock_initrwnd = NM_FLAGS_HAS(lock, 1 << RTAX_INITRWND);
     obj->ip_route.lock_mtu      = NM_FLAGS_HAS(lock, 1 << RTAX_MTU);
 
-    if (!is_v4) {
+    if (!IS_IPv4) {
         if (tb[RTA_PREF])
             obj->ip6_route.rt_pref = nla_get_u8(tb[RTA_PREF]);
     }
@@ -4715,23 +4754,23 @@ ip_route_ignored_protocol(const NMPlatformIPRoute *route)
 static struct nl_msg *
 _nl_msg_new_route(int nlmsg_type, guint16 nlmsgflags, const NMPObject *obj)
 {
-    nm_auto_nlmsg struct nl_msg *msg   = NULL;
-    const NMPClass              *klass = NMP_OBJECT_GET_CLASS(obj);
-    gboolean                     is_v4 = klass->addr_family == AF_INET;
-    const guint32                lock  = ip_route_get_lock_flag(NMP_OBJECT_CAST_IP_ROUTE(obj));
+    nm_auto_nlmsg struct nl_msg *msg     = NULL;
+    const NMPClass              *klass   = NMP_OBJECT_GET_CLASS(obj);
+    const gboolean               IS_IPv4 = NM_IS_IPv4(klass->addr_family);
+    const guint32                lock    = ip_route_get_lock_flag(NMP_OBJECT_CAST_IP_ROUTE(obj));
     const guint32                table =
         nm_platform_route_table_uncoerce(NMP_OBJECT_CAST_IP_ROUTE(obj)->table_coerced, TRUE);
     const struct rtmsg rtmsg = {
         .rtm_family   = klass->addr_family,
-        .rtm_tos      = is_v4 ? obj->ip4_route.tos : 0,
+        .rtm_tos      = IS_IPv4 ? obj->ip4_route.tos : 0,
         .rtm_table    = table <= 0xFF ? table : RT_TABLE_UNSPEC,
         .rtm_protocol = nmp_utils_ip_config_source_coerce_to_rtprot(obj->ip_route.rt_source),
         .rtm_scope =
-            is_v4 ? nm_platform_route_scope_inv(obj->ip4_route.scope_inv) : RT_SCOPE_NOWHERE,
+            IS_IPv4 ? nm_platform_route_scope_inv(obj->ip4_route.scope_inv) : RT_SCOPE_NOWHERE,
         .rtm_type    = nm_platform_route_type_uncoerce(NMP_OBJECT_CAST_IP_ROUTE(obj)->type_coerced),
         .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 : NMP_OBJECT_CAST_IP6_ROUTE(obj)->src_plen,
+        .rtm_src_len = IS_IPv4 ? 0 : NMP_OBJECT_CAST_IP6_ROUTE(obj)->src_plen,
     };
 
     gsize addr_len;
@@ -4745,28 +4784,28 @@ _nl_msg_new_route(int nlmsg_type, guint16 nlmsgflags, const NMPObject *obj)
     if (nlmsg_append_struct(msg, &rtmsg) < 0)
         goto nla_put_failure;
 
-    addr_len = is_v4 ? sizeof(in_addr_t) : sizeof(struct in6_addr);
+    addr_len = IS_IPv4 ? sizeof(in_addr_t) : sizeof(struct in6_addr);
 
     NLA_PUT(msg,
             RTA_DST,
             addr_len,
-            is_v4 ? (gconstpointer) &obj->ip4_route.network
-                  : (gconstpointer) &obj->ip6_route.network);
+            IS_IPv4 ? (gconstpointer) &obj->ip4_route.network
+                    : (gconstpointer) &obj->ip6_route.network);
 
-    if (!is_v4) {
+    if (!IS_IPv4) {
         if (!IN6_IS_ADDR_UNSPECIFIED(&NMP_OBJECT_CAST_IP6_ROUTE(obj)->src))
             NLA_PUT(msg, RTA_SRC, addr_len, &obj->ip6_route.src);
     }
 
     NLA_PUT_U32(msg,
                 RTA_PRIORITY,
-                is_v4 ? nm_platform_ip4_route_get_effective_metric(&obj->ip4_route)
-                      : nm_platform_ip6_route_get_effective_metric(&obj->ip6_route));
+                IS_IPv4 ? nm_platform_ip4_route_get_effective_metric(&obj->ip4_route)
+                        : nm_platform_ip6_route_get_effective_metric(&obj->ip6_route));
 
     if (table > 0xFF)
         NLA_PUT_U32(msg, RTA_TABLE, table);
 
-    if (is_v4) {
+    if (IS_IPv4) {
         if (NMP_OBJECT_CAST_IP4_ROUTE(obj)->pref_src)
             NLA_PUT(msg, RTA_PREFSRC, addr_len, &obj->ip4_route.pref_src);
     } else {
@@ -4801,7 +4840,7 @@ _nl_msg_new_route(int nlmsg_type, guint16 nlmsgflags, const NMPObject *obj)
     }
 
     /* We currently don't have need for multi-hop routes... */
-    if (is_v4) {
+    if (IS_IPv4) {
         NLA_PUT(msg, RTA_GATEWAY, addr_len, &obj->ip4_route.gateway);
     } else {
         if (!IN6_IS_ADDR_UNSPECIFIED(&obj->ip6_route.gateway))
@@ -4809,7 +4848,7 @@ _nl_msg_new_route(int nlmsg_type, guint16 nlmsgflags, const NMPObject *obj)
     }
     NLA_PUT_U32(msg, RTA_OIF, obj->ip_route.ifindex);
 
-    if (!is_v4 && obj->ip6_route.rt_pref != NM_ICMPV6_ROUTER_PREF_MEDIUM)
+    if (!IS_IPv4 && obj->ip6_route.rt_pref != NM_ICMPV6_ROUTER_PREF_MEDIUM)
         NLA_PUT_U8(msg, RTA_PREF, obj->ip6_route.rt_pref);
 
     return g_steal_pointer(&msg);
@@ -8736,8 +8775,8 @@ ip_route_get(NMPlatform   *platform,
              int           oif_ifindex,
              NMPObject   **out_route)
 {
-    const gboolean            is_v4     = (addr_family == AF_INET);
-    const int                 addr_len  = is_v4 ? 4 : 16;
+    const gboolean            IS_IPv4   = NM_IS_IPv4(addr_family);
+    const int                 addr_len  = IS_IPv4 ? 4 : 16;
     int                       try_count = 0;
     WaitForNlResponseResult   seq_result;
     int                       nle;
@@ -8758,7 +8797,7 @@ ip_route_get(NMPlatform   *platform,
             .n.nlmsg_type  = RTM_GETROUTE,
             .r.rtm_family  = addr_family,
             .r.rtm_tos     = 0,
-            .r.rtm_dst_len = is_v4 ? 32 : 128,
+            .r.rtm_dst_len = IS_IPv4 ? 32 : 128,
             .r.rtm_flags   = 0x1000 /* RTM_F_LOOKUP_TABLE */,
         };
 
diff --git a/src/libnm-platform/nm-platform.c b/src/libnm-platform/nm-platform.c
index a4863c0d..b95cd95e 100644
--- a/src/libnm-platform/nm-platform.c
+++ b/src/libnm-platform/nm-platform.c
@@ -3143,7 +3143,8 @@ nm_platform_wpan_set_channel(NMPlatform *self, int ifindex, guint8 page, guint8
 static const char *
 _to_string_dev(NMPlatform *self, int ifindex, char *buf, size_t size)
 {
-    g_assert(buf && size >= TO_STRING_DEV_BUF_SIZE);
+    nm_assert(buf);
+    nm_assert(size >= TO_STRING_DEV_BUF_SIZE);
 
     if (ifindex) {
         const char *name = ifindex > 0 && self ? nm_platform_link_get_name(self, ifindex) : NULL;
@@ -6458,12 +6459,19 @@ _rtm_flags_to_string_full(char *buf, gsize buf_size, unsigned rtm_flags)
 const char *
 nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsize len)
 {
-    char s_network[INET_ADDRSTRLEN], s_gateway[INET_ADDRSTRLEN];
+    char s_network[INET_ADDRSTRLEN];
+    char s_gateway[INET_ADDRSTRLEN];
     char s_pref_src[INET_ADDRSTRLEN];
     char str_dev[TO_STRING_DEV_BUF_SIZE];
     char str_table[30];
-    char str_scope[30], s_source[50];
-    char str_tos[32], str_window[32], str_cwnd[32], str_initcwnd[32], str_initrwnd[32], str_mtu[32];
+    char str_scope[30];
+    char s_source[50];
+    char str_tos[32];
+    char str_window[32];
+    char str_cwnd[32];
+    char str_initcwnd[32];
+    char str_initrwnd[32];
+    char str_mtu[32];
     char str_rtm_flags[_RTM_FLAGS_TO_STRING_MAXLEN];
     char str_type[30];
     char str_metric[30];
@@ -6472,7 +6480,11 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz
         return buf;
 
     inet_ntop(AF_INET, &route->network, s_network, sizeof(s_network));
-    inet_ntop(AF_INET, &route->gateway, s_gateway, sizeof(s_gateway));
+
+    if (route->gateway == 0)
+        s_gateway[0] = '\0';
+    else
+        inet_ntop(AF_INET, &route->gateway, s_gateway, sizeof(s_gateway));
 
     _to_string_dev(NULL, route->ifindex, str_dev, sizeof(str_dev));
 
@@ -6482,21 +6494,22 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz
         "type %s " /* type */
         "%s"       /* table */
         "%s/%d"
-        " via %s"
+        "%s%s" /* gateway */
         "%s"
         " metric %s"
-        " mss %" G_GUINT32_FORMAT " rt-src %s" /* protocol */
-        "%s"                                   /* rtm_flags */
-        "%s%s"                                 /* scope */
-        "%s%s"                                 /* pref-src */
-        "%s"                                   /* tos */
-        "%s"                                   /* window */
-        "%s"                                   /* cwnd */
-        "%s"                                   /* initcwnd */
-        "%s"                                   /* initrwnd */
-        "%s"                                   /* mtu */
-        "%s"                                   /* r_assume_config_once */
-        "%s"                                   /* r_force_commit */
+        " mss %" G_GUINT32_FORMAT /* mss */
+        " rt-src %s"              /* protocol */
+        "%s"                      /* rtm_flags */
+        "%s%s"                    /* scope */
+        "%s%s"                    /* pref-src */
+        "%s"                      /* tos */
+        "%s"                      /* window */
+        "%s"                      /* cwnd */
+        "%s"                      /* initcwnd */
+        "%s"                      /* initrwnd */
+        "%s"                      /* mtu */
+        "%s"                      /* r_assume_config_once */
+        "%s"                      /* r_force_commit */
         "",
         nm_net_aux_rtnl_rtntype_n2a_maybe_buf(nm_platform_route_type_uncoerce(route->type_coerced),
                                               str_type),
@@ -6509,6 +6522,7 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz
                    : ""),
         s_network,
         route->plen,
+        s_gateway[0] ? " via " : "",
         s_gateway,
         str_dev,
         route->metric_any
@@ -6596,7 +6610,11 @@ nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsiz
         return buf;
 
     inet_ntop(AF_INET6, &route->network, s_network, sizeof(s_network));
-    inet_ntop(AF_INET6, &route->gateway, s_gateway, sizeof(s_gateway));
+
+    if (IN6_IS_ADDR_UNSPECIFIED(&route->gateway))
+        s_gateway[0] = '\0';
+    else
+        inet_ntop(AF_INET6, &route->gateway, s_gateway, sizeof(s_gateway));
 
     if (IN6_IS_ADDR_UNSPECIFIED(&route->pref_src))
         s_pref_src[0] = 0;
@@ -6611,21 +6629,22 @@ nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsiz
         "type %s " /* type */
         "%s"       /* table */
         "%s/%d"
-        " via %s"
+        "%s%s" /* gateway */
         "%s"
         " metric %s"
-        " mss %" G_GUINT32_FORMAT " rt-src %s" /* protocol */
-        "%s"                                   /* source */
-        "%s"                                   /* rtm_flags */
-        "%s%s"                                 /* pref-src */
-        "%s"                                   /* window */
-        "%s"                                   /* cwnd */
-        "%s"                                   /* initcwnd */
-        "%s"                                   /* initrwnd */
-        "%s"                                   /* mtu */
-        "%s"                                   /* pref */
-        "%s"                                   /* r_assume_config_once */
-        "%s"                                   /* r_force_commit */
+        " mss %" G_GUINT32_FORMAT /* mss */
+        " rt-src %s"              /* protocol */
+        "%s"                      /* source */
+        "%s"                      /* rtm_flags */
+        "%s%s"                    /* pref-src */
+        "%s"                      /* window */
+        "%s"                      /* cwnd */
+        "%s"                      /* initcwnd */
+        "%s"                      /* initrwnd */
+        "%s"                      /* mtu */
+        "%s"                      /* pref */
+        "%s"                      /* r_assume_config_once */
+        "%s"                      /* r_force_commit */
         "",
         nm_net_aux_rtnl_rtntype_n2a_maybe_buf(nm_platform_route_type_uncoerce(route->type_coerced),
                                               str_type),
@@ -6638,6 +6657,7 @@ nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsiz
                    : ""),
         s_network,
         route->plen,
+        s_gateway[0] ? " via " : "",
         s_gateway,
         str_dev,
         route->metric_any
diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h
index c305a1db..6f5cd524 100644
--- a/src/libnm-platform/nm-platform.h
+++ b/src/libnm-platform/nm-platform.h
@@ -1408,6 +1408,12 @@ _nm_platform_link_get_inet6_addr_gen_mode(const NMPlatformLink *pllink)
     return _nm_platform_uint8_inv(pllink->inet6_addr_gen_mode_inv);
 }
 
+static inline gboolean
+nm_platform_route_type_is_nodev(guint8 type)
+{
+    return NM_IN_SET(type, 6 /* RTN_BLACKHOLE */, 7 /* RTN_UNREACHABLE */, 8 /* RTN_PROHIBIT */);
+}
+
 /**
  * nm_platform_route_type_coerce:
  * @table: the route type, in its original value.
diff --git a/src/libnm-platform/nmp-object.c b/src/libnm-platform/nmp-object.c
index ef94942e..d518e6e5 100644
--- a/src/libnm-platform/nmp-object.c
+++ b/src/libnm-platform/nmp-object.c
@@ -386,7 +386,11 @@ _idx_obj_part(const DedupMultiIdxType *idx_type,
                 nm_hash_update_val(h, obj_a);
             return 0;
         }
-        nm_assert(NMP_OBJECT_CAST_OBJ_WITH_IFINDEX(obj_a)->ifindex > 0);
+        nm_assert(NMP_OBJECT_CAST_OBJ_WITH_IFINDEX(obj_a)->ifindex > 0
+                  || (NMP_OBJECT_CAST_OBJ_WITH_IFINDEX(obj_a)->ifindex == 0
+                      && NM_IN_SET(NMP_OBJECT_GET_TYPE(obj_a),
+                                   NMP_OBJECT_TYPE_IP4_ROUTE,
+                                   NMP_OBJECT_TYPE_IP6_ROUTE)));
         if (obj_b) {
             return NMP_OBJECT_GET_TYPE(obj_a) == NMP_OBJECT_GET_TYPE(obj_b)
                    && NMP_OBJECT_CAST_OBJ_WITH_IFINDEX(obj_a)->ifindex
@@ -401,14 +405,14 @@ _idx_obj_part(const DedupMultiIdxType *idx_type,
     case NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID:
         obj_type = NMP_OBJECT_GET_TYPE(obj_a);
         if (!NM_IN_SET(obj_type, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE)
-            || NMP_OBJECT_CAST_IP_ROUTE(obj_a)->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)
-                   && NMP_OBJECT_CAST_IP_ROUTE(obj_b)->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,
@@ -1740,8 +1744,7 @@ _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 NMP_OBJECT_CAST_IP_ROUTE(obj)->ifindex > 0
-           && !NM_FLAGS_HAS(obj->ip_route.r_rtm_flags, RTM_F_CLONED);
+    return !NM_FLAGS_HAS(NMP_OBJECT_CAST_IP_ROUTE(obj)->r_rtm_flags, RTM_F_CLONED);
 }
 
 static gboolean
@@ -2143,7 +2146,15 @@ nmp_lookup_init_object(NMPLookup *lookup, NMPObjectType obj_type, int ifindex)
                         NMP_OBJECT_TYPE_QDISC,
                         NMP_OBJECT_TYPE_TFILTER));
 
-    if (ifindex <= 0) {
+    if (G_UNLIKELY(
+            (ifindex < 0)
+            || (ifindex == 0
+                && !NM_IN_SET(obj_type, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE)))) {
+        /* This function used to have a fallback that meant to lookup all objects, if
+         * ifindex is non-positive. As routes can have a zero ifindex, that fallback is
+         * confusing and no longer supported. Only have this code, to catch accidental bugs
+         * after the API change. */
+        nm_assert_not_reached();
         return nmp_lookup_init_obj_type(lookup, obj_type);
     }
 
diff --git a/src/libnm-platform/nmp-route-manager.c b/src/libnm-platform/nmp-route-manager.c
new file mode 100644
index 00000000..c31c9806
--- /dev/null
+++ b/src/libnm-platform/nmp-route-manager.c
@@ -0,0 +1,839 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nmp-route-manager.h"
+
+#include <linux/fib_rules.h>
+#include <linux/rtnetlink.h>
+
+#include "libnm-log-core/nm-logging.h"
+#include "libnm-std-aux/c-list-util.h"
+#include "nmp-object.h"
+
+/*****************************************************************************/
+
+struct _NMPRouteManager {
+    NMPlatform *platform;
+    GHashTable *by_obj;
+    GHashTable *by_user_tag;
+    GHashTable *by_data;
+    CList       by_obj_lst_heads[3];
+    guint       ref_count;
+};
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_PLATFORM
+#define _NMLOG_PREFIX_NAME "route-manager"
+
+#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, LOGD_PLATFORM, _NMLOG_PREFIX_NAME, __VA_ARGS__)
+
+/*****************************************************************************/
+
+static gboolean
+NMP_IS_ROUTE_MANAGER(gpointer self)
+{
+    return self && ((NMPRouteManager *) self)->ref_count > 0
+           && NM_IS_PLATFORM(((NMPRouteManager *) self)->platform);
+}
+
+/*****************************************************************************/
+
+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 multiple times (with different priorities),
+     * the one with higher priority wins. See _track_obj_data_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;
+} TrackData;
+
+typedef enum {
+    CONFIG_STATE_NONE          = 0,
+    CONFIG_STATE_ADDED_BY_US   = 1,
+    CONFIG_STATE_REMOVED_BY_US = 2,
+
+    /* ConfigState encodes whether the rule was touched by us at all (CONFIG_STATE_NONE).
+     *
+     * Maybe we would only need to track whether we touched the rule at all. But we
+     * track it more in detail what we did: did we add it (CONFIG_STATE_ADDED_BY_US)
+     * or did we remove it (CONFIG_STATE_REMOVED_BY_US)?
+     * Finally, we need CONFIG_STATE_OWNED_BY_US, which means that we didn't actively
+     * add/remove it, but whenever we are about to undo the add/remove, we need to do it.
+     * In that sense, CONFIG_STATE_OWNED_BY_US is really just a flag that we unconditionally
+     * force the state next time when necessary. */
+    CONFIG_STATE_OWNED_BY_US = 3,
+} ConfigState;
+
+typedef struct {
+    const NMPObject *obj;
+    CList            obj_lst_head;
+
+    CList by_obj_lst;
+
+    /* 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 NMPRouteManager 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 is partially fixed by NetworkManager taking over the rules that it
+     * actively configures (see %NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG). */
+    ConfigState config_state;
+} TrackObjData;
+
+typedef struct {
+    gconstpointer user_tag;
+    CList         user_tag_lst_head;
+} TrackUserTagData;
+
+/*****************************************************************************/
+
+static void _track_data_untrack(NMPRouteManager *self,
+                                TrackData       *track_data,
+                                gboolean         remove_user_tag_data,
+                                gboolean         make_owned_by_us);
+
+/*****************************************************************************/
+
+static CList *
+_by_obj_lst_head(NMPRouteManager *self, NMPObjectType obj_type)
+{
+    G_STATIC_ASSERT(G_N_ELEMENTS(self->by_obj_lst_heads) == 3);
+
+    switch (obj_type) {
+    case NMP_OBJECT_TYPE_IP4_ROUTE:
+        return &self->by_obj_lst_heads[0];
+    case NMP_OBJECT_TYPE_IP6_ROUTE:
+        return &self->by_obj_lst_heads[1];
+    case NMP_OBJECT_TYPE_ROUTING_RULE:
+        return &self->by_obj_lst_heads[2];
+    default:
+        return nm_assert_unreachable_val(NULL);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+_track_data_assert(const TrackData *track_data, gboolean linked)
+{
+    nm_assert(track_data);
+    nm_assert(NM_IN_SET(NMP_OBJECT_GET_TYPE(track_data->obj),
+                        NMP_OBJECT_TYPE_IP4_ROUTE,
+                        NMP_OBJECT_TYPE_IP6_ROUTE,
+                        NMP_OBJECT_TYPE_ROUTING_RULE));
+    nm_assert(nmp_object_is_visible(track_data->obj));
+    nm_assert(track_data->user_tag);
+    nm_assert(!linked || !c_list_is_empty(&track_data->obj_lst));
+    nm_assert(!linked || !c_list_is_empty(&track_data->user_tag_lst));
+}
+
+static guint
+_track_data_hash(gconstpointer data)
+{
+    const TrackData *track_data = data;
+    NMHashState      h;
+
+    _track_data_assert(track_data, FALSE);
+
+    nm_hash_init(&h, 269297543u);
+    nmp_object_id_hash_update(track_data->obj, &h);
+    nm_hash_update_val(&h, track_data->user_tag);
+    return nm_hash_complete(&h);
+}
+
+static gboolean
+_track_data_equal(gconstpointer data_a, gconstpointer data_b)
+{
+    const TrackData *track_data_a = data_a;
+    const TrackData *track_data_b = data_b;
+
+    _track_data_assert(track_data_a, FALSE);
+    _track_data_assert(track_data_b, FALSE);
+
+    return track_data_a->user_tag == track_data_b->user_tag
+           && nmp_object_id_equal(track_data_a->obj, track_data_b->obj);
+}
+
+static void
+_track_data_destroy(gpointer data)
+{
+    TrackData *track_data = data;
+
+    _track_data_assert(track_data, FALSE);
+
+    c_list_unlink_stale(&track_data->obj_lst);
+    c_list_unlink_stale(&track_data->user_tag_lst);
+    nmp_object_unref(track_data->obj);
+    nm_g_slice_free(track_data);
+}
+
+static const TrackData *
+_track_obj_data_get_best_data(TrackObjData *obj_data)
+{
+    TrackData       *track_data;
+    const TrackData *td_best = NULL;
+
+    c_list_for_each_entry (track_data, &obj_data->obj_lst_head, obj_lst) {
+        _track_data_assert(track_data, TRUE);
+
+        if (td_best) {
+            if (td_best->track_priority_val > track_data->track_priority_val)
+                continue;
+            if (td_best->track_priority_val == track_data->track_priority_val) {
+                if (td_best->track_priority_present || !track_data->track_priority_present) {
+                    /* if the priorities are identical, then "present" wins over
+                     * "!present" (absent). */
+                    continue;
+                }
+            }
+        }
+
+        td_best = track_data;
+    }
+
+    return td_best;
+}
+
+static guint
+_track_obj_data_hash(gconstpointer data)
+{
+    const TrackObjData *obj_data = data;
+
+    return nmp_object_id_hash(obj_data->obj);
+}
+
+static gboolean
+_track_obj_data_equal(gconstpointer data_a, gconstpointer data_b)
+{
+    const TrackObjData *obj_data_a = data_a;
+    const TrackObjData *obj_data_b = data_b;
+
+    return nmp_object_id_equal(obj_data_a->obj, obj_data_b->obj);
+}
+
+static void
+_track_obj_data_destroy(gpointer data)
+{
+    TrackObjData *obj_data = data;
+
+    c_list_unlink_stale(&obj_data->obj_lst_head);
+    c_list_unlink_stale(&obj_data->by_obj_lst);
+    nmp_object_unref(obj_data->obj);
+    nm_g_slice_free(obj_data);
+}
+
+static void
+_track_user_tag_data_destroy(gpointer data)
+{
+    TrackUserTagData *user_tag_data = data;
+
+    c_list_unlink_stale(&user_tag_data->user_tag_lst_head);
+    nm_g_slice_free(user_tag_data);
+}
+
+static TrackData *
+_track_data_lookup(GHashTable *by_data, const NMPObject *obj, gconstpointer user_tag)
+{
+    TrackData track_data_needle = {
+        .obj      = obj,
+        .user_tag = user_tag,
+    };
+
+    return g_hash_table_lookup(by_data, &track_data_needle);
+}
+
+/*****************************************************************************/
+
+/**
+ * nmp_route_manager_track:
+ * @self: the #NMPRouteManager instance
+ * @obj_type: the NMPObjectType of @obj that we are tracking.
+ * @obj: the NMPlatformObject (of type NMPObjectType) to track. Usually
+ *   a #NMPlatformRoutingRule, #NMPlatformIP4Route or #NMPlatformIP6Route
+ *   pointer.
+ * @track_priority: the priority for tracking the rule. Note that
+ *   negative values indicate a forced absence of the rule. Priorities
+ *   are compared with their absolute values (with higher absolute
+ *   value being more important). For example, if you track the same
+ *   rule twice, once with priority -5 and +10, then the rule is
+ *   present (because the positive number is more important).
+ *   The special value 0 indicates weakly-tracked rules.
+ * @user_tag: the tag associated with tracking this rule. The same tag
+ *   must be used to untrack the rule later.
+ * @user_tag_untrack: if not %NULL, at the same time untrack this user-tag
+ *   for the same rule. Note that this is different from a plain nmp_route_manager_untrack_rule(),
+ *   because it enforces ownership of the now tracked rule. On the other hand,
+ *   a plain nmp_route_manager_untrack_rule() merely forgets about the tracking.
+ *   The purpose here is to set this to %NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG.
+ *
+ * Returns: %TRUE, if something changed.
+ */
+gboolean
+nmp_route_manager_track(NMPRouteManager *self,
+                        NMPObjectType    obj_type,
+                        gconstpointer    obj,
+                        gint32           track_priority,
+                        gconstpointer    user_tag,
+                        gconstpointer    user_tag_untrack)
+{
+    NMPObject         obj_stack;
+    const NMPObject  *p_obj_stack;
+    TrackData        *track_data;
+    TrackObjData     *obj_data;
+    TrackUserTagData *user_tag_data;
+    gboolean          changed         = FALSE;
+    gboolean          changed_untrack = FALSE;
+    guint32           track_priority_val;
+    gboolean          track_priority_present;
+
+    g_return_val_if_fail(NMP_IS_ROUTE_MANAGER(self), FALSE);
+    g_return_val_if_fail(obj, FALSE);
+    g_return_val_if_fail(user_tag, FALSE);
+
+    /* The route must not be tied to an interface. We can only handle here
+     * blackhole/unreachable/prohibit route types. */
+    g_return_val_if_fail(
+        obj_type == NMP_OBJECT_TYPE_ROUTING_RULE
+            || (NM_IN_SET(obj_type, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE)
+                && ((const NMPlatformIPRoute *) obj)->ifindex == 0),
+        FALSE);
+
+    nm_assert(track_priority != G_MININT32);
+
+    p_obj_stack = nmp_object_stackinit(&obj_stack, obj_type, obj);
+
+    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;
+    }
+
+    track_data = _track_data_lookup(self->by_data, p_obj_stack, user_tag);
+
+    if (!track_data) {
+        track_data  = g_slice_new(TrackData);
+        *track_data = (TrackData){
+            .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, track_data);
+
+        obj_data = g_hash_table_lookup(self->by_obj, &track_data->obj);
+        if (!obj_data) {
+            obj_data  = g_slice_new(TrackObjData);
+            *obj_data = (TrackObjData){
+                .obj          = nmp_object_ref(track_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(_by_obj_lst_head(self, obj_type), &obj_data->by_obj_lst);
+        }
+        c_list_link_tail(&obj_data->obj_lst_head, &track_data->obj_lst);
+
+        user_tag_data = g_hash_table_lookup(self->by_user_tag, &track_data->user_tag);
+        if (!user_tag_data) {
+            user_tag_data  = g_slice_new(TrackUserTagData);
+            *user_tag_data = (TrackUserTagData){
+                .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, &track_data->user_tag_lst);
+        changed = TRUE;
+    } else {
+        track_data->dirty = FALSE;
+        if (track_data->track_priority_val != track_priority_val
+            || track_data->track_priority_present != track_priority_present) {
+            track_data->track_priority_val     = track_priority_val;
+            track_data->track_priority_present = track_priority_present;
+            changed                            = TRUE;
+        }
+    }
+
+    if (user_tag_untrack) {
+        if (user_tag != user_tag_untrack) {
+            TrackData *track_data_untrack;
+
+            track_data_untrack = _track_data_lookup(self->by_data, p_obj_stack, user_tag_untrack);
+            if (track_data_untrack) {
+                _track_data_untrack(self, track_data_untrack, FALSE, TRUE);
+                changed_untrack = TRUE;
+            }
+        } else
+            nm_assert_not_reached();
+    }
+
+    _track_data_assert(track_data, TRUE);
+
+    if (changed) {
+        _LOGD("track [" NM_HASH_OBFUSCATE_PTR_FMT ",%s%u] %s \"%s\"",
+              NM_HASH_OBFUSCATE_PTR(track_data->user_tag),
+              (track_data->track_priority_val == 0
+                   ? ""
+                   : (track_data->track_priority_present ? "+" : "-")),
+              (guint) track_data->track_priority_val,
+              NMP_OBJECT_GET_CLASS(track_data->obj)->obj_type_name,
+              nmp_object_to_string(track_data->obj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+    }
+
+    return changed || changed_untrack;
+}
+
+static void
+_track_data_untrack(NMPRouteManager *self,
+                    TrackData       *track_data,
+                    gboolean         remove_user_tag_data,
+                    gboolean         make_owned_by_us)
+{
+    TrackObjData *obj_data;
+
+    nm_assert(NMP_IS_ROUTE_MANAGER(self));
+    _track_data_assert(track_data, TRUE);
+    nm_assert(self->by_data);
+    nm_assert(g_hash_table_lookup(self->by_data, track_data) == track_data);
+
+    _LOGD("untrack [" NM_HASH_OBFUSCATE_PTR_FMT "] %s \"%s\"",
+          NM_HASH_OBFUSCATE_PTR(track_data->user_tag),
+          NMP_OBJECT_GET_CLASS(track_data->obj)->obj_type_name,
+          nmp_object_to_string(track_data->obj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+
+#if NM_MORE_ASSERTS
+    {
+        TrackUserTagData *user_tag_data;
+
+        user_tag_data = g_hash_table_lookup(self->by_user_tag, &track_data->user_tag);
+        nm_assert(user_tag_data);
+        nm_assert(c_list_contains(&user_tag_data->user_tag_lst_head, &track_data->user_tag_lst));
+    }
+#endif
+
+    nm_assert(!c_list_is_empty(&track_data->user_tag_lst));
+
+    obj_data = g_hash_table_lookup(self->by_obj, &track_data->obj);
+    nm_assert(obj_data);
+    nm_assert(c_list_contains(&obj_data->obj_lst_head, &track_data->obj_lst));
+    nm_assert(obj_data == g_hash_table_lookup(self->by_obj, &track_data->obj));
+
+    if (make_owned_by_us) {
+        if (obj_data->config_state == CONFIG_STATE_NONE) {
+            /* we need to mark this entry that it requires a touch on the next
+             * sync. */
+            obj_data->config_state = CONFIG_STATE_OWNED_BY_US;
+        }
+    } else if (remove_user_tag_data && c_list_length_is(&track_data->user_tag_lst, 1))
+        g_hash_table_remove(self->by_user_tag, &track_data->user_tag);
+
+    /* 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(&track_data->obj_lst, 1))
+        g_hash_table_remove(self->by_obj, &track_data->obj);
+
+    g_hash_table_remove(self->by_data, track_data);
+}
+
+gboolean
+nmp_route_manager_untrack(NMPRouteManager *self,
+                          NMPObjectType    obj_type,
+                          gconstpointer    obj,
+                          gconstpointer    user_tag)
+{
+    NMPObject        obj_stack;
+    const NMPObject *p_obj_stack;
+    TrackData       *track_data;
+    gboolean         changed = FALSE;
+
+    g_return_val_if_fail(NMP_IS_ROUTE_MANAGER(self), FALSE);
+    nm_assert(NM_IN_SET(obj_type,
+                        NMP_OBJECT_TYPE_IP4_ROUTE,
+                        NMP_OBJECT_TYPE_IP6_ROUTE,
+                        NMP_OBJECT_TYPE_ROUTING_RULE));
+    g_return_val_if_fail(obj, FALSE);
+    g_return_val_if_fail(user_tag, FALSE);
+
+    p_obj_stack = nmp_object_stackinit(&obj_stack, obj_type, obj);
+
+    nm_assert(nmp_object_is_visible(p_obj_stack));
+
+    track_data = _track_data_lookup(self->by_data, p_obj_stack, user_tag);
+    if (track_data) {
+        _track_data_untrack(self, track_data, TRUE, FALSE);
+        changed = TRUE;
+    }
+
+    return changed;
+}
+
+void
+nmp_route_manager_set_dirty(NMPRouteManager *self, gconstpointer user_tag)
+{
+    TrackData        *track_data;
+    TrackUserTagData *user_tag_data;
+
+    g_return_if_fail(NMP_IS_ROUTE_MANAGER(self));
+    g_return_if_fail(user_tag);
+
+    user_tag_data = g_hash_table_lookup(self->by_user_tag, &user_tag);
+    if (!user_tag_data)
+        return;
+
+    c_list_for_each_entry (track_data, &user_tag_data->user_tag_lst_head, user_tag_lst)
+        track_data->dirty = TRUE;
+}
+
+gboolean
+nmp_route_manager_untrack_all(NMPRouteManager *self,
+                              gconstpointer    user_tag,
+                              gboolean         all /* or only dirty */,
+                              gboolean         make_survivors_dirty)
+{
+    TrackData        *track_data;
+    TrackData        *track_data_safe;
+    TrackUserTagData *user_tag_data;
+    gboolean          changed = FALSE;
+
+    g_return_val_if_fail(NMP_IS_ROUTE_MANAGER(self), FALSE);
+    g_return_val_if_fail(user_tag, FALSE);
+
+    user_tag_data = g_hash_table_lookup(self->by_user_tag, &user_tag);
+    if (!user_tag_data)
+        return FALSE;
+
+    c_list_for_each_entry_safe (track_data,
+                                track_data_safe,
+                                &user_tag_data->user_tag_lst_head,
+                                user_tag_lst) {
+        if (all || track_data->dirty) {
+            _track_data_untrack(self, track_data, FALSE, FALSE);
+            changed = TRUE;
+            continue;
+        }
+        if (make_survivors_dirty)
+            track_data->dirty = TRUE;
+    }
+    if (c_list_is_empty(&user_tag_data->user_tag_lst_head))
+        g_hash_table_remove(self->by_user_tag, user_tag_data);
+
+    return changed;
+}
+
+/*****************************************************************************/
+
+void
+nmp_route_manager_sync(NMPRouteManager *self, NMPObjectType obj_type, gboolean keep_deleted)
+{
+    const NMDedupMultiHeadEntry *pl_head_entry;
+    NMDedupMultiIter             pl_iter;
+    const NMPObject             *plobj;
+    gs_unref_ptrarray GPtrArray *objs_to_delete = NULL;
+    TrackObjData                *obj_data;
+    TrackObjData                *obj_data_safe;
+    CList                       *by_obj_lst_head;
+    guint                        i;
+    const TrackData             *td_best;
+
+    g_return_if_fail(NMP_IS_ROUTE_MANAGER(self));
+    g_return_if_fail(NM_IN_SET(obj_type,
+                               NMP_OBJECT_TYPE_IP4_ROUTE,
+                               NMP_OBJECT_TYPE_IP6_ROUTE,
+                               NMP_OBJECT_TYPE_ROUTING_RULE));
+
+    _LOGD("sync %s%s",
+          nmp_class_from_type(obj_type)->obj_type_name,
+          keep_deleted ? " (don't remove any)" : "");
+
+    if (obj_type == NMP_OBJECT_TYPE_ROUTING_RULE)
+        pl_head_entry = nm_platform_lookup_obj_type(self->platform, obj_type);
+    else
+        pl_head_entry = nm_platform_lookup_object(self->platform, obj_type, 0);
+
+    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 obj is not tracked. It was externally added, hence we
+                 * ignore it. */
+                continue;
+            }
+
+            td_best = _track_obj_data_get_best_data(obj_data);
+            if (td_best) {
+                if (td_best->track_priority_present) {
+                    if (obj_data->config_state == CONFIG_STATE_OWNED_BY_US)
+                        obj_data->config_state = CONFIG_STATE_ADDED_BY_US;
+                    continue;
+                }
+                if (td_best->track_priority_val == 0) {
+                    if (!NM_IN_SET(obj_data->config_state,
+                                   CONFIG_STATE_ADDED_BY_US,
+                                   CONFIG_STATE_OWNED_BY_US)) {
+                        obj_data->config_state = CONFIG_STATE_NONE;
+                        continue;
+                    }
+                    obj_data->config_state = CONFIG_STATE_NONE;
+                }
+            }
+
+            if (keep_deleted) {
+                _LOGD("forget/leak object added by us: %s \"%s\"",
+                      NMP_OBJECT_GET_CLASS(plobj)->obj_type_name,
+                      nmp_object_to_string(plobj, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+                continue;
+            }
+
+            if (!objs_to_delete)
+                objs_to_delete = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
+
+            g_ptr_array_add(objs_to_delete, (gpointer) nmp_object_ref(plobj));
+
+            obj_data->config_state = CONFIG_STATE_REMOVED_BY_US;
+        }
+    }
+
+    if (objs_to_delete) {
+        for (i = 0; i < objs_to_delete->len; i++)
+            nm_platform_object_delete(self->platform, objs_to_delete->pdata[i]);
+    }
+
+    by_obj_lst_head = _by_obj_lst_head(self, obj_type);
+
+    c_list_for_each_entry_safe (obj_data, obj_data_safe, by_obj_lst_head, by_obj_lst) {
+        nm_assert(NMP_OBJECT_GET_TYPE(obj_data->obj) == obj_type);
+
+        td_best = _track_obj_data_get_best_data(obj_data);
+
+        if (!td_best) {
+            g_hash_table_remove(self->by_obj, obj_data);
+            continue;
+        }
+
+        if (!td_best->track_priority_present) {
+            if (obj_data->config_state == CONFIG_STATE_OWNED_BY_US)
+                obj_data->config_state = CONFIG_STATE_REMOVED_BY_US;
+            continue;
+        }
+        if (td_best->track_priority_val == 0) {
+            if (!NM_IN_SET(obj_data->config_state,
+                           CONFIG_STATE_REMOVED_BY_US,
+                           CONFIG_STATE_OWNED_BY_US)) {
+                obj_data->config_state = CONFIG_STATE_NONE;
+                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;
+
+        if (obj_type == NMP_OBJECT_TYPE_ROUTING_RULE) {
+            nm_platform_routing_rule_add(self->platform,
+                                         NMP_NLM_FLAG_ADD,
+                                         NMP_OBJECT_CAST_ROUTING_RULE(obj_data->obj));
+        } else
+            nm_platform_ip_route_add(self->platform, NMP_NLM_FLAG_APPEND, obj_data->obj);
+    }
+}
+
+/*****************************************************************************/
+
+void
+nmp_route_manager_track_rule_from_platform(NMPRouteManager *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_ROUTE_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_route_manager_track_rule(self, rr, tracking_priority, user_tag, NULL);
+    }
+}
+
+/*****************************************************************************/
+
+void
+nmp_route_manager_track_rule_default(NMPRouteManager *self,
+                                     int              addr_family,
+                                     gint32           track_priority,
+                                     gconstpointer    user_tag)
+{
+    g_return_if_fail(NMP_IS_ROUTE_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_route_manager_track_rule(self,
+                                     &((NMPlatformRoutingRule){
+                                         .addr_family = AF_INET,
+                                         .priority    = 0,
+                                         .table       = RT_TABLE_LOCAL,
+                                         .action      = FR_ACT_TO_TBL,
+                                         .protocol    = RTPROT_KERNEL,
+                                     }),
+                                     track_priority,
+                                     user_tag,
+                                     NULL);
+        nmp_route_manager_track_rule(self,
+                                     &((NMPlatformRoutingRule){
+                                         .addr_family = AF_INET,
+                                         .priority    = 32766,
+                                         .table       = RT_TABLE_MAIN,
+                                         .action      = FR_ACT_TO_TBL,
+                                         .protocol    = RTPROT_KERNEL,
+                                     }),
+                                     track_priority,
+                                     user_tag,
+                                     NULL);
+        nmp_route_manager_track_rule(self,
+                                     &((NMPlatformRoutingRule){
+                                         .addr_family = AF_INET,
+                                         .priority    = 32767,
+                                         .table       = RT_TABLE_DEFAULT,
+                                         .action      = FR_ACT_TO_TBL,
+                                         .protocol    = RTPROT_KERNEL,
+                                     }),
+                                     track_priority,
+                                     user_tag,
+                                     NULL);
+    }
+    if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET6)) {
+        nmp_route_manager_track_rule(self,
+                                     &((NMPlatformRoutingRule){
+                                         .addr_family = AF_INET6,
+                                         .priority    = 0,
+                                         .table       = RT_TABLE_LOCAL,
+                                         .action      = FR_ACT_TO_TBL,
+                                         .protocol    = RTPROT_KERNEL,
+                                     }),
+                                     track_priority,
+                                     user_tag,
+                                     NULL);
+        nmp_route_manager_track_rule(self,
+                                     &((NMPlatformRoutingRule){
+                                         .addr_family = AF_INET6,
+                                         .priority    = 32766,
+                                         .table       = RT_TABLE_MAIN,
+                                         .action      = FR_ACT_TO_TBL,
+                                         .protocol    = RTPROT_KERNEL,
+                                     }),
+                                     track_priority,
+                                     user_tag,
+                                     NULL);
+    }
+}
+
+/*****************************************************************************/
+
+NMPRouteManager *
+nmp_route_manager_new(NMPlatform *platform)
+{
+    NMPRouteManager *self;
+
+    g_return_val_if_fail(NM_IS_PLATFORM(platform), NULL);
+
+    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(TrackUserTagData, user_tag) == 0);
+
+    self  = g_slice_new(NMPRouteManager);
+    *self = (NMPRouteManager){
+        .ref_count = 1,
+        .platform  = g_object_ref(platform),
+        .by_data =
+            g_hash_table_new_full(_track_data_hash, _track_data_equal, NULL, _track_data_destroy),
+        .by_obj              = g_hash_table_new_full(_track_obj_data_hash,
+                                        _track_obj_data_equal,
+                                        NULL,
+                                        _track_obj_data_destroy),
+        .by_user_tag         = g_hash_table_new_full(nm_pdirect_hash,
+                                             nm_pdirect_equal,
+                                             NULL,
+                                             _track_user_tag_data_destroy),
+        .by_obj_lst_heads[0] = C_LIST_INIT(self->by_obj_lst_heads[0]),
+        .by_obj_lst_heads[1] = C_LIST_INIT(self->by_obj_lst_heads[1]),
+        .by_obj_lst_heads[2] = C_LIST_INIT(self->by_obj_lst_heads[2]),
+    };
+    return self;
+}
+
+NMPRouteManager *
+nmp_route_manager_ref(NMPRouteManager *self)
+{
+    g_return_val_if_fail(NMP_IS_ROUTE_MANAGER(self), NULL);
+
+    self->ref_count++;
+    return self;
+}
+
+void
+nmp_route_manager_unref(NMPRouteManager *self)
+{
+    g_return_if_fail(NMP_IS_ROUTE_MANAGER(self));
+
+    if (--self->ref_count > 0)
+        return;
+
+    g_hash_table_destroy(self->by_user_tag);
+    g_hash_table_destroy(self->by_obj);
+    g_hash_table_destroy(self->by_data);
+    nm_assert(c_list_is_empty(&self->by_obj_lst_heads[0]));
+    nm_assert(c_list_is_empty(&self->by_obj_lst_heads[1]));
+    nm_assert(c_list_is_empty(&self->by_obj_lst_heads[2]));
+    g_object_unref(self->platform);
+    nm_g_slice_free(self);
+}
diff --git a/src/libnm-platform/nmp-route-manager.h b/src/libnm-platform/nmp-route-manager.h
new file mode 100644
index 00000000..97ec3840
--- /dev/null
+++ b/src/libnm-platform/nmp-route-manager.h
@@ -0,0 +1,79 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NMP_ROUTE_MANAGER_H__
+#define __NMP_ROUTE_MANAGER_H__
+
+#include "nm-platform.h"
+
+/*****************************************************************************/
+
+#define NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG ((const void *) nmp_route_manager_new)
+
+typedef struct _NMPRouteManager NMPRouteManager;
+
+NMPRouteManager *nmp_route_manager_new(NMPlatform *platform);
+
+NMPRouteManager *nmp_route_manager_ref(NMPRouteManager *self);
+void             nmp_route_manager_unref(NMPRouteManager *self);
+
+#define nm_auto_unref_route_manager nm_auto(_nmp_route_manager_unref)
+NM_AUTO_DEFINE_FCN0(NMPRouteManager *, _nmp_route_manager_unref, nmp_route_manager_unref);
+
+gboolean nmp_route_manager_track(NMPRouteManager *self,
+                                 NMPObjectType    obj_type,
+                                 gconstpointer    obj,
+                                 gint32           track_priority,
+                                 gconstpointer    user_tag,
+                                 gconstpointer    user_tag_untrack);
+
+static inline gboolean
+nmp_route_manager_track_rule(NMPRouteManager             *self,
+                             const NMPlatformRoutingRule *routing_rule,
+                             gint32                       track_priority,
+                             gconstpointer                user_tag,
+                             gconstpointer                user_tag_untrack)
+{
+    return nmp_route_manager_track(self,
+                                   NMP_OBJECT_TYPE_ROUTING_RULE,
+                                   routing_rule,
+                                   track_priority,
+                                   user_tag,
+                                   user_tag_untrack);
+}
+
+void nmp_route_manager_track_rule_default(NMPRouteManager *self,
+                                          int              addr_family,
+                                          gint32           track_priority,
+                                          gconstpointer    user_tag);
+
+void nmp_route_manager_track_rule_from_platform(NMPRouteManager *self,
+                                                NMPlatform      *platform,
+                                                int              addr_family,
+                                                gint32           tracking_priority,
+                                                gconstpointer    user_tag);
+
+gboolean nmp_route_manager_untrack(NMPRouteManager *self,
+                                   NMPObjectType    obj_type,
+                                   gconstpointer    obj,
+                                   gconstpointer    user_tag);
+
+static inline gboolean
+nmp_route_manager_untrack_rule(NMPRouteManager             *self,
+                               const NMPlatformRoutingRule *routing_rule,
+                               gconstpointer                user_tag)
+{
+    return nmp_route_manager_untrack(self, NMP_OBJECT_TYPE_ROUTING_RULE, routing_rule, user_tag);
+}
+
+void nmp_route_manager_set_dirty(NMPRouteManager *self, gconstpointer user_tag);
+
+gboolean nmp_route_manager_untrack_all(NMPRouteManager *self,
+                                       gconstpointer    user_tag,
+                                       gboolean         all /* or only dirty */,
+                                       gboolean         make_survivors_dirty);
+
+void nmp_route_manager_sync(NMPRouteManager *self, NMPObjectType obj_type, gboolean keep_deleted);
+
+/*****************************************************************************/
+
+#endif /* __NMP_ROUTE_MANAGER_H__ */
diff --git a/src/libnm-platform/nmp-rules-manager.c b/src/libnm-platform/nmp-rules-manager.c
deleted file mode 100644
index 746d1eaa..00000000
--- a/src/libnm-platform/nmp-rules-manager.c
+++ /dev/null
@@ -1,809 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-
-#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
-
-#include "nmp-rules-manager.h"
-
-#include <linux/fib_rules.h>
-#include <linux/rtnetlink.h>
-
-#include "libnm-log-core/nm-logging.h"
-#include "libnm-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 multiple 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 encodes whether the rule was touched by us at all (CONFIG_STATE_NONE).
-     *
-     * Maybe we would only need to track whether we touched the rule at all. But we
-     * track it more in detail what we did: did we add it (CONFIG_STATE_ADDED_BY_US)
-     * or did we remove it (CONFIG_STATE_REMOVED_BY_US)?
-     * Finally, we need CONFIG_STATE_OWNED_BY_US, which means that we didn't actively
-     * add/remove it, but whenever we are about to undo the add/remove, we need to do it.
-     * In that sense, CONFIG_STATE_OWNED_BY_US is really just a flag that we unconditionally
-     * force the state next time when necessary. */
-    CONFIG_STATE_OWNED_BY_US = 3,
-} 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 is partially fixed by NetworkManager taking over the rules that it
-     * actively configures (see %NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG). */
-    ConfigState config_state;
-} RulesObjData;
-
-typedef struct {
-    gconstpointer user_tag;
-    CList         user_tag_lst_head;
-} RulesUserTagData;
-
-/*****************************************************************************/
-
-static void _rules_data_untrack(NMPRulesManager *self,
-                                RulesData       *rules_data,
-                                gboolean         remove_user_tag_data,
-                                gboolean         make_owned_by_us);
-
-/*****************************************************************************/
-
-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);
-}
-
-/**
- * nmp_rules_manager_track:
- * @self: the #NMPRulesManager instance
- * @routing_rule: the #NMPlatformRoutingRule to track or untrack
- * @track_priority: the priority for tracking the rule. Note that
- *   negative values indicate a forced absence of the rule. Priorities
- *   are compared with their absolute values (with higher absolute
- *   value being more important). For example, if you track the same
- *   rule twice, once with priority -5 and +10, then the rule is
- *   present (because the positive number is more important).
- *   The special value 0 indicates weakly-tracked rules.
- * @user_tag: the tag associated with tracking this rule. The same tag
- *   must be used to untrack the rule later.
- * @user_tag_untrack: if not %NULL, at the same time untrack this user-tag
- *   for the same rule. Note that this is different from a plain nmp_rules_manager_untrack(),
- *   because it enforces ownership of the now tracked rule. On the other hand,
- *   a plain nmp_rules_manager_untrack() merely forgets about the tracking.
- *   The purpose here is to set this to %NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG.
- */
-void
-nmp_rules_manager_track(NMPRulesManager             *self,
-                        const NMPlatformRoutingRule *routing_rule,
-                        gint32                       track_priority,
-                        gconstpointer                user_tag,
-                        gconstpointer                user_tag_untrack)
-{
-    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;
-        }
-    }
-
-    if (user_tag_untrack) {
-        if (user_tag != user_tag_untrack) {
-            RulesData *rules_data_untrack;
-
-            rules_data_untrack = _rules_data_lookup(self->by_data, p_obj_stack, user_tag_untrack);
-            if (rules_data_untrack)
-                _rules_data_untrack(self, rules_data_untrack, FALSE, TRUE);
-        } else
-            nm_assert_not_reached();
-    }
-
-    _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,
-                    gboolean         make_owned_by_us)
-{
-    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));
-
-    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 (make_owned_by_us) {
-        if (obj_data->config_state == CONFIG_STATE_NONE) {
-            /* we need to mark this entry that it requires a touch on the next
-             * sync. */
-            obj_data->config_state = CONFIG_STATE_OWNED_BY_US;
-        }
-    } else 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);
-
-    /* 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, FALSE);
-}
-
-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, 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) {
-                    if (obj_data->config_state == CONFIG_STATE_OWNED_BY_US)
-                        obj_data->config_state = CONFIG_STATE_ADDED_BY_US;
-                    continue;
-                }
-                if (rd_best->track_priority_val == 0) {
-                    if (!NM_IN_SET(obj_data->config_state,
-                                   CONFIG_STATE_ADDED_BY_US,
-                                   CONFIG_STATE_OWNED_BY_US)) {
-                        obj_data->config_state = CONFIG_STATE_NONE;
-                        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) {
-            if (obj_data->config_state == CONFIG_STATE_OWNED_BY_US)
-                obj_data->config_state = CONFIG_STATE_REMOVED_BY_US;
-            continue;
-        }
-        if (rd_best->track_priority_val == 0) {
-            if (!NM_IN_SET(obj_data->config_state,
-                           CONFIG_STATE_REMOVED_BY_US,
-                           CONFIG_STATE_OWNED_BY_US)) {
-                obj_data->config_state = CONFIG_STATE_NONE;
-                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, NULL);
-    }
-}
-
-/*****************************************************************************/
-
-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,
-                                NULL);
-        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,
-                                NULL);
-        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,
-                                NULL);
-    }
-    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,
-                                NULL);
-        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,
-                                NULL);
-    }
-}
-
-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/libnm-platform/nmp-rules-manager.h b/src/libnm-platform/nmp-rules-manager.h
deleted file mode 100644
index 65e8ddb2..00000000
--- a/src/libnm-platform/nmp-rules-manager.h
+++ /dev/null
@@ -1,53 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-
-#ifndef __NMP_RULES_MANAGER_H__
-#define __NMP_RULES_MANAGER_H__
-
-#include "nm-platform.h"
-
-/*****************************************************************************/
-
-#define NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG ((const void *) nmp_rules_manager_new)
-
-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,
-                             gconstpointer                user_tag_untrack);
-
-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__ */