summary refs log tree commit diff
path: root/src/core/nm-l3cfg.c
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2022-01-13 22:30:39 +0100
committerMichael Biebl <biebl@debian.org>2022-01-13 22:30:39 +0100
commit88c227d90a6b7b388c5c85d72802a0ca8f05ed5c (patch)
tree71f32df6617802270e8a78574bd8e1637dc532f4 /src/core/nm-l3cfg.c
parente74c568b07b50b97873fb4ee1d776dedefbd54d6 (diff)
New upstream version 1.34.0 upstream/1.34.0
Diffstat (limited to 'src/core/nm-l3cfg.c')
-rw-r--r--src/core/nm-l3cfg.c1762
1 files changed, 1176 insertions, 586 deletions
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index 93e92673..aa56c13f 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -18,6 +18,17 @@
 
 /*****************************************************************************/
 
+#define ROUTES_TEMPORARY_NOT_AVAILABLE_MAX_AGE_MSEC ((gint64) 20000)
+
+/* When a ObjStateData becomes a "zombie", we aim to delete it from platform
+ * on the next commit (until it disappears from platform). But we might have
+ * a bug, so that we fail to delete the platform (for example, related to
+ * IPv6 multicast routes). We thus rate limit how often we try to do this,
+ * before giving up. */
+#define ZOMBIE_COUNT_START 5
+
+/*****************************************************************************/
+
 G_STATIC_ASSERT(NM_ACD_TIMEOUT_RFC5227_MSEC == N_ACD_TIMEOUT_RFC5227);
 
 #define ACD_SUPPORTED_ETH_ALEN                  ETH_ALEN
@@ -97,6 +108,58 @@ typedef struct {
 
 G_STATIC_ASSERT(G_STRUCT_OFFSET(AcdData, info.addr) == 0);
 
+typedef struct {
+    const NMPObject *obj;
+
+    /* Whether obj is currently in the platform cache or not.
+     * Since "obj" is the NMPObject from the merged NML3ConfigData,
+     * the object in platform has the same ID (but may otherwise not
+     * be identical). If this is not NULL, then currently the object
+     * is configured in kernel. */
+    const NMPObject *os_plobj;
+
+    CList os_lst;
+
+    /* If we have a timeout pending, we link the instance to
+     * self->priv.p->obj_state_temporary_not_available_lst_head. */
+    CList os_temporary_not_available_lst;
+
+    /* If a NMPObject is no longer to be configured (but was configured
+     * during a previous commit), then we need to remember it so that the
+     * next commit can delete the address/route in kernel. It becomes a zombie. */
+    CList os_zombie_lst;
+
+    /* We might want to configure "obj" in platform, but it's currently not possible.
+     * For example, certain IPv6 routes can only be added after the IPv6 address
+     * becomes non-tentative (*sigh*). In such a case, we need to remember that, and
+     * retry later. If this timestamp is set to a non-zero value, then it means
+     * we tried to configure the obj (at that timestamp) and failed, but we are
+     * waiting to retry.
+     *
+     * See also self->priv.p->obj_state_temporary_not_available_lst_head
+     * and self->priv.p->obj_state_temporary_not_available_timeout_source. */
+    gint64 os_temporary_not_available_timestamp_msec;
+
+    /* When the obj is a zombie (that means, it was previously configured by NML3Cfg, but
+     * now no longer), it needs to be deleted from platform. This ratelimits the time
+     * how often we try that. When the counter reaches zero, we forget about it. */
+    guint8 os_zombie_count;
+
+    /* whether we ever saw the object in platform. */
+    bool os_was_in_platform : 1;
+
+    /* Indicates whether NetworkManager actively tried to configure the object
+     * in platform once. */
+    bool os_nm_configured : 1;
+
+    /* This flag is only used temporarily to do a bulk update and
+     * clear all the ones that are no longer in used. */
+    bool os_dirty : 1;
+    bool os_tna_dirty : 1;
+} ObjStateData;
+
+G_STATIC_ASSERT(G_STRUCT_OFFSET(ObjStateData, obj) == 0);
+
 struct _NML3CfgCommitTypeHandle {
     CList             commit_type_lst;
     NML3CfgCommitType commit_type;
@@ -104,6 +167,7 @@ struct _NML3CfgCommitTypeHandle {
 
 typedef struct {
     const NML3ConfigData *l3cd;
+    NML3CfgConfigFlags    config_flags;
     NML3ConfigMergeFlags  merge_flags;
     union {
         struct {
@@ -126,6 +190,13 @@ typedef struct {
         };
         guint32 default_route_penalty_x[2];
     };
+    union {
+        struct {
+            int default_dns_priority_6;
+            int default_dns_priority_4;
+        };
+        int default_dns_priority_x[2];
+    };
     gconstpointer     tag_confdata;
     guint64           pseudo_timestamp_confdata;
     int               priority_confdata;
@@ -146,7 +217,6 @@ enum {
 static guint signals[LAST_SIGNAL] = {0};
 
 typedef struct _NML3CfgPrivate {
-    GArray *property_emit_list;
     GArray *l3_config_datas;
 
     NML3IPv4LL *ipv4ll;
@@ -157,9 +227,11 @@ typedef struct _NML3CfgPrivate {
 
     CList commit_type_lst_head;
 
-    GHashTable *routes_temporary_not_available_hash;
+    GHashTable *obj_state_hash;
 
-    GHashTable *externally_removed_objs_hash;
+    CList obj_state_lst_head;
+    CList obj_state_zombie_lst_head;
+    CList obj_state_temporary_not_available_lst_head;
 
     GHashTable *acd_ipv4_addresses_on_link;
 
@@ -181,41 +253,30 @@ typedef struct _NML3CfgPrivate {
 
     guint64 pseudo_timestamp_counter;
 
-    union {
-        struct {
-            guint externally_removed_objs_cnt_addresses_6;
-            guint externally_removed_objs_cnt_addresses_4;
-        };
-        guint externally_removed_objs_cnt_addresses_x[2];
-    };
+    GSource *obj_state_temporary_not_available_timeout_source;
 
-    union {
-        struct {
-            guint externally_removed_objs_cnt_routes_6;
-            guint externally_removed_objs_cnt_routes_4;
-        };
-        guint externally_removed_objs_cnt_routes_x[2];
-    };
+    NML3CfgCommitType commit_on_idle_type;
 
-    union {
-        struct {
-            GPtrArray *last_addresses_6;
-            GPtrArray *last_addresses_4;
-        };
-        GPtrArray *last_addresses_x[2];
-    };
+    gint8 commit_reentrant_count;
 
-    union {
-        struct {
-            GPtrArray *last_routes_6;
-            GPtrArray *last_routes_4;
-        };
-        GPtrArray *last_routes_x[2];
-    };
+    /* The value that was set before we touched the sysctl (this only is
+     * meaningful if "ip6_privacy_set" is true. At the end, we want to restore
+     * this value. */
+    NMSettingIP6ConfigPrivacy ip6_privacy_initial : 4;
 
-    guint routes_temporary_not_available_id;
+    /* The value that we set the last time. This is cached so that we don't
+     * repeatedly try to commit the same value. */
+    NMSettingIP6ConfigPrivacy ip6_privacy_set_before : 4;
 
-    gint8 commit_reentrant_count;
+    guint32 ndisc_retrans_timer_msec;
+    guint32 ndisc_reachable_time_msec;
+    int     ndisc_hop_limit;
+
+    /* Whether "self" set the ip6_privacy sysctl (and whether it needs to be reset). */
+    bool ip6_privacy_set : 1;
+    bool ndisc_reachable_time_msec_set : 1;
+    bool ndisc_retrans_timer_msec_set : 1;
+    bool ndisc_hop_limit_set : 1;
 
     bool commit_type_update_sticky : 1;
 
@@ -268,8 +329,6 @@ G_DEFINE_TYPE(NML3Cfg, nm_l3cfg, G_TYPE_OBJECT)
 
 static void _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is_idle);
 
-static void _property_emit_notify(NML3Cfg *self, NML3CfgPropertyEmitType emit_type);
-
 static void _nm_l3cfg_emit_signal_notify_acd_event_all(NML3Cfg *self);
 
 static gboolean _acd_has_valid_link(const NMPObject *obj,
@@ -305,6 +364,7 @@ static NM_UTILS_ENUM2STR_DEFINE(
     NM_UTILS_ENUM2STR(NM_L3_CONFIG_NOTIFY_TYPE_L3CD_CHANGED, "l3cd-changed"),
     NM_UTILS_ENUM2STR(NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE, "platform-change"),
     NM_UTILS_ENUM2STR(NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE_ON_IDLE, "platform-change-on-idle"),
+    NM_UTILS_ENUM2STR(NM_L3_CONFIG_NOTIFY_TYPE_PRE_COMMIT, "pre-commit"),
     NM_UTILS_ENUM2STR(NM_L3_CONFIG_NOTIFY_TYPE_POST_COMMIT, "post-commit"),
     NM_UTILS_ENUM2STR(NM_L3_CONFIG_NOTIFY_TYPE_ROUTES_TEMPORARY_NOT_AVAILABLE_EXPIRED,
                       "routes-temporary-not-available-expired"),
@@ -314,7 +374,7 @@ static NM_UTILS_ENUM2STR_DEFINE(_l3_acd_defend_type_to_string,
                                 NML3AcdDefendType,
                                 NM_UTILS_ENUM2STR(NM_L3_ACD_DEFEND_TYPE_ALWAYS, "always"),
                                 NM_UTILS_ENUM2STR(NM_L3_ACD_DEFEND_TYPE_NEVER, "never"),
-                                NM_UTILS_ENUM2STR(NM_L3_ACD_DEFEND_TYPE_NONE, "none"),
+                                NM_UTILS_ENUM2STR(_NM_L3_ACD_DEFEND_TYPE_NONE, "none"),
                                 NM_UTILS_ENUM2STR(NM_L3_ACD_DEFEND_TYPE_ONCE, "once"), );
 
 static NM_UTILS_LOOKUP_DEFINE(_l3_acd_defend_type_to_nacd,
@@ -342,6 +402,17 @@ static NM_UTILS_LOOKUP_DEFINE(_l3_acd_addr_state_to_string,
 
 /*****************************************************************************/
 
+gboolean
+nm_l3cfg_is_vrf(const NML3Cfg *self)
+{
+    const NMPlatformLink *pllink;
+
+    pllink = nm_l3cfg_get_pllink(self, TRUE);
+    return pllink && pllink->type == NM_LINK_TYPE_VRF;
+}
+
+/*****************************************************************************/
+
 static const char *
 _l3_config_notify_data_to_string(const NML3ConfigNotifyData *notify_data,
                                  char *                      sbuf,
@@ -358,31 +429,29 @@ _l3_config_notify_data_to_string(const NML3ConfigNotifyData *notify_data,
     nm_assert(sbuf_size > 0);
 
     _l3_config_notify_type_to_string(notify_data->notify_type, s, l);
-    nm_utils_strbuf_seek_end(&s, &l);
+    nm_strbuf_seek_end(&s, &l);
 
     switch (notify_data->notify_type) {
     case NM_L3_CONFIG_NOTIFY_TYPE_L3CD_CHANGED:
-        nm_utils_strbuf_append(
-            &s,
-            &l,
-            ", l3cd-old=%s",
-            NM_HASH_OBFUSCATE_PTR_STR(notify_data->l3cd_changed.l3cd_old, sbufobf));
-        nm_utils_strbuf_append(
-            &s,
-            &l,
-            ", l3cd-new=%s",
-            NM_HASH_OBFUSCATE_PTR_STR(notify_data->l3cd_changed.l3cd_new, sbufobf));
-        nm_utils_strbuf_append(&s, &l, ", commited=%d", notify_data->l3cd_changed.commited);
+        nm_strbuf_append(&s,
+                         &l,
+                         ", l3cd-old=%s",
+                         NM_HASH_OBFUSCATE_PTR_STR(notify_data->l3cd_changed.l3cd_old, sbufobf));
+        nm_strbuf_append(&s,
+                         &l,
+                         ", l3cd-new=%s",
+                         NM_HASH_OBFUSCATE_PTR_STR(notify_data->l3cd_changed.l3cd_new, sbufobf));
+        nm_strbuf_append(&s, &l, ", commited=%d", notify_data->l3cd_changed.commited);
         break;
     case NM_L3_CONFIG_NOTIFY_TYPE_ACD_EVENT:
-        nm_utils_strbuf_append(&s,
-                               &l,
-                               ", addr=%s, state=%s",
-                               _nm_utils_inet4_ntop(notify_data->acd_event.info.addr, sbuf_addr),
-                               _l3_acd_addr_state_to_string(notify_data->acd_event.info.state));
+        nm_strbuf_append(&s,
+                         &l,
+                         ", addr=%s, state=%s",
+                         _nm_utils_inet4_ntop(notify_data->acd_event.info.addr, sbuf_addr),
+                         _l3_acd_addr_state_to_string(notify_data->acd_event.info.state));
         break;
     case NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE:
-        nm_utils_strbuf_append(
+        nm_strbuf_append(
             &s,
             &l,
             ", obj-type=%s, change=%s, obj=",
@@ -391,15 +460,15 @@ _l3_config_notify_data_to_string(const NML3ConfigNotifyData *notify_data,
         nmp_object_to_string(notify_data->platform_change.obj, NMP_OBJECT_TO_STRING_PUBLIC, s, l);
         break;
     case NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE_ON_IDLE:
-        nm_utils_strbuf_append(&s,
-                               &l,
-                               ", obj-type-flags=0x%x",
-                               notify_data->platform_change_on_idle.obj_type_flags);
+        nm_strbuf_append(&s,
+                         &l,
+                         ", obj-type-flags=0x%x",
+                         notify_data->platform_change_on_idle.obj_type_flags);
         break;
     case NM_L3_CONFIG_NOTIFY_TYPE_IPV4LL_EVENT:
         nm_assert(NM_IS_L3_IPV4LL(notify_data->ipv4ll_event.ipv4ll));
         addr4 = nm_l3_ipv4ll_get_addr(notify_data->ipv4ll_event.ipv4ll);
-        nm_utils_strbuf_append(
+        nm_strbuf_append(
             &s,
             &l,
             ", ipv4ll=" NM_HASH_OBFUSCATE_PTR_FMT "%s%s, state=%s",
@@ -555,157 +624,447 @@ _nm_n_acd_data_probe_new(NML3Cfg *self, in_addr_t addr, guint32 timeout_msec, gp
 
 /*****************************************************************************/
 
-static guint *
-_l3cfg_externally_removed_objs_counter(NML3Cfg *self, NMPObjectType obj_type)
+#define nm_assert_obj_state(self, obj_state)                                                      \
+    G_STMT_START                                                                                  \
+    {                                                                                             \
+        if (NM_MORE_ASSERTS > 0) {                                                                \
+            const NML3Cfg *     _self      = (self);                                              \
+            const ObjStateData *_obj_state = (obj_state);                                         \
+                                                                                                  \
+            nm_assert(_obj_state);                                                                \
+            nm_assert(NM_IN_SET(NMP_OBJECT_GET_TYPE(_obj_state->obj),                             \
+                                NMP_OBJECT_TYPE_IP4_ADDRESS,                                      \
+                                NMP_OBJECT_TYPE_IP6_ADDRESS,                                      \
+                                NMP_OBJECT_TYPE_IP4_ROUTE,                                        \
+                                NMP_OBJECT_TYPE_IP6_ROUTE));                                      \
+            nm_assert(!_obj_state->os_plobj || _obj_state->os_was_in_platform);                   \
+            nm_assert((_obj_state->os_temporary_not_available_timestamp_msec == 0)                \
+                      == c_list_is_empty(&_obj_state->os_temporary_not_available_lst));           \
+            if (_self) {                                                                          \
+                if (c_list_is_empty(&_obj_state->os_zombie_lst)) {                                \
+                    nm_assert(_self->priv.p->combined_l3cd_commited);                             \
+                                                                                                  \
+                    if (NM_MORE_ASSERTS > 5) {                                                    \
+                        nm_assert(c_list_contains(&_self->priv.p->obj_state_lst_head,             \
+                                                  &_obj_state->os_lst));                          \
+                        nm_assert((_obj_state->os_temporary_not_available_timestamp_msec == 0)    \
+                                  || c_list_contains(                                             \
+                                      &_self->priv.p->obj_state_temporary_not_available_lst_head, \
+                                      &_obj_state->os_temporary_not_available_lst));              \
+                        nm_assert(_obj_state->os_plobj                                            \
+                                  == nm_platform_lookup_obj(_self->priv.platform,                 \
+                                                            NMP_CACHE_ID_TYPE_OBJECT_TYPE,        \
+                                                            _obj_state->obj));                    \
+                        nm_assert(                                                                \
+                            c_list_is_empty(&obj_state->os_zombie_lst)                            \
+                                ? (_obj_state->obj                                                \
+                                   == nm_dedup_multi_entry_get_obj(nm_l3_config_data_lookup_obj(  \
+                                       _self->priv.p->combined_l3cd_commited,                     \
+                                       _obj_state->obj)))                                         \
+                                : (!nm_l3_config_data_lookup_obj(                                 \
+                                    _self->priv.p->combined_l3cd_commited,                        \
+                                    _obj_state->obj)));                                           \
+                    }                                                                             \
+                }                                                                                 \
+            }                                                                                     \
+        }                                                                                         \
+    }                                                                                             \
+    G_STMT_END
+
+static gboolean
+_obj_state_data_get_assume_config_once(const ObjStateData *obj_state)
 {
-    switch (obj_type) {
-    case NMP_OBJECT_TYPE_IP4_ADDRESS:
-        return &self->priv.p->externally_removed_objs_cnt_addresses_4;
-    case NMP_OBJECT_TYPE_IP6_ADDRESS:
-        return &self->priv.p->externally_removed_objs_cnt_addresses_6;
-    case NMP_OBJECT_TYPE_IP4_ROUTE:
-        return &self->priv.p->externally_removed_objs_cnt_routes_4;
-    case NMP_OBJECT_TYPE_IP6_ROUTE:
-        return &self->priv.p->externally_removed_objs_cnt_routes_6;
-    default:
-        return nm_assert_unreachable_val(NULL);
-    }
+    nm_assert_obj_state(NULL, obj_state);
+
+    return nmp_object_get_assume_config_once(obj_state->obj);
+}
+
+static ObjStateData *
+_obj_state_data_new(const NMPObject *obj, const NMPObject *plobj)
+{
+    ObjStateData *obj_state;
+
+    obj_state  = g_slice_new(ObjStateData);
+    *obj_state = (ObjStateData){
+        .obj                            = nmp_object_ref(obj),
+        .os_plobj                       = nmp_object_ref(plobj),
+        .os_was_in_platform             = !!plobj,
+        .os_nm_configured               = FALSE,
+        .os_dirty                       = FALSE,
+        .os_temporary_not_available_lst = C_LIST_INIT(obj_state->os_temporary_not_available_lst),
+        .os_zombie_lst                  = C_LIST_INIT(obj_state->os_zombie_lst),
+    };
+    return obj_state;
 }
 
 static void
-_l3cfg_externally_removed_objs_drop(NML3Cfg *self)
+_obj_state_data_free(gpointer data)
 {
-    nm_assert(NM_IS_L3CFG(self));
+    ObjStateData *obj_state = data;
+
+    c_list_unlink_stale(&obj_state->os_lst);
+    c_list_unlink_stale(&obj_state->os_zombie_lst);
+    c_list_unlink_stale(&obj_state->os_temporary_not_available_lst);
+    nmp_object_unref(obj_state->obj);
+    nmp_object_unref(obj_state->os_plobj);
+    nm_g_slice_free(obj_state);
+}
+
+static const char *
+_obj_state_data_to_string(const ObjStateData *obj_state, char *buf, gsize buf_size)
+{
+    const char *buf0     = buf;
+    gint64      now_msec = 0;
+
+    nm_assert(buf);
+    nm_assert(buf_size > 0);
+    nm_assert_obj_state(NULL, obj_state);
+
+    nm_strbuf_append(&buf,
+                     &buf_size,
+                     "[" NM_HASH_OBFUSCATE_PTR_FMT ", %s, ",
+                     NM_HASH_OBFUSCATE_PTR(obj_state),
+                     NMP_OBJECT_GET_CLASS(obj_state->obj)->obj_type_name);
+
+    nmp_object_to_string(obj_state->obj, NMP_OBJECT_TO_STRING_PUBLIC, buf, buf_size);
+    nm_strbuf_seek_end(&buf, &buf_size);
+    nm_strbuf_append_c(&buf, &buf_size, ']');
+
+    if (!c_list_is_empty(&obj_state->os_zombie_lst))
+        nm_strbuf_append(&buf, &buf_size, ", zombie[%u]", obj_state->os_zombie_count);
+
+    if (obj_state->os_nm_configured)
+        nm_strbuf_append_str(&buf, &buf_size, ", nm-configured");
+
+    if (obj_state->os_plobj) {
+        nm_assert(obj_state->os_was_in_platform);
+        nm_strbuf_append_str(&buf, &buf_size, ", in-platform");
+    } else if (obj_state->os_was_in_platform)
+        nm_strbuf_append_str(&buf, &buf_size, ", was-in-platform");
+
+    if (obj_state->os_temporary_not_available_timestamp_msec > 0) {
+        nm_utils_get_monotonic_timestamp_msec_cached(&now_msec);
+        nm_strbuf_append(
+            &buf,
+            &buf_size,
+            ", temporary-not-available-since=%" G_GINT64_FORMAT ".%03d",
+            (now_msec - obj_state->os_temporary_not_available_timestamp_msec) / 1000,
+            (int) ((now_msec - obj_state->os_temporary_not_available_timestamp_msec) % 1000));
+    }
+
+    return buf0;
+}
+
+static gboolean
+_obj_state_data_update(ObjStateData *obj_state, const NMPObject *obj)
+{
+    gboolean changed = FALSE;
+
+    nm_assert_obj_state(NULL, obj_state);
+    nm_assert(obj);
+    nm_assert(nmp_object_id_equal(obj_state->obj, obj));
+
+    obj_state->os_dirty = FALSE;
 
-    self->priv.p->externally_removed_objs_cnt_addresses_4 = 0;
-    self->priv.p->externally_removed_objs_cnt_addresses_6 = 0;
-    self->priv.p->externally_removed_objs_cnt_routes_4    = 0;
-    self->priv.p->externally_removed_objs_cnt_routes_6    = 0;
-    if (nm_g_hash_table_size(self->priv.p->externally_removed_objs_hash) > 0)
-        _LOGD("externally-removed: untrack all");
-    nm_clear_pointer(&self->priv.p->externally_removed_objs_hash, g_hash_table_unref);
+    if (obj_state->obj != obj) {
+        nm_auto_nmpobj const NMPObject *obj_old = NULL;
+
+        if (!nmp_object_equal(obj_state->obj, obj))
+            changed = TRUE;
+        obj_old        = g_steal_pointer(&obj_state->obj);
+        obj_state->obj = nmp_object_ref(obj);
+    }
+
+    if (!c_list_is_empty(&obj_state->os_zombie_lst)) {
+        c_list_unlink(&obj_state->os_zombie_lst);
+        changed = TRUE;
+    }
+
+    return changed;
 }
 
+/*****************************************************************************/
+
 static void
-_l3cfg_externally_removed_objs_drop_unused(NML3Cfg *self)
+_obj_states_externally_removed_track(NML3Cfg *self, const NMPObject *obj, gboolean in_platform)
 {
-    GHashTableIter   h_iter;
-    const NMPObject *obj;
-    char             sbuf[sizeof(_nm_utils_to_string_buffer)];
+    char          sbuf[sizeof(_nm_utils_to_string_buffer)];
+    ObjStateData *obj_state;
 
     nm_assert(NM_IS_L3CFG(self));
+    nm_assert_is_bool(in_platform);
 
-    if (!self->priv.p->externally_removed_objs_hash)
+    nm_assert(
+        in_platform
+            ? (obj
+               == nm_platform_lookup_obj(self->priv.platform, NMP_CACHE_ID_TYPE_OBJECT_TYPE, obj))
+            : (!nm_platform_lookup_obj(self->priv.platform, NMP_CACHE_ID_TYPE_OBJECT_TYPE, obj)));
+
+    obj_state = g_hash_table_lookup(self->priv.p->obj_state_hash, &obj);
+    if (!obj_state)
         return;
 
-    if (!self->priv.p->combined_l3cd_commited) {
-        _l3cfg_externally_removed_objs_drop(self);
+    if (!in_platform)
+        obj = NULL;
+
+    if (obj_state->os_plobj == obj)
+        goto out;
+
+    if (!in_platform && !c_list_is_empty(&obj_state->os_zombie_lst)) {
+        /* this is a zombie. We can forget about it.*/
+        nm_clear_nmp_object(&obj_state->os_plobj);
+        c_list_unlink(&obj_state->os_zombie_lst);
+        _LOGD("obj-state: zombie gone (untrack): %s",
+              _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+        g_hash_table_remove(self->priv.p->obj_state_hash, obj_state);
         return;
     }
 
-    g_hash_table_iter_init(&h_iter, self->priv.p->externally_removed_objs_hash);
-    while (g_hash_table_iter_next(&h_iter, (gpointer *) &obj, NULL)) {
-        if (!nm_l3_config_data_lookup_obj(self->priv.p->combined_l3cd_commited, obj)) {
-            /* The object is no longer tracked in the configuration.
-             * The externally_removed_objs_hash is to prevent adding entires that were
-             * removed externally, so if we don't plan to add the entry, we no longer need to track
-             * it. */
-            _LOGD("externally-removed: untrack %s",
-                  nmp_object_to_string(obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
-            (*(_l3cfg_externally_removed_objs_counter(self, NMP_OBJECT_GET_TYPE(obj))))--;
-            g_hash_table_iter_remove(&h_iter);
-        }
+    nm_assert(c_list_is_empty(&obj_state->os_zombie_lst));
+
+    if (in_platform) {
+        nmp_object_ref_set(&obj_state->os_plobj, obj);
+        obj_state->os_was_in_platform = TRUE;
+        _LOGD("obj-state: appeared in platform: %s",
+              _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+        goto out;
     }
+
+    nm_clear_nmp_object(&obj_state->os_plobj);
+    _LOGD("obj-state: remove from platform: %s",
+          _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+
+out:
+    nm_assert_obj_state(self, obj_state);
 }
 
 static void
-_l3cfg_externally_removed_objs_track(NML3Cfg *self, const NMPObject *obj, gboolean is_removed)
+_obj_states_update_all(NML3Cfg *self)
 {
-    char sbuf[1000];
+    static const NMPObjectType obj_types[] = {
+        NMP_OBJECT_TYPE_IP4_ADDRESS,
+        NMP_OBJECT_TYPE_IP6_ADDRESS,
+        NMP_OBJECT_TYPE_IP4_ROUTE,
+        NMP_OBJECT_TYPE_IP6_ROUTE,
+    };
+    char          sbuf[sizeof(_nm_utils_to_string_buffer)];
+    ObjStateData *obj_state;
+    int           i;
+    gboolean      any_dirty = FALSE;
 
     nm_assert(NM_IS_L3CFG(self));
 
-    if (!self->priv.p->combined_l3cd_commited)
-        return;
+    c_list_for_each_entry (obj_state, &self->priv.p->obj_state_lst_head, os_lst) {
+        if (!c_list_is_empty(&obj_state->os_zombie_lst)) {
+            /* we can ignore zombies. */
+            continue;
+        }
+        any_dirty           = TRUE;
+        obj_state->os_dirty = TRUE;
+    }
+
+    for (i = 0; i < (int) G_N_ELEMENTS(obj_types); i++) {
+        const NMPObjectType obj_type = obj_types[i];
+        NMDedupMultiIter    o_iter;
+        const NMPObject *   obj;
+
+        if (!self->priv.p->combined_l3cd_commited)
+            continue;
+
+        nm_l3_config_data_iter_obj_for_each (&o_iter,
+                                             self->priv.p->combined_l3cd_commited,
+                                             &obj,
+                                             obj_type) {
+            obj_state = g_hash_table_lookup(self->priv.p->obj_state_hash, &obj);
+            if (!obj_state) {
+                obj_state =
+                    _obj_state_data_new(obj,
+                                        nm_platform_lookup_obj(self->priv.platform,
+                                                               NMP_CACHE_ID_TYPE_OBJECT_TYPE,
+                                                               obj));
+                c_list_link_tail(&self->priv.p->obj_state_lst_head, &obj_state->os_lst);
+                g_hash_table_add(self->priv.p->obj_state_hash, obj_state);
+                _LOGD("obj-state: track: %s",
+                      _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+                nm_assert_obj_state(self, obj_state);
+                continue;
+            }
 
-    if (!is_removed) {
-        /* the object is still (or again) present. It no longer gets hidden. */
-        if (self->priv.p->externally_removed_objs_hash) {
-            const NMPObject *obj2;
-            gpointer         x_val;
-
-            if (g_hash_table_steal_extended(self->priv.p->externally_removed_objs_hash,
-                                            obj,
-                                            (gpointer *) &obj2,
-                                            &x_val)) {
-                (*(_l3cfg_externally_removed_objs_counter(self, NMP_OBJECT_GET_TYPE(obj2))))--;
-                _LOGD("externally-removed: untrack %s",
-                      nmp_object_to_string(obj2, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
-                nmp_object_unref(obj2);
+            if (_obj_state_data_update(obj_state, obj)) {
+                _LOGD("obj-state: update: %s",
+                      _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
             }
+
+            nm_assert_obj_state(self, obj_state);
         }
-        return;
     }
 
-    if (!nm_l3_config_data_lookup_obj(self->priv.p->combined_l3cd_commited, obj)) {
-        /* we don't care about this object, so there is nothing to hide hide */
-        return;
+    if (any_dirty) {
+        GHashTableIter h_iter;
+
+        g_hash_table_iter_init(&h_iter, self->priv.p->obj_state_hash);
+        while (g_hash_table_iter_next(&h_iter, (gpointer *) &obj_state, NULL)) {
+            if (!c_list_is_empty(&obj_state->os_zombie_lst))
+                continue;
+            if (!obj_state->os_dirty)
+                continue;
+
+            if (obj_state->os_plobj && obj_state->os_nm_configured) {
+                c_list_link_tail(&self->priv.p->obj_state_zombie_lst_head,
+                                 &obj_state->os_zombie_lst);
+                obj_state->os_zombie_count = ZOMBIE_COUNT_START;
+                _LOGD("obj-state: now zombie: %s",
+                      _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+                continue;
+            }
+
+            _LOGD("obj-state: untrack: %s",
+                  _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+            g_hash_table_iter_remove(&h_iter);
+        }
     }
+}
 
-    if (G_UNLIKELY(!self->priv.p->externally_removed_objs_hash)) {
-        self->priv.p->externally_removed_objs_hash =
-            g_hash_table_new_full((GHashFunc) nmp_object_id_hash,
-                                  (GEqualFunc) nmp_object_id_equal,
-                                  (GDestroyNotify) nmp_object_unref,
-                                  NULL);
+typedef struct {
+    NML3Cfg *         self;
+    NML3CfgCommitType commit_type;
+} ObjStatesSyncFilterData;
+
+static gboolean
+_obj_states_sync_filter(/* const NMDedupMultiObj * */ gconstpointer o, gpointer user_data)
+{
+    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;
+
+    nm_assert(sync_filter_data);
+    nm_assert(NM_IS_L3CFG(sync_filter_data->self));
+
+    obj_type = NMP_OBJECT_GET_TYPE(obj);
+
+    if (obj_type == NMP_OBJECT_TYPE_IP4_ADDRESS
+        && 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);
+
+    nm_assert_obj_state(sync_filter_data->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) {
+        NML3Cfg *self;
+
+        if (sync_filter_data->commit_type == NM_L3_CFG_COMMIT_TYPE_ASSUME
+            && !_obj_state_data_get_assume_config_once(obj_state))
+            return FALSE;
+
+        obj_state->os_nm_configured = TRUE;
+
+        self = sync_filter_data->self;
+        _LOGD("obj-state: configure-first-time: %s",
+              _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+        return TRUE;
     }
 
-    if (g_hash_table_add(self->priv.p->externally_removed_objs_hash,
-                         (gpointer) nmp_object_ref(obj))) {
-        (*(_l3cfg_externally_removed_objs_counter(self, NMP_OBJECT_GET_TYPE(obj))))++;
-        _LOGD("externally-removed: track %s",
-              nmp_object_to_string(obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
+    if (obj_state->os_temporary_not_available_timestamp_msec > 0) {
+        /* we currently try to configure this address (but failed earlier).
+         * Definitely retry. */
+        return TRUE;
     }
+
+    if (!obj_state->os_plobj && sync_filter_data->commit_type != NM_L3_CFG_COMMIT_TYPE_REAPPLY)
+        return FALSE;
+
+    return TRUE;
 }
 
 static void
-_l3cfg_externally_removed_objs_pickup(NML3Cfg *self, int addr_family)
+_obj_state_zombie_lst_get_prune_lists(NML3Cfg *   self,
+                                      int         addr_family,
+                                      GPtrArray **out_addresses_prune,
+                                      GPtrArray **out_routes_prune)
 {
-    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
-    NMDedupMultiIter iter;
-    const NMPObject *obj;
+    const int           IS_IPv4          = NM_IS_IPv4(addr_family);
+    const NMPObjectType obj_type_route   = NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4);
+    const NMPObjectType obj_type_address = NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4);
+    char                sbuf[sizeof(_nm_utils_to_string_buffer)];
+    ObjStateData *      obj_state;
+    ObjStateData *      obj_state_safe;
 
-    if (!self->priv.p->combined_l3cd_commited)
-        return;
+    nm_assert(NM_IS_L3CFG(self));
+    nm_assert(out_addresses_prune && !*out_addresses_prune);
+    nm_assert(out_routes_prune && !*out_routes_prune);
 
-    nm_l3_config_data_iter_obj_for_each (&iter,
-                                         self->priv.p->combined_l3cd_commited,
-                                         &obj,
-                                         NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4)) {
-        if (!nm_platform_lookup_entry(self->priv.platform, NMP_CACHE_ID_TYPE_OBJECT_TYPE, obj))
-            _l3cfg_externally_removed_objs_track(self, obj, TRUE);
-    }
-    nm_l3_config_data_iter_obj_for_each (&iter,
-                                         self->priv.p->combined_l3cd_commited,
-                                         &obj,
-                                         NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4)) {
-        if (!nm_platform_lookup_entry(self->priv.platform, NMP_CACHE_ID_TYPE_OBJECT_TYPE, obj))
-            _l3cfg_externally_removed_objs_track(self, obj, TRUE);
+    c_list_for_each_entry_safe (obj_state,
+                                obj_state_safe,
+                                &self->priv.p->obj_state_zombie_lst_head,
+                                os_zombie_lst) {
+        NMPObjectType obj_type;
+        GPtrArray **  p_a;
+
+        nm_assert_obj_state(self, obj_state);
+        nm_assert(obj_state->os_zombie_count > 0);
+
+        obj_type = NMP_OBJECT_GET_TYPE(obj_state->obj);
+
+        if (obj_type == obj_type_route)
+            p_a = out_routes_prune;
+        else if (obj_type == obj_type_address)
+            p_a = out_addresses_prune;
+        else
+            continue;
+
+        if (!*p_a)
+            *p_a = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
+
+        g_ptr_array_add(*p_a, (gpointer) nmp_object_ref(obj_state->obj));
+
+        if (--obj_state->os_zombie_count == 0) {
+            _LOGD("obj-state: prune zombie (untrack): %s",
+                  _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+            g_hash_table_remove(self->priv.p->obj_state_hash, obj_state);
+            continue;
+        }
+        _LOGD("obj-state: prune zombie: %s",
+              _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
     }
 }
 
-static gboolean
-_l3cfg_externally_removed_objs_filter(/* const NMDedupMultiObj * */ gconstpointer o,
-                                      gpointer                                    user_data)
+static void
+_obj_state_zombie_lst_prune_all(NML3Cfg *self, int addr_family)
 {
-    const NMPObject *obj                          = o;
-    GHashTable *     externally_removed_objs_hash = user_data;
+    char          sbuf[sizeof(_nm_utils_to_string_buffer)];
+    ObjStateData *obj_state;
+    ObjStateData *obj_state_safe;
 
-    if (NMP_OBJECT_GET_TYPE(obj) == NMP_OBJECT_TYPE_IP4_ADDRESS
-        && NMP_OBJECT_CAST_IP4_ADDRESS(obj)->ip4acd_not_ready)
-        return FALSE;
+    /* we call this during reapply. Then we delete all the routes/addresses
+     * that are configured, and not only the zombies.
+     *
+     * Still, we need to adjust the os_zombie_count and assume that we
+     * are going to drop them. */
+
+    c_list_for_each_entry_safe (obj_state,
+                                obj_state_safe,
+                                &self->priv.p->obj_state_zombie_lst_head,
+                                os_zombie_lst) {
+        nm_assert_obj_state(self, obj_state);
+        nm_assert(obj_state->os_zombie_count > 0);
+
+        if (NMP_OBJECT_GET_ADDR_FAMILY(obj_state->obj) != addr_family)
+            continue;
 
-    return !nm_g_hash_table_contains(externally_removed_objs_hash, obj);
+        if (--obj_state->os_zombie_count == 0) {
+            _LOGD("obj-state: zombie pruned during reapply (untrack): %s",
+                  _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+            g_hash_table_remove(self->priv.p->obj_state_hash, obj_state);
+            continue;
+        }
+        _LOGD("obj-state: zombie pruned during reapply: %s",
+              _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+    }
 }
 
 /*****************************************************************************/
@@ -810,11 +1169,6 @@ _nm_l3cfg_notify_platform_change_on_idle(NML3Cfg *self, guint32 obj_type_flags)
     _nm_l3cfg_emit_signal_notify(self, &notify_data);
 
     _nm_l3cfg_emit_signal_notify_acd_event_all(self);
-
-    if (NM_FLAGS_ANY(obj_type_flags, nmp_object_type_to_flags(NMP_OBJECT_TYPE_IP4_ROUTE)))
-        _property_emit_notify(self, NM_L3CFG_PROPERTY_EMIT_TYPE_IP4_ROUTE);
-    if (NM_FLAGS_ANY(obj_type_flags, nmp_object_type_to_flags(NMP_OBJECT_TYPE_IP6_ROUTE)))
-        _property_emit_notify(self, NM_L3CFG_PROPERTY_EMIT_TYPE_IP6_ROUTE);
 }
 
 void
@@ -847,7 +1201,7 @@ _nm_l3cfg_notify_platform_change(NML3Cfg *                  self,
     case NMP_OBJECT_TYPE_IP6_ADDRESS:
     case NMP_OBJECT_TYPE_IP4_ROUTE:
     case NMP_OBJECT_TYPE_IP6_ROUTE:
-        _l3cfg_externally_removed_objs_track(self, obj, change_type == NM_PLATFORM_SIGNAL_REMOVED);
+        _obj_states_externally_removed_track(self, obj, change_type != NM_PLATFORM_SIGNAL_REMOVED);
     default:
         break;
     }
@@ -864,146 +1218,6 @@ _nm_l3cfg_notify_platform_change(NML3Cfg *                  self,
 
 /*****************************************************************************/
 
-typedef struct {
-    GObject *               target_obj;
-    const GParamSpec *      target_property;
-    NML3CfgPropertyEmitType emit_type;
-} PropertyEmitData;
-
-static void
-_property_emit_notify(NML3Cfg *self, NML3CfgPropertyEmitType emit_type)
-{
-    gs_free PropertyEmitData *collected_heap = NULL;
-    PropertyEmitData *        collected      = NULL;
-    PropertyEmitData *        emit_data;
-    guint                     num;
-    guint                     i;
-    guint                     j;
-
-    if (!self->priv.p->property_emit_list)
-        return;
-
-    num       = 0;
-    emit_data = &g_array_index(self->priv.p->property_emit_list, PropertyEmitData, 0);
-    for (i = 0; i < self->priv.p->property_emit_list->len; i++, emit_data++) {
-        if (emit_data->emit_type == emit_type) {
-            collected = emit_data;
-            num++;
-        }
-    }
-
-    if (num == 0)
-        return;
-
-    if (num == 1) {
-        g_object_notify_by_pspec(collected->target_obj, (GParamSpec *) collected->target_property);
-        return;
-    }
-
-    if (num < 300u / sizeof(*collected))
-        collected = g_alloca(sizeof(PropertyEmitData) * num);
-    else {
-        collected_heap = g_new(PropertyEmitData, num);
-        collected      = collected_heap;
-    }
-
-    emit_data = &g_array_index(self->priv.p->property_emit_list, PropertyEmitData, 0);
-    for (i = 0, j = 0; i < self->priv.p->property_emit_list->len; i++, emit_data++) {
-        if (emit_data->emit_type == emit_type) {
-            collected[j++] = *emit_data;
-            g_object_ref(collected->target_obj);
-        }
-    }
-
-    nm_assert(j == num);
-
-    for (i = 0; i < num; i++) {
-        g_object_notify_by_pspec(collected[i].target_obj,
-                                 (GParamSpec *) collected[i].target_property);
-        if (i > 0)
-            g_object_unref(collected[i].target_obj);
-    }
-}
-
-void
-nm_l3cfg_property_emit_register(NML3Cfg *               self,
-                                GObject *               target_obj,
-                                const GParamSpec *      target_property,
-                                NML3CfgPropertyEmitType emit_type)
-{
-    PropertyEmitData *emit_data;
-    guint             i;
-
-    nm_assert(NM_IS_L3CFG(self));
-    nm_assert(G_IS_OBJECT(target_obj));
-    nm_assert(target_property);
-    nm_assert(NM_IN_SET(emit_type,
-                        NM_L3CFG_PROPERTY_EMIT_TYPE_IP4_ROUTE,
-                        NM_L3CFG_PROPERTY_EMIT_TYPE_IP6_ROUTE));
-    nm_assert(target_property
-              == nm_g_object_class_find_property_from_gtype(G_OBJECT_TYPE(target_obj),
-                                                            target_property->name));
-
-    if (!self->priv.p->property_emit_list)
-        self->priv.p->property_emit_list = g_array_new(FALSE, FALSE, sizeof(PropertyEmitData));
-    else {
-        emit_data = &g_array_index(self->priv.p->property_emit_list, PropertyEmitData, 0);
-        for (i = 0; i < self->priv.p->property_emit_list->len; i++, emit_data++) {
-            if (emit_data->target_obj != target_obj
-                || emit_data->target_property != target_property)
-                continue;
-            nm_assert(emit_data->emit_type == emit_type);
-            emit_data->emit_type = emit_type;
-            return;
-        }
-    }
-
-    emit_data  = nm_g_array_append_new(self->priv.p->property_emit_list, PropertyEmitData);
-    *emit_data = (PropertyEmitData){
-        .target_obj      = target_obj,
-        .target_property = target_property,
-        .emit_type       = emit_type,
-    };
-}
-
-void
-nm_l3cfg_property_emit_unregister(NML3Cfg *         self,
-                                  GObject *         target_obj,
-                                  const GParamSpec *target_property)
-{
-    PropertyEmitData *emit_data;
-    guint             i;
-
-    nm_assert(NM_IS_L3CFG(self));
-    nm_assert(G_IS_OBJECT(target_obj));
-    nm_assert(!target_property
-              || target_property
-                     == nm_g_object_class_find_property_from_gtype(G_OBJECT_TYPE(target_obj),
-                                                                   target_property->name));
-
-    if (!self->priv.p->property_emit_list)
-        return;
-
-    for (i = self->priv.p->property_emit_list->len; i > 0; i--) {
-        emit_data = &g_array_index(self->priv.p->property_emit_list, PropertyEmitData, i);
-
-        if (emit_data->target_obj != target_obj)
-            continue;
-        if (target_property && emit_data->target_property != target_property)
-            continue;
-
-        g_array_remove_index_fast(self->priv.p->property_emit_list, i);
-
-        if (target_property) {
-            /* if a target-property is given, we don't have another entry in
-             * the list. */
-            return;
-        }
-    }
-}
-
-/*****************************************************************************/
-
 gboolean
 nm_l3cfg_get_acd_is_pending(NML3Cfg *self)
 {
@@ -1012,12 +1226,6 @@ nm_l3cfg_get_acd_is_pending(NML3Cfg *self)
     return self->priv.p->acd_is_pending;
 }
 
-static gboolean
-_acd_track_data_is_not_dirty(const NML3AcdAddrTrackInfo *acd_track)
-{
-    return acd_track && !acd_track->_priv.acd_dirty_track;
-}
-
 static void
 _acd_track_data_clear(NML3AcdAddrTrackInfo *acd_track)
 {
@@ -1044,7 +1252,7 @@ _acd_data_collect_tracks_data(const AcdData *    acd_data,
                               guint32 *          out_best_acd_timeout_msec,
                               NML3AcdDefendType *out_best_acd_defend_type)
 {
-    NML3AcdDefendType best_acd_defend_type  = NM_L3_ACD_DEFEND_TYPE_NONE;
+    NML3AcdDefendType best_acd_defend_type  = _NM_L3_ACD_DEFEND_TYPE_NONE;
     guint32           best_acd_timeout_msec = G_MAXUINT32;
     guint             n                     = 0;
     guint             i;
@@ -1063,7 +1271,7 @@ _acd_data_collect_tracks_data(const AcdData *    acd_data,
             best_acd_defend_type = acd_track->_priv.acd_defend_type_track;
     }
 
-    nm_assert(n == 0 || best_acd_defend_type > NM_L3_ACD_DEFEND_TYPE_NONE);
+    nm_assert(n == 0 || best_acd_defend_type > _NM_L3_ACD_DEFEND_TYPE_NONE);
     nm_assert(best_acd_defend_type <= NM_L3_ACD_DEFEND_TYPE_ALWAYS);
 
     NM_SET_OUT(out_best_acd_timeout_msec, n > 0 ? best_acd_timeout_msec : 0u);
@@ -1136,9 +1344,9 @@ _l3_acd_nacd_event_down_timeout_cb(gpointer user_data)
 static gboolean
 _l3_acd_nacd_event(int fd, GIOCondition condition, gpointer user_data)
 {
-    NML3Cfg *self    = user_data;
-    gboolean success = FALSE;
-    int      r;
+    gs_unref_object NML3Cfg *self    = g_object_ref(user_data);
+    gboolean                 success = FALSE;
+    int                      r;
 
     nm_assert(NM_IS_L3CFG(self));
     nm_assert(self->priv.p->nacd);
@@ -1155,6 +1363,13 @@ _l3_acd_nacd_event(int fd, GIOCondition condition, gpointer user_data)
         AcdData *          acd_data;
         NAcdEvent *        event;
 
+        if (!self->priv.p->nacd) {
+            /* In the loop we emit signals, where *anything* might happen.
+             * Check that we still have the nacd instance. */
+            success = TRUE;
+            goto out;
+        }
+
         r = n_acd_pop_event(self->priv.p->nacd, &event);
         if (r) {
             _LOGT("acd: pop-event failed with error %d", r);
@@ -1286,7 +1501,7 @@ _l3_acd_nacd_instance_reset(NML3Cfg *self, NMTernary start_timer, gboolean acd_d
     switch (start_timer) {
     case NM_TERNARY_FALSE:
         _l3_changed_configs_set_dirty(self);
-        nm_l3cfg_commit_on_idle_schedule(self);
+        nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
         break;
     case NM_TERNARY_TRUE:
         self->priv.p->nacd_instance_ensure_retry =
@@ -1527,8 +1742,8 @@ _l3_acd_data_add(NML3Cfg *             self,
             .n_track_infos_alloc       = 0,
             .acd_event_notify_lst      = C_LIST_INIT(acd_data->acd_event_notify_lst),
             .probing_timestamp_msec    = 0,
-            .acd_defend_type_desired   = NM_L3_ACD_DEFEND_TYPE_NONE,
-            .acd_defend_type_current   = NM_L3_ACD_DEFEND_TYPE_NONE,
+            .acd_defend_type_desired   = _NM_L3_ACD_DEFEND_TYPE_NONE,
+            .acd_defend_type_current   = _NM_L3_ACD_DEFEND_TYPE_NONE,
             .acd_defend_type_is_active = FALSE,
         };
         c_list_link_tail(&self->priv.p->acd_lst_head, &acd_data->acd_lst);
@@ -1794,7 +2009,7 @@ _nm_printf(5, 6) static void _l3_acd_data_state_set_full(NML3Cfg *        self,
         /* The availability of an address just changed (and we are instructed to
          * trigger a new commit). Do it. */
         _l3_changed_configs_set_dirty(self);
-        nm_l3cfg_commit_on_idle_schedule(self);
+        nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
     }
 }
 
@@ -2464,7 +2679,7 @@ handle_start_defending:
                                       ACD_STATE_CHANGE_MODE_INIT_REAPPLY,
                                       ACD_STATE_CHANGE_MODE_POST_COMMIT));
 
-    nm_assert(acd_data->acd_defend_type_desired > NM_L3_ACD_DEFEND_TYPE_NONE);
+    nm_assert(acd_data->acd_defend_type_desired > _NM_L3_ACD_DEFEND_TYPE_NONE);
     nm_assert(acd_data->acd_defend_type_desired <= NM_L3_ACD_DEFEND_TYPE_ALWAYS);
 
     if (acd_data->acd_defend_type_desired != acd_data->acd_defend_type_current) {
@@ -2570,33 +2785,163 @@ nm_l3cfg_get_acd_addr_info(NML3Cfg *self, in_addr_t addr)
 
 /*****************************************************************************/
 
+gboolean
+nm_l3cfg_check_ready(NML3Cfg *              self,
+                     const NML3ConfigData * l3cd,
+                     int                    addr_family,
+                     NML3CfgCheckReadyFlags flags,
+                     gboolean *             acd_used)
+{
+    NMDedupMultiIter iter;
+    const NMPObject *obj;
+
+    nm_assert(NM_IS_L3CFG(self));
+    nm_assert_addr_family_or_unspec(addr_family);
+
+    NM_SET_OUT(acd_used, FALSE);
+
+    if (!l3cd)
+        return TRUE;
+
+    if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET)
+        && NM_FLAGS_HAS(flags, NM_L3CFG_CHECK_READY_FLAGS_IP4_ACD_READY)) {
+        gboolean pending = FALSE;
+
+        nm_l3_config_data_iter_obj_for_each (&iter, l3cd, &obj, NMP_OBJECT_TYPE_IP4_ADDRESS) {
+            const NML3AcdAddrInfo *addr_info;
+
+            addr_info = nm_l3cfg_get_acd_addr_info(self, NMP_OBJECT_CAST_IP4_ADDRESS(obj)->address);
+            if (!addr_info) {
+                /* We don't track the this address? That's odd. Not ready. */
+                pending = TRUE;
+            } else {
+                if (addr_info->state <= NM_L3_ACD_ADDR_STATE_PROBING) {
+                    /* Still probing. Not ready. */
+                    pending = TRUE;
+                } else if (addr_info->state == NM_L3_ACD_ADDR_STATE_USED) {
+                    NM_SET_OUT(acd_used, TRUE);
+                }
+            }
+            /* we only care that we don't have ACD still pending. Otherwise we are ready,
+             * including if we have no addr_info about this address or the address is in use. */
+        }
+        if (pending)
+            return FALSE;
+    }
+
+    if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET6)
+        && NM_FLAGS_HAS(flags, NM_L3CFG_CHECK_READY_FLAGS_IP6_DAD_READY)) {
+        nm_l3_config_data_iter_obj_for_each (&iter, l3cd, &obj, NMP_OBJECT_TYPE_IP6_ADDRESS) {
+            ObjStateData *obj_state;
+
+            obj_state = g_hash_table_lookup(self->priv.p->obj_state_hash, &obj);
+
+            if (!obj_state) {
+                /* Hm, we don't track this object? That is odd. Not ready. */
+                return FALSE;
+            }
+
+            if (!obj_state->os_nm_configured && !obj_state->os_plobj) {
+                /* We didn't (yet) configure this address and it also is not in platform.
+                 * Not ready. */
+                return FALSE;
+            }
+
+            if (obj_state->os_plobj
+                && NM_FLAGS_HAS(NMP_OBJECT_CAST_IP6_ADDRESS(obj_state->os_plobj)->n_ifa_flags,
+                                IFA_F_TENTATIVE)) {
+                /* The address is configured in kernel, but still tentative. Not ready. */
+                return FALSE;
+            }
+
+            /* This address is ready. Even if it is not (not anymore) configured in kernel (as
+             * indicated by obj_state->os_plobj). We apparently did configure it once, and
+             * it's no longer tentative. This address are good. */
+        }
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
 static gboolean
 _l3_commit_on_idle_cb(gpointer user_data)
 {
-    NML3Cfg *self = user_data;
+    _nm_unused gs_unref_object NML3Cfg *self_keep_alive = NULL;
+    NML3Cfg *                           self            = user_data;
+    NML3CfgCommitType                   commit_type;
+
+    commit_type = self->priv.p->commit_on_idle_type;
+
+    if (nm_clear_g_source_inst(&self->priv.p->commit_on_idle_source))
+        self_keep_alive = self;
+    else
+        nm_assert_not_reached();
 
-    nm_clear_g_source_inst(&self->priv.p->commit_on_idle_source);
+    self->priv.p->commit_on_idle_type = NM_L3_CFG_COMMIT_TYPE_AUTO;
 
-    _LOGT("commit on idle");
-    _l3_commit(self, NM_L3_CFG_COMMIT_TYPE_AUTO, TRUE);
+    _l3_commit(self, commit_type, TRUE);
     return G_SOURCE_REMOVE;
 }
 
+/* DOC(l3cfg:commit-type):
+ *
+ * Usually we don't want to call the synchronous nm_l3cfg_commit(), because
+ * that has side effects and might not be safe to do (depending on the current
+ * circumstances in which commit is called). The usually proper thing to do
+ * is schedule a commit on an idle handler. Use this function.
+ *
+ * During commit, the actually used commit-type (that is, the level of "how much"
+ * will be synced) is determined by users who register their desired commit
+ * type via nm_l3cfg_commit_type_register(), where always the "maxium" is used.
+ *
+ * nm_l3cfg_commit() and nm_l3cfg_commit_on_idle_schedule() also accept an additional
+ * commit_type argument. This acts like a one-shot registration.
+ */
 gboolean
-nm_l3cfg_commit_on_idle_schedule(NML3Cfg *self)
+nm_l3cfg_commit_on_idle_schedule(NML3Cfg *self, NML3CfgCommitType commit_type)
 {
+    char sbuf_commit_type[50];
+
     nm_assert(NM_IS_L3CFG(self));
+    nm_assert(NM_IN_SET(commit_type,
+                        NM_L3_CFG_COMMIT_TYPE_AUTO,
+                        NM_L3_CFG_COMMIT_TYPE_ASSUME,
+                        NM_L3_CFG_COMMIT_TYPE_UPDATE,
+                        NM_L3_CFG_COMMIT_TYPE_REAPPLY));
 
-    if (self->priv.p->commit_on_idle_source)
+    if (self->priv.p->commit_on_idle_source) {
+        if (self->priv.p->commit_on_idle_type < commit_type) {
+            /* For multiple calls, we collect the maximum "commit-type". */
+            _LOGT("commit on idle (scheduled) (update to %s)",
+                  _l3_cfg_commit_type_to_string(commit_type,
+                                                sbuf_commit_type,
+                                                sizeof(sbuf_commit_type)));
+            self->priv.p->commit_on_idle_type = commit_type;
+        }
         return FALSE;
+    }
+
+    _LOGT("commit on idle (scheduled) (%s)",
+          _l3_cfg_commit_type_to_string(commit_type, sbuf_commit_type, sizeof(sbuf_commit_type)));
+    self->priv.p->commit_on_idle_source = nm_g_idle_add_source(_l3_commit_on_idle_cb, self);
+    self->priv.p->commit_on_idle_type   = commit_type;
+
+    /* While we have an idle update scheduled, we need to keep the instance alive. */
+    g_object_ref(self);
 
-    _LOGT("commit on idle (scheduled)");
-    self->priv.p->commit_on_idle_source =
-        nm_g_idle_source_new(G_PRIORITY_DEFAULT, _l3_commit_on_idle_cb, self, NULL);
-    g_source_attach(self->priv.p->commit_on_idle_source, NULL);
     return TRUE;
 }
 
+gboolean
+nm_l3cfg_commit_on_idle_is_scheduled(NML3Cfg *self)
+{
+    nm_assert(NM_IS_L3CFG(self));
+
+    return !!(self->priv.p->commit_on_idle_source);
+}
+
 /*****************************************************************************/
 
 #define _l3_config_datas_at(l3_config_datas, idx) \
@@ -2695,8 +3040,11 @@ nm_l3cfg_add_config(NML3Cfg *             self,
                     guint32               default_route_metric_6,
                     guint32               default_route_penalty_4,
                     guint32               default_route_penalty_6,
+                    int                   default_dns_priority_4,
+                    int                   default_dns_priority_6,
                     NML3AcdDefendType     acd_defend_type,
                     guint32               acd_timeout_msec,
+                    NML3CfgConfigFlags    config_flags,
                     NML3ConfigMergeFlags  merge_flags)
 {
     L3ConfigData *l3_config_data;
@@ -2761,6 +3109,7 @@ nm_l3cfg_add_config(NML3Cfg *             self,
         *l3_config_data = (L3ConfigData){
             .tag_confdata              = tag,
             .l3cd                      = nm_l3_config_data_ref_and_seal(l3cd),
+            .config_flags              = config_flags,
             .merge_flags               = merge_flags,
             .default_route_table_4     = default_route_table_4,
             .default_route_table_6     = default_route_table_6,
@@ -2768,6 +3117,8 @@ nm_l3cfg_add_config(NML3Cfg *             self,
             .default_route_metric_6    = default_route_metric_6,
             .default_route_penalty_4   = default_route_penalty_4,
             .default_route_penalty_6   = default_route_penalty_6,
+            .default_dns_priority_4    = default_dns_priority_4,
+            .default_dns_priority_6    = default_dns_priority_6,
             .acd_defend_type_confdata  = acd_defend_type,
             .acd_timeout_msec_confdata = acd_timeout_msec,
             .priority_confdata         = priority,
@@ -2784,6 +3135,10 @@ nm_l3cfg_add_config(NML3Cfg *             self,
             l3_config_data->priority_confdata = priority;
             changed                           = TRUE;
         }
+        if (l3_config_data->config_flags != config_flags) {
+            l3_config_data->config_flags = config_flags;
+            changed                      = TRUE;
+        }
         if (l3_config_data->merge_flags != merge_flags) {
             l3_config_data->merge_flags = merge_flags;
             changed                     = TRUE;
@@ -2812,6 +3167,14 @@ nm_l3cfg_add_config(NML3Cfg *             self,
             l3_config_data->default_route_penalty_6 = default_route_penalty_6;
             changed                                 = TRUE;
         }
+        if (l3_config_data->default_dns_priority_4 != default_dns_priority_4) {
+            l3_config_data->default_dns_priority_4 = default_dns_priority_4;
+            changed                                = TRUE;
+        }
+        if (l3_config_data->default_dns_priority_6 != default_dns_priority_6) {
+            l3_config_data->default_dns_priority_6 = default_dns_priority_6;
+            changed                                = TRUE;
+        }
         if (l3_config_data->acd_defend_type_confdata != acd_defend_type) {
             l3_config_data->acd_defend_type_confdata = acd_defend_type;
             changed                                  = TRUE;
@@ -2879,17 +3242,23 @@ _l3cfg_remove_config(NML3Cfg *             self,
 }
 
 gboolean
-nm_l3cfg_remove_config(NML3Cfg *self, gconstpointer tag, const NML3ConfigData *ifcfg)
+nm_l3cfg_remove_config(NML3Cfg *self, gconstpointer tag, const NML3ConfigData *l3cd)
 {
-    nm_assert(ifcfg);
+    nm_assert(l3cd);
+
+    return _l3cfg_remove_config(self, tag, FALSE, l3cd);
+}
 
-    return _l3cfg_remove_config(self, tag, FALSE, ifcfg);
+gboolean
+nm_l3cfg_remove_config_all(NML3Cfg *self, gconstpointer tag)
+{
+    return _l3cfg_remove_config(self, tag, FALSE, NULL);
 }
 
 gboolean
-nm_l3cfg_remove_config_all(NML3Cfg *self, gconstpointer tag, gboolean only_dirty)
+nm_l3cfg_remove_config_all_dirty(NML3Cfg *self, gconstpointer tag)
 {
-    return _l3cfg_remove_config(self, tag, only_dirty, NULL);
+    return _l3cfg_remove_config(self, tag, TRUE, NULL);
 }
 
 /*****************************************************************************/
@@ -2897,13 +3266,15 @@ nm_l3cfg_remove_config_all(NML3Cfg *self, gconstpointer tag, gboolean only_dirty
 typedef struct {
     NML3Cfg *     self;
     gconstpointer tag;
+    bool          assume_config_once;
+    bool          to_commit;
 } L3ConfigMergeHookAddObjData;
 
 static gboolean
-_l3_hook_add_addr_cb(const NML3ConfigData *l3cd,
-                     const NMPObject *     obj,
-                     NMTernary *           out_ip4acd_not_ready,
-                     gpointer              user_data)
+_l3_hook_add_obj_cb(const NML3ConfigData *     l3cd,
+                    const NMPObject *          obj,
+                    NML3ConfigMergeHookResult *hook_result,
+                    gpointer                   user_data)
 {
     const L3ConfigMergeHookAddObjData *hook_data = user_data;
     NML3Cfg *                          self      = hook_data->self;
@@ -2911,40 +3282,68 @@ _l3_hook_add_addr_cb(const NML3ConfigData *l3cd,
     in_addr_t                          addr;
     gboolean                           acd_bad = FALSE;
 
-    nm_assert(out_ip4acd_not_ready && *out_ip4acd_not_ready == NM_TERNARY_DEFAULT);
+    nm_assert(obj);
+    nm_assert(hook_result);
+    nm_assert(hook_result->ip4acd_not_ready == NM_OPTION_BOOL_DEFAULT);
+    nm_assert(hook_result->assume_config_once == NM_OPTION_BOOL_DEFAULT);
 
-    if (NMP_OBJECT_GET_TYPE(obj) != NMP_OBJECT_TYPE_IP4_ADDRESS)
-        return TRUE;
+    hook_result->assume_config_once = hook_data->assume_config_once;
 
-    addr = NMP_OBJECT_CAST_IP4_ADDRESS(obj)->address;
+    switch (NMP_OBJECT_GET_TYPE(obj)) {
+    case NMP_OBJECT_TYPE_IP4_ADDRESS:
 
-    if (ACD_ADDR_SKIP(addr))
-        goto out;
+        addr = NMP_OBJECT_CAST_IP4_ADDRESS(obj)->address;
 
-    acd_data = _l3_acd_data_find(self, addr);
+        if (ACD_ADDR_SKIP(addr))
+            goto out_ip4_address;
 
-    if (!acd_data) {
-        /* we don't yet track an ACD state for this address. That can only
-         * happend during _l3cfg_update_combined_config() with !to_commit,
-         * where we didn't update the ACD state.
-         *
-         * This means, unless you actually commit, nm_l3cfg_get_combined_l3cd(self, get_commited = FALSE)
-         * won't consider IPv4 addresses ready, that have no known ACD state yet. */
-        nm_assert(self->priv.p->changed_configs_acd_state);
-        acd_bad = TRUE;
-        goto out;
-    }
+        acd_data = _l3_acd_data_find(self, addr);
 
-    nm_assert(
-        _acd_track_data_is_not_dirty(_acd_data_find_track(acd_data, l3cd, obj, hook_data->tag)));
-    if (!NM_IN_SET(acd_data->info.state,
-                   NM_L3_ACD_ADDR_STATE_READY,
-                   NM_L3_ACD_ADDR_STATE_DEFENDING))
-        acd_bad = TRUE;
+        if (!hook_data->to_commit) {
+            nm_assert(self->priv.p->changed_configs_acd_state);
+            /* We don't do an actual commit in _l3cfg_update_combined_config(). That means our acd-data
+             * is not up to date. Check whether we have no acd_data ready, and if not, consider the address
+             * as not ready. It cannot be ready until the next commit starts ACD. */
+            if (!acd_data) {
+                acd_bad = TRUE;
+                goto out_ip4_address;
+            }
+            nm_assert(({
+                NML3AcdAddrTrackInfo *_ti =
+                    _acd_data_find_track(acd_data, l3cd, obj, hook_data->tag);
 
-out:
-    *out_ip4acd_not_ready = acd_bad ? NM_TERNARY_TRUE : NM_TERNARY_FALSE;
-    return TRUE;
+                !_ti || _ti->_priv.acd_dirty_track;
+            }));
+        } else {
+            /* If we commit, we called _l3_acd_data_add_all(), thus our acd_data must be present
+             * and not dirty. */
+            nm_assert(({
+                NML3AcdAddrTrackInfo *_ti =
+                    _acd_data_find_track(acd_data, l3cd, obj, hook_data->tag);
+
+                _ti && !_ti->_priv.acd_dirty_track;
+            }));
+        }
+
+        if (!NM_IN_SET(acd_data->info.state,
+                       NM_L3_ACD_ADDR_STATE_READY,
+                       NM_L3_ACD_ADDR_STATE_DEFENDING)) {
+            acd_bad = TRUE;
+            goto out_ip4_address;
+        }
+
+out_ip4_address:
+        hook_result->ip4acd_not_ready = acd_bad ? NM_OPTION_BOOL_TRUE : NM_OPTION_BOOL_FALSE;
+        return TRUE;
+
+    default:
+        nm_assert_not_reached();
+        /* fall-through */
+    case NMP_OBJECT_TYPE_IP6_ADDRESS:
+    case NMP_OBJECT_TYPE_IP4_ROUTE:
+    case NMP_OBJECT_TYPE_IP6_ROUTE:
+        return TRUE;
+    }
 }
 
 static void
@@ -3007,29 +3406,54 @@ _l3cfg_update_combined_config(NML3Cfg *              self,
 
     if (l3_config_datas_len > 0) {
         L3ConfigMergeHookAddObjData hook_data = {
-            .self = self,
+            .self      = self,
+            .to_commit = to_commit,
         };
 
         l3cd = nm_l3_config_data_new(nm_platform_get_multi_idx(self->priv.platform),
-                                     self->priv.ifindex);
+                                     self->priv.ifindex,
+                                     NM_IP_CONFIG_SOURCE_UNKNOWN);
 
         for (i = 0; i < l3_config_datas_len; i++) {
             const L3ConfigData *l3cd_data = l3_config_datas_arr[i];
 
-            if (NM_FLAGS_HAS(l3cd_data->merge_flags, NM_L3_CONFIG_MERGE_FLAGS_ONLY_FOR_ACD))
+            if (NM_FLAGS_HAS(l3cd_data->config_flags, NM_L3CFG_CONFIG_FLAGS_ONLY_FOR_ACD))
                 continue;
 
             hook_data.tag = l3cd_data->tag_confdata;
+            hook_data.assume_config_once =
+                NM_FLAGS_HAS(l3cd_data->config_flags, NM_L3CFG_CONFIG_FLAGS_ASSUME_CONFIG_ONCE);
+
             nm_l3_config_data_merge(l3cd,
                                     l3cd_data->l3cd,
                                     l3cd_data->merge_flags,
                                     l3cd_data->default_route_table_x,
                                     l3cd_data->default_route_metric_x,
                                     l3cd_data->default_route_penalty_x,
-                                    _l3_hook_add_addr_cb,
+                                    l3cd_data->default_dns_priority_x,
+                                    _l3_hook_add_obj_cb,
                                     &hook_data);
         }
 
+        for (i = 0; i < l3_config_datas_len; i++) {
+            const L3ConfigData *l3cd_data = l3_config_datas_arr[i];
+            int                 IS_IPv4;
+
+            if (NM_FLAGS_HAS(l3cd_data->config_flags, NM_L3CFG_CONFIG_FLAGS_ONLY_FOR_ACD))
+                continue;
+
+            for (IS_IPv4 = 1; IS_IPv4 >= 0; IS_IPv4--) {
+                nm_l3_config_data_add_dependent_device_routes(
+                    l3cd,
+                    IS_IPv4 ? AF_INET : AF_INET6,
+                    l3cd_data->default_route_table_x[IS_IPv4],
+                    l3cd_data->default_route_metric_x[IS_IPv4],
+                    l3cd_data->l3cd);
+            }
+        }
+
+        nm_l3_config_data_add_dependent_onlink_routes(l3cd, AF_UNSPEC);
+
         nm_assert(l3cd);
         nm_assert(nm_l3_config_data_get_ifindex(l3cd) == self->priv.ifindex);
 
@@ -3060,6 +3484,8 @@ out:
             nm_l3_config_data_ref(self->priv.p->combined_l3cd_merged);
         commited_changed = TRUE;
 
+        _obj_states_update_all(self);
+
         _nm_l3cfg_emit_signal_notify_l3cd_changed(self,
                                                   l3cd_commited_old,
                                                   self->priv.p->combined_l3cd_commited,
@@ -3095,75 +3521,45 @@ out:
 
 /*****************************************************************************/
 
-typedef struct {
-    const NMPObject *obj;
-    gint64           timestamp_msec;
-    bool             dirty;
-} RoutesTemporaryNotAvailableData;
-
-static void
-_routes_temporary_not_available_data_free(gpointer user_data)
-{
-    RoutesTemporaryNotAvailableData *data = user_data;
-
-    nmp_object_unref(data->obj);
-    nm_g_slice_free(data);
-}
-
-#define ROUTES_TEMPORARY_NOT_AVAILABLE_MAX_AGE_MSEC ((gint64) 20000)
-
 static gboolean
 _routes_temporary_not_available_timeout(gpointer user_data)
 {
-    RoutesTemporaryNotAvailableData *data;
-    NML3Cfg *                        self = NM_L3CFG(user_data);
-    GHashTableIter                   iter;
-    gint64                           expiry_threshold_msec;
-    gboolean                         any_expired = FALSE;
-    gint64                           now_msec;
-    gint64                           oldest_msec;
+    NML3Cfg *     self = NM_L3CFG(user_data);
+    ObjStateData *obj_state;
+    gint64        now_msec;
+    gint64        expiry_msec;
 
-    self->priv.p->routes_temporary_not_available_id = 0;
+    nm_clear_g_source_inst(&self->priv.p->obj_state_temporary_not_available_timeout_source);
 
-    if (!self->priv.p->routes_temporary_not_available_hash)
-        return G_SOURCE_REMOVE;
+    obj_state = c_list_first_entry(&self->priv.p->obj_state_temporary_not_available_lst_head,
+                                   ObjStateData,
+                                   os_temporary_not_available_lst);
 
-    /* we check the timeouts again. That is, because we allow to remove
-     * entries from routes_temporary_not_available_hash, without rescheduling
-     * out timeouts. */
+    if (!obj_state)
+        return G_SOURCE_CONTINUE;
 
     now_msec = nm_utils_get_monotonic_timestamp_msec();
 
-    expiry_threshold_msec = now_msec - ROUTES_TEMPORARY_NOT_AVAILABLE_MAX_AGE_MSEC;
-    oldest_msec           = G_MAXINT64;
+    expiry_msec = obj_state->os_temporary_not_available_timestamp_msec
+                  + ROUTES_TEMPORARY_NOT_AVAILABLE_MAX_AGE_MSEC;
 
-    g_hash_table_iter_init(&iter, self->priv.p->routes_temporary_not_available_hash);
-    while (g_hash_table_iter_next(&iter, (gpointer *) &data, NULL)) {
-        if (data->timestamp_msec >= expiry_threshold_msec) {
-            any_expired = TRUE;
-            break;
-        }
-        if (data->timestamp_msec < oldest_msec)
-            oldest_msec = data->timestamp_msec;
-    }
-
-    if (any_expired) {
-        /* a route expired. We emit a signal, but we don't schedule it again. That will
-         * only happen if the user calls nm_l3cfg_commit() again. */
-        _nm_l3cfg_emit_signal_notify_simple(
-            self,
-            NM_L3_CONFIG_NOTIFY_TYPE_ROUTES_TEMPORARY_NOT_AVAILABLE_EXPIRED);
-        return G_SOURCE_REMOVE;
+    if (now_msec < expiry_msec) {
+        /* the timeout is not yet reached. Restart the timer... */
+        self->priv.p->obj_state_temporary_not_available_timeout_source =
+            nm_g_timeout_add_source(expiry_msec - now_msec,
+                                    _routes_temporary_not_available_timeout,
+                                    self);
+        return G_SOURCE_CONTINUE;
     }
 
-    if (oldest_msec != G_MAXINT64) {
-        /* we have a timeout still. Reschedule. */
-        self->priv.p->routes_temporary_not_available_id =
-            g_timeout_add(oldest_msec + ROUTES_TEMPORARY_NOT_AVAILABLE_MAX_AGE_MSEC - now_msec,
-                          _routes_temporary_not_available_timeout,
-                          self);
-    }
-    return G_SOURCE_REMOVE;
+    /* One (or several) routes expired. We emit a signal, but we don't schedule it again.
+     * We expect the callers to commit again, which will one last time try to configure
+     * the route. If that again fails, we detect the timeout, log a warning and don't
+     * track the object as not temporary-not-available anymore. */
+    _nm_l3cfg_emit_signal_notify_simple(
+        self,
+        NM_L3_CONFIG_NOTIFY_TYPE_ROUTES_TEMPORARY_NOT_AVAILABLE_EXPIRED);
+    return G_SOURCE_CONTINUE;
 }
 
 static gboolean
@@ -3171,14 +3567,15 @@ _routes_temporary_not_available_update(NML3Cfg *  self,
                                        int        addr_family,
                                        GPtrArray *routes_temporary_not_available_arr)
 {
-    RoutesTemporaryNotAvailableData *data;
-    GHashTableIter                   iter;
-    gint64                           oldest_msec;
-    gint64                           now_msec;
-    gboolean                         prune_all = FALSE;
-    gboolean                         success   = TRUE;
-    guint                            i;
-
+    ObjStateData *  obj_state;
+    ObjStateData *  obj_state_safe;
+    gint64          now_msec;
+    gboolean        prune_all = FALSE;
+    gboolean        success   = TRUE;
+    guint           i;
+    const NMPClass *klass;
+
+    klass    = nmp_class_from_type(NMP_OBJECT_TYPE_IP_ROUTE(NM_IS_IPv4(addr_family)));
     now_msec = nm_utils_get_monotonic_timestamp_msec();
 
     if (nm_g_ptr_array_len(routes_temporary_not_available_arr) <= 0) {
@@ -3186,36 +3583,45 @@ _routes_temporary_not_available_update(NML3Cfg *  self,
         goto out_prune;
     }
 
-    if (self->priv.p->routes_temporary_not_available_hash) {
-        g_hash_table_iter_init(&iter, self->priv.p->routes_temporary_not_available_hash);
-        while (g_hash_table_iter_next(&iter, (gpointer *) &data, NULL)) {
-            if (NMP_OBJECT_GET_ADDR_FAMILY(data->obj) == addr_family)
-                data->dirty = TRUE;
+    c_list_for_each_entry (obj_state,
+                           &self->priv.p->obj_state_temporary_not_available_lst_head,
+                           os_temporary_not_available_lst) {
+        if (NMP_OBJECT_GET_CLASS(obj_state->obj) == klass) {
+            nm_assert(obj_state->os_temporary_not_available_timestamp_msec > 0);
+            obj_state->os_tna_dirty = TRUE;
         }
-    } else {
-        self->priv.p->routes_temporary_not_available_hash =
-            g_hash_table_new_full(nmp_object_indirect_id_hash,
-                                  nmp_object_indirect_id_equal,
-                                  _routes_temporary_not_available_data_free,
-                                  NULL);
     }
 
     for (i = 0; i < routes_temporary_not_available_arr->len; i++) {
         const NMPObject *o = routes_temporary_not_available_arr->pdata[i];
-        char             sbuf[1024];
+        char             sbuf[sizeof(_nm_utils_to_string_buffer)];
 
         nm_assert(NMP_OBJECT_GET_TYPE(o) == NMP_OBJECT_TYPE_IP_ROUTE(NM_IS_IPv4(addr_family)));
 
-        data = g_hash_table_lookup(self->priv.p->routes_temporary_not_available_hash, &o);
+        obj_state = g_hash_table_lookup(self->priv.p->obj_state_hash, &o);
 
-        if (data) {
-            if (!data->dirty)
-                continue;
+        if (!obj_state) {
+            /* Hm? We don't track this object? Very odd, a bug? */
+            nm_assert_not_reached();
+            continue;
+        }
 
-            nm_assert(data->timestamp_msec > 0 && data->timestamp_msec <= now_msec);
+        if (obj_state->os_temporary_not_available_timestamp_msec > 0) {
+            nm_assert(obj_state->os_temporary_not_available_timestamp_msec > 0
+                      && obj_state->os_temporary_not_available_timestamp_msec <= now_msec);
 
-            if (now_msec > data->timestamp_msec + ROUTES_TEMPORARY_NOT_AVAILABLE_MAX_AGE_MSEC) {
-                /* timeout. Could not add this address. */
+            if (!obj_state->os_tna_dirty) {
+                /* Odd, this only can happen if routes_temporary_not_available_arr contains duplicates.
+                 * It should not. */
+                nm_assert_not_reached();
+                continue;
+            }
+
+            if (now_msec > obj_state->os_temporary_not_available_timestamp_msec
+                               + ROUTES_TEMPORARY_NOT_AVAILABLE_MAX_AGE_MSEC) {
+                /* Timeout. Could not add this address.
+                 *
+                 * For now, keep it obj_state->os_tna_dirty and prune it below. */
                 _LOGW("failure to add IPv%c route: %s",
                       nm_utils_addr_family_to_char(addr_family),
                       nmp_object_to_string(o, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
@@ -3223,7 +3629,7 @@ _routes_temporary_not_available_update(NML3Cfg *  self,
                 continue;
             }
 
-            data->dirty = FALSE;
+            obj_state->os_tna_dirty = FALSE;
             continue;
         }
 
@@ -3231,47 +3637,229 @@ _routes_temporary_not_available_update(NML3Cfg *  self,
               nm_utils_addr_family_to_char(addr_family),
               nmp_object_to_string(o, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
 
-        data  = g_slice_new(RoutesTemporaryNotAvailableData);
-        *data = (RoutesTemporaryNotAvailableData){
-            .obj            = nmp_object_ref(o),
-            .timestamp_msec = now_msec,
-            .dirty          = FALSE,
-        };
-        g_hash_table_add(self->priv.p->routes_temporary_not_available_hash, data);
+        obj_state->os_tna_dirty                              = FALSE;
+        obj_state->os_temporary_not_available_timestamp_msec = now_msec;
+        c_list_link_tail(&self->priv.p->obj_state_temporary_not_available_lst_head,
+                         &obj_state->os_temporary_not_available_lst);
     }
 
 out_prune:
-    oldest_msec = G_MAXINT64;
-
-    if (self->priv.p->routes_temporary_not_available_hash) {
-        g_hash_table_iter_init(&iter, self->priv.p->routes_temporary_not_available_hash);
-        while (g_hash_table_iter_next(&iter, (gpointer *) &data, NULL)) {
-            nm_assert(NMP_OBJECT_GET_ADDR_FAMILY(data->obj) == addr_family || !data->dirty);
-            if (!prune_all && !data->dirty) {
-                if (data->timestamp_msec < oldest_msec)
-                    oldest_msec = data->timestamp_msec;
-                continue;
+    c_list_for_each_entry_safe (obj_state,
+                                obj_state_safe,
+                                &self->priv.p->obj_state_temporary_not_available_lst_head,
+                                os_temporary_not_available_lst) {
+        if (prune_all || obj_state->os_tna_dirty) {
+            if (NMP_OBJECT_GET_CLASS(obj_state->obj) == klass) {
+                obj_state->os_temporary_not_available_timestamp_msec = 0;
+                c_list_unlink(&obj_state->os_temporary_not_available_lst);
             }
-            g_hash_table_iter_remove(&iter);
         }
-        if (oldest_msec != G_MAXINT64)
-            nm_clear_pointer(&self->priv.p->routes_temporary_not_available_hash,
-                             g_hash_table_unref);
     }
 
-    nm_clear_g_source(&self->priv.p->routes_temporary_not_available_id);
-    if (oldest_msec != G_MAXINT64) {
-        nm_assert(oldest_msec + ROUTES_TEMPORARY_NOT_AVAILABLE_MAX_AGE_MSEC < now_msec);
-        self->priv.p->routes_temporary_not_available_id =
-            g_timeout_add(oldest_msec + ROUTES_TEMPORARY_NOT_AVAILABLE_MAX_AGE_MSEC - now_msec,
-                          _routes_temporary_not_available_timeout,
-                          self);
+    nm_clear_g_source_inst(&self->priv.p->obj_state_temporary_not_available_timeout_source);
+
+    obj_state = c_list_first_entry(&self->priv.p->obj_state_temporary_not_available_lst_head,
+                                   ObjStateData,
+                                   os_temporary_not_available_lst);
+    if (obj_state) {
+        self->priv.p->obj_state_temporary_not_available_timeout_source =
+            nm_g_timeout_add_source((obj_state->os_temporary_not_available_timestamp_msec
+                                     + ROUTES_TEMPORARY_NOT_AVAILABLE_MAX_AGE_MSEC - now_msec),
+                                    _routes_temporary_not_available_timeout,
+                                    self);
     }
 
     return success;
 }
 
 /*****************************************************************************/
+static const char *
+ip6_privacy_to_str(NMSettingIP6ConfigPrivacy ip6_privacy)
+{
+    switch (ip6_privacy) {
+    case NM_SETTING_IP6_CONFIG_PRIVACY_DISABLED:
+        return "0";
+    case NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_PUBLIC_ADDR:
+        return "1";
+    case NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR:
+        return "2";
+    case NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN:
+        break;
+    }
+    return nm_assert_unreachable_val("0");
+}
+
+static void
+_l3_commit_ndisc_params(NML3Cfg *self, NML3CfgCommitType commit_type)
+{
+    const NML3ConfigData *l3cd;
+    gboolean              retrans_set   = FALSE;
+    gboolean              reachable_set = FALSE;
+    gboolean              hop_limit_set = FALSE;
+    guint32               reachable     = 0;
+    guint32               retrans       = 0;
+    int                   hop_limit     = 0;
+    const char *          ifname;
+
+    if (commit_type < NM_L3_CFG_COMMIT_TYPE_UPDATE) {
+        self->priv.p->ndisc_reachable_time_msec_set = FALSE;
+        self->priv.p->ndisc_retrans_timer_msec_set  = FALSE;
+        self->priv.p->ndisc_hop_limit_set           = FALSE;
+        return;
+    }
+
+    l3cd = self->priv.p->combined_l3cd_commited;
+    if (l3cd) {
+        reachable_set = nm_l3_config_data_get_ndisc_reachable_time_msec(l3cd, &reachable);
+        retrans_set   = nm_l3_config_data_get_ndisc_retrans_timer_msec(l3cd, &retrans);
+        hop_limit     = nm_l3_config_data_get_ndisc_hop_limit(l3cd, &hop_limit);
+    }
+    ifname = nm_l3cfg_get_ifname(self, TRUE);
+
+    if (reachable_set
+        && (!self->priv.p->ndisc_reachable_time_msec_set
+            || self->priv.p->ndisc_reachable_time_msec != reachable)) {
+        self->priv.p->ndisc_reachable_time_msec     = reachable;
+        self->priv.p->ndisc_reachable_time_msec_set = TRUE;
+        if (ifname) {
+            nm_platform_sysctl_ip_neigh_set_ipv6_reachable_time(self->priv.platform,
+                                                                ifname,
+                                                                reachable);
+        }
+    }
+
+    if (retrans_set
+        && (!self->priv.p->ndisc_retrans_timer_msec_set
+            || self->priv.p->ndisc_reachable_time_msec != retrans)) {
+        self->priv.p->ndisc_retrans_timer_msec     = retrans;
+        self->priv.p->ndisc_retrans_timer_msec_set = TRUE;
+        if (ifname) {
+            nm_platform_sysctl_ip_neigh_set_ipv6_retrans_time(self->priv.platform, ifname, retrans);
+        }
+    }
+
+    if (hop_limit_set
+        && (!self->priv.p->ndisc_hop_limit_set || self->priv.p->ndisc_hop_limit != hop_limit)) {
+        self->priv.p->ndisc_hop_limit     = hop_limit;
+        self->priv.p->ndisc_hop_limit_set = TRUE;
+        if (ifname) {
+            nm_platform_sysctl_ip_conf_set_ipv6_hop_limit_safe(self->priv.platform,
+                                                               ifname,
+                                                               hop_limit);
+        }
+    }
+
+    // FIXME: restore values if necessary
+}
+
+static void
+_l3_commit_ip6_privacy(NML3Cfg *self, NML3CfgCommitType commit_type)
+{
+    NMSettingIP6ConfigPrivacy ip6_privacy;
+    NMSettingIP6ConfigPrivacy ip6_privacy_set_before;
+    const char *              ifname;
+
+    if (commit_type < NM_L3_CFG_COMMIT_TYPE_UPDATE)
+        ip6_privacy = NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN;
+    else
+        ip6_privacy = nm_l3_config_data_get_ip6_privacy(self->priv.p->combined_l3cd_commited);
+
+    if (ip6_privacy == NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN) {
+        if (!self->priv.p->ip6_privacy_set) {
+            /* Nothing to set. But do we need to reset a previous value? */
+            return;
+        }
+        self->priv.p->ip6_privacy_set = FALSE;
+        ip6_privacy                   = self->priv.p->ip6_privacy_initial;
+        ifname                        = nm_l3cfg_get_ifname(self, TRUE);
+        _LOGT("commit-ip6-privacy: reset initial value %d (was %d)%s%s",
+              (int) ip6_privacy,
+              (int) self->priv.p->ip6_privacy_set_before,
+              NM_PRINT_FMT_QUOTED2(ifname, ", ifname ", ifname, " (skip, no interface)"));
+        if (ip6_privacy == NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN)
+            return;
+        if (!ifname)
+            return;
+        goto set;
+    }
+
+    nm_assert(NM_IN_SET(ip6_privacy,
+                        NM_SETTING_IP6_CONFIG_PRIVACY_DISABLED,
+                        NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_PUBLIC_ADDR,
+                        NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR));
+
+    if (self->priv.p->ip6_privacy_set && self->priv.p->ip6_privacy_set_before == ip6_privacy
+        && commit_type < NM_L3_CFG_COMMIT_TYPE_REAPPLY) {
+        /* Already set. We leave this alone except during reapply. */
+        return;
+    }
+
+    ip6_privacy_set_before               = self->priv.p->ip6_privacy_set_before;
+    self->priv.p->ip6_privacy_set_before = ip6_privacy;
+
+    if (!self->priv.p->ip6_privacy_set) {
+        gint64 s = G_MININT64;
+
+        self->priv.p->ip6_privacy_set = TRUE;
+        ifname                        = nm_l3cfg_get_ifname(self, TRUE);
+        if (ifname) {
+            s = nm_platform_sysctl_ip_conf_get_int_checked(self->priv.platform,
+                                                           AF_INET6,
+                                                           ifname,
+                                                           "use_tempaddr",
+                                                           10,
+                                                           G_MININT32,
+                                                           G_MAXINT32,
+                                                           G_MININT64);
+            if (s != G_MININT64)
+                s = NM_CLAMP(s, 0, 2);
+        }
+        switch (s) {
+        case 0:
+            self->priv.p->ip6_privacy_initial = NM_SETTING_IP6_CONFIG_PRIVACY_DISABLED;
+            break;
+        case 1:
+            self->priv.p->ip6_privacy_initial = NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_PUBLIC_ADDR;
+            break;
+        case 2:
+            self->priv.p->ip6_privacy_initial = NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR;
+            break;
+        default:
+            nm_assert_not_reached();
+            /* fall-through */
+        case G_MININT64:
+            self->priv.p->ip6_privacy_initial = NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN;
+            break;
+        }
+        _LOGT("commit-ip6-privacy: set value %d (initial value was %d)%s%s",
+              (int) ip6_privacy,
+              (int) self->priv.p->ip6_privacy_initial,
+              NM_PRINT_FMT_QUOTED2(ifname, ", ifname ", ifname, " (skip, no interface)"));
+        if (!ifname)
+            return;
+        /* The first time, we always set the value, and don't skip it based on what we
+         * read. */
+        goto set;
+    }
+
+    ifname = nm_l3cfg_get_ifname(self, TRUE);
+    _LOGT("commit-ip6-privacy: set value %d (after %d, initial value was %d)%s%s",
+          (int) ip6_privacy,
+          (int) ip6_privacy_set_before,
+          (int) self->priv.p->ip6_privacy_initial,
+          NM_PRINT_FMT_QUOTED2(ifname, ", ifname ", ifname, " (skip, no interface)"));
+    if (!ifname)
+        return;
+
+set:
+    nm_assert(ifname);
+    self->priv.p->ip6_privacy_set_before = ip6_privacy;
+    nm_platform_sysctl_ip_conf_set(self->priv.platform,
+                                   AF_INET6,
+                                   ifname,
+                                   "use_tempaddr",
+                                   ip6_privacy_to_str(ip6_privacy));
+}
 
 static gboolean
 _l3_commit_one(NML3Cfg *             self,
@@ -3303,58 +3891,35 @@ _l3_commit_one(NML3Cfg *             self,
           nm_utils_addr_family_to_char(addr_family),
           _l3_cfg_commit_type_to_string(commit_type, sbuf_commit_type, sizeof(sbuf_commit_type)));
 
-    if (changed_combined_l3cd) {
-        /* our combined configuration changed. We may track entries in externally_removed_objs_hash,
-         * which are not longer to be considered by our configuration. We need to forget about them. */
-        _l3cfg_externally_removed_objs_drop_unused(self);
-    }
-
-    if (commit_type == NM_L3_CFG_COMMIT_TYPE_ASSUME) {
-        /* we need to artificially pre-populate the externally remove hash. */
-        _l3cfg_externally_removed_objs_pickup(self, addr_family);
-    }
-
     if (self->priv.p->combined_l3cd_commited) {
-        GHashTable *                   externally_removed_objs_hash;
-        NMDedupMultiFcnSelectPredicate predicate;
-        const NMDedupMultiHeadEntry *  head_entry;
-
-        if (commit_type != NM_L3_CFG_COMMIT_TYPE_REAPPLY
-            && self->priv.p->externally_removed_objs_cnt_addresses_x[IS_IPv4] > 0) {
-            predicate                    = _l3cfg_externally_removed_objs_filter;
-            externally_removed_objs_hash = self->priv.p->externally_removed_objs_hash;
-        } else {
-            if (IS_IPv4)
-                predicate = _l3cfg_externally_removed_objs_filter;
-            else
-                predicate = NULL;
-            externally_removed_objs_hash = NULL;
-        }
+        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));
         addresses  = nm_dedup_multi_objs_to_ptr_array_head(head_entry,
-                                                          predicate,
-                                                          externally_removed_objs_hash);
+                                                          _obj_states_sync_filter,
+                                                          (gpointer) &sync_filter_data);
 
-        if (commit_type != NM_L3_CFG_COMMIT_TYPE_REAPPLY
-            && self->priv.p->externally_removed_objs_cnt_routes_x[IS_IPv4] > 0) {
-            predicate                    = _l3cfg_externally_removed_objs_filter;
-            externally_removed_objs_hash = self->priv.p->externally_removed_objs_hash;
-        } else {
-            predicate                    = NULL;
-            externally_removed_objs_hash = NULL;
-        }
         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,
-                                                       predicate,
-                                                       externally_removed_objs_hash);
+                                                       _obj_states_sync_filter,
+                                                       (gpointer) &sync_filter_data);
 
         route_table_sync =
             nm_l3_config_data_get_route_table_sync(self->priv.p->combined_l3cd_commited,
                                                    addr_family);
     }
 
+    if (!IS_IPv4) {
+        _l3_commit_ip6_privacy(self, commit_type);
+        _l3_commit_ndisc_params(self, commit_type);
+    }
+
     if (route_table_sync == NM_IP_ROUTE_TABLE_SYNC_MODE_NONE)
         route_table_sync = NM_IP_ROUTE_TABLE_SYNC_MODE_ALL;
 
@@ -3367,17 +3932,11 @@ _l3_commit_one(NML3Cfg *             self,
                                                            addr_family,
                                                            self->priv.ifindex,
                                                            route_table_sync);
-    } else if (commit_type == NM_L3_CFG_COMMIT_TYPE_UPDATE) {
-        addresses_prune = nm_g_ptr_array_ref(self->priv.p->last_addresses_x[IS_IPv4]);
-        routes_prune    = nm_g_ptr_array_ref(self->priv.p->last_routes_x[IS_IPv4]);
-    }
-
-    nm_g_ptr_array_set(&self->priv.p->last_addresses_x[IS_IPv4], addresses);
-    nm_g_ptr_array_set(&self->priv.p->last_routes_x[IS_IPv4], routes);
+        _obj_state_zombie_lst_prune_all(self, addr_family);
+    } else
+        _obj_state_zombie_lst_get_prune_lists(self, addr_family, &addresses_prune, &routes_prune);
 
-    /* FIXME(l3cfg): need to honor and set nm_l3_config_data_get_ip6_privacy(). */
     /* FIXME(l3cfg): need to honor and set nm_l3_config_data_get_ndisc_*(). */
-    /* FIXME(l3cfg): need to honor and set nm_l3_config_data_get_ip6_mtu(). */
     /* FIXME(l3cfg): need to honor and set nm_l3_config_data_get_mtu(). */
 
     nm_platform_ip_address_sync(self->priv.platform,
@@ -3409,8 +3968,11 @@ _l3_commit_one(NML3Cfg *             self,
 static void
 _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is_idle)
 {
-    nm_auto_unref_l3cd const NML3ConfigData *l3cd_old             = NULL;
-    gboolean                                 commit_type_detected = FALSE;
+    _nm_unused gs_unref_object NML3Cfg *self_keep_alive = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd_old   = NULL;
+    NML3CfgCommitType                        commit_type_auto;
+    gboolean                                 commit_type_from_auto = FALSE;
+    gboolean                                 is_sticky_update      = FALSE;
     char                                     sbuf_ct[30];
     gboolean                                 changed_combined_l3cd;
 
@@ -3423,44 +3985,45 @@ _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is_idle)
                         NM_L3_CFG_COMMIT_TYPE_REAPPLY));
     nm_assert(self->priv.p->commit_reentrant_count == 0);
 
-    switch (commit_type) {
-    case NM_L3_CFG_COMMIT_TYPE_AUTO:
-        /* if in "AUTO" mode we currently have commit-type "UPDATE", that
-         * causes also the following update to still be "UPDATE". Either
-         * the same commit */
-        commit_type_detected = TRUE;
-        commit_type          = nm_l3cfg_commit_type_get(self);
-        if (commit_type == NM_L3_CFG_COMMIT_TYPE_UPDATE)
-            self->priv.p->commit_type_update_sticky = TRUE;
-        else if (self->priv.p->commit_type_update_sticky) {
+    /* The actual commit type is always the maximum of what is requested
+     * and what is registered via nm_l3cfg_commit_type_register(), combined
+     * with the ad-hoc requested @commit_type argument. */
+    commit_type_auto = nm_l3cfg_commit_type_get(self);
+    if (commit_type == NM_L3_CFG_COMMIT_TYPE_AUTO || commit_type_auto > commit_type) {
+        commit_type_from_auto = TRUE;
+        commit_type           = commit_type_auto;
+    }
+
+    /* Levels UPDATE and higher are sticky. That means, when do perform such a commit
+     * type, then the next one will at least be of level "UPDATE". The idea is
+     * that if the current commit adds an address, then the following needs
+     * to do at least "UPDATE" level to remove it again. Even if in the meantime
+     * the "UPDATE" is unregistered (nm_l3cfg_commit_type_unregister()). */
+    if (commit_type < NM_L3_CFG_COMMIT_TYPE_UPDATE) {
+        if (self->priv.p->commit_type_update_sticky) {
             self->priv.p->commit_type_update_sticky = FALSE;
             commit_type                             = NM_L3_CFG_COMMIT_TYPE_UPDATE;
+            is_sticky_update                        = TRUE;
         }
-        break;
-    case NM_L3_CFG_COMMIT_TYPE_ASSUME:
-        break;
-    case NM_L3_CFG_COMMIT_TYPE_REAPPLY:
-    case NM_L3_CFG_COMMIT_TYPE_UPDATE:
-        self->priv.p->commit_type_update_sticky = FALSE;
-        break;
-    case NM_L3_CFG_COMMIT_TYPE_NONE:
-        break;
-    }
+    } else
+        self->priv.p->commit_type_update_sticky = TRUE;
 
-    _LOGT("commit %s%s%s",
+    _LOGT("commit %s%s%s%s",
           _l3_cfg_commit_type_to_string(commit_type, sbuf_ct, sizeof(sbuf_ct)),
-          commit_type_detected ? " (auto)" : "",
+          commit_type_from_auto ? " (auto)" : "",
+          is_sticky_update ? " (sticky-update)" : "",
           is_idle ? " (idle handler)" : "");
 
-    if (commit_type == NM_L3_CFG_COMMIT_TYPE_NONE)
-        return;
+    nm_assert(commit_type > NM_L3_CFG_COMMIT_TYPE_AUTO);
 
-    self->priv.p->commit_reentrant_count++;
+    if (nm_clear_g_source_inst(&self->priv.p->commit_on_idle_source))
+        self_keep_alive = self;
+    self->priv.p->commit_on_idle_type = NM_L3_CFG_COMMIT_TYPE_AUTO;
 
-    nm_clear_g_source_inst(&self->priv.p->commit_on_idle_source);
+    if (commit_type <= NM_L3_CFG_COMMIT_TYPE_NONE)
+        return;
 
-    if (commit_type == NM_L3_CFG_COMMIT_TYPE_REAPPLY)
-        _l3cfg_externally_removed_objs_drop(self);
+    self->priv.p->commit_reentrant_count++;
 
     _l3cfg_update_combined_config(self,
                                   TRUE,
@@ -3468,7 +4031,7 @@ _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is_idle)
                                   &l3cd_old,
                                   &changed_combined_l3cd);
 
-    /* FIXME(l3cfg): handle items currently not configurable in kernel. */
+    _nm_l3cfg_emit_signal_notify_simple(self, NM_L3_CONFIG_NOTIFY_TYPE_PRE_COMMIT);
 
     _l3_commit_one(self, AF_INET, commit_type, changed_combined_l3cd, l3cd_old);
     _l3_commit_one(self, AF_INET6, commit_type, changed_combined_l3cd, l3cd_old);
@@ -3481,6 +4044,7 @@ _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is_idle)
     _nm_l3cfg_emit_signal_notify_simple(self, NM_L3_CONFIG_NOTIFY_TYPE_POST_COMMIT);
 }
 
+/* See DOC(l3cfg:commit-type) */
 void
 nm_l3cfg_commit(NML3Cfg *self, NML3CfgCommitType commit_type)
 {
@@ -3508,29 +4072,39 @@ nm_l3cfg_commit_type_get(NML3Cfg *self)
  * @commit_type: the commit type to register
  * @existing_handle: instead of being a new registration, update an existing handle.
  *   This may be %NULL, which is like having no previous registration.
+ * @source: the source of the commit type, for logging.
  *
  * NML3Cfg needs to know whether it is in charge of an interface (and how "much").
  * By default, it is not in charge, but various users can register themself with
  * a certain @commit_type. The "higher" commit type is the used one when calling
  * nm_l3cfg_commit() with %NM_L3_CFG_COMMIT_TYPE_AUTO.
  *
- * Returns: a handle tracking the registration, or %NULL of @commit_type
+ * Returns: a handle tracking the registration, or %NULL if @commit_type
  *   is %NM_L3_CFG_COMMIT_TYPE_NONE.
  */
 NML3CfgCommitTypeHandle *
 nm_l3cfg_commit_type_register(NML3Cfg *                self,
                               NML3CfgCommitType        commit_type,
-                              NML3CfgCommitTypeHandle *existing_handle)
+                              NML3CfgCommitTypeHandle *existing_handle,
+                              const char *             source)
 {
     NML3CfgCommitTypeHandle *handle;
     NML3CfgCommitTypeHandle *h;
     gboolean                 linked;
+    NML3CfgCommitTypeHandle *ret = NULL;
+    char                     buf[64];
 
     nm_assert(NM_IS_L3CFG(self));
     nm_assert(NM_IN_SET(commit_type,
                         NM_L3_CFG_COMMIT_TYPE_NONE,
                         NM_L3_CFG_COMMIT_TYPE_ASSUME,
                         NM_L3_CFG_COMMIT_TYPE_UPDATE));
+
+    /* It would be easy (and maybe convenient) to allow that @existing_handle
+     * can currently be registered on another NML3Cfg instance. But then we couldn't
+     * do this assertion, and it seems error prone to allow arbitrary handles where
+     * we cannot check whether it is valid. So if @existing_handle is given, it
+     * must be tracked by @self (and only by @self). */
     nm_assert(
         !existing_handle
         || c_list_contains(&self->priv.p->commit_type_lst_head, &existing_handle->commit_type_lst));
@@ -3538,21 +4112,24 @@ nm_l3cfg_commit_type_register(NML3Cfg *                self,
     if (existing_handle) {
         if (commit_type == NM_L3_CFG_COMMIT_TYPE_NONE) {
             nm_l3cfg_commit_type_unregister(self, existing_handle);
-            return NULL;
+            goto out;
+        }
+        if (existing_handle->commit_type == commit_type) {
+            ret = existing_handle;
+            goto out;
         }
-        if (existing_handle->commit_type == commit_type)
-            return existing_handle;
         c_list_unlink_stale(&existing_handle->commit_type_lst);
         handle = existing_handle;
     } else {
         if (commit_type == NM_L3_CFG_COMMIT_TYPE_NONE)
-            return NULL;
-        handle              = g_slice_new(NML3CfgCommitTypeHandle);
-        handle->commit_type = commit_type;
+            goto out;
+        handle = g_slice_new(NML3CfgCommitTypeHandle);
         if (c_list_is_empty(&self->priv.p->commit_type_lst_head))
             g_object_ref(self);
     }
 
+    handle->commit_type = commit_type;
+
     linked = FALSE;
     c_list_for_each_entry (h, &self->priv.p->commit_type_lst_head, commit_type_lst) {
         if (handle->commit_type >= h->commit_type) {
@@ -3564,7 +4141,15 @@ nm_l3cfg_commit_type_register(NML3Cfg *                self,
     if (!linked)
         c_list_link_tail(&self->priv.p->commit_type_lst_head, &handle->commit_type_lst);
 
-    return handle;
+    ret = handle;
+out:
+    _LOGT("commit type register (type \"%s\", source \"%s\", existing " NM_HASH_OBFUSCATE_PTR_FMT
+          ") -> " NM_HASH_OBFUSCATE_PTR_FMT "",
+          _l3_cfg_commit_type_to_string(commit_type, buf, sizeof(buf)),
+          source,
+          NM_HASH_OBFUSCATE_PTR(existing_handle),
+          NM_HASH_OBFUSCATE_PTR(ret));
+    return ret;
 }
 
 void
@@ -3577,6 +4162,8 @@ nm_l3cfg_commit_type_unregister(NML3Cfg *self, NML3CfgCommitTypeHandle *handle)
 
     nm_assert(c_list_contains(&self->priv.p->commit_type_lst_head, &handle->commit_type_lst));
 
+    _LOGT("commit type unregister " NM_HASH_OBFUSCATE_PTR_FMT "", NM_HASH_OBFUSCATE_PTR(handle));
+
     c_list_unlink_stale(&handle->commit_type_lst);
     if (c_list_is_empty(&self->priv.p->commit_type_lst_head))
         g_object_unref(self);
@@ -3688,7 +4275,7 @@ _nm_l3cfg_unregister_ipv4ll(NML3Cfg *self)
 {
     nm_assert(NM_IS_L3CFG(self));
 
-    /* we don't own the refernce to "self->priv.p->ipv4ll", but
+    /* we don't own the reference to "self->priv.p->ipv4ll", but
      * when that instance gets destroyed, we get called back to
      * forget about it. Basically, it's like a weak pointer. */
 
@@ -3747,6 +4334,14 @@ nm_l3cfg_init(NML3Cfg *self)
     c_list_init(&self->priv.p->acd_lst_head);
     c_list_init(&self->priv.p->acd_event_notify_lst_head);
     c_list_init(&self->priv.p->commit_type_lst_head);
+    c_list_init(&self->priv.p->obj_state_lst_head);
+    c_list_init(&self->priv.p->obj_state_temporary_not_available_lst_head);
+    c_list_init(&self->priv.p->obj_state_zombie_lst_head);
+
+    self->priv.p->obj_state_hash = g_hash_table_new_full(nmp_object_indirect_id_hash,
+                                                         nmp_object_indirect_id_equal,
+                                                         _obj_state_data_free,
+                                                         NULL);
 }
 
 static void
@@ -3786,9 +4381,7 @@ finalize(GObject *object)
 
     nm_assert(c_list_is_empty(&self->priv.p->commit_type_lst_head));
 
-    nm_clear_g_source_inst(&self->priv.p->commit_on_idle_source);
-
-    nm_assert(nm_g_array_len(self->priv.p->property_emit_list) == 0u);
+    nm_assert(!self->priv.p->commit_on_idle_source);
 
     _l3_acd_data_prune(self, TRUE);
 
@@ -3801,15 +4394,12 @@ finalize(GObject *object)
     nm_clear_g_source_inst(&self->priv.p->nacd_source);
     nm_clear_g_source_inst(&self->priv.p->nacd_instance_ensure_retry);
 
-    nm_clear_pointer(&self->priv.p->last_addresses_4, g_ptr_array_unref);
-    nm_clear_pointer(&self->priv.p->last_addresses_6, g_ptr_array_unref);
-    nm_clear_pointer(&self->priv.p->last_routes_4, g_ptr_array_unref);
-    nm_clear_pointer(&self->priv.p->last_routes_6, g_ptr_array_unref);
-
-    nm_clear_g_source(&self->priv.p->routes_temporary_not_available_id);
-    nm_clear_pointer(&self->priv.p->routes_temporary_not_available_hash, g_hash_table_unref);
+    nm_clear_g_source_inst(&self->priv.p->obj_state_temporary_not_available_timeout_source);
 
-    nm_clear_pointer(&self->priv.p->externally_removed_objs_hash, g_hash_table_unref);
+    nm_clear_pointer(&self->priv.p->obj_state_hash, g_hash_table_destroy);
+    nm_assert(c_list_is_empty(&self->priv.p->obj_state_lst_head));
+    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));
 
     g_clear_object(&self->priv.netns);
     g_clear_object(&self->priv.platform);