about summary refs log tree commit diff
path: root/src/core/nm-l3cfg.c
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2023-02-10 11:50:34 +0100
committerMichael Biebl <biebl@debian.org>2023-02-10 11:50:34 +0100
commit1372848511cb896b80b51ed1a3e9606bd9816631 (patch)
tree674792b9385bdef935988894b45f06b2af39f88c /src/core/nm-l3cfg.c
parent40ec077ea305994c1fc2130add6787ca0c73e2c6 (diff)
New upstream version 1.42.0 upstream/1.42.0
Diffstat (limited to 'src/core/nm-l3cfg.c')
-rw-r--r--src/core/nm-l3cfg.c287
1 files changed, 219 insertions, 68 deletions
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index 8fa1e07d..a49654fe 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -7,7 +7,7 @@
 #include "libnm-std-aux/nm-linux-compat.h"
 
 #include <net/if.h>
-#include <linux/if_addr.h>
+#include "nm-compat-headers/linux/if_addr.h"
 #include <linux/if_ether.h>
 #include <linux/rtnetlink.h>
 
@@ -358,16 +358,16 @@ G_DEFINE_TYPE(NML3Cfg, nm_l3cfg, G_TYPE_OBJECT)
     }                                                                         \
     G_STMT_END
 
-#define _LOGT_acd(acd_data, ...)                                      \
-    G_STMT_START                                                      \
-    {                                                                 \
-        char _sbuf_acd[NM_UTILS_INET_ADDRSTRLEN];                     \
-                                                                      \
-        _LOGT("acd[%s, %s]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),     \
-              _nm_utils_inet4_ntop((acd_data)->info.addr, _sbuf_acd), \
-              _l3_acd_addr_state_to_string((acd_data)->info.state)    \
-                  _NM_UTILS_MACRO_REST(__VA_ARGS__));                 \
-    }                                                                 \
+#define _LOGT_acd(acd_data, ...)                                   \
+    G_STMT_START                                                   \
+    {                                                              \
+        char _sbuf_acd[NM_INET_ADDRSTRLEN];                        \
+                                                                   \
+        _LOGT("acd[%s, %s]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),  \
+              nm_inet4_ntop((acd_data)->info.addr, _sbuf_acd),     \
+              _l3_acd_addr_state_to_string((acd_data)->info.state) \
+                  _NM_UTILS_MACRO_REST(__VA_ARGS__));              \
+    }                                                              \
     G_STMT_END
 
 /*****************************************************************************/
@@ -548,7 +548,7 @@ _l3_config_notify_data_to_string(const NML3ConfigNotifyData *notify_data,
                                  char                       *sbuf,
                                  gsize                       sbuf_size)
 {
-    char      sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char      sbuf_addr[NM_INET_ADDRSTRLEN];
     char      sbuf100[100];
     char      sbufobf[NM_HASH_OBFUSCATE_PTR_STR_BUF_SIZE];
     char     *s = sbuf;
@@ -577,7 +577,7 @@ _l3_config_notify_data_to_string(const NML3ConfigNotifyData *notify_data,
         nm_strbuf_append(&s,
                          &l,
                          ", addr=%s, state=%s",
-                         _nm_utils_inet4_ntop(notify_data->acd_event.info.addr, sbuf_addr),
+                         nm_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:
@@ -603,7 +603,7 @@ _l3_config_notify_data_to_string(const NML3ConfigNotifyData *notify_data,
             &l,
             ", ipv4ll=" NM_HASH_OBFUSCATE_PTR_FMT "%s%s, state=%s",
             NM_HASH_OBFUSCATE_PTR(notify_data->ipv4ll_event.ipv4ll),
-            NM_PRINT_FMT_QUOTED2(addr4 != 0, ", addr=", _nm_utils_inet4_ntop(addr4, sbuf_addr), ""),
+            NM_PRINT_FMT_QUOTED2(addr4 != 0, ", addr=", nm_inet4_ntop(addr4, sbuf_addr), ""),
             nm_l3_ipv4ll_state_to_string(nm_l3_ipv4ll_get_state(notify_data->ipv4ll_event.ipv4ll),
                                          sbuf100,
                                          sizeof(sbuf100)));
@@ -1143,7 +1143,7 @@ _commit_collect_routes(NML3Cfg          *self,
                                                NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4));
 
     if (!head_entry)
-        return;
+        goto loop_done;
 
     c_list_for_each_entry (entry, &head_entry->lst_entries_head, lst_entries) {
         const NMPObject *obj = entry->obj;
@@ -1152,6 +1152,14 @@ _commit_collect_routes(NML3Cfg          *self,
         if (_obj_is_route_nodev(obj))
             r = routes_nodev;
         else {
+            nm_assert(NMP_OBJECT_CAST_IP_ROUTE(obj)->ifindex == self->priv.ifindex);
+
+            if (IS_IPv4 && NMP_OBJECT_CAST_IP4_ROUTE(obj)->weight > 0) {
+                /* This route needs to be registered as ECMP route. */
+                nm_netns_ip_route_ecmp_register(self->priv.netns, self, obj);
+                continue;
+            }
+
             if (!_obj_states_sync_filter(self, obj, commit_type))
                 continue;
             r = routes;
@@ -1162,6 +1170,36 @@ _commit_collect_routes(NML3Cfg          *self,
 
         g_ptr_array_add(*r, (gpointer) nmp_object_ref(obj));
     }
+
+loop_done:
+
+    if (IS_IPv4) {
+        gs_unref_ptrarray GPtrArray *singlehop_routes = NULL;
+        guint                        i;
+
+        /* NMNetns will merge the routes. The ones that found a merge partner are true multihop
+         * routes, with potentially a next hop on different interfaces. The routes
+         * that didn't find a merge partner are returned in "singlehop_routes". */
+        nm_netns_ip_route_ecmp_commit(self->priv.netns,
+                                      self,
+                                      &singlehop_routes,
+                                      NM_IN_SET(commit_type, NM_L3_CFG_COMMIT_TYPE_REAPPLY));
+
+        if (singlehop_routes) {
+            for (i = 0; i < singlehop_routes->len; i++) {
+                const NMPObject *obj = singlehop_routes->pdata[i];
+
+                if (!_obj_states_sync_filter(self, obj, commit_type))
+                    continue;
+
+                if (!*routes)
+                    *routes =
+                        g_ptr_array_new_with_free_func((GDestroyNotify) nm_dedup_multi_obj_unref);
+
+                g_ptr_array_add(*routes, (gpointer) nmp_object_ref(obj));
+            }
+        }
+    }
 }
 
 static void
@@ -1430,7 +1468,8 @@ _acd_data_free(AcdData *acd_data)
 }
 
 static guint
-_acd_data_collect_tracks_data(const AcdData     *acd_data,
+_acd_data_collect_tracks_data(NML3Cfg           *self,
+                              const AcdData     *acd_data,
                               NMTernary          dirty_selector,
                               guint32           *out_best_acd_timeout_msec,
                               NML3AcdDefendType *out_best_acd_defend_type)
@@ -1466,6 +1505,12 @@ _acd_data_collect_tracks_data(const AcdData     *acd_data,
     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);
 
+    if (self->priv.ifindex == NM_LOOPBACK_IFINDEX) {
+        /* On loopback interface, ACD makes no sense. We always force the
+         * timeout to zero, which means no ACD. */
+        best_acd_timeout_msec = 0;
+    }
+
     NM_SET_OUT(out_best_acd_timeout_msec, n > 0 ? best_acd_timeout_msec : 0u);
     NM_SET_OUT(out_best_acd_defend_type, best_acd_defend_type);
     return n;
@@ -2230,7 +2275,7 @@ _l3_acd_data_state_change(NML3Cfg           *self,
     gint64            now_msec;
     const char       *log_reason;
     char              sbuf256[256];
-    char              sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char              sbuf_addr[NM_INET_ADDRSTRLEN];
 
     if (!p_now_msec) {
         now_msec   = 0;
@@ -2313,7 +2358,8 @@ _l3_acd_data_state_change(NML3Cfg           *self,
         return;
 
 handle_init:
-        if (_acd_data_collect_tracks_data(acd_data,
+        if (_acd_data_collect_tracks_data(self,
+                                          acd_data,
                                           NM_TERNARY_FALSE,
                                           &acd_timeout_msec,
                                           &acd_defend_type)
@@ -2362,7 +2408,8 @@ handle_init:
 
         /* we just did a commit of the IP configuration and now visit all ACD states
          * and kick off the necessary actions... */
-        if (_acd_data_collect_tracks_data(acd_data,
+        if (_acd_data_collect_tracks_data(self,
+                                          acd_data,
                                           NM_TERNARY_TRUE,
                                           &acd_timeout_msec,
                                           &acd_defend_type)
@@ -2465,7 +2512,8 @@ handle_init:
             /* after a timeout, re-probe the address. This only happens if the caller
              * does not deconfigure the address after USED/CONFLICT. But in that case,
              * we eventually want to retry. */
-            if (_acd_data_collect_tracks_data(acd_data,
+            if (_acd_data_collect_tracks_data(self,
+                                              acd_data,
                                               NM_TERNARY_TRUE,
                                               &acd_timeout_msec,
                                               &acd_defend_type)
@@ -2550,7 +2598,7 @@ handle_init:
         if (!_l3_acd_data_defendconflict_warning_ratelimited(acd_data, p_now_msec)) {
             _LOGI("IPv4 address %s is used on network connected to interface %d%s%s%s from "
                   "host %s",
-                  _nm_utils_inet4_ntop(acd_data->info.addr, sbuf_addr),
+                  nm_inet4_ntop(acd_data->info.addr, sbuf_addr),
                   self->priv.ifindex,
                   NM_PRINT_FMT_QUOTED(self->priv.plobj_next,
                                       " (",
@@ -2565,7 +2613,7 @@ handle_init:
         nm_assert(acd_data->info.state == NM_L3_ACD_ADDR_STATE_DEFENDING);
         _LOGT_acd(acd_data,
                   "address %s defended from %s",
-                  _nm_utils_inet4_ntop(acd_data->info.addr, sbuf_addr),
+                  nm_inet4_ntop(acd_data->info.addr, sbuf_addr),
                   nm_ether_addr_to_string_a(sender_addr));
         /* we just log an info message. Nothing else to do. */
         return;
@@ -2575,7 +2623,7 @@ handle_init:
 
         _LOGT_acd(acd_data,
                   "address conflict for %s detected with %s",
-                  _nm_utils_inet4_ntop(acd_data->info.addr, sbuf_addr),
+                  nm_inet4_ntop(acd_data->info.addr, sbuf_addr),
                   nm_ether_addr_to_string_a(sender_addr));
 
         if (!_l3_acd_data_defendconflict_warning_ratelimited(acd_data, p_now_msec)) {
@@ -2587,7 +2635,7 @@ handle_init:
                                       NMP_OBJECT_CAST_LINK(self->priv.plobj_next)->name,
                                       ")",
                                       ""),
-                  _nm_utils_inet4_ntop(acd_data->info.addr, sbuf_addr),
+                  nm_inet4_ntop(acd_data->info.addr, sbuf_addr),
                   nm_ether_addr_to_string_a(sender_addr));
         }
 
@@ -2990,68 +3038,103 @@ nm_l3cfg_check_ready(NML3Cfg               *self,
                      const NML3ConfigData  *l3cd,
                      int                    addr_family,
                      NML3CfgCheckReadyFlags flags,
-                     gboolean              *acd_used)
+                     GArray               **conflicts)
 {
     NMDedupMultiIter iter;
     const NMPObject *obj;
+    gboolean         ready = TRUE;
 
     nm_assert(NM_IS_L3CFG(self));
-    nm_assert_addr_family_or_unspec(addr_family);
-
-    NM_SET_OUT(acd_used, FALSE);
+    nm_assert_addr_family(addr_family);
+    nm_assert(!conflicts || !*conflicts);
 
     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);
+    if (addr_family == AF_INET) {
+        if (NM_FLAGS_HAS(flags, NM_L3CFG_CHECK_READY_FLAGS_IP4_ACD_READY)) {
+            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. */
+                    ready = FALSE;
+                } else {
+                    if (addr_info->state <= NM_L3_ACD_ADDR_STATE_PROBING) {
+                        /* Still probing. Not ready. */
+                        ready = FALSE;
+                    } else if (addr_info->state == NM_L3_ACD_ADDR_STATE_USED) {
+                        if (conflicts) {
+                            if (!*conflicts)
+                                *conflicts = g_array_new(FALSE,
+                                                         FALSE,
+                                                         nm_utils_addr_family_to_size(addr_family));
+                            g_array_append_val(*conflicts, addr_info->addr);
+                        }
+                    }
                 }
+                /* 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. */
             }
-            /* 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;
+        return ready;
     }
 
-    if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET6)
-        && NM_FLAGS_HAS(flags, NM_L3CFG_CHECK_READY_FLAGS_IP6_DAD_READY)) {
+    /* AF_INET6 */
+    if (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;
+            gboolean      dadfailed = FALSE;
 
             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;
+                ready = FALSE;
+                continue;
             }
 
             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;
+                ready = FALSE;
+                continue;
+            }
+
+            if (obj_state->os_plobj
+                && NM_FLAGS_HAS(NMP_OBJECT_CAST_IP6_ADDRESS(obj_state->os_plobj)->n_ifa_flags,
+                                IFA_F_DADFAILED)) {
+                /* The address is still present with DADFAILED flag. */
+                dadfailed = TRUE;
+            } else if (obj_state->os_nm_configured && !obj_state->os_plobj
+                       && nm_platform_ip6_dadfailed_check(
+                           self->priv.platform,
+                           self->priv.ifindex,
+                           &NMP_OBJECT_CAST_IP6_ADDRESS(obj_state->obj)->address)) {
+                /* We configured the address and kernel removed it with DADFAILED flag. */
+                dadfailed = TRUE;
+            }
+
+            if (dadfailed) {
+                if (conflicts) {
+                    if (!*conflicts) {
+                        *conflicts =
+                            g_array_new(FALSE, FALSE, nm_utils_addr_family_to_size(addr_family));
+                    }
+                    g_array_append_val(*conflicts,
+                                       NMP_OBJECT_CAST_IP6_ADDRESS(obj_state->obj)->address);
+                }
+                continue;
             }
 
             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;
+                ready = FALSE;
+                continue;
             }
 
             /* This address is ready. Even if it is not (not anymore) configured in kernel (as
@@ -3060,7 +3143,7 @@ nm_l3cfg_check_ready(NML3Cfg               *self,
         }
     }
 
-    return TRUE;
+    return ready;
 }
 
 /*****************************************************************************/
@@ -3144,7 +3227,7 @@ nm_l3cfg_commit_on_idle_is_scheduled(NML3Cfg *self)
 /*****************************************************************************/
 
 #define _l3_config_datas_at(l3_config_datas, idx) \
-    (&g_array_index((l3_config_datas), L3ConfigData, (idx)))
+    (&nm_g_array_index((l3_config_datas), L3ConfigData, (idx)))
 
 static gssize
 _l3_config_datas_find_next(GArray               *l3_config_datas,
@@ -3395,8 +3478,10 @@ nm_l3cfg_add_config(NML3Cfg              *self,
 
     nm_assert(l3_config_data->acd_defend_type_confdata == acd_defend_type);
 
-    if (changed)
+    if (changed) {
         _l3_changed_configs_set_dirty(self);
+        nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
+    }
 
     return changed;
 }
@@ -3440,6 +3525,9 @@ _l3cfg_remove_config(NML3Cfg              *self,
         }
     }
 
+    if (changed)
+        nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
+
     if (self->priv.p->l3_config_datas->len == 0) {
         nm_assert(changed);
         nm_clear_pointer(&self->priv.p->l3_config_datas, g_array_unref);
@@ -3713,6 +3801,38 @@ _l3cfg_update_combined_config(NML3Cfg               *self,
                                     &hook_data);
         }
 
+        if (self->priv.ifindex == NM_LOOPBACK_IFINDEX) {
+            NMPlatformIPXAddress ax;
+            NMPlatformIPXRoute   rx;
+
+            if (!nm_l3_config_data_lookup_address_4(l3cd,
+                                                    NM_IPV4LO_ADDR1,
+                                                    NM_IPV4LO_PREFIXLEN,
+                                                    NM_IPV4LO_ADDR1)) {
+                nm_l3_config_data_add_address_4(
+                    l3cd,
+                    nm_platform_ip4_address_init_loopback_addr1(&ax.a4));
+            }
+            if (!nm_l3_config_data_lookup_address_6(l3cd, &in6addr_loopback)) {
+                nm_l3_config_data_add_address_6(l3cd,
+                                                nm_platform_ip6_address_init_loopback(&ax.a6));
+            }
+
+            rx.r4 = (NMPlatformIP4Route){
+                .ifindex       = NM_LOOPBACK_IFINDEX,
+                .rt_source     = NM_IP_CONFIG_SOURCE_KERNEL,
+                .network       = NM_IPV4LO_ADDR1,
+                .plen          = NM_IPV4LO_PREFIXLEN,
+                .table_coerced = nm_platform_route_table_coerce(RT_TABLE_LOCAL),
+                .scope_inv     = nm_platform_route_scope_inv(RT_SCOPE_HOST),
+                .type_coerced  = nm_platform_route_type_coerce(RTN_LOCAL),
+                .pref_src      = NM_IPV4LO_ADDR1,
+            };
+            nm_platform_ip_route_normalize(AF_INET, &rx.rx);
+            if (!nm_l3_config_data_lookup_route(l3cd, AF_INET, &rx.rx)) {
+                nm_l3_config_data_add_route_4(l3cd, &rx.r4);
+            }
+        }
         for (i = 0; i < l3_config_datas_len; i++) {
             const L3ConfigData *l3cd_data = l3_config_datas_arr[i];
             int                 IS_IPv4;
@@ -4371,9 +4491,9 @@ _l3_commit_mptcp_af(NML3Cfg          *self,
                 default:
                     if (IS_IPv4) {
                         /* We take all addresses, including rfc1918 private addresses
-                         * (nm_utils_ip_is_site_local()). */
+                         * (nm_ip_addr_is_site_local()). */
                     } else {
-                        if (nm_utils_ip6_is_ula(&addr->a6.address)) {
+                        if (nm_ip6_addr_is_ula(&addr->a6.address)) {
                             /* Exclude unique local IPv6 addresses fc00::/7. */
                             goto skip_addr;
                         } else {
@@ -4470,7 +4590,7 @@ _l3_commit_one(NML3Cfg              *self,
     gs_unref_ptrarray GPtrArray *addresses_prune                    = NULL;
     gs_unref_ptrarray GPtrArray *routes_prune                       = NULL;
     gs_unref_ptrarray GPtrArray *routes_temporary_not_available_arr = NULL;
-    NMIPRouteTableSyncMode       route_table_sync = NM_IP_ROUTE_TABLE_SYNC_MODE_NONE;
+    NMIPRouteTableSyncMode       route_table_sync;
     gboolean                     final_failure_for_temporary_not_available = FALSE;
     char                         sbuf_commit_type[50];
     gboolean                     success = TRUE;
@@ -4487,15 +4607,15 @@ _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 (self->priv.p->combined_l3cd_commited) {
-        addresses = _commit_collect_addresses(self, addr_family, commit_type);
+    addresses = _commit_collect_addresses(self, addr_family, commit_type);
 
-        _commit_collect_routes(self, addr_family, commit_type, &routes, &routes_nodev);
+    _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,
-                                                   addr_family);
-    }
+    route_table_sync =
+        self->priv.p->combined_l3cd_commited
+            ? nm_l3_config_data_get_route_table_sync(self->priv.p->combined_l3cd_commited,
+                                                     addr_family)
+            : NM_IP_ROUTE_TABLE_SYNC_MODE_NONE;
 
     if (!IS_IPv4) {
         _l3_commit_ip6_privacy(self, commit_type);
@@ -4552,6 +4672,23 @@ _l3_commit_one(NML3Cfg              *self,
         }
     }
 
+    if (self->priv.ifindex == NM_LOOPBACK_IFINDEX) {
+        if (!addresses) {
+            NMPlatformIPXAddress ax;
+
+            addresses = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
+            if (IS_IPv4) {
+                g_ptr_array_add(
+                    addresses,
+                    nmp_object_new(NMP_OBJECT_TYPE_IP4_ADDRESS,
+                                   nm_platform_ip4_address_init_loopback_addr1(&ax.a4)));
+            } else {
+                g_ptr_array_add(addresses,
+                                nmp_object_new(NMP_OBJECT_TYPE_IP6_ADDRESS,
+                                               nm_platform_ip6_address_init_loopback(&ax.a6)));
+            }
+        }
+    }
     /* 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_mtu(). */
 
@@ -4559,7 +4696,10 @@ _l3_commit_one(NML3Cfg              *self,
                                 addr_family,
                                 self->priv.ifindex,
                                 addresses,
-                                addresses_prune);
+                                addresses_prune,
+                                self->priv.ifindex == NM_LOOPBACK_IFINDEX
+                                    ? NMP_IP_ADDRESS_SYNC_FLAGS_NONE
+                                    : NMP_IP_ADDRESS_SYNC_FLAGS_WITH_NOPREFIXROUTE);
 
     _nodev_routes_sync(self, addr_family, commit_type, routes_nodev);
 
@@ -4810,6 +4950,9 @@ nm_l3cfg_commit_type_register(NML3Cfg                 *self,
         c_list_link_tail(&self->priv.p->commit_type_lst_head, &handle->commit_type_lst);
 
     ret = handle;
+
+    nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
+
 out:
     _LOGT("commit type register (type \"%s\", source \"%s\", existing " NM_HASH_OBFUSCATE_PTR_FMT
           ") -> " NM_HASH_OBFUSCATE_PTR_FMT "",
@@ -4832,6 +4975,8 @@ nm_l3cfg_commit_type_unregister(NML3Cfg *self, NML3CfgCommitTypeHandle *handle)
 
     _LOGT("commit type unregister " NM_HASH_OBFUSCATE_PTR_FMT "", NM_HASH_OBFUSCATE_PTR(handle));
 
+    nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
+
     c_list_unlink_stale(&handle->commit_type_lst);
     if (c_list_is_empty(&self->priv.p->commit_type_lst_head))
         g_object_unref(self);
@@ -5024,6 +5169,9 @@ nm_l3cfg_init(NML3Cfg *self)
     c_list_init(&self->priv.p->blocked_lst_head_4);
     c_list_init(&self->priv.p->blocked_lst_head_6);
 
+    c_list_init(&self->internal_netns.signal_pending_lst);
+    c_list_init(&self->internal_netns.ecmp_track_ifindex_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,
@@ -5066,6 +5214,9 @@ finalize(GObject *object)
     NML3Cfg *self = NM_L3CFG(object);
     gboolean changed;
 
+    nm_assert(c_list_is_empty(&self->internal_netns.signal_pending_lst));
+    nm_assert(c_list_is_empty(&self->internal_netns.ecmp_track_ifindex_lst_head));
+
     nm_assert(!self->priv.p->ipconfig_4);
     nm_assert(!self->priv.p->ipconfig_6);