about summary refs log tree commit diff
path: root/src/core
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/core
parentda4d2b990df5b0119d1dffd76ff68141517d6136 (diff)
parent2f94dba7385fd0e0ef19a06eb4a2fcf6c43d7946 (diff)
Merge tag 'upstream/1.35.91' into ubuntu/master
Upstream version 1.35.91
Diffstat (limited to 'src/core')
-rw-r--r--src/core/NetworkManagerUtils.c13
-rw-r--r--src/core/devices/nm-device-wireguard.c2
-rw-r--r--src/core/devices/nm-device.c40
-rw-r--r--src/core/nm-l3-config-data.c34
-rw-r--r--src/core/nm-l3cfg.c263
-rw-r--r--src/core/nm-l3cfg.h14
-rw-r--r--src/core/nm-netns.c34
-rw-r--r--src/core/nm-netns.h2
-rw-r--r--src/core/platform/nm-fake-platform.c11
-rw-r--r--src/core/platform/tests/test-common.c6
-rw-r--r--src/core/platform/tests/test-platform-general.c26
-rw-r--r--src/core/platform/tests/test-route.c128
12 files changed, 440 insertions, 133 deletions
diff --git a/src/core/NetworkManagerUtils.c b/src/core/NetworkManagerUtils.c
index 0d08efbb..c50dbada 100644
--- a/src/core/NetworkManagerUtils.c
+++ b/src/core/NetworkManagerUtils.c
@@ -1316,6 +1316,7 @@ nm_utils_ip_route_attribute_to_platform(int                addr_family,
                                         NMPlatformIPRoute *r,
                                         gint64             route_table)
 {
+    const int           IS_IPv4 = NM_IS_IPv4(addr_family);
     GVariant           *variant;
     guint32             table;
     NMIPAddr            addr;
@@ -1324,7 +1325,7 @@ nm_utils_ip_route_attribute_to_platform(int                addr_family,
     gboolean            onlink;
 
     nm_assert(s_route);
-    nm_assert_addr_family(addr_family);
+    nm_assert(addr_family == nm_ip_route_get_family(s_route));
     nm_assert(r);
     nm_assert(route_table >= -1);
     nm_assert(route_table <= (gint64) G_MAXUINT32);
@@ -1346,7 +1347,8 @@ nm_utils_ip_route_attribute_to_platform(int                addr_family,
         int type;
 
         type = nm_net_aux_rtnl_rtntype_a2n(g_variant_get_string(variant, NULL));
-        nm_assert(NM_IN_SET(type, RTN_UNICAST, RTN_LOCAL));
+        nm_assert(
+            NM_IN_SET(type, RTN_UNICAST, RTN_LOCAL, RTN_BLACKHOLE, RTN_UNREACHABLE, RTN_PROHIBIT));
 
         r->type_coerced = nm_platform_route_type_coerce(type);
     } else
@@ -1365,7 +1367,7 @@ nm_utils_ip_route_attribute_to_platform(int                addr_family,
     else
         r->table_any = TRUE;
 
-    if (NM_IS_IPv4(addr_family)) {
+    if (IS_IPv4) {
         guint8 scope;
 
         GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_TOS, r4->tos, BYTE, byte, 0);
@@ -1391,15 +1393,14 @@ nm_utils_ip_route_attribute_to_platform(int                addr_family,
     if ((variant = nm_ip_route_get_attribute(s_route, NM_IP_ROUTE_ATTRIBUTE_SRC))
         && g_variant_is_of_type(variant, G_VARIANT_TYPE_STRING)) {
         if (inet_pton(addr_family, g_variant_get_string(variant, NULL), &addr) == 1) {
-            if (NM_IS_IPv4(addr_family))
+            if (IS_IPv4)
                 r4->pref_src = addr.addr4;
             else
                 r6->pref_src = addr.addr6;
         }
     }
 
-    if (!NM_IS_IPv4(addr_family)
-        && (variant = nm_ip_route_get_attribute(s_route, NM_IP_ROUTE_ATTRIBUTE_FROM))
+    if (!IS_IPv4 && (variant = nm_ip_route_get_attribute(s_route, NM_IP_ROUTE_ATTRIBUTE_FROM))
         && g_variant_is_of_type(variant, G_VARIANT_TYPE_STRING)) {
         int prefix;
 
diff --git a/src/core/devices/nm-device-wireguard.c b/src/core/devices/nm-device-wireguard.c
index 4399e834..bdb96cb2 100644
--- a/src/core/devices/nm-device-wireguard.c
+++ b/src/core/devices/nm-device-wireguard.c
@@ -18,7 +18,7 @@
 #include "nm-device-private.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nmp-object.h"
-#include "libnm-platform/nmp-rules-manager.h"
+#include "libnm-platform/nmp-route-manager.h"
 #include "nm-device-factory.h"
 #include "nm-active-connection.h"
 #include "nm-act-request.h"
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index 6dda5431..696c0df8 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -41,7 +41,7 @@
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nm-platform-utils.h"
 #include "libnm-platform/nmp-object.h"
-#include "libnm-platform/nmp-rules-manager.h"
+#include "libnm-platform/nmp-route-manager.h"
 #include "ndisc/nm-ndisc.h"
 #include "ndisc/nm-lndp-ndisc.h"
 
@@ -2943,7 +2943,7 @@ _dev_ip_state_req_timeout_cb_x(NMDevice *self, int addr_family)
 
     _LOGD_ip(addr_family, "required-timeout: expired");
     nm_clear_g_source_inst(&priv->ip_data_x[NM_IS_IPv4(addr_family)].req_timeout_source);
-    _dev_ip_state_check(self, addr_family);
+    _dev_ip_state_check(self, nm_utils_addr_family_other(addr_family));
     return G_SOURCE_CONTINUE;
 }
 
@@ -9280,7 +9280,7 @@ static void
 _routing_rules_sync(NMDevice *self, NMTernary set_mode)
 {
     NMDevicePrivate *priv               = NM_DEVICE_GET_PRIVATE(self);
-    NMPRulesManager *rules_manager      = nm_netns_get_rules_manager(nm_device_get_netns(self));
+    NMPRouteManager *route_manager      = nm_netns_get_route_manager(nm_device_get_netns(self));
     NMDeviceClass   *klass              = NM_DEVICE_GET_CLASS(self);
     gboolean         untrack_only_dirty = FALSE;
     gboolean         keep_deleted_rules;
@@ -9298,9 +9298,6 @@ _routing_rules_sync(NMDevice *self, NMTernary set_mode)
         int                is_ipv4;
 
         untrack_only_dirty = TRUE;
-        nmp_rules_manager_set_dirty(rules_manager, user_tag_1);
-        if (klass->get_extra_rules)
-            nmp_rules_manager_set_dirty(rules_manager, user_tag_2);
 
         applied_connection = nm_device_get_applied_connection(self);
 
@@ -9320,13 +9317,13 @@ _routing_rules_sync(NMDevice *self, NMTernary set_mode)
                 nm_ip_routing_rule_to_platform(rule, &plrule);
 
                 /* We track this rule, but we also make it explicitly not weakly-tracked
-                 * (meaning to untrack NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG at
+                 * (meaning to untrack NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG at
                  * the same time). */
-                nmp_rules_manager_track(rules_manager,
-                                        &plrule,
-                                        10,
-                                        user_tag_1,
-                                        NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG);
+                nmp_route_manager_track_rule(route_manager,
+                                             &plrule,
+                                             10,
+                                             user_tag_1,
+                                             NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG);
             }
         }
 
@@ -9336,24 +9333,25 @@ _routing_rules_sync(NMDevice *self, NMTernary set_mode)
             extra_rules = klass->get_extra_rules(self);
             if (extra_rules) {
                 for (i = 0; i < extra_rules->len; i++) {
-                    nmp_rules_manager_track(rules_manager,
-                                            NMP_OBJECT_CAST_ROUTING_RULE(extra_rules->pdata[i]),
-                                            10,
-                                            user_tag_2,
-                                            NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG);
+                    nmp_route_manager_track_rule(
+                        route_manager,
+                        NMP_OBJECT_CAST_ROUTING_RULE(extra_rules->pdata[i]),
+                        10,
+                        user_tag_2,
+                        NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG);
                 }
             }
         }
     }
 
-    nmp_rules_manager_untrack_all(rules_manager, user_tag_1, !untrack_only_dirty);
+    nmp_route_manager_untrack_all(route_manager, user_tag_1, !untrack_only_dirty, TRUE);
     if (klass->get_extra_rules)
-        nmp_rules_manager_untrack_all(rules_manager, user_tag_2, !untrack_only_dirty);
+        nmp_route_manager_untrack_all(route_manager, user_tag_2, !untrack_only_dirty, TRUE);
 
     keep_deleted_rules = FALSE;
     if (set_mode == NM_TERNARY_DEFAULT) {
         /* when exiting NM, we leave the device up and the rules configured.
-         * We just all nmp_rules_manager_sync() to forget about the synced rules,
+         * We just call nmp_route_manager_sync() to forget about the synced rules,
          * but we don't actually delete them.
          *
          * FIXME: that is a problem after restart of NetworkManager, because these
@@ -9367,7 +9365,7 @@ _routing_rules_sync(NMDevice *self, NMTernary set_mode)
          * file and track them after restart again. */
         keep_deleted_rules = TRUE;
     }
-    nmp_rules_manager_sync(rules_manager, keep_deleted_rules);
+    nmp_route_manager_sync(route_manager, NMP_OBJECT_TYPE_ROUTING_RULE, keep_deleted_rules);
 }
 
 static gboolean
diff --git a/src/core/nm-l3-config-data.c b/src/core/nm-l3-config-data.c
index 226ccd00..03593ea2 100644
--- a/src/core/nm-l3-config-data.c
+++ b/src/core/nm-l3-config-data.c
@@ -1094,8 +1094,22 @@ _l3_config_data_add_obj(NMDedupMultiIndex      *multi_idx,
                         NMP_OBJECT_TYPE_IP4_ROUTE,
                         NMP_OBJECT_TYPE_IP6_ADDRESS,
                         NMP_OBJECT_TYPE_IP6_ROUTE));
+    nm_assert((!!obj_new) != (!!pl_new));
+
+    if (NM_IN_SET(idx_type->obj_type, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE)) {
+        const NMPlatformIPRoute *r;
+
+        r = obj_new ? NMP_OBJECT_CAST_IP_ROUTE(obj_new) : (NMPlatformIPRoute *) pl_new;
+
+        if (nm_platform_route_type_is_nodev(nm_platform_route_type_uncoerce(r->type_coerced))) {
+            /* such routes don't have a device/next-hop. We track them without ifindex. */
+            ifindex = 0;
+        }
+    }
+
     /* we go through extra lengths to accept a full obj_new object. That one,
-     * can be reused by increasing the ref-count. */
+     * can be reused by increasing the ref-count. We thus accept any ifindex, and
+     * set it here. */
     if (!obj_new) {
         nm_assert(pl_new);
         obj_new = nmp_object_stackinit(&obj_new_stackinit, idx_type->obj_type, pl_new);
@@ -2995,12 +3009,12 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
                         NML3ConfigMergeHookAddObj hook_add_obj,
                         gpointer                  hook_user_data)
 {
-    static const guint32 x_default_route_table_x[2]   = {RT_TABLE_MAIN, RT_TABLE_MAIN};
     static const guint32 x_default_route_metric_x[2]  = {NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP6,
                                                         NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP4};
     static const guint32 x_default_route_penalty_x[2] = {0, 0};
     static const int     x_default_dns_priority_x[2]  = {NM_DNS_PRIORITY_DEFAULT_NORMAL,
                                                     NM_DNS_PRIORITY_DEFAULT_NORMAL};
+    guint32              default_route_table_coerced_x[2];
     NMDedupMultiIter     iter;
     const NMPObject     *obj;
     int                  IS_IPv4;
@@ -3008,8 +3022,16 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
     nm_assert(_NM_IS_L3_CONFIG_DATA(self, FALSE));
     nm_assert(_NM_IS_L3_CONFIG_DATA(src, TRUE));
 
-    if (!default_route_table_x)
-        default_route_table_x = x_default_route_table_x;
+    if (default_route_table_x) {
+        default_route_table_coerced_x[0] = nm_platform_route_table_coerce(default_route_table_x[0]);
+        default_route_table_coerced_x[1] = nm_platform_route_table_coerce(default_route_table_x[1]);
+    } else {
+        default_route_table_coerced_x[0] = nm_platform_route_table_coerce(RT_TABLE_MAIN);
+        default_route_table_coerced_x[1] = nm_platform_route_table_coerce(RT_TABLE_MAIN);
+    }
+    nm_assert(nm_platform_route_table_uncoerce(default_route_table_coerced_x[0], FALSE) != 0);
+    nm_assert(nm_platform_route_table_uncoerce(default_route_table_coerced_x[1], FALSE) != 0);
+
     if (!default_route_metric_x)
         default_route_metric_x = x_default_route_metric_x;
     if (!default_route_penalty_x)
@@ -3017,8 +3039,6 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
     if (!default_dns_priority_x)
         default_dns_priority_x = x_default_dns_priority_x;
 
-    nm_assert(default_route_table_x[0] != 0);
-    nm_assert(default_route_table_x[1] != 0);
     nm_assert(default_route_metric_x[0] != 0); /* IPv6 route metric cannot be zero. */
 
     nm_assert(!NM_FLAGS_HAS(merge_flags, NM_L3_CONFIG_MERGE_FLAGS_CLONE)
@@ -3145,7 +3165,7 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
                     if (r_src->table_any) {
                         _ensure_r();
                         r.rx.table_any     = FALSE;
-                        r.rx.table_coerced = default_route_table_x[IS_IPv4];
+                        r.rx.table_coerced = default_route_table_coerced_x[IS_IPv4];
                     }
 
                     if (r_src->metric_any) {
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index aed4cf05..bcbe140c 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -12,6 +12,7 @@
 #include "libnm-glib-aux/nm-time-utils.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nmp-object.h"
+#include "libnm-platform/nmp-route-manager.h"
 #include "nm-netns.h"
 #include "n-acd/src/n-acd.h"
 #include "nm-l3-ipv4ll.h"
@@ -410,6 +411,92 @@ static NM_UTILS_LOOKUP_DEFINE(_l3_acd_addr_state_to_string,
                                                    "external-removed"),
                               NM_UTILS_LOOKUP_ITEM(NM_L3_ACD_ADDR_STATE_USED, "used"), );
 
+static gboolean
+_obj_is_route_nodev(const NMPObject *obj)
+{
+    gboolean has_ifindex;
+
+    nm_assert(obj);
+
+    has_ifindex = (NMP_OBJECT_CAST_OBJ_WITH_IFINDEX(obj)->ifindex > 0);
+
+    nm_assert(has_ifindex
+              == !(NM_IN_SET(NMP_OBJECT_GET_TYPE(obj),
+                             NMP_OBJECT_TYPE_IP4_ROUTE,
+                             NMP_OBJECT_TYPE_IP6_ROUTE)
+                   && nm_platform_route_type_is_nodev(nm_platform_route_type_uncoerce(
+                       NMP_OBJECT_CAST_IP_ROUTE(obj)->type_coerced))));
+
+    return !has_ifindex;
+}
+
+/*****************************************************************************/
+
+NMIPConfig *
+nm_l3cfg_ipconfig_get(NML3Cfg *self, int addr_family)
+{
+    g_return_val_if_fail(NM_IS_L3CFG(self), NULL);
+    nm_assert_addr_family(addr_family);
+
+    return self->priv.p->ipconfig_x[NM_IS_IPv4(addr_family)];
+}
+
+static void
+_ipconfig_toggle_notify(gpointer data, GObject *object, gboolean is_last_ref)
+{
+    NML3Cfg    *self     = NM_L3CFG(data);
+    NMIPConfig *ipconfig = NM_IP_CONFIG(object);
+
+    if (!is_last_ref) {
+        /* This happens while we take another ref below. Ignore the signal. */
+        nm_assert(!NM_IN_SET(ipconfig, self->priv.p->ipconfig_4, self->priv.p->ipconfig_6));
+        return;
+    }
+
+    if (ipconfig == self->priv.p->ipconfig_4)
+        self->priv.p->ipconfig_4 = NULL;
+    else {
+        nm_assert(ipconfig == self->priv.p->ipconfig_6);
+        self->priv.p->ipconfig_6 = NULL;
+    }
+
+    /* We take a second reference to keep the instance alive, while also removing the
+     * toggle ref. This will notify the function again, but we will ignore that. */
+    g_object_ref(ipconfig);
+
+    g_object_remove_toggle_ref(G_OBJECT(ipconfig), _ipconfig_toggle_notify, self);
+
+    /* pass on the reference, and unexport on idle. */
+    nm_ip_config_take_and_unexport_on_idle(g_steal_pointer(&ipconfig));
+}
+
+NMIPConfig *
+nm_l3cfg_ipconfig_acquire(NML3Cfg *self, int addr_family)
+{
+    NMIPConfig *ipconfig;
+
+    g_return_val_if_fail(NM_IS_L3CFG(self), NULL);
+    nm_assert_addr_family(addr_family);
+
+    ipconfig = self->priv.p->ipconfig_x[NM_IS_IPv4(addr_family)];
+
+    if (ipconfig)
+        return g_object_ref(ipconfig);
+
+    ipconfig = nm_ip_config_new(addr_family, self);
+
+    self->priv.p->ipconfig_x[NM_IS_IPv4(addr_family)] = ipconfig;
+
+    /* The ipconfig keeps self alive. We use a toggle reference
+     * to avoid a cycle. But we anyway wouldn't want a strong reference,
+     * because the user releases the instance by unrefing it, and we
+     * notice that via the weak reference. */
+    g_object_add_toggle_ref(G_OBJECT(ipconfig), _ipconfig_toggle_notify, self);
+
+    /* We keep the toggle reference, and return the other reference to the caller. */
+    return g_steal_pointer(&ipconfig);
+}
+
 /*****************************************************************************/
 
 NMIPConfig *
@@ -952,6 +1039,11 @@ _obj_states_update_all(NML3Cfg *self)
                                              self->priv.p->combined_l3cd_commited,
                                              &obj,
                                              obj_type) {
+            if (_obj_is_route_nodev(obj)) {
+                /* this is a nodev route. We don't track an obj-state for this. */
+                continue;
+            }
+
             obj_state = g_hash_table_lookup(self->priv.p->obj_state_hash, &obj);
             if (!obj_state) {
                 obj_state =
@@ -1008,18 +1100,11 @@ typedef struct {
 } ObjStatesSyncFilterData;
 
 static gboolean
-_obj_states_sync_filter(/* const NMDedupMultiObj * */ gconstpointer o, gpointer user_data)
+_obj_states_sync_filter(NML3Cfg *self, const NMPObject *obj, NML3CfgCommitType commit_type)
 {
-    char                           sbuf[sizeof(_nm_utils_to_string_buffer)];
-    const NMPObject               *obj              = o;
-    const ObjStatesSyncFilterData *sync_filter_data = user_data;
-    NMPObjectType                  obj_type;
-    ObjStateData                  *obj_state;
-    NML3Cfg                       *self;
-
-    nm_assert(sync_filter_data);
-    nm_assert(NM_IS_L3CFG(sync_filter_data->self));
-    self = sync_filter_data->self;
+    char          sbuf[sizeof(_nm_utils_to_string_buffer)];
+    NMPObjectType obj_type;
+    ObjStateData *obj_state;
 
     obj_type = NMP_OBJECT_GET_TYPE(obj);
 
@@ -1027,14 +1112,14 @@ _obj_states_sync_filter(/* const NMDedupMultiObj * */ gconstpointer o, gpointer
         && NMP_OBJECT_CAST_IP4_ADDRESS(obj)->a_acd_not_ready)
         return FALSE;
 
-    obj_state = g_hash_table_lookup(sync_filter_data->self->priv.p->obj_state_hash, &obj);
+    obj_state = g_hash_table_lookup(self->priv.p->obj_state_hash, &obj);
 
-    nm_assert_obj_state(sync_filter_data->self, obj_state);
+    nm_assert_obj_state(self, obj_state);
     nm_assert(obj_state->obj == obj);
     nm_assert(c_list_is_empty(&obj_state->os_zombie_lst));
 
     if (!obj_state->os_nm_configured) {
-        if (sync_filter_data->commit_type == NM_L3_CFG_COMMIT_TYPE_ASSUME
+        if (commit_type == NM_L3_CFG_COMMIT_TYPE_ASSUME
             && !_obj_state_data_get_assume_config_once(obj_state))
             return FALSE;
 
@@ -1051,13 +1136,78 @@ _obj_states_sync_filter(/* const NMDedupMultiObj * */ gconstpointer o, gpointer
         return TRUE;
     }
 
-    if (!obj_state->os_plobj && sync_filter_data->commit_type != NM_L3_CFG_COMMIT_TYPE_REAPPLY
+    if (!obj_state->os_plobj && commit_type != NM_L3_CFG_COMMIT_TYPE_REAPPLY
         && !nmp_object_get_force_commit(obj))
         return FALSE;
 
     return TRUE;
 }
 
+static gboolean
+_obj_states_sync_filter_predicate(gconstpointer o, gpointer user_data)
+{
+    const NMPObject               *obj              = o;
+    const ObjStatesSyncFilterData *sync_filter_data = user_data;
+
+    return _obj_states_sync_filter(sync_filter_data->self, obj, sync_filter_data->commit_type);
+}
+
+static GPtrArray *
+_commit_collect_addresses(NML3Cfg *self, int addr_family, NML3CfgCommitType commit_type)
+{
+    const int                     IS_IPv4 = NM_IS_IPv4(addr_family);
+    const NMDedupMultiHeadEntry  *head_entry;
+    const ObjStatesSyncFilterData sync_filter_data = {
+        .self        = self,
+        .commit_type = commit_type,
+    };
+
+    head_entry = nm_l3_config_data_lookup_objs(self->priv.p->combined_l3cd_commited,
+                                               NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4));
+    return nm_dedup_multi_objs_to_ptr_array_head(head_entry,
+                                                 _obj_states_sync_filter_predicate,
+                                                 (gpointer) &sync_filter_data);
+}
+
+static void
+_commit_collect_routes(NML3Cfg          *self,
+                       int               addr_family,
+                       NML3CfgCommitType commit_type,
+                       GPtrArray       **routes,
+                       GPtrArray       **routes_nodev)
+{
+    const int                    IS_IPv4 = NM_IS_IPv4(addr_family);
+    const NMDedupMultiHeadEntry *head_entry;
+    const NMDedupMultiEntry     *entry;
+
+    nm_assert(routes && !*routes);
+    nm_assert(routes_nodev && !*routes_nodev);
+
+    head_entry = nm_l3_config_data_lookup_objs(self->priv.p->combined_l3cd_commited,
+                                               NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4));
+
+    if (!head_entry)
+        return;
+
+    c_list_for_each_entry (entry, &head_entry->lst_entries_head, lst_entries) {
+        const NMPObject *obj = entry->obj;
+        GPtrArray      **r;
+
+        if (_obj_is_route_nodev(obj))
+            r = routes_nodev;
+        else {
+            if (!_obj_states_sync_filter(self, obj, commit_type))
+                continue;
+            r = routes;
+        }
+
+        if (!*r)
+            *r = g_ptr_array_new_full(head_entry->len, (GDestroyNotify) nm_dedup_multi_obj_unref);
+
+        g_ptr_array_add(*r, (gpointer) nmp_object_ref(obj));
+    }
+}
+
 static void
 _obj_state_zombie_lst_get_prune_lists(NML3Cfg    *self,
                                       int         addr_family,
@@ -2386,9 +2536,9 @@ handle_init:
             nm_utils_get_monotonic_timestamp_msec_cached(p_now_msec);
 
             if (acd_data->info.state == NM_L3_ACD_ADDR_STATE_PROBING) {
-                if (acd_data->probing_timestamp_msec + ACD_WAIT_PROBING_EXTRA_TIME_MSEC
-                        + ACD_WAIT_PROBING_EXTRA_TIME2_MSEC
-                    >= (*p_now_msec)) {
+                if ((*p_now_msec) > acd_data->probing_timestamp_msec
+                                        + ACD_WAIT_PROBING_EXTRA_TIME_MSEC
+                                        + ACD_WAIT_PROBING_EXTRA_TIME2_MSEC) {
                     /* hm. We failed to create a new probe too long. Something is really wrong
                      * internally, but let's ignore the issue and assume the address is good. What
                      * else would we do? Assume the address is USED? */
@@ -3351,6 +3501,53 @@ nm_l3cfg_remove_config_all_dirty(NML3Cfg *self, gconstpointer tag)
 
 /*****************************************************************************/
 
+#define _NODEV_ROUTES_TAG(self, IS_IPv4) ((gconstpointer) (&(&(self)->priv.route_manager)[IS_IPv4]))
+
+static gboolean
+_nodev_routes_untrack(NML3Cfg *self, int addr_family)
+{
+    return nmp_route_manager_untrack_all(self->priv.route_manager,
+                                         _NODEV_ROUTES_TAG(self, NM_IS_IPv4(addr_family)),
+                                         FALSE,
+                                         TRUE);
+}
+
+static void
+_nodev_routes_sync(NML3Cfg          *self,
+                   int               addr_family,
+                   NML3CfgCommitType commit_type,
+                   GPtrArray        *routes_nodev)
+{
+    const int           IS_IPv4  = NM_IS_IPv4(addr_family);
+    const NMPObjectType obj_type = NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4);
+    guint               i;
+    gboolean            changed = FALSE;
+
+    if (!routes_nodev)
+        goto out_clear;
+
+    for (i = 0; i < routes_nodev->len; i++) {
+        const NMPObject *obj = routes_nodev->pdata[i];
+
+        if (nmp_route_manager_track(self->priv.route_manager,
+                                    obj_type,
+                                    NMP_OBJECT_CAST_IP_ROUTE(obj),
+                                    1,
+                                    _NODEV_ROUTES_TAG(self, IS_IPv4),
+                                    NULL))
+            changed = TRUE;
+    }
+
+out_clear:
+    if (_nodev_routes_untrack(self, addr_family))
+        changed = TRUE;
+
+    if (changed || commit_type >= NM_L3_CFG_COMMIT_TYPE_REAPPLY)
+        nmp_route_manager_sync(self->priv.route_manager, NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4), FALSE);
+}
+
+/*****************************************************************************/
+
 typedef struct {
     NML3Cfg      *self;
     gconstpointer tag;
@@ -3789,6 +3986,7 @@ out_prune:
 }
 
 /*****************************************************************************/
+
 static const char *
 ip6_privacy_to_str(NMSettingIP6ConfigPrivacy ip6_privacy)
 {
@@ -4049,6 +4247,7 @@ _l3_commit_one(NML3Cfg              *self,
     const int                    IS_IPv4                            = NM_IS_IPv4(addr_family);
     gs_unref_ptrarray GPtrArray *addresses                          = NULL;
     gs_unref_ptrarray GPtrArray *routes                             = NULL;
+    gs_unref_ptrarray GPtrArray *routes_nodev                       = NULL;
     gs_unref_ptrarray GPtrArray *addresses_prune                    = NULL;
     gs_unref_ptrarray GPtrArray *routes_prune                       = NULL;
     gs_unref_ptrarray GPtrArray *routes_temporary_not_available_arr = NULL;
@@ -4070,23 +4269,9 @@ _l3_commit_one(NML3Cfg              *self,
           _l3_cfg_commit_type_to_string(commit_type, sbuf_commit_type, sizeof(sbuf_commit_type)));
 
     if (self->priv.p->combined_l3cd_commited) {
-        const NMDedupMultiHeadEntry  *head_entry;
-        const ObjStatesSyncFilterData sync_filter_data = {
-            .self        = self,
-            .commit_type = commit_type,
-        };
+        addresses = _commit_collect_addresses(self, addr_family, commit_type);
 
-        head_entry = nm_l3_config_data_lookup_objs(self->priv.p->combined_l3cd_commited,
-                                                   NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4));
-        addresses  = nm_dedup_multi_objs_to_ptr_array_head(head_entry,
-                                                          _obj_states_sync_filter,
-                                                          (gpointer) &sync_filter_data);
-
-        head_entry = nm_l3_config_data_lookup_objs(self->priv.p->combined_l3cd_commited,
-                                                   NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4));
-        routes     = nm_dedup_multi_objs_to_ptr_array_head(head_entry,
-                                                       _obj_states_sync_filter,
-                                                       (gpointer) &sync_filter_data);
+        _commit_collect_routes(self, addr_family, commit_type, &routes, &routes_nodev);
 
         route_table_sync =
             nm_l3_config_data_get_route_table_sync(self->priv.p->combined_l3cd_commited,
@@ -4124,6 +4309,8 @@ _l3_commit_one(NML3Cfg              *self,
                                 addresses,
                                 addresses_prune);
 
+    _nodev_routes_sync(self, addr_family, commit_type, routes_nodev);
+
     if (!nm_platform_ip_route_sync(self->priv.platform,
                                    addr_family,
                                    self->priv.ifindex,
@@ -4558,6 +4745,8 @@ constructed(GObject *object)
     self->priv.platform = g_object_ref(nm_netns_get_platform(self->priv.netns));
     nm_assert(NM_IS_PLATFORM(self->priv.platform));
 
+    self->priv.route_manager = nmp_route_manager_ref(nm_netns_get_route_manager(self->priv.netns));
+
     _LOGT("created (netns=" NM_HASH_OBFUSCATE_PTR_FMT ")", NM_HASH_OBFUSCATE_PTR(self->priv.netns));
 
     G_OBJECT_CLASS(nm_l3cfg_parent_class)->constructed(object);
@@ -4608,8 +4797,14 @@ finalize(GObject *object)
     nm_assert(c_list_is_empty(&self->priv.p->obj_state_temporary_not_available_lst_head));
     nm_assert(c_list_is_empty(&self->priv.p->obj_state_zombie_lst_head));
 
+    if (_nodev_routes_untrack(self, AF_INET))
+        nmp_route_manager_sync(self->priv.route_manager, NMP_OBJECT_TYPE_IP4_ROUTE, FALSE);
+    if (_nodev_routes_untrack(self, AF_INET6))
+        nmp_route_manager_sync(self->priv.route_manager, NMP_OBJECT_TYPE_IP6_ROUTE, FALSE);
+
     g_clear_object(&self->priv.netns);
     g_clear_object(&self->priv.platform);
+    nm_clear_pointer(&self->priv.route_manager, nmp_route_manager_unref);
 
     nm_clear_l3cd(&self->priv.p->combined_l3cd_merged);
     nm_clear_l3cd(&self->priv.p->combined_l3cd_commited);
diff --git a/src/core/nm-l3cfg.h b/src/core/nm-l3cfg.h
index 6fd8f9de..7dc9facc 100644
--- a/src/core/nm-l3cfg.h
+++ b/src/core/nm-l3cfg.h
@@ -195,16 +195,18 @@ typedef struct {
 } NML3ConfigNotifyData;
 
 struct _NML3CfgPrivate;
+struct _NMPRouteManager;
 
 struct _NML3Cfg {
     GObject parent;
     struct {
-        struct _NML3CfgPrivate *p;
-        NMNetns                *netns;
-        NMPlatform             *platform;
-        const NMPObject        *plobj;
-        const NMPObject        *plobj_next;
-        int                     ifindex;
+        struct _NML3CfgPrivate  *p;
+        NMNetns                 *netns;
+        NMPlatform              *platform;
+        struct _NMPRouteManager *route_manager;
+        const NMPObject         *plobj;
+        const NMPObject         *plobj_next;
+        int                      ifindex;
     } priv;
 };
 
diff --git a/src/core/nm-netns.c b/src/core/nm-netns.c
index 4d3c3172..f120e43d 100644
--- a/src/core/nm-netns.c
+++ b/src/core/nm-netns.c
@@ -15,7 +15,7 @@
 #include "nm-l3cfg.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nmp-netns.h"
-#include "libnm-platform/nmp-rules-manager.h"
+#include "libnm-platform/nmp-route-manager.h"
 
 /*****************************************************************************/
 
@@ -25,7 +25,7 @@ typedef struct {
     NMNetns         *_self_signal_user_data;
     NMPlatform      *platform;
     NMPNetns        *platform_netns;
-    NMPRulesManager *rules_manager;
+    NMPRouteManager *route_manager;
     GHashTable      *l3cfgs;
     GHashTable      *shared_ips;
     CList            l3cfg_signal_pending_lst_head;
@@ -79,10 +79,10 @@ nm_netns_get_platform(NMNetns *self)
     return NM_NETNS_GET_PRIVATE(self)->platform;
 }
 
-NMPRulesManager *
-nm_netns_get_rules_manager(NMNetns *self)
+NMPRouteManager *
+nm_netns_get_route_manager(NMNetns *self)
 {
-    return NM_NETNS_GET_PRIVATE(self)->rules_manager;
+    return NM_NETNS_GET_PRIVATE(self)->route_manager;
 }
 
 NMDedupMultiIndex *
@@ -397,14 +397,14 @@ constructed(GObject *object)
 
     priv->platform_netns = nm_platform_netns_get(priv->platform);
 
-    priv->rules_manager = nmp_rules_manager_new(priv->platform);
+    priv->route_manager = nmp_route_manager_new(priv->platform);
 
     /* Weakly track the default rules with a dummy user-tag. These
      * rules are always weekly tracked... */
-    nmp_rules_manager_track_default(priv->rules_manager,
-                                    AF_UNSPEC,
-                                    0,
-                                    nm_netns_parent_class /* static dummy user-tag */);
+    nmp_route_manager_track_rule_default(priv->route_manager,
+                                         AF_UNSPEC,
+                                         0,
+                                         nm_netns_parent_class /* static dummy user-tag */);
 
     /* Also weakly track all existing rules. These were added before NetworkManager
      * starts, so they are probably none of NetworkManager's business.
@@ -414,12 +414,12 @@ constructed(GObject *object)
      * of NetworkManager, we just don't know.
      *
      * For that reason, whenever we will touch such rules later one, we make them
-     * fully owned and no longer weekly tracked. See %NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG. */
-    nmp_rules_manager_track_from_platform(priv->rules_manager,
-                                          NULL,
-                                          AF_UNSPEC,
-                                          0,
-                                          NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG);
+     * fully owned and no longer weekly tracked. See %NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG. */
+    nmp_route_manager_track_rule_from_platform(priv->route_manager,
+                                               NULL,
+                                               AF_UNSPEC,
+                                               0,
+                                               NMP_ROUTE_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG);
 
     G_OBJECT_CLASS(nm_netns_parent_class)->constructed(object);
 
@@ -469,7 +469,7 @@ dispose(GObject *object)
     g_clear_object(&priv->platform);
     nm_clear_pointer(&priv->l3cfgs, g_hash_table_unref);
 
-    nm_clear_pointer(&priv->rules_manager, nmp_rules_manager_unref);
+    nm_clear_pointer(&priv->route_manager, nmp_route_manager_unref);
 
     G_OBJECT_CLASS(nm_netns_parent_class)->dispose(object);
 }
diff --git a/src/core/nm-netns.h b/src/core/nm-netns.h
index 0cb56375..deb1d1f0 100644
--- a/src/core/nm-netns.h
+++ b/src/core/nm-netns.h
@@ -29,7 +29,7 @@ NMNetns *nm_netns_new(struct _NMPlatform *platform);
 struct _NMPlatform *nm_netns_get_platform(NMNetns *self);
 NMPNetns           *nm_netns_get_platform_netns(NMNetns *self);
 
-struct _NMPRulesManager *nm_netns_get_rules_manager(NMNetns *self);
+struct _NMPRouteManager *nm_netns_get_route_manager(NMNetns *self);
 
 struct _NMDedupMultiIndex *nm_netns_get_multi_idx(NMNetns *self);
 
diff --git a/src/core/platform/nm-fake-platform.c b/src/core/platform/nm-fake-platform.c
index ebb6a795..7d8986d7 100644
--- a/src/core/platform/nm-fake-platform.c
+++ b/src/core/platform/nm-fake-platform.c
@@ -957,11 +957,10 @@ ipx_address_delete(NMPlatform   *platform,
     peer_addr_i = peer_addr ? *((guint32 *) peer_addr) : 0;
 
     nmp_cache_iter_for_each (&iter,
-                             nm_platform_lookup_object(platform,
-                                                       addr_family == AF_INET
-                                                           ? NMP_OBJECT_TYPE_IP4_ADDRESS
-                                                           : NMP_OBJECT_TYPE_IP6_ADDRESS,
-                                                       0),
+                             nm_platform_lookup_obj_type(platform,
+                                                         addr_family == AF_INET
+                                                             ? NMP_OBJECT_TYPE_IP4_ADDRESS
+                                                             : NMP_OBJECT_TYPE_IP6_ADDRESS),
                              &o) {
         const NMPObject *obj_old = NULL;
 
@@ -1139,7 +1138,7 @@ ip_route_add(NMPlatform              *platform,
         gboolean has_route_to_gw = FALSE;
 
         nmp_cache_iter_for_each (&iter,
-                                 nm_platform_lookup_object(platform, NMP_OBJECT_GET_TYPE(obj), 0),
+                                 nm_platform_lookup_obj_type(platform, NMP_OBJECT_GET_TYPE(obj)),
                                  &o) {
             if (addr_family == AF_INET) {
                 const NMPlatformIP4Route *item = NMP_OBJECT_CAST_IP4_ROUTE(o);
diff --git a/src/core/platform/tests/test-common.c b/src/core/platform/tests/test-common.c
index 24426a54..9053d351 100644
--- a/src/core/platform/tests/test-common.c
+++ b/src/core/platform/tests/test-common.c
@@ -102,7 +102,11 @@ nmtstp_platform_ip_address_find(NMPlatform *self, int ifindex, int addr_family,
     nm_assert_addr_family(addr_family);
     nm_assert(addr);
 
-    nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4), ifindex);
+    if (ifindex > 0)
+        nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4), ifindex);
+    else
+        nmp_lookup_init_obj_type(&lookup, NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4));
+
     nm_platform_iter_obj_for_each (&iter, self, &lookup, &obj) {
         const NMPlatformIPAddress *a = NMP_OBJECT_CAST_IP_ADDRESS(obj);
 
diff --git a/src/core/platform/tests/test-platform-general.c b/src/core/platform/tests/test-platform-general.c
index 7597f4f1..9629326a 100644
--- a/src/core/platform/tests/test-platform-general.c
+++ b/src/core/platform/tests/test-platform-general.c
@@ -748,6 +748,31 @@ test_platform_ip_address_pretty_sort_cmp(gconstpointer test_data)
 
 /*****************************************************************************/
 
+static void
+test_route_type_is_nodev(void)
+{
+    int i;
+
+    for (i = -1; i <= 257; i++) {
+        gboolean is_nodev;
+
+        switch ((guint8) i) {
+        case RTN_BLACKHOLE:
+        case RTN_UNREACHABLE:
+        case RTN_PROHIBIT:
+            is_nodev = TRUE;
+            break;
+        default:
+            is_nodev = FALSE;
+            break;
+        }
+
+        g_assert_cmpint(is_nodev, ==, nm_platform_route_type_is_nodev(i));
+    }
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE();
 
 int
@@ -767,6 +792,7 @@ main(int argc, char **argv)
     g_test_add_data_func("/general/platform_ip_address_pretty_sort_cmp/6/2",
                          GINT_TO_POINTER(2),
                          test_platform_ip_address_pretty_sort_cmp);
+    g_test_add_func("/general/test_route_type_is_nodev", test_route_type_is_nodev);
 
     return g_test_run();
 }
diff --git a/src/core/platform/tests/test-route.c b/src/core/platform/tests/test-route.c
index 37909d55..cf0236e0 100644
--- a/src/core/platform/tests/test-route.c
+++ b/src/core/platform/tests/test-route.c
@@ -10,7 +10,7 @@
 
 #include "nm-core-utils.h"
 #include "libnm-platform/nm-platform-utils.h"
-#include "libnm-platform/nmp-rules-manager.h"
+#include "libnm-platform/nmp-route-manager.h"
 
 #include "test-common.h"
 
@@ -1637,8 +1637,8 @@ again:
 
     if (TEST_SYNC) {
         gs_unref_hashtable GHashTable *unique_priorities = g_hash_table_new(NULL, NULL);
-        nm_auto_unref_rules_manager NMPRulesManager *rules_manager =
-            nmp_rules_manager_new(platform);
+        nm_auto_unref_route_manager NMPRouteManager *route_manager =
+            nmp_route_manager_new(platform);
         gs_unref_ptrarray GPtrArray *objs_sync  = NULL;
         gconstpointer                USER_TAG_1 = &platform;
         gconstpointer                USER_TAG_2 = &unique_priorities;
@@ -1660,29 +1660,29 @@ again:
         }
 
         for (i = 0; i < objs_sync->len; i++) {
-            nmp_rules_manager_track(rules_manager,
-                                    NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
-                                    1,
-                                    USER_TAG_1,
-                                    NULL);
+            nmp_route_manager_track_rule(route_manager,
+                                         NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
+                                         1,
+                                         USER_TAG_1,
+                                         NULL);
             if (nmtst_get_rand_bool()) {
                 /* this has no effect, because a negative priority (of same absolute value)
                  * has lower priority than the positive priority above. */
-                nmp_rules_manager_track(rules_manager,
-                                        NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
-                                        -1,
-                                        USER_TAG_2,
-                                        NULL);
+                nmp_route_manager_track_rule(route_manager,
+                                             NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
+                                             -1,
+                                             USER_TAG_2,
+                                             NULL);
             }
             if (nmtst_get_rand_uint32() % objs_sync->len == 0) {
-                nmp_rules_manager_sync(rules_manager, FALSE);
+                nmp_route_manager_sync(route_manager, NMP_OBJECT_TYPE_ROUTING_RULE, FALSE);
                 g_assert_cmpint(nmtstp_platform_routing_rules_get_count(platform, AF_UNSPEC),
                                 ==,
                                 i + 1);
             }
         }
 
-        nmp_rules_manager_sync(rules_manager, FALSE);
+        nmp_route_manager_sync(route_manager, NMP_OBJECT_TYPE_ROUTING_RULE, FALSE);
         g_assert_cmpint(nmtstp_platform_routing_rules_get_count(platform, AF_UNSPEC),
                         ==,
                         objs_sync->len);
@@ -1690,37 +1690,37 @@ again:
         for (i = 0; i < objs_sync->len; i++) {
             switch (nmtst_get_rand_uint32() % 3) {
             case 0:
-                nmp_rules_manager_untrack(rules_manager,
-                                          NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
-                                          USER_TAG_1);
-                nmp_rules_manager_untrack(rules_manager,
-                                          NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
-                                          USER_TAG_1);
+                nmp_route_manager_untrack_rule(route_manager,
+                                               NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
+                                               USER_TAG_1);
+                nmp_route_manager_untrack_rule(route_manager,
+                                               NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
+                                               USER_TAG_1);
                 break;
             case 1:
-                nmp_rules_manager_track(rules_manager,
-                                        NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
-                                        -1,
-                                        USER_TAG_1,
-                                        NULL);
+                nmp_route_manager_track_rule(route_manager,
+                                             NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
+                                             -1,
+                                             USER_TAG_1,
+                                             NULL);
                 break;
             case 2:
-                nmp_rules_manager_track(rules_manager,
-                                        NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
-                                        -2,
-                                        USER_TAG_2,
-                                        NULL);
+                nmp_route_manager_track_rule(route_manager,
+                                             NMP_OBJECT_CAST_ROUTING_RULE(objs_sync->pdata[i]),
+                                             -2,
+                                             USER_TAG_2,
+                                             NULL);
                 break;
             }
             if (nmtst_get_rand_uint32() % objs_sync->len == 0) {
-                nmp_rules_manager_sync(rules_manager, FALSE);
+                nmp_route_manager_sync(route_manager, NMP_OBJECT_TYPE_ROUTING_RULE, FALSE);
                 g_assert_cmpint(nmtstp_platform_routing_rules_get_count(platform, AF_UNSPEC),
                                 ==,
                                 objs_sync->len - i - 1);
             }
         }
 
-        nmp_rules_manager_sync(rules_manager, FALSE);
+        nmp_route_manager_sync(route_manager, NMP_OBJECT_TYPE_ROUTING_RULE, FALSE);
 
     } else {
         for (i = 0; i < objs->len;) {
@@ -1886,6 +1886,64 @@ again:
 
 /*****************************************************************************/
 
+static void
+test_blackhole(gconstpointer test_data)
+{
+    int                          TEST_IDX    = GPOINTER_TO_INT(test_data);
+    const int                    addr_family = (TEST_IDX == 1) ? AF_INET : AF_INET6;
+    const int                    IS_IPv4     = NM_IS_IPv4(addr_family);
+    const NMDedupMultiHeadEntry *head_entry;
+    NMDedupMultiIter             iter;
+    const NMPObject             *obj;
+    NMPObject                    obj_stack;
+    NMPlatformIPXRoute           rr = {};
+    int                          r  = -1;
+    int                          i;
+
+    if (IS_IPv4) {
+        rr.r4 = (const NMPlatformIP4Route){
+            .type_coerced = nmtst_rand_select(RTN_BLACKHOLE, RTN_UNREACHABLE, RTN_PROHIBIT),
+        };
+    } else {
+        rr.r6 = (const NMPlatformIP6Route){
+            .type_coerced = nmtst_rand_select(RTN_BLACKHOLE, RTN_UNREACHABLE, RTN_PROHIBIT),
+            .metric       = 1000,
+        };
+    }
+
+    nm_platform_ip_route_normalize(addr_family, &rr.rx);
+
+    if (IS_IPv4)
+        r = nm_platform_ip4_route_add(NM_PLATFORM_GET, NMP_NLM_FLAG_APPEND, &rr.r4);
+    else
+        r = nm_platform_ip6_route_add(NM_PLATFORM_GET, NMP_NLM_FLAG_APPEND, &rr.r6);
+
+    g_assert_cmpint(r, ==, 0);
+
+    nmp_object_stackinit(&obj_stack, NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4), &rr);
+
+    obj = nm_platform_lookup_obj(NM_PLATFORM_GET, NMP_CACHE_ID_TYPE_OBJECT_TYPE, &obj_stack);
+
+    _LOGT(">>> adding %s",
+          nmp_object_to_string(&obj_stack, NMP_OBJECT_TO_STRING_ALL, g_alloca(1000), 1000));
+    _LOGT(">>> found  %s",
+          nmp_object_to_string(obj, NMP_OBJECT_TO_STRING_ALL, g_alloca(1000), 1000));
+
+    g_assert(obj);
+
+    head_entry = nm_platform_lookup_object(NM_PLATFORM_GET, NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4), 0);
+    g_assert(head_entry);
+    g_assert_cmpint(head_entry->len, ==, 1);
+    i = 0;
+    nm_dedup_multi_iter_for_each (&iter, head_entry) {
+        i++;
+        g_assert(iter.current->obj == obj);
+    }
+    g_assert_cmpint(i, ==, 1);
+}
+
+/*****************************************************************************/
+
 NMTstpSetupFunc const _nmtstp_setup_platform_func = SETUP;
 
 void
@@ -1923,4 +1981,8 @@ _nmtstp_setup_tests(void)
         add_test_func_data("/route/rule/3", test_rule, GINT_TO_POINTER(3));
         add_test_func_data("/route/rule/4", test_rule, GINT_TO_POINTER(4));
     }
+    if (nmtstp_is_root_test()) {
+        add_test_func_data("/route/blackhole/1", test_blackhole, GINT_TO_POINTER(1));
+        add_test_func_data("/route/blackhole/2", test_blackhole, GINT_TO_POINTER(2));
+    }
 }