summary refs log tree commit diff
path: root/src/core/nm-l3cfg.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/nm-l3cfg.c')
-rw-r--r--src/core/nm-l3cfg.c753
1 files changed, 544 insertions, 209 deletions
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index aa56c13f..9258decf 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -12,9 +12,11 @@
 #include "libnm-glib-aux/nm-time-utils.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nmp-object.h"
+#include "libnm-platform/nmp-route-manager.h"
 #include "nm-netns.h"
 #include "n-acd/src/n-acd.h"
 #include "nm-l3-ipv4ll.h"
+#include "nm-ip-config.h"
 
 /*****************************************************************************/
 
@@ -203,6 +205,7 @@ typedef struct {
     guint32           acd_timeout_msec_confdata;
     NML3AcdDefendType acd_defend_type_confdata : 3;
     bool              dirty_confdata : 1;
+    gboolean          force_commit_once : 1;
 } L3ConfigData;
 
 /*****************************************************************************/
@@ -240,12 +243,20 @@ typedef struct _NML3CfgPrivate {
 
     CList acd_event_notify_lst_head;
 
-    NAcd *   nacd;
+    NAcd    *nacd;
     GSource *nacd_source;
 
     GSource *nacd_event_down_source;
     gint64   nacd_event_down_ratelimited_until_msec;
 
+    union {
+        struct {
+            NMIPConfig *ipconfig_6;
+            NMIPConfig *ipconfig_4;
+        };
+        NMIPConfig *ipconfig_x[2];
+    };
+
     /* This is for rate-limiting the creation of nacd instance. */
     GSource *nacd_instance_ensure_retry;
 
@@ -332,17 +343,17 @@ static void _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is
 static void _nm_l3cfg_emit_signal_notify_acd_event_all(NML3Cfg *self);
 
 static gboolean _acd_has_valid_link(const NMPObject *obj,
-                                    const guint8 **  out_addr_bin,
-                                    gboolean *       out_acd_not_supported);
+                                    const guint8   **out_addr_bin,
+                                    gboolean        *out_acd_not_supported);
 
 static void
 _l3_acd_nacd_instance_reset(NML3Cfg *self, NMTernary start_timer, gboolean acd_data_notify);
 
-static void _l3_acd_data_state_change(NML3Cfg *          self,
-                                      AcdData *          acd_data,
+static void _l3_acd_data_state_change(NML3Cfg           *self,
+                                      AcdData           *acd_data,
                                       AcdStateChangeMode mode,
                                       const NMEtherAddr *sender,
-                                      gint64 *           p_now_msec);
+                                      gint64            *p_now_msec);
 
 static AcdData *_l3_acd_data_find(NML3Cfg *self, in_addr_t addr);
 
@@ -400,6 +411,92 @@ static NM_UTILS_LOOKUP_DEFINE(_l3_acd_addr_state_to_string,
                                                    "external-removed"),
                               NM_UTILS_LOOKUP_ITEM(NM_L3_ACD_ADDR_STATE_USED, "used"), );
 
+static gboolean
+_obj_is_route_nodev(const NMPObject *obj)
+{
+    gboolean has_ifindex;
+
+    nm_assert(obj);
+
+    has_ifindex = (NMP_OBJECT_CAST_OBJ_WITH_IFINDEX(obj)->ifindex > 0);
+
+    nm_assert(has_ifindex
+              == !(NM_IN_SET(NMP_OBJECT_GET_TYPE(obj),
+                             NMP_OBJECT_TYPE_IP4_ROUTE,
+                             NMP_OBJECT_TYPE_IP6_ROUTE)
+                   && nm_platform_route_type_is_nodev(nm_platform_route_type_uncoerce(
+                       NMP_OBJECT_CAST_IP_ROUTE(obj)->type_coerced))));
+
+    return !has_ifindex;
+}
+
+/*****************************************************************************/
+
+NMIPConfig *
+nm_l3cfg_ipconfig_get(NML3Cfg *self, int addr_family)
+{
+    g_return_val_if_fail(NM_IS_L3CFG(self), NULL);
+    nm_assert_addr_family(addr_family);
+
+    return self->priv.p->ipconfig_x[NM_IS_IPv4(addr_family)];
+}
+
+static void
+_ipconfig_toggle_notify(gpointer data, GObject *object, gboolean is_last_ref)
+{
+    NML3Cfg    *self     = NM_L3CFG(data);
+    NMIPConfig *ipconfig = NM_IP_CONFIG(object);
+
+    if (!is_last_ref) {
+        /* This happens while we take another ref below. Ignore the signal. */
+        nm_assert(!NM_IN_SET(ipconfig, self->priv.p->ipconfig_4, self->priv.p->ipconfig_6));
+        return;
+    }
+
+    if (ipconfig == self->priv.p->ipconfig_4)
+        self->priv.p->ipconfig_4 = NULL;
+    else {
+        nm_assert(ipconfig == self->priv.p->ipconfig_6);
+        self->priv.p->ipconfig_6 = NULL;
+    }
+
+    /* We take a second reference to keep the instance alive, while also removing the
+     * toggle ref. This will notify the function again, but we will ignore that. */
+    g_object_ref(ipconfig);
+
+    g_object_remove_toggle_ref(G_OBJECT(ipconfig), _ipconfig_toggle_notify, self);
+
+    /* pass on the reference, and unexport on idle. */
+    nm_ip_config_take_and_unexport_on_idle(g_steal_pointer(&ipconfig));
+}
+
+NMIPConfig *
+nm_l3cfg_ipconfig_acquire(NML3Cfg *self, int addr_family)
+{
+    NMIPConfig *ipconfig;
+
+    g_return_val_if_fail(NM_IS_L3CFG(self), NULL);
+    nm_assert_addr_family(addr_family);
+
+    ipconfig = self->priv.p->ipconfig_x[NM_IS_IPv4(addr_family)];
+
+    if (ipconfig)
+        return g_object_ref(ipconfig);
+
+    ipconfig = nm_ip_config_new(addr_family, self);
+
+    self->priv.p->ipconfig_x[NM_IS_IPv4(addr_family)] = ipconfig;
+
+    /* The ipconfig keeps self alive. We use a toggle reference
+     * to avoid a cycle. But we anyway wouldn't want a strong reference,
+     * because the user releases the instance by unrefing it, and we
+     * notice that via the weak reference. */
+    g_object_add_toggle_ref(G_OBJECT(ipconfig), _ipconfig_toggle_notify, self);
+
+    /* We keep the toggle reference, and return the other reference to the caller. */
+    return g_steal_pointer(&ipconfig);
+}
+
 /*****************************************************************************/
 
 gboolean
@@ -415,13 +512,13 @@ nm_l3cfg_is_vrf(const NML3Cfg *self)
 
 static const char *
 _l3_config_notify_data_to_string(const NML3ConfigNotifyData *notify_data,
-                                 char *                      sbuf,
+                                 char                       *sbuf,
                                  gsize                       sbuf_size)
 {
     char      sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
     char      sbuf100[100];
     char      sbufobf[NM_HASH_OBFUSCATE_PTR_STR_BUF_SIZE];
-    char *    s = sbuf;
+    char     *s = sbuf;
     gsize     l = sbuf_size;
     in_addr_t addr4;
 
@@ -509,7 +606,7 @@ _nm_l3cfg_emit_signal_notify_simple(NML3Cfg *self, NML3ConfigNotifyType notify_t
 }
 
 static void
-_nm_l3cfg_emit_signal_notify_l3cd_changed(NML3Cfg *             self,
+_nm_l3cfg_emit_signal_notify_l3cd_changed(NML3Cfg              *self,
                                           const NML3ConfigData *l3cd_old,
                                           const NML3ConfigData *l3cd_new,
                                           gboolean              commited)
@@ -538,7 +635,7 @@ _l3_changed_configs_set_dirty(NML3Cfg *self)
 /*****************************************************************************/
 
 static void
-_l3_acd_ipv4_addresses_on_link_update(NML3Cfg * self,
+_l3_acd_ipv4_addresses_on_link_update(NML3Cfg  *self,
                                       in_addr_t addr,
                                       gboolean  add /* or else remove */)
 {
@@ -596,7 +693,7 @@ static NAcdProbe *
 _nm_n_acd_data_probe_new(NML3Cfg *self, in_addr_t addr, guint32 timeout_msec, gpointer user_data)
 {
     nm_auto(n_acd_probe_config_freep) NAcdProbeConfig *probe_config = NULL;
-    NAcdProbe *                                        probe;
+    NAcdProbe                                         *probe;
     int                                                r;
 
     nm_assert(self);
@@ -628,7 +725,7 @@ _nm_n_acd_data_probe_new(NML3Cfg *self, in_addr_t addr, guint32 timeout_msec, gp
     G_STMT_START                                                                                  \
     {                                                                                             \
         if (NM_MORE_ASSERTS > 0) {                                                                \
-            const NML3Cfg *     _self      = (self);                                              \
+            const NML3Cfg      *_self      = (self);                                              \
             const ObjStateData *_obj_state = (obj_state);                                         \
                                                                                                   \
             nm_assert(_obj_state);                                                                \
@@ -866,7 +963,7 @@ _obj_states_update_all(NML3Cfg *self)
     for (i = 0; i < (int) G_N_ELEMENTS(obj_types); i++) {
         const NMPObjectType obj_type = obj_types[i];
         NMDedupMultiIter    o_iter;
-        const NMPObject *   obj;
+        const NMPObject    *obj;
 
         if (!self->priv.p->combined_l3cd_commited)
             continue;
@@ -875,6 +972,11 @@ _obj_states_update_all(NML3Cfg *self)
                                              self->priv.p->combined_l3cd_commited,
                                              &obj,
                                              obj_type) {
+            if (_obj_is_route_nodev(obj)) {
+                /* this is a nodev route. We don't track an obj-state for this. */
+                continue;
+            }
+
             obj_state = g_hash_table_lookup(self->priv.p->obj_state_hash, &obj);
             if (!obj_state) {
                 obj_state =
@@ -926,21 +1028,16 @@ _obj_states_update_all(NML3Cfg *self)
 }
 
 typedef struct {
-    NML3Cfg *         self;
+    NML3Cfg          *self;
     NML3CfgCommitType commit_type;
 } ObjStatesSyncFilterData;
 
 static gboolean
-_obj_states_sync_filter(/* const NMDedupMultiObj * */ gconstpointer o, gpointer user_data)
+_obj_states_sync_filter(NML3Cfg *self, const NMPObject *obj, NML3CfgCommitType commit_type)
 {
-    char                           sbuf[sizeof(_nm_utils_to_string_buffer)];
-    const NMPObject *              obj              = o;
-    const ObjStatesSyncFilterData *sync_filter_data = user_data;
-    NMPObjectType                  obj_type;
-    ObjStateData *                 obj_state;
-
-    nm_assert(sync_filter_data);
-    nm_assert(NM_IS_L3CFG(sync_filter_data->self));
+    char          sbuf[sizeof(_nm_utils_to_string_buffer)];
+    NMPObjectType obj_type;
+    ObjStateData *obj_state;
 
     obj_type = NMP_OBJECT_GET_TYPE(obj);
 
@@ -948,22 +1045,19 @@ _obj_states_sync_filter(/* const NMDedupMultiObj * */ gconstpointer o, gpointer
         && NMP_OBJECT_CAST_IP4_ADDRESS(obj)->a_acd_not_ready)
         return FALSE;
 
-    obj_state = g_hash_table_lookup(sync_filter_data->self->priv.p->obj_state_hash, &obj);
+    obj_state = g_hash_table_lookup(self->priv.p->obj_state_hash, &obj);
 
-    nm_assert_obj_state(sync_filter_data->self, obj_state);
+    nm_assert_obj_state(self, obj_state);
     nm_assert(obj_state->obj == obj);
     nm_assert(c_list_is_empty(&obj_state->os_zombie_lst));
 
     if (!obj_state->os_nm_configured) {
-        NML3Cfg *self;
-
-        if (sync_filter_data->commit_type == NM_L3_CFG_COMMIT_TYPE_ASSUME
+        if (commit_type == NM_L3_CFG_COMMIT_TYPE_ASSUME
             && !_obj_state_data_get_assume_config_once(obj_state))
             return FALSE;
 
         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;
@@ -975,14 +1069,80 @@ _obj_states_sync_filter(/* const NMDedupMultiObj * */ gconstpointer o, gpointer
         return TRUE;
     }
 
-    if (!obj_state->os_plobj && sync_filter_data->commit_type != NM_L3_CFG_COMMIT_TYPE_REAPPLY)
+    if (!obj_state->os_plobj && commit_type != NM_L3_CFG_COMMIT_TYPE_REAPPLY
+        && !nmp_object_get_force_commit(obj))
         return FALSE;
 
     return TRUE;
 }
 
+static gboolean
+_obj_states_sync_filter_predicate(gconstpointer o, gpointer user_data)
+{
+    const NMPObject               *obj              = o;
+    const ObjStatesSyncFilterData *sync_filter_data = user_data;
+
+    return _obj_states_sync_filter(sync_filter_data->self, obj, sync_filter_data->commit_type);
+}
+
+static GPtrArray *
+_commit_collect_addresses(NML3Cfg *self, int addr_family, NML3CfgCommitType commit_type)
+{
+    const int                     IS_IPv4 = NM_IS_IPv4(addr_family);
+    const NMDedupMultiHeadEntry  *head_entry;
+    const ObjStatesSyncFilterData sync_filter_data = {
+        .self        = self,
+        .commit_type = commit_type,
+    };
+
+    head_entry = nm_l3_config_data_lookup_objs(self->priv.p->combined_l3cd_commited,
+                                               NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4));
+    return nm_dedup_multi_objs_to_ptr_array_head(head_entry,
+                                                 _obj_states_sync_filter_predicate,
+                                                 (gpointer) &sync_filter_data);
+}
+
+static void
+_commit_collect_routes(NML3Cfg          *self,
+                       int               addr_family,
+                       NML3CfgCommitType commit_type,
+                       GPtrArray       **routes,
+                       GPtrArray       **routes_nodev)
+{
+    const int                    IS_IPv4 = NM_IS_IPv4(addr_family);
+    const NMDedupMultiHeadEntry *head_entry;
+    const NMDedupMultiEntry     *entry;
+
+    nm_assert(routes && !*routes);
+    nm_assert(routes_nodev && !*routes_nodev);
+
+    head_entry = nm_l3_config_data_lookup_objs(self->priv.p->combined_l3cd_commited,
+                                               NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4));
+
+    if (!head_entry)
+        return;
+
+    c_list_for_each_entry (entry, &head_entry->lst_entries_head, lst_entries) {
+        const NMPObject *obj = entry->obj;
+        GPtrArray      **r;
+
+        if (_obj_is_route_nodev(obj))
+            r = routes_nodev;
+        else {
+            if (!_obj_states_sync_filter(self, obj, commit_type))
+                continue;
+            r = routes;
+        }
+
+        if (!*r)
+            *r = g_ptr_array_new_full(head_entry->len, (GDestroyNotify) nm_dedup_multi_obj_unref);
+
+        g_ptr_array_add(*r, (gpointer) nmp_object_ref(obj));
+    }
+}
+
 static void
-_obj_state_zombie_lst_get_prune_lists(NML3Cfg *   self,
+_obj_state_zombie_lst_get_prune_lists(NML3Cfg    *self,
                                       int         addr_family,
                                       GPtrArray **out_addresses_prune,
                                       GPtrArray **out_routes_prune)
@@ -991,8 +1151,8 @@ _obj_state_zombie_lst_get_prune_lists(NML3Cfg *   self,
     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;
+    ObjStateData       *obj_state;
+    ObjStateData       *obj_state_safe;
 
     nm_assert(NM_IS_L3CFG(self));
     nm_assert(out_addresses_prune && !*out_addresses_prune);
@@ -1003,7 +1163,7 @@ _obj_state_zombie_lst_get_prune_lists(NML3Cfg *   self,
                                 &self->priv.p->obj_state_zombie_lst_head,
                                 os_zombie_lst) {
         NMPObjectType obj_type;
-        GPtrArray **  p_a;
+        GPtrArray   **p_a;
 
         nm_assert_obj_state(self, obj_state);
         nm_assert(obj_state->os_zombie_count > 0);
@@ -1073,16 +1233,16 @@ static void
 _load_link(NML3Cfg *self, gboolean initial)
 {
     nm_auto_nmpobj const NMPObject *obj_old = NULL;
-    const NMPObject *               obj;
-    const char *                    ifname;
-    const char *                    ifname_old;
+    const NMPObject                *obj;
+    const char                     *ifname;
+    const char                     *ifname_old;
     gboolean                        nacd_changed;
     gboolean                        nacd_new_valid;
     gboolean                        nacd_old_valid;
-    const guint8 *                  nacd_old_addr = NULL;
-    const guint8 *                  nacd_new_addr = NULL;
+    const guint8                   *nacd_old_addr = NULL;
+    const guint8                   *nacd_new_addr = NULL;
     gboolean                        nacd_link_now_up;
-    AcdData *                       acd_data;
+    AcdData                        *acd_data;
 
     if (initial) {
         obj = nm_platform_link_get_obj(self->priv.platform, self->priv.ifindex, TRUE);
@@ -1172,9 +1332,9 @@ _nm_l3cfg_notify_platform_change_on_idle(NML3Cfg *self, guint32 obj_type_flags)
 }
 
 void
-_nm_l3cfg_notify_platform_change(NML3Cfg *                  self,
+_nm_l3cfg_notify_platform_change(NML3Cfg                   *self,
                                  NMPlatformSignalChangeType change_type,
-                                 const NMPObject *          obj)
+                                 const NMPObject           *obj)
 {
     NML3ConfigNotifyData notify_data;
     NMPObjectType        obj_type;
@@ -1247,9 +1407,9 @@ _acd_data_free(AcdData *acd_data)
 }
 
 static guint
-_acd_data_collect_tracks_data(const AcdData *    acd_data,
+_acd_data_collect_tracks_data(const AcdData     *acd_data,
                               NMTernary          dirty_selector,
-                              guint32 *          out_best_acd_timeout_msec,
+                              guint32           *out_best_acd_timeout_msec,
                               NML3AcdDefendType *out_best_acd_defend_type)
 {
     NML3AcdDefendType best_acd_defend_type  = _NM_L3_ACD_DEFEND_TYPE_NONE;
@@ -1257,6 +1417,15 @@ _acd_data_collect_tracks_data(const AcdData *    acd_data,
     guint             n                     = 0;
     guint             i;
 
+    /* We do a simple search over all track-infos for the best, which determines
+     * our ACD state. That is, we prefer ACD disabled, and otherwise the
+     * shortest configured timeout.
+     *
+     * This linear search is probably fast enough, because we expect that each
+     * address/acd_data has few trackers.
+     * The alternative would be caching the best result, but that is more complicated,
+     * so not done. */
+
     for (i = 0; i < acd_data->info.n_track_infos; i++) {
         const NML3AcdAddrTrackInfo *acd_track = &acd_data->info.track_infos[i];
 
@@ -1280,9 +1449,9 @@ _acd_data_collect_tracks_data(const AcdData *    acd_data,
 }
 
 static NML3AcdAddrTrackInfo *
-_acd_data_find_track(const AcdData *       acd_data,
+_acd_data_find_track(const AcdData        *acd_data,
                      const NML3ConfigData *l3cd,
-                     const NMPObject *     obj,
+                     const NMPObject      *obj,
                      gconstpointer         tag)
 {
     guint i;
@@ -1301,11 +1470,11 @@ _acd_data_find_track(const AcdData *       acd_data,
 
 static gboolean
 _acd_has_valid_link(const NMPObject *obj,
-                    const guint8 **  out_addr_bin,
-                    gboolean *       out_acd_not_supported)
+                    const guint8   **out_addr_bin,
+                    gboolean        *out_acd_not_supported)
 {
     const NMPlatformLink *link;
-    const guint8 *        addr_bin;
+    const guint8         *addr_bin;
     gsize                 addr_len;
 
     if (!obj) {
@@ -1360,8 +1529,8 @@ _l3_acd_nacd_event(int fd, GIOCondition condition, gpointer user_data)
     while (TRUE) {
         NMEtherAddr        sender_addr_data;
         const NMEtherAddr *sender_addr;
-        AcdData *          acd_data;
-        NAcdEvent *        event;
+        AcdData           *acd_data;
+        NAcdEvent         *event;
 
         if (!self->priv.p->nacd) {
             /* In the loop we emit signals, where *anything* might happen.
@@ -1440,12 +1609,7 @@ _l3_acd_nacd_event(int fd, GIOCondition condition, gpointer user_data)
                       "in %u msec)",
                       timeout_msec);
                 self->priv.p->nacd_event_down_source =
-                    nm_g_timeout_source_new(timeout_msec,
-                                            G_PRIORITY_DEFAULT,
-                                            _l3_acd_nacd_event_down_timeout_cb,
-                                            self,
-                                            NULL);
-                g_source_attach(self->priv.p->nacd_event_down_source, NULL);
+                    nm_g_timeout_add_source(timeout_msec, _l3_acd_nacd_event_down_timeout_cb, self);
             }
             break;
         default:
@@ -1494,6 +1658,7 @@ _l3_acd_nacd_instance_reset(NML3Cfg *self, NMTernary start_timer, gboolean acd_d
     }
     nm_clear_g_source_inst(&self->priv.p->nacd_source);
     nm_clear_g_source_inst(&self->priv.p->nacd_instance_ensure_retry);
+    nm_clear_g_source_inst(&self->priv.p->nacd_event_down_source);
 
     if (c_list_is_empty(&self->priv.p->acd_lst_head))
         start_timer = NM_TERNARY_DEFAULT;
@@ -1505,12 +1670,9 @@ _l3_acd_nacd_instance_reset(NML3Cfg *self, NMTernary start_timer, gboolean acd_d
         break;
     case NM_TERNARY_TRUE:
         self->priv.p->nacd_instance_ensure_retry =
-            nm_g_timeout_source_new_seconds(ACD_ENSURE_RATELIMIT_MSEC / 1000u,
-                                            G_PRIORITY_DEFAULT,
+            nm_g_timeout_add_seconds_source(ACD_ENSURE_RATELIMIT_MSEC / 1000u,
                                             _l3_acd_nacd_instance_ensure_retry_cb,
-                                            self,
-                                            NULL);
-        g_source_attach(self->priv.p->nacd_instance_ensure_retry, NULL);
+                                            self);
         break;
     case NM_TERNARY_DEFAULT:
         break;
@@ -1534,8 +1696,8 @@ static NAcd *
 _l3_acd_nacd_instance_ensure(NML3Cfg *self, gboolean *out_acd_not_supported)
 {
     nm_auto(n_acd_config_freep) NAcdConfig *config = NULL;
-    nm_auto(n_acd_unrefp) NAcd *            nacd   = NULL;
-    const guint8 *                          addr_bin;
+    nm_auto(n_acd_unrefp) NAcd             *nacd   = NULL;
+    const guint8                           *addr_bin;
     gboolean                                acd_not_supported;
     gboolean                                valid;
     int                                     fd;
@@ -1599,11 +1761,11 @@ failed_create_acd:
 }
 
 static NAcdProbe *
-_l3_acd_nacd_instance_create_probe(NML3Cfg *    self,
+_l3_acd_nacd_instance_create_probe(NML3Cfg     *self,
                                    in_addr_t    addr,
                                    guint32      timeout_msec,
                                    gpointer     user_data,
-                                   gboolean *   out_acd_not_supported,
+                                   gboolean    *out_acd_not_supported,
                                    const char **out_failure_reason)
 {
     gboolean   acd_not_supported;
@@ -1699,17 +1861,17 @@ _l3_acd_data_defendconflict_warning_ratelimited(AcdData *acd_data, gint64 *p_now
 }
 
 static void
-_l3_acd_data_add(NML3Cfg *             self,
+_l3_acd_data_add(NML3Cfg              *self,
                  const NML3ConfigData *l3cd,
-                 const NMPObject *     obj,
+                 const NMPObject      *obj,
                  gconstpointer         tag,
                  NML3AcdDefendType     acd_defend_type,
                  guint32               acd_timeout_msec)
 {
     in_addr_t             addr = NMP_OBJECT_CAST_IP4_ADDRESS(obj)->address;
     NML3AcdAddrTrackInfo *acd_track;
-    AcdData *             acd_data;
-    const char *          track_mode;
+    AcdData              *acd_data;
+    const char           *track_mode;
     char                  sbuf100[100];
 
     if (ACD_ADDR_SKIP(addr))
@@ -1795,7 +1957,7 @@ _l3_acd_data_add(NML3Cfg *             self,
 }
 
 static void
-_l3_acd_data_add_all(NML3Cfg *                  self,
+_l3_acd_data_add_all(NML3Cfg                   *self,
                      const L3ConfigData *const *infos,
                      guint                      infos_len,
                      gboolean                   reapply)
@@ -1817,7 +1979,7 @@ _l3_acd_data_add_all(NML3Cfg *                  self,
     for (i_info = 0; i_info < infos_len; i_info++) {
         const L3ConfigData *info = infos[i_info];
         NMDedupMultiIter    iter;
-        const NMPObject *   obj;
+        const NMPObject    *obj;
 
         nm_l3_config_data_iter_obj_for_each (&iter, info->l3cd, &obj, NMP_OBJECT_TYPE_IP4_ADDRESS) {
             _l3_acd_data_add(self,
@@ -1859,14 +2021,24 @@ _l3_acd_data_timeout_cb(gpointer user_data)
 static void
 _l3_acd_data_timeout_schedule(AcdData *acd_data, gint64 timeout_msec)
 {
+    /* in _l3_acd_data_state_set_full() we clear the timer. At the same time,
+     * in _l3_acd_data_state_change(ACD_STATE_CHANGE_MODE_TIMEOUT) we only
+     * expect timeouts in certain states.
+     *
+     * That means, scheduling a timeout is only correct if we are in a certain
+     * state, which allows to handle timeouts. This assert checks for that to
+     * ensure we don't call a timeout in an unexpected state. */
+    nm_assert(NM_IN_SET(acd_data->info.state,
+                        NM_L3_ACD_ADDR_STATE_PROBING,
+                        NM_L3_ACD_ADDR_STATE_USED,
+                        NM_L3_ACD_ADDR_STATE_DEFENDING,
+                        NM_L3_ACD_ADDR_STATE_CONFLICT));
+
     nm_clear_g_source_inst(&acd_data->acd_data_timeout_source);
     acd_data->acd_data_timeout_source =
-        nm_g_timeout_source_new(NM_CLAMP((gint64) 0, timeout_msec, (gint64) G_MAXUINT),
-                                G_PRIORITY_DEFAULT,
+        nm_g_timeout_add_source(NM_CLAMP((gint64) 0, timeout_msec, (gint64) G_MAXUINT),
                                 _l3_acd_data_timeout_cb,
-                                acd_data,
-                                NULL);
-    g_source_attach(acd_data->acd_data_timeout_source, NULL);
+                                acd_data);
 }
 
 static void
@@ -1899,7 +2071,7 @@ _nm_l3cfg_emit_signal_notify_acd_event(NML3Cfg *self, AcdData *acd_data)
 {
     gs_free NML3AcdAddrTrackInfo *track_infos_clone = NULL;
     NML3ConfigNotifyData          notify_data;
-    NML3AcdAddrInfo *             info;
+    NML3AcdAddrInfo              *info;
     guint                         i;
 
     nm_assert(acd_data);
@@ -1908,7 +2080,7 @@ _nm_l3cfg_emit_signal_notify_acd_event(NML3Cfg *self, AcdData *acd_data)
 
     notify_data.notify_type = NM_L3_CONFIG_NOTIFY_TYPE_ACD_EVENT;
     notify_data.acd_event   = (typeof(notify_data.acd_event)){
-        .info = acd_data->info,
+          .info = acd_data->info,
     };
 
     /* we need to clone the track-data, because the callee is allowed to add/remove
@@ -1952,7 +2124,7 @@ static void
 _nm_l3cfg_emit_signal_notify_acd_event_all(NML3Cfg *self)
 {
     gs_unref_object NML3Cfg *self_keep_alive = NULL;
-    AcdData *                acd_data;
+    AcdData                 *acd_data;
 
     while ((acd_data = c_list_first_entry(&self->priv.p->acd_event_notify_lst_head,
                                           AcdData,
@@ -1964,11 +2136,11 @@ _nm_l3cfg_emit_signal_notify_acd_event_all(NML3Cfg *self)
     }
 }
 
-_nm_printf(5, 6) static void _l3_acd_data_state_set_full(NML3Cfg *        self,
-                                                         AcdData *        acd_data,
+_nm_printf(5, 6) static void _l3_acd_data_state_set_full(NML3Cfg         *self,
+                                                         AcdData         *acd_data,
                                                          NML3AcdAddrState state,
                                                          gboolean         allow_commit,
-                                                         const char *     format,
+                                                         const char      *format,
                                                          ...)
 {
     NML3AcdAddrState old_state;
@@ -1993,17 +2165,19 @@ _nm_printf(5, 6) static void _l3_acd_data_state_set_full(NML3Cfg *        self,
     else
         changed = FALSE;
 
-    if (format) {
-        gs_free char *msg = NULL;
-        va_list       args;
+    if (_LOGT_ENABLED()) {
+        if (format) {
+            gs_free char *msg = NULL;
+            va_list       args;
 
-        va_start(args, format);
-        msg = g_strdup_vprintf(format, args);
-        va_end(args);
+            va_start(args, format);
+            msg = g_strdup_vprintf(format, args);
+            va_end(args);
 
-        _LOGT_acd(acd_data, "set state to %s (%s)", _l3_acd_addr_state_to_string(state), msg);
-    } else
-        _LOGT_acd(acd_data, "set state to %s", _l3_acd_addr_state_to_string(state));
+            _LOGT_acd(acd_data, "set state to %s (%s)", _l3_acd_addr_state_to_string(state), msg);
+        } else
+            _LOGT_acd(acd_data, "set state to %s", _l3_acd_addr_state_to_string(state));
+    }
 
     if (changed && allow_commit) {
         /* The availability of an address just changed (and we are instructed to
@@ -2014,8 +2188,8 @@ _nm_printf(5, 6) static void _l3_acd_data_state_set_full(NML3Cfg *        self,
 }
 
 static void
-_l3_acd_data_state_set(NML3Cfg *        self,
-                       AcdData *        acd_data,
+_l3_acd_data_state_set(NML3Cfg         *self,
+                       AcdData         *acd_data,
                        NML3AcdAddrState state,
                        gboolean         allow_commit)
 {
@@ -2023,17 +2197,17 @@ _l3_acd_data_state_set(NML3Cfg *        self,
 }
 
 static void
-_l3_acd_data_state_change(NML3Cfg *          self,
-                          AcdData *          acd_data,
+_l3_acd_data_state_change(NML3Cfg           *self,
+                          AcdData           *acd_data,
                           AcdStateChangeMode state_change_mode,
                           const NMEtherAddr *sender_addr,
-                          gint64 *           p_now_msec)
+                          gint64            *p_now_msec)
 
 {
     guint32           acd_timeout_msec;
     NML3AcdDefendType acd_defend_type;
     gint64            now_msec;
-    const char *      log_reason;
+    const char       *log_reason;
     char              sbuf256[256];
     char              sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
 
@@ -2260,22 +2434,7 @@ handle_init:
                 /* we are already probing. There is nothing to do for this timeout. */
                 return;
             }
-
-            nm_utils_get_monotonic_timestamp_msec_cached(p_now_msec);
-
-            if (acd_data->probing_timestamp_msec + ACD_WAIT_PROBING_EXTRA_TIME_MSEC
-                    + ACD_WAIT_PROBING_EXTRA_TIME2_MSEC
-                >= (*p_now_msec)) {
-                /* hm. We failed to create a new probe too long. Something is really wrong
-                 * internally, but let's ignore the issue and assume the address is good. What
-                 * else would we do? Assume the address is USED? */
-                _LOGT_acd(acd_data,
-                          "probe-good (waiting for creating probe timed out. Assume good)");
-                goto handle_start_defending;
-            }
-
-            log_reason = "retry probing on timeout";
-            goto handle_start_probing;
+            /* fall-through */
 
         case NM_L3_ACD_ADDR_STATE_USED:
         case NM_L3_ACD_ADDR_STATE_CONFLICT:
@@ -2295,7 +2454,10 @@ handle_init:
             acd_data->acd_defend_type_desired = acd_defend_type;
 
             if (acd_timeout_msec <= 0) {
-                log_reason = "acd disabled by configuration (restart after previous conflict)";
+                if (acd_data->info.state == NM_L3_ACD_ADDR_STATE_PROBING)
+                    log_reason = "acd disabled by configuration (timeout during probing)";
+                else
+                    log_reason = "acd disabled by configuration (restart after previous conflict)";
                 goto handle_probing_done;
             }
 
@@ -2305,6 +2467,23 @@ handle_init:
             }
 
             nm_utils_get_monotonic_timestamp_msec_cached(p_now_msec);
+
+            if (acd_data->info.state == NM_L3_ACD_ADDR_STATE_PROBING) {
+                if ((*p_now_msec) > acd_data->probing_timestamp_msec
+                                        + ACD_WAIT_PROBING_EXTRA_TIME_MSEC
+                                        + ACD_WAIT_PROBING_EXTRA_TIME2_MSEC) {
+                    /* hm. We failed to create a new probe too long. Something is really wrong
+                     * internally, but let's ignore the issue and assume the address is good. What
+                     * else would we do? Assume the address is USED? */
+                    _LOGT_acd(acd_data,
+                              "probe-good (waiting for creating probe timed out. Assume good)");
+                    goto handle_start_defending;
+                }
+
+                log_reason = "retry probing on timeout";
+                goto handle_start_probing;
+            }
+
             acd_data->probing_timestamp_msec = (*p_now_msec);
             acd_data->probing_timeout_msec   = acd_timeout_msec;
             if (acd_data->info.state == NM_L3_ACD_ADDR_STATE_USED)
@@ -2525,40 +2704,23 @@ handle_init:
         return;
 
     case ACD_STATE_CHANGE_MODE_INSTANCE_RESET:
+        nm_assert(NM_IN_SET(acd_data->info.state,
+                            NM_L3_ACD_ADDR_STATE_PROBING,
+                            NM_L3_ACD_ADDR_STATE_DEFENDING,
+                            NM_L3_ACD_ADDR_STATE_READY,
+                            NM_L3_ACD_ADDR_STATE_USED,
+                            NM_L3_ACD_ADDR_STATE_CONFLICT,
+                            NM_L3_ACD_ADDR_STATE_EXTERNAL_REMOVED));
 
-        switch (acd_data->info.state) {
-        case NM_L3_ACD_ADDR_STATE_INIT:
-            nm_assert_not_reached();
-            return;
-        case NM_L3_ACD_ADDR_STATE_PROBING:
-        case NM_L3_ACD_ADDR_STATE_DEFENDING:
-
-            if (!acd_data->nacd_probe) {
-                /* we failed starting to probe before and have a timer running to
-                 * restart. We don't do anything now, but let the timer handle it.
-                 * This also implements some rate limiting for us. */
-                _LOGT_acd(acd_data,
-                          "n-acd instance reset. Ignore event while restarting %s",
-                          (acd_data->info.state == NM_L3_ACD_ADDR_STATE_PROBING) ? "probing"
-                                                                                 : "defending");
-                return;
-            }
-
-            _LOGT_acd(acd_data,
-                      "n-acd instance reset. Trigger a restart of the %s",
-                      (acd_data->info.state == NM_L3_ACD_ADDR_STATE_PROBING) ? "probing"
-                                                                             : "defending");
-            acd_data->nacd_probe = n_acd_probe_free(acd_data->nacd_probe);
-            _l3_acd_data_timeout_schedule(acd_data, 0);
-            return;
-        case NM_L3_ACD_ADDR_STATE_READY:
-        case NM_L3_ACD_ADDR_STATE_USED:
-        case NM_L3_ACD_ADDR_STATE_CONFLICT:
-        case NM_L3_ACD_ADDR_STATE_EXTERNAL_REMOVED:
-            nm_assert(!acd_data->nacd_probe);
-            return;
-        }
-        nm_assert_not_reached();
+        /* an instance-reset is a dramatic event. We start over with probing. */
+        _LOGT_acd(acd_data,
+                  "n-acd instance reset. Reset to probing while in state %s",
+                  _l3_acd_addr_state_to_string(acd_data->info.state));
+        acd_data->nacd_probe                         = n_acd_probe_free(acd_data->nacd_probe);
+        acd_data->last_defendconflict_timestamp_msec = 0;
+        acd_data->probing_timestamp_msec = nm_utils_get_monotonic_timestamp_msec_cached(p_now_msec);
+        _l3_acd_data_state_set(self, acd_data, NM_L3_ACD_ADDR_STATE_PROBING, FALSE);
+        _l3_acd_data_timeout_schedule(acd_data, 0);
         return;
     }
 
@@ -2569,7 +2731,7 @@ handle_start_probing:
     if (TRUE) {
         const NML3AcdAddrState                orig_state = acd_data->info.state;
         nm_auto(n_acd_probe_freep) NAcdProbe *probe      = NULL;
-        const char *                          failure_reason;
+        const char                           *failure_reason;
         gboolean                              acd_not_supported;
 
         nm_assert(NM_IN_SET(acd_data->info.state,
@@ -2646,10 +2808,10 @@ handle_probing_done:
         goto handle_start_defending;
     case NM_L3_ACD_ADDR_STATE_READY:
     case NM_L3_ACD_ADDR_STATE_DEFENDING:
+    case NM_L3_ACD_ADDR_STATE_EXTERNAL_REMOVED:
         goto handle_start_defending;
     case NM_L3_ACD_ADDR_STATE_CONFLICT:
         return;
-    case NM_L3_ACD_ADDR_STATE_EXTERNAL_REMOVED:
         nm_assert_not_reached();
         return;
     }
@@ -2689,7 +2851,7 @@ handle_start_defending:
 
     if (!acd_data->nacd_probe) {
         const char *failure_reason;
-        NAcdProbe * probe;
+        NAcdProbe  *probe;
 
         if (acd_data->acd_data_timeout_source) {
             /* we already failed to create a probe. We are ratelimited to retry, but
@@ -2786,11 +2948,11 @@ nm_l3cfg_get_acd_addr_info(NML3Cfg *self, in_addr_t addr)
 /*****************************************************************************/
 
 gboolean
-nm_l3cfg_check_ready(NML3Cfg *              self,
-                     const NML3ConfigData * l3cd,
+nm_l3cfg_check_ready(NML3Cfg               *self,
+                     const NML3ConfigData  *l3cd,
                      int                    addr_family,
                      NML3CfgCheckReadyFlags flags,
-                     gboolean *             acd_used)
+                     gboolean              *acd_used)
 {
     NMDedupMultiIter iter;
     const NMPObject *obj;
@@ -2869,7 +3031,7 @@ static gboolean
 _l3_commit_on_idle_cb(gpointer user_data)
 {
     _nm_unused gs_unref_object NML3Cfg *self_keep_alive = NULL;
-    NML3Cfg *                           self            = user_data;
+    NML3Cfg                            *self            = user_data;
     NML3CfgCommitType                   commit_type;
 
     commit_type = self->priv.p->commit_on_idle_type;
@@ -2948,7 +3110,7 @@ nm_l3cfg_commit_on_idle_is_scheduled(NML3Cfg *self)
     (&g_array_index((l3_config_datas), L3ConfigData, (idx)))
 
 static gssize
-_l3_config_datas_find_next(GArray *              l3_config_datas,
+_l3_config_datas_find_next(GArray               *l3_config_datas,
                            guint                 start_idx,
                            gconstpointer         needle_tag,
                            const NML3ConfigData *needle_l3cd)
@@ -2978,9 +3140,9 @@ _l3_config_datas_get_sorted_cmp(gconstpointer p_a, gconstpointer p_b, gpointer u
     nm_assert(b);
     nm_assert(nm_l3_config_data_get_ifindex(a->l3cd) == nm_l3_config_data_get_ifindex(b->l3cd));
 
-    /* we sort the entries with higher priority (more important, lower numerical value)
+    /* we sort the entries with higher priority (higher numerical value, more important)
      * first. */
-    NM_CMP_FIELD(a, b, priority_confdata);
+    NM_CMP_FIELD(b, a, priority_confdata);
 
     /* if the priority is not unique, we sort them in the order they were added,
      * with the oldest first (lower numerical value). */
@@ -3028,8 +3190,16 @@ nm_l3cfg_mark_config_dirty(NML3Cfg *self, gconstpointer tag, gboolean dirty)
     }
 }
 
+/*
+ * nm_l3cfg_add_config:
+ * @priority: all l3cd get merged/combined. This merging requires that some
+ *   l3cd are more important than others. For example, coming from static IP
+ *   configuration needs to take precedence over DHCP. The @priority determines
+ *   the order in which l3cds get merged (and thus the outcome). Higher numbers
+ *   mean more important!!
+ */
 gboolean
-nm_l3cfg_add_config(NML3Cfg *             self,
+nm_l3cfg_add_config(NML3Cfg              *self,
                     gconstpointer         tag,
                     gboolean              replace_same_tag,
                     const NML3ConfigData *l3cd,
@@ -3123,7 +3293,8 @@ nm_l3cfg_add_config(NML3Cfg *             self,
             .acd_timeout_msec_confdata = acd_timeout_msec,
             .priority_confdata         = priority,
             .pseudo_timestamp_confdata = ++self->priv.p->pseudo_timestamp_counter,
-            .dirty_confdata            = FALSE,
+            .force_commit_once = NM_FLAGS_HAS(config_flags, NM_L3CFG_CONFIG_FLAGS_FORCE_ONCE),
+            .dirty_confdata    = FALSE,
         };
         changed = TRUE;
     } else {
@@ -3194,7 +3365,7 @@ nm_l3cfg_add_config(NML3Cfg *             self,
 }
 
 static gboolean
-_l3cfg_remove_config(NML3Cfg *             self,
+_l3cfg_remove_config(NML3Cfg              *self,
                      gconstpointer         tag,
                      gboolean              only_dirty,
                      const NML3ConfigData *l3cd)
@@ -3263,22 +3434,70 @@ nm_l3cfg_remove_config_all_dirty(NML3Cfg *self, gconstpointer tag)
 
 /*****************************************************************************/
 
+#define _NODEV_ROUTES_TAG(self, IS_IPv4) ((gconstpointer) (&(&(self)->priv.route_manager)[IS_IPv4]))
+
+static gboolean
+_nodev_routes_untrack(NML3Cfg *self, int addr_family)
+{
+    return nmp_route_manager_untrack_all(self->priv.route_manager,
+                                         _NODEV_ROUTES_TAG(self, NM_IS_IPv4(addr_family)),
+                                         FALSE,
+                                         TRUE);
+}
+
+static void
+_nodev_routes_sync(NML3Cfg          *self,
+                   int               addr_family,
+                   NML3CfgCommitType commit_type,
+                   GPtrArray        *routes_nodev)
+{
+    const int           IS_IPv4  = NM_IS_IPv4(addr_family);
+    const NMPObjectType obj_type = NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4);
+    guint               i;
+    gboolean            changed = FALSE;
+
+    if (!routes_nodev)
+        goto out_clear;
+
+    for (i = 0; i < routes_nodev->len; i++) {
+        const NMPObject *obj = routes_nodev->pdata[i];
+
+        if (nmp_route_manager_track(self->priv.route_manager,
+                                    obj_type,
+                                    NMP_OBJECT_CAST_IP_ROUTE(obj),
+                                    1,
+                                    _NODEV_ROUTES_TAG(self, IS_IPv4),
+                                    NULL))
+            changed = TRUE;
+    }
+
+out_clear:
+    if (_nodev_routes_untrack(self, addr_family))
+        changed = TRUE;
+
+    if (changed || commit_type >= NM_L3_CFG_COMMIT_TYPE_REAPPLY)
+        nmp_route_manager_sync(self->priv.route_manager, NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4), FALSE);
+}
+
+/*****************************************************************************/
+
 typedef struct {
-    NML3Cfg *     self;
+    NML3Cfg      *self;
     gconstpointer tag;
     bool          assume_config_once;
     bool          to_commit;
+    bool          force_commit_once;
 } L3ConfigMergeHookAddObjData;
 
 static gboolean
-_l3_hook_add_obj_cb(const NML3ConfigData *     l3cd,
-                    const NMPObject *          obj,
+_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;
-    AcdData *                          acd_data;
+    NML3Cfg                           *self      = hook_data->self;
+    AcdData                           *acd_data;
     in_addr_t                          addr;
     gboolean                           acd_bad = FALSE;
 
@@ -3286,8 +3505,10 @@ _l3_hook_add_obj_cb(const NML3ConfigData *     l3cd,
     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);
+    nm_assert(hook_result->force_commit == NM_OPTION_BOOL_DEFAULT);
 
     hook_result->assume_config_once = hook_data->assume_config_once;
+    hook_result->force_commit       = hook_data->force_commit_once;
 
     switch (NMP_OBJECT_GET_TYPE(obj)) {
     case NMP_OBJECT_TYPE_IP4_ADDRESS:
@@ -3327,7 +3548,8 @@ _l3_hook_add_obj_cb(const NML3ConfigData *     l3cd,
 
         if (!NM_IN_SET(acd_data->info.state,
                        NM_L3_ACD_ADDR_STATE_READY,
-                       NM_L3_ACD_ADDR_STATE_DEFENDING)) {
+                       NM_L3_ACD_ADDR_STATE_DEFENDING,
+                       NM_L3_ACD_ADDR_STATE_EXTERNAL_REMOVED)) {
             acd_bad = TRUE;
             goto out_ip4_address;
         }
@@ -3347,21 +3569,21 @@ out_ip4_address:
 }
 
 static void
-_l3cfg_update_combined_config(NML3Cfg *              self,
+_l3cfg_update_combined_config(NML3Cfg               *self,
                               gboolean               to_commit,
                               gboolean               reapply,
                               const NML3ConfigData **out_old /* transfer reference */,
-                              gboolean *             out_changed_combined_l3cd)
+                              gboolean              *out_changed_combined_l3cd)
 {
-    nm_auto_unref_l3cd const NML3ConfigData *l3cd_commited_old = NULL;
-    nm_auto_unref_l3cd const NML3ConfigData *l3cd_old          = NULL;
-    nm_auto_unref_l3cd_init NML3ConfigData *l3cd               = NULL;
-    gs_free const L3ConfigData **l3_config_datas_free          = NULL;
-    const L3ConfigData **        l3_config_datas_arr;
-    guint                        l3_config_datas_len;
-    guint                        i;
-    gboolean                     merged_changed   = FALSE;
-    gboolean                     commited_changed = FALSE;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd_commited_old    = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd_old             = NULL;
+    nm_auto_unref_l3cd_init NML3ConfigData  *l3cd                 = NULL;
+    gs_free const L3ConfigData             **l3_config_datas_free = NULL;
+    const L3ConfigData                     **l3_config_datas_arr;
+    guint                                    l3_config_datas_len;
+    guint                                    i;
+    gboolean                                 merged_changed   = FALSE;
+    gboolean                                 commited_changed = FALSE;
 
     nm_assert(NM_IS_L3CFG(self));
     nm_assert(!out_old || !*out_old);
@@ -3388,6 +3610,20 @@ _l3cfg_update_combined_config(NML3Cfg *              self,
         l3_config_datas_arr[i] = _l3_config_datas_at(self->priv.p->l3_config_datas, i);
 
     if (l3_config_datas_len > 1) {
+        /* We are about to merge the l3cds. The order in which we do that matters.
+         *
+         * Below, we iterate over the l3cds and merge them into a new one. nm_l3_config_data_merge()
+         * uses "NM_L3_CONFIG_ADD_FLAGS_EXCLUSIVE" flag, which means to keep the first entry.
+         *
+         * Consider for example addresses/routes, which have a set of ID attributes  (based on
+         * which no duplicates can be accepted) and additional attributes. For example, trying
+         * to add the same address twice ("same" according to their ID), only one can be added.
+         * If they differ in their lifetimes, we need to make a choice.
+         * We could merge the attributes in a sensible way. Instead, NM_L3_CONFIG_ADD_FLAGS_EXCLUSIVE
+         * takes care to only take the first one.
+         *
+         * So we want to sort the more important entries *first*, and this is based on
+         * the priority_confdata.  */
         g_qsort_with_data(l3_config_datas_arr,
                           l3_config_datas_len,
                           sizeof(l3_config_datas_arr[0]),
@@ -3417,12 +3653,21 @@ _l3cfg_update_combined_config(NML3Cfg *              self,
         for (i = 0; i < l3_config_datas_len; i++) {
             const L3ConfigData *l3cd_data = l3_config_datas_arr[i];
 
+            /* more important entries must be sorted *first*. */
+            nm_assert(
+                i == 0
+                || (l3_config_datas_arr[i - 1]->priority_confdata > l3cd_data->priority_confdata)
+                || (l3_config_datas_arr[i - 1]->priority_confdata == l3cd_data->priority_confdata
+                    && l3_config_datas_arr[i - 1]->pseudo_timestamp_confdata
+                           < l3cd_data->pseudo_timestamp_confdata));
+
             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);
+            hook_data.force_commit_once = l3cd_data->force_commit_once;
 
             nm_l3_config_data_merge(l3cd,
                                     l3cd_data->l3cd,
@@ -3448,6 +3693,7 @@ _l3cfg_update_combined_config(NML3Cfg *              self,
                     IS_IPv4 ? AF_INET : AF_INET6,
                     l3cd_data->default_route_table_x[IS_IPv4],
                     l3cd_data->default_route_metric_x[IS_IPv4],
+                    l3cd_data->force_commit_once,
                     l3cd_data->l3cd);
             }
         }
@@ -3524,7 +3770,7 @@ out:
 static gboolean
 _routes_temporary_not_available_timeout(gpointer user_data)
 {
-    NML3Cfg *     self = NM_L3CFG(user_data);
+    NML3Cfg      *self = NM_L3CFG(user_data);
     ObjStateData *obj_state;
     gint64        now_msec;
     gint64        expiry_msec;
@@ -3563,12 +3809,12 @@ _routes_temporary_not_available_timeout(gpointer user_data)
 }
 
 static gboolean
-_routes_temporary_not_available_update(NML3Cfg *  self,
+_routes_temporary_not_available_update(NML3Cfg   *self,
                                        int        addr_family,
                                        GPtrArray *routes_temporary_not_available_arr)
 {
-    ObjStateData *  obj_state;
-    ObjStateData *  obj_state_safe;
+    ObjStateData   *obj_state;
+    ObjStateData   *obj_state_safe;
     gint64          now_msec;
     gboolean        prune_all = FALSE;
     gboolean        success   = TRUE;
@@ -3673,6 +3919,7 @@ out_prune:
 }
 
 /*****************************************************************************/
+
 static const char *
 ip6_privacy_to_str(NMSettingIP6ConfigPrivacy ip6_privacy)
 {
@@ -3699,7 +3946,7 @@ _l3_commit_ndisc_params(NML3Cfg *self, NML3CfgCommitType commit_type)
     guint32               reachable     = 0;
     guint32               retrans       = 0;
     int                   hop_limit     = 0;
-    const char *          ifname;
+    const char           *ifname;
 
     if (commit_type < NM_L3_CFG_COMMIT_TYPE_UPDATE) {
         self->priv.p->ndisc_reachable_time_msec_set = FALSE;
@@ -3757,7 +4004,7 @@ _l3_commit_ip6_privacy(NML3Cfg *self, NML3CfgCommitType commit_type)
 {
     NMSettingIP6ConfigPrivacy ip6_privacy;
     NMSettingIP6ConfigPrivacy ip6_privacy_set_before;
-    const char *              ifname;
+    const char               *ifname;
 
     if (commit_type < NM_L3_CFG_COMMIT_TYPE_UPDATE)
         ip6_privacy = NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN;
@@ -3861,16 +4108,79 @@ set:
                                    ip6_privacy_to_str(ip6_privacy));
 }
 
+static void
+_l3_commit_ip6_token(NML3Cfg *self, NML3CfgCommitType commit_type)
+{
+    NMUtilsIPv6IfaceId    token;
+    const NMPlatformLink *pllink;
+    int                   val;
+
+    if (commit_type < NM_L3_CFG_COMMIT_TYPE_UPDATE || !self->priv.p->combined_l3cd_commited)
+        token.id = 0;
+    else
+        token = nm_l3_config_data_get_ip6_token(self->priv.p->combined_l3cd_commited);
+
+    pllink = nm_l3cfg_get_pllink(self, TRUE);
+    if (!pllink || pllink->inet6_token.id == token.id)
+        return;
+
+    if (_LOGT_ENABLED()) {
+        struct in6_addr addr     = {};
+        struct in6_addr addr_old = {};
+        char            addr_str[INET6_ADDRSTRLEN];
+        char            addr_str_old[INET6_ADDRSTRLEN];
+
+        nm_utils_ipv6_addr_set_interface_identifier(&addr, &token);
+        nm_utils_ipv6_addr_set_interface_identifier(&addr_old, &pllink->inet6_token);
+
+        _LOGT("commit-ip6-token: set value %s (was %s)",
+              inet_ntop(AF_INET6, &addr, addr_str, INET6_ADDRSTRLEN),
+              inet_ntop(AF_INET6, &addr_old, addr_str_old, INET6_ADDRSTRLEN));
+    }
+
+    /* The kernel allows setting a token only when 'accept_ra'
+     * is 1: temporarily flip it if necessary; unfortunately
+     * this will also generate an additional Router Solicitation
+     * from kernel. */
+    val = nm_platform_sysctl_ip_conf_get_int_checked(self->priv.platform,
+                                                     AF_INET6,
+                                                     pllink->name,
+                                                     "accept_ra",
+                                                     10,
+                                                     G_MININT32,
+                                                     G_MAXINT32,
+                                                     1);
+
+    if (val != 1) {
+        nm_platform_sysctl_ip_conf_set(self->priv.platform,
+                                       AF_INET6,
+                                       pllink->name,
+                                       "accept_ra",
+                                       "1");
+    }
+
+    nm_platform_link_set_ipv6_token(self->priv.platform, self->priv.ifindex, &token);
+
+    if (val != 1) {
+        nm_platform_sysctl_ip_conf_set_int64(self->priv.platform,
+                                             AF_INET6,
+                                             pllink->name,
+                                             "accept_ra",
+                                             val);
+    }
+}
+
 static gboolean
-_l3_commit_one(NML3Cfg *             self,
+_l3_commit_one(NML3Cfg              *self,
                int                   addr_family,
                NML3CfgCommitType     commit_type,
                gboolean              changed_combined_l3cd,
                const NML3ConfigData *l3cd_old)
 {
-    const int         IS_IPv4                                       = NM_IS_IPv4(addr_family);
+    const int                    IS_IPv4                            = NM_IS_IPv4(addr_family);
     gs_unref_ptrarray GPtrArray *addresses                          = NULL;
     gs_unref_ptrarray GPtrArray *routes                             = NULL;
+    gs_unref_ptrarray GPtrArray *routes_nodev                       = NULL;
     gs_unref_ptrarray GPtrArray *addresses_prune                    = NULL;
     gs_unref_ptrarray GPtrArray *routes_prune                       = NULL;
     gs_unref_ptrarray GPtrArray *routes_temporary_not_available_arr = NULL;
@@ -3892,23 +4202,9 @@ _l3_commit_one(NML3Cfg *             self,
           _l3_cfg_commit_type_to_string(commit_type, sbuf_commit_type, sizeof(sbuf_commit_type)));
 
     if (self->priv.p->combined_l3cd_commited) {
-        const NMDedupMultiHeadEntry * head_entry;
-        const ObjStatesSyncFilterData sync_filter_data = {
-            .self        = self,
-            .commit_type = commit_type,
-        };
-
-        head_entry = nm_l3_config_data_lookup_objs(self->priv.p->combined_l3cd_commited,
-                                                   NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4));
-        addresses  = nm_dedup_multi_objs_to_ptr_array_head(head_entry,
-                                                          _obj_states_sync_filter,
-                                                          (gpointer) &sync_filter_data);
+        addresses = _commit_collect_addresses(self, addr_family, commit_type);
 
-        head_entry = nm_l3_config_data_lookup_objs(self->priv.p->combined_l3cd_commited,
-                                                   NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4));
-        routes     = nm_dedup_multi_objs_to_ptr_array_head(head_entry,
-                                                       _obj_states_sync_filter,
-                                                       (gpointer) &sync_filter_data);
+        _commit_collect_routes(self, addr_family, commit_type, &routes, &routes_nodev);
 
         route_table_sync =
             nm_l3_config_data_get_route_table_sync(self->priv.p->combined_l3cd_commited,
@@ -3918,10 +4214,11 @@ _l3_commit_one(NML3Cfg *             self,
     if (!IS_IPv4) {
         _l3_commit_ip6_privacy(self, commit_type);
         _l3_commit_ndisc_params(self, commit_type);
+        _l3_commit_ip6_token(self, commit_type);
     }
 
     if (route_table_sync == NM_IP_ROUTE_TABLE_SYNC_MODE_NONE)
-        route_table_sync = NM_IP_ROUTE_TABLE_SYNC_MODE_ALL;
+        route_table_sync = NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN;
 
     if (commit_type == NM_L3_CFG_COMMIT_TYPE_REAPPLY) {
         addresses_prune = nm_platform_ip_address_get_prune_list(self->priv.platform,
@@ -3945,6 +4242,8 @@ _l3_commit_one(NML3Cfg *             self,
                                 addresses,
                                 addresses_prune);
 
+    _nodev_routes_sync(self, addr_family, commit_type, routes_nodev);
+
     if (!nm_platform_ip_route_sync(self->priv.platform,
                                    addr_family,
                                    self->priv.ifindex,
@@ -3968,13 +4267,14 @@ _l3_commit_one(NML3Cfg *             self,
 static void
 _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is_idle)
 {
-    _nm_unused gs_unref_object NML3Cfg *self_keep_alive = NULL;
-    nm_auto_unref_l3cd const NML3ConfigData *l3cd_old   = NULL;
+    _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;
+    guint                                    i;
 
     g_return_if_fail(NM_IS_L3CFG(self));
     nm_assert(NM_IN_SET(commit_type,
@@ -4038,6 +4338,15 @@ _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is_idle)
 
     _l3_acd_data_process_changes(self);
 
+    if (self->priv.p->l3_config_datas) {
+        for (i = 0; i < self->priv.p->l3_config_datas->len; i++) {
+            L3ConfigData *l3_config_data = _l3_config_datas_at(self->priv.p->l3_config_datas, i);
+
+            if (l3_config_data->force_commit_once)
+                l3_config_data->force_commit_once = FALSE;
+        }
+    }
+
     nm_assert(self->priv.p->commit_reentrant_count == 1);
     self->priv.p->commit_reentrant_count--;
 
@@ -4083,10 +4392,10 @@ nm_l3cfg_commit_type_get(NML3Cfg *self)
  *   is %NM_L3_CFG_COMMIT_TYPE_NONE.
  */
 NML3CfgCommitTypeHandle *
-nm_l3cfg_commit_type_register(NML3Cfg *                self,
+nm_l3cfg_commit_type_register(NML3Cfg                 *self,
                               NML3CfgCommitType        commit_type,
                               NML3CfgCommitTypeHandle *existing_handle,
-                              const char *             source)
+                              const char              *source)
 {
     NML3CfgCommitTypeHandle *handle;
     NML3CfgCommitTypeHandle *h;
@@ -4170,6 +4479,20 @@ nm_l3cfg_commit_type_unregister(NML3Cfg *self, NML3CfgCommitTypeHandle *handle)
     nm_g_slice_free(handle);
 }
 
+void
+nm_l3cfg_commit_type_reset_update(NML3Cfg *self)
+{
+    NML3CfgCommitTypeHandle *h;
+
+    c_list_for_each_entry (h, &self->priv.p->commit_type_lst_head, commit_type_lst) {
+        if (h->commit_type >= NM_L3_CFG_COMMIT_TYPE_UPDATE) {
+            return;
+        }
+    }
+
+    self->priv.p->commit_type_update_sticky = FALSE;
+}
+
 /*****************************************************************************/
 
 const NML3ConfigData *
@@ -4202,7 +4525,7 @@ gboolean
 nm_l3cfg_has_commited_ip6_addresses_pending_dad(NML3Cfg *self)
 {
     const NML3ConfigData *l3cd;
-    const NMPObject *     plat_obj;
+    const NMPObject      *plat_obj;
     NMPLookup             plat_lookup;
     NMDedupMultiIter      iter;
 
@@ -4224,7 +4547,7 @@ nm_l3cfg_has_commited_ip6_addresses_pending_dad(NML3Cfg *self)
 
     nm_platform_iter_obj_for_each (&iter, self->priv.platform, &plat_lookup, &plat_obj) {
         const NMPlatformIP6Address *plat_addr = NMP_OBJECT_CAST_IP6_ADDRESS(plat_obj);
-        const NMDedupMultiEntry *   l3cd_entry;
+        const NMDedupMultiEntry    *l3cd_entry;
 
         if (!NM_FLAGS_HAS(plat_addr->n_ifa_flags, IFA_F_TENTATIVE)
             || NM_FLAGS_ANY(plat_addr->n_ifa_flags, IFA_F_DADFAILED | IFA_F_OPTIMISTIC))
@@ -4355,6 +4678,8 @@ constructed(GObject *object)
     self->priv.platform = g_object_ref(nm_netns_get_platform(self->priv.netns));
     nm_assert(NM_IS_PLATFORM(self->priv.platform));
 
+    self->priv.route_manager = nmp_route_manager_ref(nm_netns_get_route_manager(self->priv.netns));
+
     _LOGT("created (netns=" NM_HASH_OBFUSCATE_PTR_FMT ")", NM_HASH_OBFUSCATE_PTR(self->priv.netns));
 
     G_OBJECT_CLASS(nm_l3cfg_parent_class)->constructed(object);
@@ -4376,6 +4701,9 @@ finalize(GObject *object)
 {
     NML3Cfg *self = NM_L3CFG(object);
 
+    nm_assert(!self->priv.p->ipconfig_4);
+    nm_assert(!self->priv.p->ipconfig_6);
+
     nm_assert(!self->priv.p->l3_config_datas);
     nm_assert(!self->priv.p->ipv4ll);
 
@@ -4393,6 +4721,7 @@ finalize(GObject *object)
     nm_clear_pointer(&self->priv.p->nacd, n_acd_unref);
     nm_clear_g_source_inst(&self->priv.p->nacd_source);
     nm_clear_g_source_inst(&self->priv.p->nacd_instance_ensure_retry);
+    nm_clear_g_source_inst(&self->priv.p->nacd_event_down_source);
 
     nm_clear_g_source_inst(&self->priv.p->obj_state_temporary_not_available_timeout_source);
 
@@ -4401,8 +4730,14 @@ finalize(GObject *object)
     nm_assert(c_list_is_empty(&self->priv.p->obj_state_temporary_not_available_lst_head));
     nm_assert(c_list_is_empty(&self->priv.p->obj_state_zombie_lst_head));
 
+    if (_nodev_routes_untrack(self, AF_INET))
+        nmp_route_manager_sync(self->priv.route_manager, NMP_OBJECT_TYPE_IP4_ROUTE, FALSE);
+    if (_nodev_routes_untrack(self, AF_INET6))
+        nmp_route_manager_sync(self->priv.route_manager, NMP_OBJECT_TYPE_IP6_ROUTE, FALSE);
+
     g_clear_object(&self->priv.netns);
     g_clear_object(&self->priv.platform);
+    nm_clear_pointer(&self->priv.route_manager, nmp_route_manager_unref);
 
     nm_clear_l3cd(&self->priv.p->combined_l3cd_merged);
     nm_clear_l3cd(&self->priv.p->combined_l3cd_commited);