summary refs log tree commit diff
path: root/src/libnm-platform
diff options
context:
space:
mode:
Diffstat (limited to 'src/libnm-platform')
-rw-r--r--src/libnm-platform/meson.build2
-rw-r--r--src/libnm-platform/nm-linux-platform.c588
-rw-r--r--src/libnm-platform/nm-linux-platform.h9
-rw-r--r--src/libnm-platform/nm-netlink.c7
-rw-r--r--src/libnm-platform/nm-netlink.h9
-rw-r--r--src/libnm-platform/nm-platform.c1815
-rw-r--r--src/libnm-platform/nm-platform.h634
-rw-r--r--src/libnm-platform/nmp-base.c68
-rw-r--r--src/libnm-platform/nmp-base.h25
-rw-r--r--src/libnm-platform/nmp-global-tracker.c6
-rw-r--r--src/libnm-platform/nmp-netns.c56
-rw-r--r--src/libnm-platform/nmp-netns.h2
-rw-r--r--src/libnm-platform/nmp-object.c680
-rw-r--r--src/libnm-platform/nmp-object.h85
-rw-r--r--src/libnm-platform/nmp-plobj.c637
-rw-r--r--src/libnm-platform/nmp-plobj.h296
-rw-r--r--src/libnm-platform/tests/test-nm-platform.c113
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils-nl80211.h2
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils-wext.c2
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils-wext.h2
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils.h2
-rw-r--r--src/libnm-platform/wpan/nm-wpan-utils.h2
22 files changed, 3191 insertions, 1851 deletions
diff --git a/src/libnm-platform/meson.build b/src/libnm-platform/meson.build
index e273046f..696ca1a6 100644
--- a/src/libnm-platform/meson.build
+++ b/src/libnm-platform/meson.build
@@ -7,9 +7,11 @@ libnm_platform = static_library(
     'nm-netlink.c',
     'nm-platform-utils.c',
     'nm-platform.c',
+    'nmp-base.c',
     'nmp-global-tracker.c',
     'nmp-netns.c',
     'nmp-object.c',
+    'nmp-plobj.c',
     'wifi/nm-wifi-utils-nl80211.c',
     'wifi/nm-wifi-utils.c',
     'wpan/nm-wpan-utils.c',
diff --git a/src/libnm-platform/nm-linux-platform.c b/src/libnm-platform/nm-linux-platform.c
index b798d12d..d4ab36f9 100644
--- a/src/libnm-platform/nm-linux-platform.c
+++ b/src/libnm-platform/nm-linux-platform.c
@@ -222,6 +222,16 @@ G_STATIC_ASSERT(RTA_MAX == (__RTA_MAX - 1));
 
 /*****************************************************************************/
 
+#define IFLA_VTI_UNSPEC 0
+#define IFLA_VTI_LINK   1
+#define IFLA_VTI_IKEY   2
+#define IFLA_VTI_OKEY   3
+#define IFLA_VTI_LOCAL  4
+#define IFLA_VTI_REMOTE 5
+#define IFLA_VTI_FWMARK 6
+
+/*****************************************************************************/
+
 #define WG_CMD_GET_DEVICE 0
 #define WG_CMD_SET_DEVICE 1
 
@@ -580,25 +590,29 @@ NM_LINUX_PLATFORM_FROM_PRIVATE(NMLinuxPlatformPrivate *priv)
 #define _NMLOG2(level, ...)            _LOG(level, _NMLOG2_DOMAIN, NULL, __VA_ARGS__)
 #define _NMLOG2_err(errsv, level, ...) _LOG_err(errsv, level, _NMLOG2_DOMAIN, NULL, __VA_ARGS__)
 
-#define _LOG_print(__level, __domain, __errsv, self, ...)                                 \
-    G_STMT_START                                                                          \
-    {                                                                                     \
-        char              __prefix[32];                                                   \
-        const char       *__p_prefix = _NMLOG_PREFIX_NAME;                                \
-        NMPlatform *const __self     = (self);                                            \
-                                                                                          \
-        if (__self && nm_platform_get_log_with_ptr(__self)) {                             \
-            g_snprintf(__prefix, sizeof(__prefix), "%s[%p]", _NMLOG_PREFIX_NAME, __self); \
-            __p_prefix = __prefix;                                                        \
-        }                                                                                 \
-        _nm_log(__level,                                                                  \
-                __domain,                                                                 \
-                __errsv,                                                                  \
-                NULL,                                                                     \
-                NULL,                                                                     \
-                "%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                                \
-                __p_prefix _NM_UTILS_MACRO_REST(__VA_ARGS__));                            \
-    }                                                                                     \
+#define _LOG_print(__level, __domain, __errsv, self, ...)      \
+    G_STMT_START                                               \
+    {                                                          \
+        char              __prefix[64];                        \
+        const char       *__p_prefix = _NMLOG_PREFIX_NAME;     \
+        NMPlatform *const __self     = (self);                 \
+                                                               \
+        if (__self && nm_platform_get_log_with_ptr(__self)) {  \
+            g_snprintf(__prefix,                               \
+                       sizeof(__prefix),                       \
+                       "%s[" NM_HASH_OBFUSCATE_PTR_FMT "]",    \
+                       _NMLOG_PREFIX_NAME,                     \
+                       NM_HASH_OBFUSCATE_PTR(__self));         \
+            __p_prefix = __prefix;                             \
+        }                                                      \
+        _nm_log(__level,                                       \
+                __domain,                                      \
+                __errsv,                                       \
+                NULL,                                          \
+                NULL,                                          \
+                "%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),     \
+                __p_prefix _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+    }                                                          \
     G_STMT_END
 
 #define _LOG(level, domain, self, ...)                           \
@@ -811,6 +825,8 @@ static const LinkDesc link_descs[] = {
     [NM_LINK_TYPE_VETH]        = {"veth", "veth", NULL},
     [NM_LINK_TYPE_VLAN]        = {"vlan", "vlan", "vlan"},
     [NM_LINK_TYPE_VRF]         = {"vrf", "vrf", "vrf"},
+    [NM_LINK_TYPE_VTI]         = {"vti", "vti", NULL},
+    [NM_LINK_TYPE_VTI6]        = {"vti6", "vti6", NULL},
     [NM_LINK_TYPE_VXLAN]       = {"vxlan", "vxlan", "vxlan"},
     [NM_LINK_TYPE_WIREGUARD]   = {"wireguard", "wireguard", "wireguard"},
 
@@ -853,6 +869,8 @@ _link_type_from_rtnl_type(const char *name)
         NM_LINK_TYPE_VETH,        /* "veth"        */
         NM_LINK_TYPE_VLAN,        /* "vlan"        */
         NM_LINK_TYPE_VRF,         /* "vrf"         */
+        NM_LINK_TYPE_VTI,         /* "vti"         */
+        NM_LINK_TYPE_VTI6,        /* "vti6"        */
         NM_LINK_TYPE_VXLAN,       /* "vxlan"       */
         NM_LINK_TYPE_WIMAX,       /* "wimax"       */
         NM_LINK_TYPE_WIREGUARD,   /* "wireguard"   */
@@ -1158,7 +1176,7 @@ _linktype_read_devtype(int dirfd)
         end = strpbrk(cont, "\r\n");
         if (end)
             *end++ = '\0';
-        if (strncmp(cont, DEVTYPE_PREFIX, NM_STRLEN(DEVTYPE_PREFIX)) == 0) {
+        if (NM_STR_HAS_PREFIX(cont, DEVTYPE_PREFIX)) {
             cont += NM_STRLEN(DEVTYPE_PREFIX);
             memmove(contents, cont, strlen(cont) + 1);
             return g_steal_pointer(&contents);
@@ -1289,14 +1307,36 @@ _linktype_get_type(NMPlatform       *platform,
         }
 
         if (arptype == ARPHRD_ETHER) {
-            /* Misc non-upstream WWAN drivers.  rmnet is Qualcomm's proprietary
-             * modem interface, ccmni is MediaTek's.  FIXME: these drivers should
-             * really set devtype=WWAN.
-             */
-            if (g_str_has_prefix(ifname, "rmnet") || g_str_has_prefix(ifname, "rev_rmnet")
-                || g_str_has_prefix(ifname, "ccmni"))
+            /* The USB gadget interfaces behave and look like ordinary ethernet devices
+             * aside from the DEVTYPE. */
+            if (nm_streq0(devtype, "gadget"))
+                return NM_LINK_TYPE_ETHERNET;
+
+            /* Distributed Switch Architecture switch chips */
+            if (nm_streq0(devtype, "dsa"))
+                return NM_LINK_TYPE_ETHERNET;
+        }
+
+        /* Misc non-upstream WWAN drivers.  rmnet is Qualcomm's proprietary
+         * modem interface, ccmni is MediaTek's.  FIXME: these drivers should
+         * really set devtype=WWAN.
+         *
+         * Value "530" is the out-of-tree version of ARPHRD_RAWIP before it's
+         * merged in Linux 4.14. For the mainline version, this has the value
+         * of "519".
+         *
+         * [1] https://github.com/LineageOS/android_kernel_google_msm-4.9/commit/54948008c293fdf48552a5c39c91c09c3eb76ed2
+         */
+        if (NM_IN_SET(arptype,
+                      ARPHRD_ETHER,
+                      519 /* ARPHRD_RAWIP */,
+                      530 /* out-of-tree ARPHRD_RAWIP */)) {
+            if (NM_STR_HAS_PREFIX(ifname, "rmnet") || NM_STR_HAS_PREFIX(ifname, "rev_rmnet")
+                || NM_STR_HAS_PREFIX(ifname, "ccmni"))
                 return NM_LINK_TYPE_WWAN_NET;
+        }
 
+        if (arptype == ARPHRD_ETHER) {
             /* Standard wired ethernet interfaces don't report an rtnl_link_type, so
              * only allow fallback to Ethernet if no type is given.  This should
              * prevent future virtual network drivers from being treated as Ethernet
@@ -1304,15 +1344,6 @@ _linktype_get_type(NMPlatform       *platform,
              */
             if (!kind && !devtype)
                 return NM_LINK_TYPE_ETHERNET;
-
-            /* The USB gadget interfaces behave and look like ordinary ethernet devices
-             * aside from the DEVTYPE. */
-            if (nm_streq0(devtype, "gadget"))
-                return NM_LINK_TYPE_ETHERNET;
-
-            /* Distributed Switch Architecture switch chips */
-            if (nm_streq0(devtype, "dsa"))
-                return NM_LINK_TYPE_ETHERNET;
         }
     }
 
@@ -1338,7 +1369,8 @@ typedef struct {
     };
 } ParseNlmsgIter;
 
-#define NLMSG_TAIL(nmsg) ((struct rtattr *) (((char *) (nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len)))
+#define NLMSG_TAIL(nmsg) \
+    NM_CAST_ALIGN(struct rtattr, ((char *) (nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len))
 
 /* copied from iproute2's addattr_l(). */
 static gboolean
@@ -2244,6 +2276,11 @@ _parse_lnk_vlan(const char *kind, struct nlattr *info_data)
     obj              = nmp_object_new(NMP_OBJECT_TYPE_LNK_VLAN, NULL);
     obj->lnk_vlan.id = nla_get_u16(tb[IFLA_VLAN_ID]);
 
+    if (tb[IFLA_VLAN_PROTOCOL])
+        obj->lnk_vlan.protocol = ntohs(nla_get_u16(tb[IFLA_VLAN_PROTOCOL]));
+    else
+        obj->lnk_vlan.protocol = ETH_P_8021Q;
+
     if (tb[IFLA_VLAN_FLAGS]) {
         struct ifla_vlan_flags flags;
 
@@ -2389,6 +2426,76 @@ _parse_lnk_vxlan(const char *kind, struct nlattr *info_data)
 }
 
 static NMPObject *
+_parse_lnk_vti(const char *kind, struct nlattr *info_data)
+{
+    static const struct nla_policy policy[] = {
+        [IFLA_VTI_LINK]   = {.type = NLA_U32},
+        [IFLA_VTI_LOCAL]  = {.type = NLA_U32},
+        [IFLA_VTI_REMOTE] = {.type = NLA_U32},
+        [IFLA_VTI_IKEY]   = {.type = NLA_U32},
+        [IFLA_VTI_OKEY]   = {.type = NLA_U32},
+        [IFLA_VTI_FWMARK] = {.type = NLA_U32},
+    };
+    struct nlattr    *tb[G_N_ELEMENTS(policy)];
+    NMPObject        *obj;
+    NMPlatformLnkVti *props;
+
+    if (!info_data || !nm_streq0(kind, "vti"))
+        return NULL;
+
+    if (nla_parse_nested_arr(tb, info_data, policy) < 0)
+        return NULL;
+
+    obj   = nmp_object_new(NMP_OBJECT_TYPE_LNK_VTI, NULL);
+    props = &obj->lnk_vti;
+
+    props->parent_ifindex = tb[IFLA_VTI_LINK] ? nla_get_u32(tb[IFLA_VTI_LINK]) : 0;
+    props->local          = tb[IFLA_VTI_LOCAL] ? nla_get_u32(tb[IFLA_VTI_LOCAL]) : 0;
+    props->remote         = tb[IFLA_VTI_REMOTE] ? nla_get_u32(tb[IFLA_VTI_REMOTE]) : 0;
+    props->ikey           = tb[IFLA_VTI_IKEY] ? ntohl(nla_get_u32(tb[IFLA_VTI_IKEY])) : 0;
+    props->okey           = tb[IFLA_VTI_OKEY] ? ntohl(nla_get_u32(tb[IFLA_VTI_OKEY])) : 0;
+    props->fwmark         = tb[IFLA_VTI_FWMARK] ? nla_get_u32(tb[IFLA_VTI_FWMARK]) : 0;
+
+    return obj;
+}
+
+static NMPObject *
+_parse_lnk_vti6(const char *kind, struct nlattr *info_data)
+{
+    static const struct nla_policy policy[] = {
+        [IFLA_VTI_LINK]   = {.type = NLA_U32},
+        [IFLA_VTI_LOCAL]  = {.minlen = sizeof(struct in6_addr)},
+        [IFLA_VTI_REMOTE] = {.minlen = sizeof(struct in6_addr)},
+        [IFLA_VTI_IKEY]   = {.type = NLA_U32},
+        [IFLA_VTI_OKEY]   = {.type = NLA_U32},
+        [IFLA_VTI_FWMARK] = {.type = NLA_U32},
+    };
+    struct nlattr     *tb[G_N_ELEMENTS(policy)];
+    NMPObject         *obj;
+    NMPlatformLnkVti6 *props;
+
+    if (!info_data || !nm_streq0(kind, "vti6"))
+        return NULL;
+
+    if (nla_parse_nested_arr(tb, info_data, policy) < 0)
+        return NULL;
+
+    obj   = nmp_object_new(NMP_OBJECT_TYPE_LNK_VTI6, NULL);
+    props = &obj->lnk_vti6;
+
+    props->parent_ifindex = tb[IFLA_VTI_LINK] ? nla_get_u32(tb[IFLA_VTI_LINK]) : 0;
+    if (tb[IFLA_VTI_LOCAL])
+        props->local = *nla_data_as(struct in6_addr, tb[IFLA_VTI_LOCAL]);
+    if (tb[IFLA_VTI_REMOTE])
+        props->remote = *nla_data_as(struct in6_addr, tb[IFLA_VTI_REMOTE]);
+    props->ikey   = tb[IFLA_VTI_IKEY] ? ntohl(nla_get_u32(tb[IFLA_VTI_IKEY])) : 0;
+    props->okey   = tb[IFLA_VTI_OKEY] ? ntohl(nla_get_u32(tb[IFLA_VTI_OKEY])) : 0;
+    props->fwmark = tb[IFLA_VTI_FWMARK] ? nla_get_u32(tb[IFLA_VTI_FWMARK]) : 0;
+
+    return obj;
+}
+
+static NMPObject *
 _parse_lnk_vrf(const char *kind, struct nlattr *info_data)
 {
     static const struct nla_policy policy[] = {
@@ -2540,16 +2647,15 @@ _wireguard_update_from_peers_nla(CList *peers, GArray **p_allowed_ips, struct nl
         GArray        *allowed_ips = *p_allowed_ips;
 
         nla_for_each_nested (attr, tb[WGPEER_A_ALLOWEDIPS], rem) {
+            NMPWireGuardAllowedIP *new;
+
             if (!allowed_ips) {
                 allowed_ips    = g_array_new(FALSE, FALSE, sizeof(NMPWireGuardAllowedIP));
                 *p_allowed_ips = allowed_ips;
-                g_array_set_size(allowed_ips, 1);
-            } else
-                g_array_set_size(allowed_ips, allowed_ips->len + 1);
+            }
 
-            if (!_wireguard_update_from_allowed_ips_nla(
-                    &g_array_index(allowed_ips, NMPWireGuardAllowedIP, allowed_ips->len - 1),
-                    attr)) {
+            new = nm_g_array_append_new(allowed_ips, NMPWireGuardAllowedIP);
+            if (!_wireguard_update_from_allowed_ips_nla(new, attr)) {
                 /* we ignore the error of parsing one allowed-ip. */
                 g_array_set_size(allowed_ips, allowed_ips->len - 1);
                 continue;
@@ -3379,6 +3485,12 @@ _new_from_nl_link(NMPlatform            *platform,
     case NM_LINK_TYPE_VRF:
         lnk_data = _parse_lnk_vrf(nl_info_kind, nl_info_data);
         break;
+    case NM_LINK_TYPE_VTI:
+        lnk_data = _parse_lnk_vti(nl_info_kind, nl_info_data);
+        break;
+    case NM_LINK_TYPE_VTI6:
+        lnk_data = _parse_lnk_vti6(nl_info_kind, nl_info_data);
+        break;
     case NM_LINK_TYPE_VXLAN:
         lnk_data = _parse_lnk_vxlan(nl_info_kind, nl_info_data);
         break;
@@ -3631,21 +3743,27 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter
     struct {
         gboolean found;
         gboolean has_more;
+        guint8   weight;
         int      ifindex;
         NMIPAddr gateway;
     } nh = {
         .found    = FALSE,
         .has_more = FALSE,
     };
-    guint32  mss;
-    guint32  window   = 0;
-    guint32  cwnd     = 0;
-    guint32  initcwnd = 0;
-    guint32  initrwnd = 0;
-    guint32  mtu      = 0;
-    guint32  rto_min  = 0;
-    guint32  lock     = 0;
-    gboolean quickack = FALSE;
+    guint                           v4_n_nexthops = 0;
+    NMPlatformIP4RtNextHop          v4_nh_extra_nexthops_stack[10];
+    gs_free NMPlatformIP4RtNextHop *v4_nh_extra_nexthops_heap = NULL;
+    NMPlatformIP4RtNextHop         *v4_nh_extra_nexthops      = v4_nh_extra_nexthops_stack;
+    guint                           v4_nh_extra_alloc = G_N_ELEMENTS(v4_nh_extra_nexthops_stack);
+    guint32                         mss;
+    guint32                         window   = 0;
+    guint32                         cwnd     = 0;
+    guint32                         initcwnd = 0;
+    guint32                         initrwnd = 0;
+    guint32                         mtu      = 0;
+    guint32                         rto_min  = 0;
+    guint32                         lock     = 0;
+    gboolean                        quickack = FALSE;
 
     nm_assert((parse_nlmsg_iter->iter_more && parse_nlmsg_iter->ip6_route.next_multihop > 0)
               || (!parse_nlmsg_iter->iter_more && parse_nlmsg_iter->ip6_route.next_multihop == 0));
@@ -3669,15 +3787,6 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter
     else
         return NULL;
 
-    if (!NM_IN_SET(rtm->rtm_type,
-                   RTN_UNICAST,
-                   RTN_LOCAL,
-                   RTN_BLACKHOLE,
-                   RTN_UNREACHABLE,
-                   RTN_PROHIBIT,
-                   RTN_THROW))
-        return NULL;
-
     if (nlmsg_parse_arr(nlh, sizeof(struct rtmsg), tb, policy) < 0)
         return NULL;
 
@@ -3703,9 +3812,57 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter
 
         idx = 0;
         while (TRUE) {
-            if (idx == multihop_idx) {
+            if (nh.found && IS_IPv4) {
+                NMPlatformIP4RtNextHop *new_nexthop;
+
+                /* we parsed the first IPv4 nexthop in "nh", let's parse the following ones.
+                 *
+                 * At this point, v4_n_nexthops still counts how many hops we already added,
+                 * now we are about to add the (v4_n_nexthops+1) hop.
+                 *
+                 * Note that the first hop (of then v4_n_nexthops) is tracked in "nh".
+                 * v4_nh_extra_nexthops tracks the additional hops.
+                 *
+                 * v4_nh_extra_alloc is how many space is allocated for
+                 * v4_nh_extra_nexthops (note that in the end we will only add (v4_n_nexthops-1)
+                 * hops in this list). */
+                nm_assert(v4_n_nexthops > 0u);
+                if (v4_n_nexthops - 1u >= v4_nh_extra_alloc) {
+                    v4_nh_extra_alloc = NM_MAX(4, v4_nh_extra_alloc * 2u);
+                    if (!v4_nh_extra_nexthops_heap) {
+                        v4_nh_extra_nexthops_heap =
+                            g_new(NMPlatformIP4RtNextHop, v4_nh_extra_alloc);
+                        memcpy(v4_nh_extra_nexthops_heap,
+                               v4_nh_extra_nexthops_stack,
+                               G_N_ELEMENTS(v4_nh_extra_nexthops_stack));
+                    } else {
+                        v4_nh_extra_nexthops_heap = g_renew(NMPlatformIP4RtNextHop,
+                                                            v4_nh_extra_nexthops_heap,
+                                                            v4_nh_extra_alloc);
+                    }
+                    v4_nh_extra_nexthops = v4_nh_extra_nexthops_heap;
+                }
+                nm_assert(v4_n_nexthops - 1u < v4_nh_extra_alloc);
+                new_nexthop          = &v4_nh_extra_nexthops[v4_n_nexthops - 1u];
+                new_nexthop->ifindex = rtnh->rtnh_ifindex;
+                new_nexthop->weight  = NM_MAX(((guint) rtnh->rtnh_hops) + 1u, 1u);
+                if (rtnh->rtnh_len > sizeof(*rtnh)) {
+                    struct nlattr *ntb[RTA_MAX + 1];
+
+                    if (nla_parse_arr(ntb,
+                                      (struct nlattr *) RTNH_DATA(rtnh),
+                                      rtnh->rtnh_len - sizeof(*rtnh),
+                                      NULL)
+                        < 0)
+                        return NULL;
+
+                    if (_check_addr_or_return_null(ntb, RTA_GATEWAY, addr_len))
+                        memcpy(&new_nexthop->gateway, nla_data(ntb[RTA_GATEWAY]), addr_len);
+                }
+            } else if (IS_IPv4 || idx == multihop_idx) {
                 nh.found   = TRUE;
                 nh.ifindex = rtnh->rtnh_ifindex;
+                nh.weight  = NM_MAX(((guint) rtnh->rtnh_hops) + 1u, 1u);
                 if (rtnh->rtnh_len > sizeof(*rtnh)) {
                     struct nlattr *ntb[RTA_MAX + 1];
 
@@ -3722,15 +3879,6 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter
             } else if (nh.found) {
                 /* we just parsed a nexthop, but there is yet another hop afterwards. */
                 nm_assert(idx == multihop_idx + 1);
-                if (IS_IPv4) {
-                    /* for IPv4, multihop routes are currently not supported.
-                     *
-                     * If we ever support them, then the next-hop list is part of the NMPlatformIPRoute,
-                     * that is, for IPv4 we truly have multihop routes. Unlike for IPv6.
-                     *
-                     * For now, just error out. */
-                    return NULL;
-                }
 
                 /* For IPv6 multihop routes, we need to remember to iterate again.
                  * For each next-hop, we will create a distinct single-hop NMPlatformIP6Route. */
@@ -3738,6 +3886,9 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter
                 break;
             }
 
+            if (IS_IPv4)
+                v4_n_nexthops++;
+
             if (tlen < RTNH_ALIGN(rtnh->rtnh_len) + sizeof(*rtnh))
                 break;
 
@@ -3771,6 +3922,9 @@ rta_multipath_done:
             nh.ifindex = ifindex;
             nh.gateway = gateway;
             nh.found   = TRUE;
+            nm_assert(v4_n_nexthops == 0);
+            if (IS_IPv4)
+                v4_n_nexthops = 1;
         } else {
             /* Kernel supports new style nexthop configuration,
              * verify that it is a duplicate and ignore old-style nexthop. */
@@ -3861,6 +4015,23 @@ rta_multipath_done:
 
     obj->ip_route.ifindex = nh.ifindex;
 
+    if (IS_IPv4) {
+        nm_assert((!!nh.found) == (v4_n_nexthops > 0u));
+        obj->ip4_route.n_nexthops = v4_n_nexthops;
+        if (v4_n_nexthops > 1) {
+            /* We only set the weight for multihop routes. I think that corresponds to what kernel
+             * does. The weight is mostly undefined for single-hop. */
+            obj->ip4_route.weight = NM_MAX(nh.weight, 1u);
+
+            obj->_ip4_route.extra_nexthops =
+                (v4_nh_extra_alloc == v4_n_nexthops - 1u
+                 && v4_nh_extra_nexthops == v4_nh_extra_nexthops_heap)
+                    ? g_steal_pointer(&v4_nh_extra_nexthops_heap)
+                    : nm_memdup(v4_nh_extra_nexthops,
+                                sizeof(v4_nh_extra_nexthops[0]) * (v4_n_nexthops - 1u));
+        }
+    }
+
     if (_check_addr_or_return_null(tb, RTA_DST, addr_len))
         memcpy(obj->ip_route.network_ptr, nla_data(tb[RTA_DST]), addr_len);
 
@@ -4580,11 +4751,13 @@ _nl_msg_new_link_set_linkinfo(struct nl_msg *msg, NMLinkType link_type, gconstpo
         const NMPlatformLnkVlan *props = extra_data;
 
         nm_assert(extra_data);
+        nm_assert(props->protocol != 0);
 
         if (!(data = nla_nest_start(msg, IFLA_INFO_DATA)))
             goto nla_put_failure;
 
         NLA_PUT_U16(msg, IFLA_VLAN_ID, props->id);
+        NLA_PUT_U16(msg, IFLA_VLAN_PROTOCOL, htons(props->protocol));
 
         {
             struct ifla_vlan_flags flags = {
@@ -4835,6 +5008,44 @@ _nl_msg_new_link_set_linkinfo(struct nl_msg *msg, NMLinkType link_type, gconstpo
         NLA_PUT_U16(msg, IFLA_MACVLAN_FLAGS, props->no_promisc ? MACVLAN_FLAG_NOPROMISC : 0);
         break;
     }
+    case NM_LINK_TYPE_VTI:
+    {
+        const NMPlatformLnkVti *props = extra_data;
+
+        nm_assert(props);
+
+        if (!(data = nla_nest_start(msg, IFLA_INFO_DATA)))
+            goto nla_put_failure;
+
+        if (props->parent_ifindex > 0)
+            NLA_PUT_U32(msg, IFLA_VTI_LINK, props->parent_ifindex);
+        NLA_PUT_U32(msg, IFLA_VTI_LOCAL, props->local);
+        NLA_PUT_U32(msg, IFLA_VTI_REMOTE, props->remote);
+        NLA_PUT_U32(msg, IFLA_VTI_IKEY, htonl(props->ikey));
+        NLA_PUT_U32(msg, IFLA_VTI_OKEY, htonl(props->okey));
+        NLA_PUT_U32(msg, IFLA_VTI_FWMARK, props->fwmark);
+        break;
+    }
+    case NM_LINK_TYPE_VTI6:
+    {
+        const NMPlatformLnkVti6 *props = extra_data;
+
+        nm_assert(props);
+
+        if (!(data = nla_nest_start(msg, IFLA_INFO_DATA)))
+            goto nla_put_failure;
+
+        if (props->parent_ifindex > 0)
+            NLA_PUT_U32(msg, IFLA_VTI_LINK, props->parent_ifindex);
+        if (!IN6_IS_ADDR_UNSPECIFIED(&props->local))
+            NLA_PUT(msg, IFLA_VTI_LOCAL, sizeof(props->local), &props->local);
+        if (!IN6_IS_ADDR_UNSPECIFIED(&props->remote))
+            NLA_PUT(msg, IFLA_VTI_REMOTE, sizeof(props->remote), &props->remote);
+        NLA_PUT_U32(msg, IFLA_VTI_IKEY, htonl(props->ikey));
+        NLA_PUT_U32(msg, IFLA_VTI_OKEY, htonl(props->okey));
+        NLA_PUT_U32(msg, IFLA_VTI_FWMARK, props->fwmark);
+        break;
+    }
     default:
         nm_assert(!extra_data);
         break;
@@ -5088,7 +5299,7 @@ ip_route_get_lock_flag(const NMPlatformIPRoute *route)
 }
 
 static gboolean
-ip_route_ignored_protocol(const NMPlatformIPRoute *route)
+ip_route_is_alive(const NMPlatformIPRoute *route)
 {
     guint8 prot;
 
@@ -5100,12 +5311,29 @@ ip_route_ignored_protocol(const NMPlatformIPRoute *route)
 
     nm_assert(nmp_utils_ip_config_source_from_rtprot(prot) == route->rt_source);
 
-    /* We ignore all routes outside a certain subest of rtm_protocol. NetworkManager
-     * itself wouldn't configure those, so they are always configured by somebody
-     * external. We thus ignore them to avoid the overhead that processing them brings.
-     * For example, the BGP daemon "bird"  might configure a huge number of RTPROT_BIRD routes. */
+    if (prot > RTPROT_STATIC && !NM_IN_SET(prot, RTPROT_DHCP, RTPROT_RA)) {
+        /* We ignore certain rtm_protocol, because NetworkManager would only ever
+         * configure certain protocols. Other routes are not configured by NetworkManager
+         * and we don't track them in the platform cache.
+         *
+         * This is to help with the performance overhead of a huge number of
+         * routes, for example with the bird BGP software, that adds routes
+         * with RTPROT_BIRD protocol. */
+        return FALSE;
+    }
 
-    return prot > RTPROT_STATIC && !NM_IN_SET(prot, RTPROT_DHCP, RTPROT_RA);
+    if (!NM_IN_SET(nm_platform_route_type_uncoerce(route->type_coerced),
+                   RTN_UNICAST,
+                   RTN_LOCAL,
+                   RTN_BLACKHOLE,
+                   RTN_UNREACHABLE,
+                   RTN_PROHIBIT,
+                   RTN_THROW)) {
+        /* Certain route types are ignored and not placed into the cache. */
+        return FALSE;
+    }
+
+    return TRUE;
 }
 
 /* Copied and modified from libnl3's build_route_msg() and rtnl_route_build_msg(). */
@@ -5118,7 +5346,7 @@ _nl_msg_new_route(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPObject *ob
     const guint32                lock    = ip_route_get_lock_flag(NMP_OBJECT_CAST_IP_ROUTE(obj));
     const guint32                table =
         nm_platform_route_table_uncoerce(NMP_OBJECT_CAST_IP_ROUTE(obj)->table_coerced, TRUE);
-    const struct rtmsg rtmsg = {
+    struct rtmsg rtmsg = {
         .rtm_family   = klass->addr_family,
         .rtm_tos      = IS_IPv4 ? obj->ip4_route.tos : 0,
         .rtm_table    = table <= 0xFF ? table : RT_TABLE_UNSPEC,
@@ -5126,7 +5354,7 @@ _nl_msg_new_route(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPObject *ob
         .rtm_scope =
             IS_IPv4 ? nm_platform_route_scope_inv(obj->ip4_route.scope_inv) : RT_SCOPE_NOWHERE,
         .rtm_type    = nm_platform_route_type_uncoerce(NMP_OBJECT_CAST_IP_ROUTE(obj)->type_coerced),
-        .rtm_flags   = obj->ip_route.r_rtm_flags & ((unsigned) (RTNH_F_ONLINK)),
+        .rtm_flags   = 0,
         .rtm_dst_len = obj->ip_route.plen,
         .rtm_src_len = IS_IPv4 ? 0 : NMP_OBJECT_CAST_IP6_ROUTE(obj)->src_plen,
     };
@@ -5137,6 +5365,17 @@ _nl_msg_new_route(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPObject *ob
         NM_IN_SET(NMP_OBJECT_GET_TYPE(obj), NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE));
     nm_assert(NM_IN_SET(nlmsg_type, RTM_NEWROUTE, RTM_DELROUTE));
 
+    if (NM_FLAGS_HAS(obj->ip_route.r_rtm_flags, ((unsigned) (RTNH_F_ONLINK)))) {
+        if (IS_IPv4 && obj->ip4_route.gateway == 0) {
+            /* Kernel does not allow setting the onlink flag, if there is no gateway.
+             * We silently don't configure the flag.
+             *
+             * For multi-hop routes, we will set the flag for each next-hop (which
+             * has a gateway set). */
+        } else
+            rtmsg.rtm_flags |= ((unsigned) RTNH_F_ONLINK);
+    }
+
     msg = nlmsg_alloc_new(0, nlmsg_type, nlmsg_flags);
 
     if (nlmsg_append_struct(msg, &rtmsg) < 0)
@@ -5171,6 +5410,54 @@ _nl_msg_new_route(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPObject *ob
             NLA_PUT(msg, RTA_PREFSRC, addr_len, &obj->ip6_route.pref_src);
     }
 
+    if (IS_IPv4 && obj->ip4_route.n_nexthops > 1u) {
+        struct nlattr *multipath;
+        guint          i;
+
+        if (!(multipath = nla_nest_start(msg, RTA_MULTIPATH)))
+            goto nla_put_failure;
+
+        for (i = 0u; i < obj->ip4_route.n_nexthops; i++) {
+            struct rtnexthop *rtnh;
+            in_addr_t         gw;
+
+            rtnh = nlmsg_reserve(msg, sizeof(*rtnh), NLMSG_ALIGNTO);
+            if (!rtnh)
+                goto nla_put_failure;
+
+            /* For multihop routes, a valid weight must be in range 1-256 (on netlink's
+             * "rtnh_hops" this is 0-255). We allow that the caller leaves the value unset
+             * at zero. */
+            if (i == 0u) {
+                rtnh->rtnh_hops    = NM_MAX(obj->ip4_route.weight, 1u) - 1u;
+                rtnh->rtnh_ifindex = obj->ip4_route.ifindex;
+                gw                 = obj->ip4_route.gateway;
+            } else {
+                const NMPlatformIP4RtNextHop *n = &obj->_ip4_route.extra_nexthops[i - 1u];
+
+                rtnh->rtnh_hops    = NM_MAX(n->weight, 1u) - 1u;
+                rtnh->rtnh_ifindex = n->ifindex;
+                gw                 = n->gateway;
+            }
+            NLA_PUT_U32(msg, RTA_GATEWAY, gw);
+
+            rtnh->rtnh_flags = 0;
+
+            if (obj->ip4_route.n_nexthops > 1
+                && NM_FLAGS_HAS(obj->ip_route.r_rtm_flags, (unsigned) (RTNH_F_ONLINK)) && gw != 0) {
+                /* Unlike kernel, we only track the onlink flag per NMPlatformIP4Address, and
+                 * not per nexthop. That is fine for NetworkManager configuring addresses.
+                 * It is not fine for tracking addresses from kernel in platform cache,
+                 * because the rtnh_flags of the nexthops need to be part of nmp_object_id_cmp(). */
+                rtnh->rtnh_flags |= RTNH_F_ONLINK;
+            }
+
+            rtnh->rtnh_len = (char *) nlmsg_tail(nlmsg_hdr(msg)) - (char *) rtnh;
+        }
+
+        nla_nest_end(msg, multipath);
+    }
+
     if (obj->ip_route.mss || obj->ip_route.window || obj->ip_route.cwnd || obj->ip_route.initcwnd
         || obj->ip_route.initrwnd || obj->ip_route.mtu || obj->ip_route.quickack
         || obj->ip_route.rto_min || lock) {
@@ -5462,7 +5749,7 @@ _nl_msg_new_tfilter(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPlatformT
         if (nm_streq(action->kind, NM_PLATFORM_ACTION_KIND_SIMPLE)) {
             const NMPlatformActionSimple *simple = &action->simple;
             struct tc_defact              sel    = {
-                                0,
+                0,
             };
 
             if (!(act_options = nla_nest_start(msg, TCA_ACT_OPTIONS)))
@@ -5476,7 +5763,7 @@ _nl_msg_new_tfilter(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPlatformT
         } else if (nm_streq(action->kind, NM_PLATFORM_ACTION_KIND_MIRRED)) {
             const NMPlatformActionMirred *mirred = &action->mirred;
             struct tc_mirred              sel    = {
-                                0,
+                0,
             };
 
             if (!(act_options = nla_nest_start(msg, TCA_ACT_OPTIONS)))
@@ -5512,25 +5799,25 @@ nla_put_failure:
 
 /*****************************************************************************/
 
-#define ASSERT_SYSCTL_ARGS(pathid, dirfd, path)                                                 \
-    G_STMT_START                                                                                \
-    {                                                                                           \
-        const char *const _pathid = (pathid);                                                   \
-        const int         _dirfd  = (dirfd);                                                    \
-        const char *const _path   = (path);                                                     \
-                                                                                                \
-        nm_assert(_path &&_path[0]);                                                            \
-        g_assert(!strstr(_path, "/../"));                                                       \
-        if (_dirfd < 0) {                                                                       \
-            nm_assert(!_pathid);                                                                \
-            nm_assert(_path[0] == '/');                                                         \
-            nm_assert(g_str_has_prefix(_path, "/proc/sys/") || g_str_has_prefix(_path, "/sys/") \
-                      || g_str_has_prefix(_path, "/proc/net"));                                 \
-        } else {                                                                                \
-            nm_assert(_pathid &&_pathid[0] && _pathid[0] != '/');                               \
-            nm_assert(_path[0] != '/');                                                         \
-        }                                                                                       \
-    }                                                                                           \
+#define ASSERT_SYSCTL_ARGS(pathid, dirfd, path)                                                   \
+    G_STMT_START                                                                                  \
+    {                                                                                             \
+        const char *const _pathid = (pathid);                                                     \
+        const int         _dirfd  = (dirfd);                                                      \
+        const char *const _path   = (path);                                                       \
+                                                                                                  \
+        nm_assert(_path &&_path[0]);                                                              \
+        g_assert(!strstr(_path, "/../"));                                                         \
+        if (_dirfd < 0) {                                                                         \
+            nm_assert(!_pathid);                                                                  \
+            nm_assert(_path[0] == '/');                                                           \
+            nm_assert(NM_STR_HAS_PREFIX(_path, "/proc/sys/") || NM_STR_HAS_PREFIX(_path, "/sys/") \
+                      || NM_STR_HAS_PREFIX(_path, "/proc/net"));                                  \
+        } else {                                                                                  \
+            nm_assert(_pathid &&_pathid[0] && _pathid[0] != '/');                                 \
+            nm_assert(_path[0] != '/');                                                           \
+        }                                                                                         \
+    }                                                                                             \
     G_STMT_END
 
 /*****************************************************************************/
@@ -5599,12 +5886,14 @@ sysctl_set_internal(NMPlatform *platform,
                     const char *path,
                     const char *value)
 {
-    int           fd, tries;
+    int           fd;
+    int           tries;
     gssize        nwrote;
     gssize        len;
     char         *actual;
     gs_free char *actual_free = NULL;
     int           errsv;
+    int           r;
 
     if (dirfd < 0) {
         pathid = path;
@@ -5698,18 +5987,13 @@ sysctl_set_internal(NMPlatform *platform,
         _LOGE("sysctl: failed to set '%s' to '%s' after three attempts", path, value);
     }
 
-    if (nwrote < len - 1) {
-        if (nm_close(fd) != 0) {
-            if (errsv != 0)
-                errno = errsv;
-        } else if (errsv != 0)
-            errno = errsv;
-        else
-            errno = EIO;
-        return FALSE;
-    }
-    if (nm_close(fd) != 0) {
-        /* errno is already properly set. */
+    r = nm_close_with_error(fd);
+    if (r < 0 || nwrote < len - 1) {
+        if (errsv == 0) {
+            /* propagate the error from nm_close_with_error(). */
+            errsv = (r < 0) ? -r : EIO;
+        }
+        errno = errsv;
         return FALSE;
     }
 
@@ -6260,11 +6544,11 @@ static NM_UTILS_LOOKUP_STR_DEFINE(
     NM_UTILS_LOOKUP_ITEM_IGNORE(DELAYED_ACTION_TYPE_REFRESH_ALL_RTNL_ROUTING_RULES_ALL),
     NM_UTILS_LOOKUP_ITEM_IGNORE(__DELAYED_ACTION_TYPE_MAX), );
 
-#define delayed_action_get_list_wait_for_resonse(priv, netlink_protocol, idx)                   \
-    (&g_array_index((priv)->delayed_action.list_wait_for_response_x[nmp_netlink_protocol_check( \
-                        (netlink_protocol))],                                                   \
-                    DelayedActionWaitForNlResponseData,                                         \
-                    (idx)))
+#define delayed_action_get_list_wait_for_resonse(priv, netlink_protocol, idx)                      \
+    (&nm_g_array_index((priv)->delayed_action.list_wait_for_response_x[nmp_netlink_protocol_check( \
+                           (netlink_protocol))],                                                   \
+                       DelayedActionWaitForNlResponseData,                                         \
+                       (idx)))
 
 static const char *
 delayed_action_to_string_full(DelayedActionType action_type,
@@ -6753,6 +7037,14 @@ cache_prune_one_type(NMPlatform *platform, const NMPLookup *lookup)
 
         obj = main_entry->obj;
 
+        if (NMP_OBJECT_GET_TYPE(obj) == NMP_OBJECT_TYPE_IP6_ADDRESS) {
+            const NMPlatformIP6Address *pladdr = NMP_OBJECT_CAST_IP6_ADDRESS(obj);
+
+            if (pladdr->n_ifa_flags & IFA_F_TENTATIVE) {
+                nm_platform_ip6_dadfailed_set(platform, pladdr->ifindex, &pladdr->address, TRUE);
+            }
+        }
+
         _LOGt("cache-prune: prune %s",
               nmp_object_to_string(obj, NMP_OBJECT_TO_STRING_ALL, sbuf, sizeof(sbuf)));
 
@@ -7568,6 +7860,7 @@ _rtnl_handle_msg(NMPlatform *platform, const struct nl_msg_lite *msg)
             gboolean                        resync_required = FALSE;
             gboolean                        only_dirty      = FALSE;
             gboolean                        is_ipv6;
+            gboolean                        route_is_alive;
 
             /* IPv4 routes that are a response to RTM_GETROUTE must have
              * the cloned flag while IPv6 routes don't have to. */
@@ -7597,24 +7890,13 @@ _rtnl_handle_msg(NMPlatform *platform, const struct nl_msg_lite *msg)
                 }
             }
 
-            if (ip_route_ignored_protocol(NMP_OBJECT_CAST_IP_ROUTE(obj))) {
-                /* We ignore certain rtm_protocol, because NetworkManager would only ever
-                 * configure certain protocols. Other routes were not added by NetworkManager
-                 * and we don't need to track them in the platform cache.
-                 *
-                 * This is to help with the performance overhead of a huge number of
-                 * routes, for example with the bird BGP software, that adds routes
-                 * with RTPROT_BIRD protocol.
-                 *
-                 * Even if this is a IPv6 multipath route, we abort (parse_nlmsg_iter). There
-                 * is nothing for us to do. */
-                return;
-            }
+            route_is_alive = ip_route_is_alive(NMP_OBJECT_CAST_IP_ROUTE(obj));
 
             cache_op = nmp_cache_update_netlink_route(cache,
                                                       obj,
                                                       is_dump,
                                                       msghdr->nlmsg_flags,
+                                                      route_is_alive,
                                                       &obj_old,
                                                       &obj_new,
                                                       &obj_replace,
@@ -7661,11 +7943,21 @@ _rtnl_handle_msg(NMPlatform *platform, const struct nl_msg_lite *msg)
                 delayed_action_schedule(platform,
                                         delayed_action_refresh_from_needle_object(obj),
                                         NULL);
+                /* We are done here. */
+                return;
             }
             break;
         }
 
         case RTM_DELADDR:
+            if (NMP_OBJECT_GET_TYPE(obj) == NMP_OBJECT_TYPE_IP6_ADDRESS) {
+                const NMPlatformIP6Address *ip6 = NMP_OBJECT_CAST_IP6_ADDRESS(obj);
+
+                if (ip6->n_ifa_flags & IFA_F_DADFAILED) {
+                    nm_platform_ip6_dadfailed_set(platform, ip6->ifindex, &ip6->address, TRUE);
+                }
+            }
+            /* fall-through */
         case RTM_DELLINK:
         case RTM_DELQDISC:
         case RTM_DELROUTE:
@@ -7948,7 +8240,8 @@ retry:
     } else if (NM_IN_SET(seq_result, -ESRCH, -ENOENT)) {
         log_detail = ", firmware not found";
         result     = -NME_PL_NO_FIRMWARE;
-    } else if (NM_IN_SET(seq_result, -ERANGE) && change_link_type == CHANGE_LINK_TYPE_SET_MTU) {
+    } else if (NM_IN_SET(seq_result, -ERANGE, -EINVAL)
+               && change_link_type == CHANGE_LINK_TYPE_SET_MTU) {
         log_detail = ", setting MTU to requested size is not possible";
         result     = -NME_PL_CANT_SET_MTU;
     } else if (NM_IN_SET(seq_result, -ENFILE) && change_link_type == CHANGE_LINK_TYPE_SET_ADDRESS
@@ -8126,7 +8419,7 @@ static gboolean
 link_set_token(NMPlatform *platform, int ifindex, const NMUtilsIPv6IfaceId *iid)
 {
     nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
-    char                         sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    char                         sbuf[NM_INET_ADDRSTRLEN];
 
     _LOGD("link: change %d: token: set IPv6 address generation token to %s",
           ifindex,
@@ -8202,7 +8495,7 @@ link_set_address(NMPlatform *platform, int ifindex, gconstpointer address, size_
     nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
     const ChangeLinkData         d     = {
                     .set_address =
-                        {
+            {
                             .address = address,
                             .length  = length,
             },
@@ -9310,25 +9603,15 @@ ip6_address_delete(NMPlatform *platform, int ifindex, struct in6_addr addr, guin
 /*****************************************************************************/
 
 static int
-ip_route_add(NMPlatform              *platform,
-             NMPNlmFlags              flags,
-             int                      addr_family,
-             const NMPlatformIPRoute *route)
+ip_route_add(NMPlatform *platform, NMPNlmFlags flags, NMPObject *obj_stack)
 {
     nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
-    NMPObject                    obj;
-
-    nmp_object_stackinit(&obj,
-                         NMP_OBJECT_TYPE_IP_ROUTE(NM_IS_IPv4(addr_family)),
-                         (const NMPlatformObject *) route);
-
-    nm_platform_ip_route_normalize(addr_family, NMP_OBJECT_CAST_IP_ROUTE(&obj));
 
-    nlmsg = _nl_msg_new_route(RTM_NEWROUTE, flags & NMP_NLM_FLAG_FMASK, &obj);
+    nlmsg = _nl_msg_new_route(RTM_NEWROUTE, flags & NMP_NLM_FLAG_FMASK, obj_stack);
     if (!nlmsg)
         g_return_val_if_reached(-NME_BUG);
     return do_add_addrroute(platform,
-                            &obj,
+                            obj_stack,
                             nlmsg,
                             NM_FLAGS_HAS(flags, NMP_NLM_FLAG_SUPPRESS_NETLINK_FAILURE));
 }
@@ -9820,7 +10103,7 @@ continue_reading:
         goto stop;
     }
 
-    hdr = (struct nlmsghdr *) priv->netlink_recv_buf.buf;
+    hdr = NM_CAST_ALIGN(struct nlmsghdr, priv->netlink_recv_buf.buf);
     while (nlmsg_ok(hdr, n)) {
         WaitForNlResponseResult  seq_result;
         gboolean                 process_valid_msg = FALSE;
@@ -10588,8 +10871,8 @@ constructed(GObject *_object)
               : (!nmp_netns_get_current()
                      ? "no netns support"
                      : nm_sprintf_bufa(100,
-                                       "in netns[%p]%s",
-                                       nmp_netns_get_current(),
+                                       "in netns[" NM_HASH_OBFUSCATE_PTR_FMT "]%s",
+                                       NM_HASH_OBFUSCATE_PTR(nmp_netns_get_current()),
                                        nmp_netns_get_current() == nmp_netns_get_initial() ? "/main"
                                                                                           : "")),
           nm_platform_get_use_udev(platform) ? "use" : "no",
@@ -10732,7 +11015,10 @@ path_is_read_only_fs(const char *path)
 }
 
 NMPlatform *
-nm_linux_platform_new(gboolean log_with_ptr, gboolean netns_support, gboolean cache_tc)
+nm_linux_platform_new(NMDedupMultiIndex *multi_idx,
+                      gboolean           log_with_ptr,
+                      gboolean           netns_support,
+                      gboolean           cache_tc)
 {
     gboolean use_udev = FALSE;
 
@@ -10740,6 +11026,8 @@ nm_linux_platform_new(gboolean log_with_ptr, gboolean netns_support, gboolean ca
         use_udev = TRUE;
 
     return g_object_new(NM_TYPE_LINUX_PLATFORM,
+                        NM_PLATFORM_MULTI_IDX,
+                        multi_idx,
                         NM_PLATFORM_LOG_WITH_PTR,
                         log_with_ptr,
                         NM_PLATFORM_USE_UDEV,
diff --git a/src/libnm-platform/nm-linux-platform.h b/src/libnm-platform/nm-linux-platform.h
index 85e06934..08135a4a 100644
--- a/src/libnm-platform/nm-linux-platform.h
+++ b/src/libnm-platform/nm-linux-platform.h
@@ -10,7 +10,7 @@
 
 #define NM_TYPE_LINUX_PLATFORM (nm_linux_platform_get_type())
 #define NM_LINUX_PLATFORM(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_LINUX_PLATFORM, NMLinuxPlatform))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_LINUX_PLATFORM, NMLinuxPlatform))
 #define NM_LINUX_PLATFORM_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_LINUX_PLATFORM, NMLinuxPlatformClass))
 #define NM_IS_LINUX_PLATFORM(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_LINUX_PLATFORM))
@@ -23,6 +23,11 @@ typedef struct _NMLinuxPlatformClass NMLinuxPlatformClass;
 
 GType nm_linux_platform_get_type(void);
 
-NMPlatform *nm_linux_platform_new(gboolean log_with_ptr, gboolean netns_support, gboolean cache_tc);
+struct _NMDedupMultiIndex;
+
+NMPlatform *nm_linux_platform_new(struct _NMDedupMultiIndex *multi_idx,
+                                  gboolean                   log_with_ptr,
+                                  gboolean                   netns_support,
+                                  gboolean                   cache_tc);
 
 #endif /* __NETWORKMANAGER_LINUX_PLATFORM_H__ */
diff --git a/src/libnm-platform/nm-netlink.c b/src/libnm-platform/nm-netlink.c
index e08eee5e..fc704226 100644
--- a/src/libnm-platform/nm-netlink.c
+++ b/src/libnm-platform/nm-netlink.c
@@ -325,7 +325,8 @@ nlmsg_parse_error(const struct nlmsghdr *nlh, const char **out_extack_msg)
         struct nlattr *tb[G_N_ELEMENTS(policy)];
         struct nlattr *tlvs;
 
-        tlvs = (struct nlattr *) ((char *) e + sizeof(*e) + e->msg.nlmsg_len - NLMSG_HDRLEN);
+        tlvs = NM_CAST_ALIGN(struct nlattr,
+                             (((char *) e) + sizeof(*e) + e->msg.nlmsg_len - NLMSG_HDRLEN));
         if (nla_parse_arr(tb, tlvs, nlh->nlmsg_len - sizeof(*e) - e->msg.nlmsg_len, policy) >= 0) {
             if (tb[NLMSGERR_ATTR_MSG])
                 *out_extack_msg = nla_get_string(tb[NLMSGERR_ATTR_MSG]);
@@ -842,7 +843,7 @@ genlmsg_len(const struct genlmsghdr *gnlh)
 {
     const struct nlmsghdr *nlh;
 
-    nlh = (const struct nlmsghdr *) ((const unsigned char *) gnlh - NLMSG_HDRLEN);
+    nlh = NM_CAST_ALIGN(const struct nlmsghdr, (((char *) gnlh) - NLMSG_HDRLEN));
     return (nlh->nlmsg_len - GENL_HDRLEN - NLMSG_HDRLEN);
 }
 
@@ -1253,7 +1254,7 @@ continue_reading:
     if (n <= 0)
         return n;
 
-    hdr = (struct nlmsghdr *) buf;
+    hdr = NM_CAST_ALIGN(struct nlmsghdr, buf);
     while (nlmsg_ok(hdr, n)) {
         nm_auto_nlmsg struct nl_msg *msg = NULL;
 
diff --git a/src/libnm-platform/nm-netlink.h b/src/libnm-platform/nm-netlink.h
index 634be2b4..cdb8a81c 100644
--- a/src/libnm-platform/nm-netlink.h
+++ b/src/libnm-platform/nm-netlink.h
@@ -340,7 +340,7 @@ nla_next(const struct nlattr *nla, int *remaining)
     int totlen = NLA_ALIGN(nla->nla_len);
 
     *remaining -= totlen;
-    return (struct nlattr *) ((char *) nla + totlen);
+    return NM_CAST_ALIGN(struct nlattr, (((char *) nla) + totlen));
 }
 
 #define nla_for_each_attr(pos, head, len, rem) \
@@ -434,7 +434,7 @@ nlmsg_next(struct nlmsghdr *nlh, int *remaining)
 
     *remaining -= totlen;
 
-    return (struct nlmsghdr *) ((unsigned char *) nlh + totlen);
+    return NM_CAST_ALIGN(struct nlmsghdr, (((char *) nlh) + totlen));
 }
 
 int  nlmsg_get_proto(struct nl_msg *msg);
@@ -493,8 +493,9 @@ nlmsg_attrlen(const struct nlmsghdr *nlh, int hdrlen)
 static inline struct nlattr *
 nlmsg_attrdata(const struct nlmsghdr *nlh, int hdrlen)
 {
-    unsigned char *data = nlmsg_data(nlh);
-    return (struct nlattr *) (data + NLMSG_ALIGN(hdrlen));
+    char *data = nlmsg_data(nlh);
+
+    return NM_CAST_ALIGN(struct nlattr, (data + NLMSG_ALIGN(hdrlen)));
 }
 
 static inline struct nlattr *
diff --git a/src/libnm-platform/nm-platform.c b/src/libnm-platform/nm-platform.c
index 198e5f0a..c80d9648 100644
--- a/src/libnm-platform/nm-platform.c
+++ b/src/libnm-platform/nm-platform.c
@@ -115,28 +115,32 @@ nmp_link_address_get_as_bytes(const NMPLinkAddress *addr)
 #define _NMLOG_DOMAIN      LOGD_PLATFORM
 #define _NMLOG_PREFIX_NAME "platform"
 
-#define NMLOG_COMMON(level, name, ...)                                                    \
-    G_STMT_START                                                                          \
-    {                                                                                     \
-        char                    __prefix[32];                                             \
-        const char             *__p_prefix = _NMLOG_PREFIX_NAME;                          \
-        const NMPlatform *const __self     = (self);                                      \
-        const char             *__name     = name;                                        \
-                                                                                          \
-        if (__self && NM_PLATFORM_GET_PRIVATE(__self)->log_with_ptr) {                    \
-            g_snprintf(__prefix, sizeof(__prefix), "%s[%p]", _NMLOG_PREFIX_NAME, __self); \
-            __p_prefix = __prefix;                                                        \
-        }                                                                                 \
-        _nm_log((level),                                                                  \
-                _NMLOG_DOMAIN,                                                            \
-                0,                                                                        \
-                __name,                                                                   \
-                NULL,                                                                     \
-                "%s: %s%s%s" _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                          \
-                __p_prefix,                                                               \
-                NM_PRINT_FMT_QUOTED(__name, "(", __name, ") ", "")                        \
-                    _NM_UTILS_MACRO_REST(__VA_ARGS__));                                   \
-    }                                                                                     \
+#define NMLOG_COMMON(level, name, ...)                                 \
+    G_STMT_START                                                       \
+    {                                                                  \
+        char                    __prefix[64];                          \
+        const char             *__p_prefix = _NMLOG_PREFIX_NAME;       \
+        const NMPlatform *const __self     = (self);                   \
+        const char             *__name     = name;                     \
+                                                                       \
+        if (__self && NM_PLATFORM_GET_PRIVATE(__self)->log_with_ptr) { \
+            g_snprintf(__prefix,                                       \
+                       sizeof(__prefix),                               \
+                       "%s[" NM_HASH_OBFUSCATE_PTR_FMT "]",            \
+                       _NMLOG_PREFIX_NAME,                             \
+                       NM_HASH_OBFUSCATE_PTR(__self));                 \
+            __p_prefix = __prefix;                                     \
+        }                                                              \
+        _nm_log((level),                                               \
+                _NMLOG_DOMAIN,                                         \
+                0,                                                     \
+                __name,                                                \
+                NULL,                                                  \
+                "%s: %s%s%s" _NM_UTILS_MACRO_FIRST(__VA_ARGS__),       \
+                __p_prefix,                                            \
+                NM_PRINT_FMT_QUOTED(__name, "(", __name, ") ", "")     \
+                    _NM_UTILS_MACRO_REST(__VA_ARGS__));                \
+    }                                                                  \
     G_STMT_END
 
 #define _NMLOG(level, ...)                                \
@@ -180,6 +184,7 @@ static guint signals[_NM_PLATFORM_SIGNAL_ID_LAST] = {0};
 
 enum {
     PROP_0,
+    PROP_MULTI_IDX,
     PROP_NETNS_SUPPORT,
     PROP_USE_UDEV,
     PROP_LOG_WITH_PTR,
@@ -195,6 +200,7 @@ typedef struct _NMPlatformPrivate {
     guint              ip4_dev_route_blacklist_check_id;
     guint              ip4_dev_route_blacklist_gc_timeout_id;
     GHashTable        *ip4_dev_route_blacklist_hash;
+    CList              ip6_dadfailed_lst_head;
     NMDedupMultiIndex *multi_idx;
     NMPCache          *cache;
 } NMPlatformPrivate;
@@ -948,9 +954,9 @@ _link_get_all_presort(gconstpointer p_a, gconstpointer p_b, gpointer sort_by_nam
     const NMPlatformLink *b = NMP_OBJECT_CAST_LINK(*((const NMPObject **) p_b));
 
     /* Loopback always first */
-    if (a->ifindex == 1)
+    if (a->ifindex == NM_LOOPBACK_IFINDEX)
         return -1;
-    if (b->ifindex == 1)
+    if (b->ifindex == NM_LOOPBACK_IFINDEX)
         return 1;
 
     if (GPOINTER_TO_INT(sort_by_name)) {
@@ -1369,6 +1375,18 @@ nm_platform_link_add(NMPlatform            *self,
                                                      buf_p,
                                                      buf_len);
                    break;
+               case NM_LINK_TYPE_VTI:
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_platform_lnk_vti_to_string((const NMPlatformLnkVti *) extra_data,
+                                                 buf_p,
+                                                 buf_len);
+                   break;
+               case NM_LINK_TYPE_VTI6:
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_platform_lnk_vti6_to_string((const NMPlatformLnkVti6 *) extra_data,
+                                                  buf_p,
+                                                  buf_len);
+                   break;
                case NM_LINK_TYPE_BOND:
                    nm_strbuf_append_str(&buf_p, &buf_len, ", ");
                    nm_platform_lnk_bond_to_string((const NMPlatformLnkBond *) extra_data,
@@ -2417,6 +2435,18 @@ nm_platform_link_get_lnk_vrf(NMPlatform *self, int ifindex, const NMPlatformLink
     return _link_get_lnk(self, ifindex, NM_LINK_TYPE_VRF, out_link);
 }
 
+const NMPlatformLnkVti *
+nm_platform_link_get_lnk_vti(NMPlatform *self, int ifindex, const NMPlatformLink **out_link)
+{
+    return _link_get_lnk(self, ifindex, NM_LINK_TYPE_VTI, out_link);
+}
+
+const NMPlatformLnkVti6 *
+nm_platform_link_get_lnk_vti6(NMPlatform *self, int ifindex, const NMPlatformLink **out_link)
+{
+    return _link_get_lnk(self, ifindex, NM_LINK_TYPE_VTI6, out_link);
+}
+
 const NMPlatformLnkVxlan *
 nm_platform_link_get_lnk_vxlan(NMPlatform *self, int ifindex, const NMPlatformLink **out_link)
 {
@@ -3305,52 +3335,14 @@ nm_platform_wpan_set_channel(NMPlatform *self, int ifindex, guint8 page, guint8
     return klass->wpan_set_channel(self, ifindex, page, channel);
 }
 
-#define TO_STRING_DEV_BUF_SIZE (5 + 15 + 1)
-static const char *
-_to_string_dev(NMPlatform *self, int ifindex, char *buf, size_t size)
-{
-    nm_assert(buf);
-    nm_assert(size >= TO_STRING_DEV_BUF_SIZE);
-
-    if (ifindex) {
-        const char *name = ifindex > 0 && self ? nm_platform_link_get_name(self, ifindex) : NULL;
-        char       *buf2;
-
-        strcpy(buf, " dev ");
-        buf2 = buf + 5;
-        size -= 5;
-
-        if (name)
-            g_strlcpy(buf2, name, size);
-        else
-            g_snprintf(buf2, size, "%d", ifindex);
-    } else
-        buf[0] = 0;
-
-    return buf;
-}
-
-#define TO_STRING_IFA_FLAGS_BUF_SIZE 256
-
-static const char *
-_to_string_ifa_flags(guint32 ifa_flags, char *buf, gsize size)
-{
-#define S_FLAGS_PREFIX " flags "
-    nm_assert(buf && size >= TO_STRING_IFA_FLAGS_BUF_SIZE && size > NM_STRLEN(S_FLAGS_PREFIX));
+/*****************************************************************************/
 
-    if (!ifa_flags)
-        buf[0] = '\0';
-    else {
-        nm_platform_addr_flags2str(ifa_flags,
-                                   &buf[NM_STRLEN(S_FLAGS_PREFIX)],
-                                   size - NM_STRLEN(S_FLAGS_PREFIX));
-        if (buf[NM_STRLEN(S_FLAGS_PREFIX)] == '\0')
-            buf[0] = '\0';
-        else
-            memcpy(buf, S_FLAGS_PREFIX, NM_STRLEN(S_FLAGS_PREFIX));
-    }
-    return buf;
-}
+#define _to_string_dev(arr, ifindex)                                                   \
+    ({                                                                                 \
+        const int _ifindex = (ifindex);                                                \
+                                                                                       \
+        _ifindex ? nm_sprintf_buf((arr), " dev %d", ifindex) : nm_str_truncate((arr)); \
+    })
 
 /*****************************************************************************/
 
@@ -3557,72 +3549,6 @@ nm_platform_lookup_clone(NMPlatform            *self,
                                                  user_data);
 }
 
-void
-nm_platform_ip4_address_set_addr(NMPlatformIP4Address *addr, in_addr_t address, guint8 plen)
-{
-    nm_assert(plen <= 32);
-
-    addr->address      = address;
-    addr->peer_address = address;
-    addr->plen         = plen;
-}
-
-const struct in6_addr *
-nm_platform_ip6_address_get_peer(const NMPlatformIP6Address *addr)
-{
-    if (IN6_IS_ADDR_UNSPECIFIED(&addr->peer_address)
-        || IN6_ARE_ADDR_EQUAL(&addr->peer_address, &addr->address))
-        return &addr->address;
-    return &addr->peer_address;
-}
-
-gboolean
-nm_platform_ip_address_match(int                        addr_family,
-                             const NMPlatformIPAddress *address,
-                             NMPlatformMatchFlags       match_flag)
-{
-    nm_assert(!NM_FLAGS_ANY(
-        match_flag,
-        ~(NM_PLATFORM_MATCH_WITH_ADDRTYPE__ANY | NM_PLATFORM_MATCH_WITH_ADDRSTATE__ANY)));
-    nm_assert(NM_FLAGS_ANY(match_flag, NM_PLATFORM_MATCH_WITH_ADDRTYPE__ANY));
-    nm_assert(NM_FLAGS_ANY(match_flag, NM_PLATFORM_MATCH_WITH_ADDRSTATE__ANY));
-
-    if (addr_family == AF_INET) {
-        if (nm_utils_ip4_address_is_link_local(((NMPlatformIP4Address *) address)->address)) {
-            if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL))
-                return FALSE;
-        } else {
-            if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRTYPE_NORMAL))
-                return FALSE;
-        }
-    } else {
-        if (IN6_IS_ADDR_LINKLOCAL(address->address_ptr)) {
-            if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL))
-                return FALSE;
-        } else {
-            if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRTYPE_NORMAL))
-                return FALSE;
-        }
-    }
-
-    if (NM_FLAGS_HAS(address->n_ifa_flags, IFA_F_DADFAILED)) {
-        if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRSTATE_DADFAILED))
-            return FALSE;
-    } else if (NM_FLAGS_HAS(address->n_ifa_flags, IFA_F_TENTATIVE)
-               && !NM_FLAGS_HAS(address->n_ifa_flags, IFA_F_OPTIMISTIC)) {
-        if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRSTATE_TENTATIVE))
-            return FALSE;
-    } else if (NM_FLAGS_HAS(address->n_ifa_flags, IFA_F_DEPRECATED)) {
-        if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRSTATE_DEPRECATED))
-            return FALSE;
-    } else {
-        if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRSTATE_NORMAL))
-            return FALSE;
-    }
-
-    return TRUE;
-}
-
 gboolean
 nm_platform_ip4_address_add(NMPlatform *self,
                             int         ifindex,
@@ -3711,6 +3637,9 @@ nm_platform_ip6_address_add(NMPlatform     *self,
         _LOG3D("address: adding or updating IPv6 address: %s",
                nm_platform_ip6_address_to_string(&addr, sbuf, sizeof(sbuf)));
     }
+
+    nm_platform_ip6_dadfailed_set(self, ifindex, &address, FALSE);
+
     return klass
         ->ip6_address_add(self, ifindex, address, plen, peer_address, lifetime, preferred, flags);
 }
@@ -3722,9 +3651,9 @@ nm_platform_ip4_address_delete(NMPlatform *self,
                                guint8      plen,
                                in_addr_t   peer_address)
 {
-    char str_dev[TO_STRING_DEV_BUF_SIZE];
-    char b1[NM_UTILS_INET_ADDRSTRLEN];
-    char b2[NM_UTILS_INET_ADDRSTRLEN];
+    char str_dev[30];
+    char b1[NM_INET_ADDRSTRLEN];
+    char b2[NM_INET_ADDRSTRLEN];
     char str_peer[INET_ADDRSTRLEN + 50];
 
     _CHECK_SELF(self, klass, FALSE);
@@ -3733,20 +3662,20 @@ nm_platform_ip4_address_delete(NMPlatform *self,
     g_return_val_if_fail(plen <= 32, FALSE);
 
     _LOG3D("address: deleting IPv4 address %s/%d, %s%s",
-           _nm_utils_inet4_ntop(address, b1),
+           nm_inet4_ntop(address, b1),
            plen,
            peer_address != address
-               ? nm_sprintf_buf(str_peer, "peer %s, ", _nm_utils_inet4_ntop(peer_address, b2))
+               ? nm_sprintf_buf(str_peer, "peer %s, ", nm_inet4_ntop(peer_address, b2))
                : "",
-           _to_string_dev(self, ifindex, str_dev, sizeof(str_dev)));
+           _to_string_dev(str_dev, ifindex));
     return klass->ip4_address_delete(self, ifindex, address, plen, peer_address);
 }
 
 gboolean
 nm_platform_ip6_address_delete(NMPlatform *self, int ifindex, struct in6_addr address, guint8 plen)
 {
-    char str_dev[TO_STRING_DEV_BUF_SIZE];
-    char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    char str_dev[30];
+    char sbuf[NM_INET_ADDRSTRLEN];
 
     _CHECK_SELF(self, klass, FALSE);
 
@@ -3754,9 +3683,9 @@ nm_platform_ip6_address_delete(NMPlatform *self, int ifindex, struct in6_addr ad
     g_return_val_if_fail(plen <= 128, FALSE);
 
     _LOG3D("address: deleting IPv6 address %s/%d, %s",
-           _nm_utils_inet6_ntop(&address, sbuf),
+           nm_inet6_ntop(&address, sbuf),
            plen,
-           _to_string_dev(self, ifindex, str_dev, sizeof(str_dev)));
+           _to_string_dev(str_dev, ifindex));
     return klass->ip6_address_delete(self, ifindex, address, plen);
 }
 
@@ -3940,13 +3869,10 @@ static guint
 _ip4_addr_subnets_hash(gconstpointer ptr)
 {
     const NMPlatformIP4Address *addr = NMP_OBJECT_CAST_IP4_ADDRESS(ptr);
-    NMHashState                 h;
 
-    nm_hash_init(&h, 3282159733);
-    nm_hash_update_vals(&h,
+    return nm_hash_vals(3282159733,
                         addr->plen,
-                        nm_utils_ip4_address_clear_host_address(addr->address, addr->plen));
-    return nm_hash_complete(&h);
+                        nm_ip4_addr_clear_host_address(addr->address, addr->plen));
 }
 
 static gboolean
@@ -3956,8 +3882,8 @@ _ip4_addr_subnets_equal(gconstpointer p_a, gconstpointer p_b)
     const NMPlatformIP4Address *b = NMP_OBJECT_CAST_IP4_ADDRESS(p_b);
 
     return a->plen == b->plen
-           && (nm_utils_ip4_address_clear_host_address(a->address, a->plen)
-               == nm_utils_ip4_address_clear_host_address(b->address, b->plen));
+           && (nm_ip4_addr_clear_host_address(a->address, a->plen)
+               == nm_ip4_addr_clear_host_address(b->address, b->plen));
 }
 
 static GHashTable *
@@ -4107,6 +4033,9 @@ ip6_address_scope_cmp_descending(gconstpointer p_a, gconstpointer p_b, gpointer
  *   by the function.
  *   Addresses that are both contained in @known_addresses and @addresses_prune
  *   will be configured.
+ * @flags: #NMPIPAddressSyncFlags to affect the sync. If "with-noprefixroute"
+ *   flag is set, the method will automatically set IFA_F_NOPREFIXROUTE for
+ *   all addresses.
  *
  * A convenience function to synchronize addresses for a specific interface
  * with the least possible disturbance. It simply removes addresses that are
@@ -4115,11 +4044,12 @@ ip6_address_scope_cmp_descending(gconstpointer p_a, gconstpointer p_b, gpointer
  * Returns: %TRUE on success.
  */
 gboolean
-nm_platform_ip_address_sync(NMPlatform *self,
-                            int         addr_family,
-                            int         ifindex,
-                            GPtrArray  *known_addresses,
-                            GPtrArray  *addresses_prune)
+nm_platform_ip_address_sync(NMPlatform           *self,
+                            int                   addr_family,
+                            int                   ifindex,
+                            GPtrArray            *known_addresses,
+                            GPtrArray            *addresses_prune,
+                            NMPIPAddressSyncFlags flags)
 {
     gint32                         now     = 0;
     const int                      IS_IPv4 = NM_IS_IPv4(addr_family);
@@ -4531,7 +4461,9 @@ next_plat:;
                     nm_platform_ip4_broadcast_address_from_addr(&known_address->a4),
                     lifetime,
                     preferred,
-                    known_address->ax.a_no_auto_noprefixroute ? 0 : IFA_F_NOPREFIXROUTE,
+                    NM_FLAGS_HAS(flags, NMP_IP_ADDRESS_SYNC_FLAGS_WITH_NOPREFIXROUTE)
+                        ? IFA_F_NOPREFIXROUTE
+                        : 0,
                     known_address->a4.label))
                 success = FALSE;
         } else {
@@ -4543,7 +4475,9 @@ next_plat:;
                     known_address->a6.peer_address,
                     lifetime,
                     preferred,
-                    (known_address->ax.a_no_auto_noprefixroute ? 0 : IFA_F_NOPREFIXROUTE)
+                    (NM_FLAGS_HAS(flags, NMP_IP_ADDRESS_SYNC_FLAGS_WITH_NOPREFIXROUTE)
+                         ? IFA_F_NOPREFIXROUTE
+                         : 0)
                         | known_address->a6.n_ifa_flags))
                 success = FALSE;
         }
@@ -4572,7 +4506,12 @@ nm_platform_ip_address_flush(NMPlatform *self, int addr_family, int ifindex)
         addresses_prune =
             nm_platform_ip_address_get_prune_list(self, addr_family2, ifindex, NULL, 0);
 
-        if (!nm_platform_ip_address_sync(self, addr_family2, ifindex, NULL, addresses_prune))
+        if (!nm_platform_ip_address_sync(self,
+                                         addr_family2,
+                                         ifindex,
+                                         NULL,
+                                         addresses_prune,
+                                         NMP_IP_ADDRESS_SYNC_FLAGS_NONE))
             success = FALSE;
     }
     return success;
@@ -4654,7 +4593,26 @@ nm_platform_ip_address_get_prune_list(NMPlatform            *self,
     c_list_for_each (iter, &head_entry->lst_entries_head) {
         const NMPObject *obj = c_list_entry(iter, NMDedupMultiEntry, lst_entries)->obj;
 
-        if (!IS_IPv4) {
+        if (IS_IPv4) {
+            const NMPlatformIP4Address *a4 = NMP_OBJECT_CAST_IP4_ADDRESS(obj);
+
+            if (a4->address == NM_IPV4LO_ADDR1 && a4->plen == NM_IPV4LO_PREFIXLEN) {
+                const NMPlatformIP4Address addr = (NMPlatformIP4Address){
+                    .ifindex                   = NM_LOOPBACK_IFINDEX,
+                    .address                   = NM_IPV4LO_ADDR1,
+                    .peer_address              = NM_IPV4LO_ADDR1,
+                    .plen                      = NM_IPV4LO_PREFIXLEN,
+                    .use_ip4_broadcast_address = TRUE,
+                };
+
+                if (nm_platform_ip4_address_cmp(a4,
+                                                &addr,
+                                                NM_PLATFORM_IP_ADDRESS_CMP_TYPE_SEMANTICALLY)
+                    == 0) {
+                    continue;
+                }
+            }
+        } else {
             const NMPlatformIP6Address *a6 = NMP_OBJECT_CAST_IP6_ADDRESS(obj);
 
             if (NM_FLAGS_HAS(a6->n_ifa_flags, IFA_F_SECONDARY)
@@ -4710,9 +4668,6 @@ nm_platform_ip_route_get_prune_list(NMPlatform            *self,
     GPtrArray                   *routes_prune = NULL;
     const NMDedupMultiHeadEntry *head_entry;
     CList                       *iter;
-    NMPlatformIP4Route           rt_local4;
-    NMPlatformIP6Route           rt_local6;
-    NMPlatformIP6Route           rt_mcast6;
     const NMPlatformLink        *pllink;
     const NMPlatformLnkVrf      *lnk_vrf;
     guint32                      local_table;
@@ -4737,10 +4692,6 @@ nm_platform_ip_route_get_prune_list(NMPlatform            *self,
         lnk_vrf = nm_platform_link_get_lnk_vrf(self, pllink->master, NULL);
     local_table = lnk_vrf ? lnk_vrf->table : RT_TABLE_LOCAL;
 
-    rt_local4.plen = 0;
-    rt_local6.plen = 0;
-    rt_mcast6.plen = 0;
-
     c_list_for_each (iter, &head_entry->lst_entries_head) {
         const NMPObject          *obj = c_list_entry(iter, NMDedupMultiEntry, lst_entries)->obj;
         const NMPlatformIPXRoute *rt  = NMP_OBJECT_CAST_IPX_ROUTE(obj);
@@ -4762,6 +4713,44 @@ nm_platform_ip_route_get_prune_list(NMPlatform            *self,
              * pruning them. */
 
             if (NM_IS_IPv4(addr_family)) {
+                if (ifindex == NM_LOOPBACK_IFINDEX
+                    && NM_IN_SET(rt->r4.network, NM_IPV4LO_ADDR1, NM_IPV4LO_NETWORK)) {
+                    NMPlatformIP4Route r;
+
+                    if (rt->r4.network == NM_IPV4LO_ADDR1) {
+                        r = (NMPlatformIP4Route){
+                            .ifindex       = NM_LOOPBACK_IFINDEX,
+                            .type_coerced  = nm_platform_route_type_coerce(RTN_LOCAL),
+                            .table_coerced = nm_platform_route_table_coerce(local_table),
+                            .network       = NM_IPV4LO_ADDR1,
+                            .plen          = 32,
+                            .metric        = 0,
+                            .rt_source     = NM_IPV4LO_ADDR1,
+                            .scope_inv     = nm_platform_route_scope_inv(RT_SCOPE_HOST),
+                            .pref_src      = NM_IPV4LO_ADDR1,
+                        };
+                    } else {
+                        r = (NMPlatformIP4Route){
+                            .ifindex       = NM_LOOPBACK_IFINDEX,
+                            .type_coerced  = nm_platform_route_type_coerce(RTN_LOCAL),
+                            .table_coerced = nm_platform_route_table_coerce(local_table),
+                            .network       = NM_IPV4LO_NETWORK,
+                            .plen          = NM_IPV4LO_PREFIXLEN,
+                            .metric        = 0,
+                            .rt_source     = NM_IPV4LO_ADDR1,
+                            .scope_inv     = nm_platform_route_scope_inv(RT_SCOPE_HOST),
+                            .pref_src      = NM_IPV4LO_ADDR1,
+                        };
+                    }
+
+                    if (nm_platform_ip4_route_cmp(&rt->r4,
+                                                  &r,
+                                                  NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
+                        == 0) {
+                        continue;
+                    }
+                }
+
                 /* for each IPv4 address kernel adds a route like
                  *
                  *  local $ADDR dev $IFACE table local proto kernel scope host src $PRIMARY_ADDR
@@ -4772,29 +4761,26 @@ nm_platform_ip_route_get_prune_list(NMPlatform            *self,
                     && rt->rx.metric == 0
                     && rt->r4.scope_inv == nm_platform_route_scope_inv(RT_SCOPE_HOST)
                     && rt->r4.gateway == INADDR_ANY) {
-                    if (rt_local4.plen == 0) {
-                        rt_local4 = (NMPlatformIP4Route){
-                            .ifindex       = ifindex,
-                            .type_coerced  = nm_platform_route_type_coerce(RTN_LOCAL),
-                            .plen          = 32,
-                            .rt_source     = NM_IP_CONFIG_SOURCE_RTPROT_KERNEL,
-                            .metric        = 0,
-                            .table_coerced = nm_platform_route_table_coerce(local_table),
-                            .scope_inv     = nm_platform_route_scope_inv(RT_SCOPE_HOST),
-                            .gateway       = INADDR_ANY,
-                        };
-                    }
-
-                    /* the possible "network" depends on the addresses we have. We don't check that
-                     * carefully. If the other parameters match, we assume that this route is the one
-                     * generated by kernel. */
-                    rt_local4.network  = rt->r4.network;
-                    rt_local4.pref_src = rt->r4.pref_src;
+                    const NMPlatformIP4Route r = {
+                        .ifindex       = ifindex,
+                        .type_coerced  = nm_platform_route_type_coerce(RTN_LOCAL),
+                        .plen          = 32,
+                        .rt_source     = NM_IP_CONFIG_SOURCE_RTPROT_KERNEL,
+                        .metric        = 0,
+                        .table_coerced = nm_platform_route_table_coerce(local_table),
+                        .scope_inv     = nm_platform_route_scope_inv(RT_SCOPE_HOST),
+                        .gateway       = INADDR_ANY,
+                        /* the possible "network" depends on the addresses we have. We don't check that
+                         * carefully. If the other parameters match, we assume that this route is the one
+                         * generated by kernel. */
+                        .network  = rt->r4.network,
+                        .pref_src = rt->r4.pref_src,
+                    };
 
                     /* to be more confident about comparing the value, use our nm_platform_ip4_route_cmp()
                      * implementation. That will also consider parameters that we leave unspecified here. */
                     if (nm_platform_ip4_route_cmp(&rt->r4,
-                                                  &rt_local4,
+                                                  &r,
                                                   NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
                         == 0)
                         continue;
@@ -4809,23 +4795,20 @@ nm_platform_ip_route_get_prune_list(NMPlatform            *self,
                     && rt->rx.plen == 128 && rt->rx.rt_source == NM_IP_CONFIG_SOURCE_RTPROT_KERNEL
                     && rt->rx.metric == 0 && rt->r6.rt_pref == NM_ICMPV6_ROUTER_PREF_MEDIUM
                     && IN6_IS_ADDR_UNSPECIFIED(&rt->r6.gateway)) {
-                    if (rt_local6.plen == 0) {
-                        rt_local6 = (NMPlatformIP6Route){
-                            .ifindex       = ifindex,
-                            .type_coerced  = nm_platform_route_type_coerce(RTN_LOCAL),
-                            .plen          = 128,
-                            .rt_source     = NM_IP_CONFIG_SOURCE_RTPROT_KERNEL,
-                            .metric        = 0,
-                            .table_coerced = nm_platform_route_table_coerce(local_table),
-                            .rt_pref       = NM_ICMPV6_ROUTER_PREF_MEDIUM,
-                            .gateway       = IN6ADDR_ANY_INIT,
-                        };
-                    }
-
-                    rt_local6.network = rt->r6.network;
+                    const NMPlatformIP6Route r = {
+                        .ifindex       = ifindex,
+                        .type_coerced  = nm_platform_route_type_coerce(RTN_LOCAL),
+                        .plen          = 128,
+                        .rt_source     = NM_IP_CONFIG_SOURCE_RTPROT_KERNEL,
+                        .metric        = 0,
+                        .table_coerced = nm_platform_route_table_coerce(local_table),
+                        .rt_pref       = NM_ICMPV6_ROUTER_PREF_MEDIUM,
+                        .gateway       = IN6ADDR_ANY_INIT,
+                        .network       = rt->r6.network,
+                    };
 
                     if (nm_platform_ip6_route_cmp(&rt->r6,
-                                                  &rt_local6,
+                                                  &r,
                                                   NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
                         == 0)
                         continue;
@@ -4847,22 +4830,21 @@ nm_platform_ip_route_get_prune_list(NMPlatform            *self,
                     && rt->rx.plen == 8 && rt->rx.rt_source == NM_IP_CONFIG_SOURCE_RTPROT_BOOT
                     && rt->rx.metric == 256 && rt->r6.rt_pref == NM_ICMPV6_ROUTER_PREF_MEDIUM
                     && IN6_IS_ADDR_UNSPECIFIED(&rt->r6.gateway)) {
-                    if (rt_mcast6.plen == 0) {
-                        rt_mcast6 = (NMPlatformIP6Route){
-                            .ifindex       = ifindex,
-                            .type_coerced  = nm_platform_route_type_coerce(RTN_UNICAST),
-                            .plen          = 8,
-                            .rt_source     = NM_IP_CONFIG_SOURCE_RTPROT_BOOT,
-                            .metric        = 256,
-                            .table_coerced = nm_platform_route_table_coerce(local_table),
-                            .rt_pref       = NM_ICMPV6_ROUTER_PREF_MEDIUM,
-                            .gateway       = IN6ADDR_ANY_INIT,
-                            .network = {{{0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}},
-                        };
-                    }
+                    const NMPlatformIP6Route r = {
+                        .ifindex       = ifindex,
+                        .type_coerced  = nm_platform_route_type_coerce(RTN_UNICAST),
+                        .plen          = 8,
+                        .rt_source     = NM_IP_CONFIG_SOURCE_RTPROT_BOOT,
+                        .metric        = 256,
+                        .table_coerced = nm_platform_route_table_coerce(local_table),
+                        .rt_pref       = NM_ICMPV6_ROUTER_PREF_MEDIUM,
+                        .gateway       = IN6ADDR_ANY_INIT,
+                        .network =
+                            NM_IN6ADDR_INIT(0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+                    };
 
                     if (nm_platform_ip6_route_cmp(&rt->r6,
-                                                  &rt_mcast6,
+                                                  &r,
                                                   NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
                         == 0)
                         continue;
@@ -5251,15 +5233,15 @@ nm_platform_ip_route_normalize(int addr_family, NMPlatformIPRoute *route)
         r4                = (NMPlatformIP4Route *) route;
         route->metric     = nm_platform_ip4_route_get_effective_metric(r4);
         route->metric_any = FALSE;
-        r4->network       = nm_utils_ip4_address_clear_host_address(r4->network, r4->plen);
+        r4->network       = nm_ip4_addr_clear_host_address(r4->network, r4->plen);
         r4->scope_inv     = _ip_route_scope_inv_get_normalized(r4);
         break;
     case AF_INET6:
         r6                = (NMPlatformIP6Route *) route;
         route->metric     = nm_platform_ip6_route_get_effective_metric(r6);
         route->metric_any = FALSE;
-        nm_utils_ip6_address_clear_host_address(&r6->network, &r6->network, r6->plen);
-        nm_utils_ip6_address_clear_host_address(&r6->src, &r6->src, r6->src_plen);
+        nm_ip6_addr_clear_host_address(&r6->network, &r6->network, r6->plen);
+        nm_ip6_addr_clear_host_address(&r6->src, &r6->src, r6->src_plen);
         break;
     default:
         nm_assert_not_reached();
@@ -5268,55 +5250,107 @@ nm_platform_ip_route_normalize(int addr_family, NMPlatformIPRoute *route)
 }
 
 static int
-_ip_route_add(NMPlatform *self, NMPNlmFlags flags, int addr_family, gconstpointer route)
+_ip_route_add(NMPlatform *self, NMPNlmFlags flags, NMPObject *obj_stack)
 {
     char sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
     int  ifindex;
 
     _CHECK_SELF(self, klass, FALSE);
 
-    nm_assert(route);
-    nm_assert(NM_IN_SET(addr_family, AF_INET, AF_INET6));
+    /* The caller already ensures that this is a stack allocated copy, that
+     * - stays alive for the duration of the call.
+     * - that the ip_route_add() implementation is allowed to modify.
+     */
+    nm_assert(obj_stack);
+    nm_assert(NMP_OBJECT_IS_STACKINIT(obj_stack));
+    nm_assert(NM_IN_SET(NMP_OBJECT_GET_TYPE(obj_stack),
+                        NMP_OBJECT_TYPE_IP4_ROUTE,
+                        NMP_OBJECT_TYPE_IP6_ROUTE));
+
+    nm_assert(NMP_OBJECT_GET_TYPE(obj_stack) != NMP_OBJECT_TYPE_IP4_ROUTE
+              || obj_stack->ip4_route.n_nexthops <= 1u || obj_stack->_ip4_route.extra_nexthops);
+
+    nm_platform_ip_route_normalize(NMP_OBJECT_GET_ADDR_FAMILY((obj_stack)),
+                                   NMP_OBJECT_CAST_IP_ROUTE(obj_stack));
+
+    ifindex = obj_stack->ip_route.ifindex;
 
-    ifindex = ((const NMPlatformIPRoute *) route)->ifindex;
     _LOG3D("route: %-10s IPv%c route: %s",
            _nmp_nlm_flag_to_string(flags & NMP_NLM_FLAG_FMASK),
-           nm_utils_addr_family_to_char(addr_family),
-           NM_IS_IPv4(addr_family) ? nm_platform_ip4_route_to_string(route, sbuf, sizeof(sbuf))
-                                   : nm_platform_ip6_route_to_string(route, sbuf, sizeof(sbuf)));
-
-    return klass->ip_route_add(self, flags, addr_family, route);
+           nm_utils_addr_family_to_char(NMP_OBJECT_GET_ADDR_FAMILY(obj_stack)),
+           nmp_object_to_string(obj_stack, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
+
+    /* At this point, we pass "obj_stack" to the klass->ip_route_add() implementation.
+     * The callee can rely on:
+     * - the object being normalized and validated.
+     * - staying fully alive until the function returns. In this case it
+     *   is stack allocated (and the potential "extra_nexthops" array is
+     *   guaranteed to stay alive too).
+     */
+    return klass->ip_route_add(self, flags, obj_stack);
 }
 
 int
-nm_platform_ip_route_add(NMPlatform *self, NMPNlmFlags flags, const NMPObject *route)
+nm_platform_ip_route_add(NMPlatform *self, NMPNlmFlags flags, const NMPObject *obj)
 {
-    int addr_family;
+    nm_auto_nmpobj const NMPObject *obj_keep_alive = NULL;
+    NMPObject                       obj_stack;
 
-    switch (NMP_OBJECT_GET_TYPE(route)) {
-    case NMP_OBJECT_TYPE_IP4_ROUTE:
-        addr_family = AF_INET;
-        break;
-    case NMP_OBJECT_TYPE_IP6_ROUTE:
-        addr_family = AF_INET6;
-        break;
-    default:
-        g_return_val_if_reached(FALSE);
+    nm_assert(
+        NM_IN_SET(NMP_OBJECT_GET_TYPE(obj), NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE));
+
+    nmp_object_stackinit(&obj_stack, NMP_OBJECT_GET_TYPE(obj), &obj->ip_route);
+
+    if (NMP_OBJECT_GET_TYPE(obj) == NMP_OBJECT_TYPE_IP4_ROUTE && obj->ip4_route.n_nexthops > 1u) {
+        /* Ensure @obj stays alive, so we can alias extra_nexthops from the stackallocated
+         * @obj_stack. */
+        nm_assert(obj->_ip4_route.extra_nexthops);
+        obj_keep_alive                      = nmp_object_ref(obj);
+        obj_stack._ip4_route.extra_nexthops = obj->_ip4_route.extra_nexthops;
     }
 
-    return _ip_route_add(self, flags, addr_family, NMP_OBJECT_CAST_IP_ROUTE(route));
+    return _ip_route_add(self, flags, &obj_stack);
 }
 
 int
-nm_platform_ip4_route_add(NMPlatform *self, NMPNlmFlags flags, const NMPlatformIP4Route *route)
+nm_platform_ip4_route_add(NMPlatform                   *self,
+                          NMPNlmFlags                   flags,
+                          const NMPlatformIP4Route     *route,
+                          const NMPlatformIP4RtNextHop *extra_nexthops)
 {
-    return _ip_route_add(self, flags, AF_INET, route);
+    gs_free NMPlatformIP4RtNextHop *extra_nexthops_free = NULL;
+    NMPObject                       obj;
+
+    nm_assert(route);
+    nm_assert(route->n_nexthops <= 1u || extra_nexthops);
+
+    nmp_object_stackinit(&obj, NMP_OBJECT_TYPE_IP4_ROUTE, (const NMPlatformObject *) route);
+
+    if (route->n_nexthops > 1u) {
+        nm_assert(extra_nexthops);
+        /* we need to ensure that @extra_nexthops stays alive until the function returns.
+         * Copy the buffer.
+         *
+         * This is probably not necessary, because likely the caller will somehow ensure that
+         * the extra_nexthops stay alive. Still do it, because it is a very unusual case and
+         * likely cheap. */
+        obj._ip4_route.extra_nexthops =
+            nm_memdup_maybe_a(500u,
+                              extra_nexthops,
+                              sizeof(extra_nexthops[0]) * (route->n_nexthops - 1u),
+                              &extra_nexthops_free);
+    }
+
+    return _ip_route_add(self, flags, &obj);
 }
 
 int
 nm_platform_ip6_route_add(NMPlatform *self, NMPNlmFlags flags, const NMPlatformIP6Route *route)
 {
-    return _ip_route_add(self, flags, AF_INET6, route);
+    NMPObject obj;
+
+    nmp_object_stackinit(&obj, NMP_OBJECT_TYPE_IP6_ROUTE, (const NMPlatformObject *) route);
+    return _ip_route_add(self, flags, &obj);
 }
 
 gboolean
@@ -5364,7 +5398,7 @@ nm_platform_ip_route_get(NMPlatform   *self,
     char                      sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
     nm_auto_nmpobj NMPObject *route = NULL;
     int                       result;
-    char                      buf[NM_UTILS_INET_ADDRSTRLEN];
+    char                      buf[NM_INET_ADDRSTRLEN];
     char                      buf_oif[64];
 
     _CHECK_SELF(self, klass, FALSE);
@@ -5839,37 +5873,6 @@ nm_platform_vlan_qos_mapping_to_string(const char             *name,
     return buf;
 }
 
-static const char *
-_lifetime_to_string(guint32 timestamp, guint32 lifetime, gint32 now, char *buf, size_t buf_size)
-{
-    if (lifetime == NM_PLATFORM_LIFETIME_PERMANENT)
-        return "forever";
-
-    g_snprintf(buf,
-               buf_size,
-               "%usec",
-               nmp_utils_lifetime_rebase_relative_time_on_now(timestamp, lifetime, now));
-    return buf;
-}
-
-static const char *
-_lifetime_summary_to_string(gint32  now,
-                            guint32 timestamp,
-                            guint32 preferred,
-                            guint32 lifetime,
-                            char   *buf,
-                            size_t  buf_size)
-{
-    g_snprintf(buf,
-               buf_size,
-               " lifetime %d-%u[%u,%u]",
-               (signed) now,
-               (unsigned) timestamp,
-               (unsigned) preferred,
-               (unsigned) lifetime);
-    return buf;
-}
-
 /**
  * nm_platform_link_to_string:
  * @route: pointer to NMPlatformLink address structure
@@ -5893,7 +5896,7 @@ nm_platform_link_to_string(const NMPlatformLink *link, char *buf, gsize len)
     char        str_address[_NM_UTILS_HWADDR_LEN_MAX * 3];
     char        str_perm_address[_NM_UTILS_HWADDR_LEN_MAX * 3];
     char        str_broadcast[_NM_UTILS_HWADDR_LEN_MAX * 3];
-    char        str_inet6_token[NM_UTILS_INET_ADDRSTRLEN];
+    char        str_inet6_token[NM_INET_ADDRSTRLEN];
     const char *str_link_type;
 
     if (!nm_utils_to_string_buffer_init_null(link, &buf, &len))
@@ -6180,7 +6183,7 @@ nm_platform_lnk_bond_to_string(const NMPlatformLnkBond *lnk, char *buf, gsize le
             char target[INET_ADDRSTRLEN];
 
             nm_strbuf_append_c(&buf, &len, ' ');
-            nm_strbuf_append_str(&buf, &len, _nm_utils_inet4_ntop(lnk->arp_ip_target[i], target));
+            nm_strbuf_append_str(&buf, &len, nm_inet4_ntop(lnk->arp_ip_target[i], target));
         }
     }
     return buf;
@@ -6190,18 +6193,18 @@ const char *
 nm_platform_lnk_gre_to_string(const NMPlatformLnkGre *lnk, char *buf, gsize len)
 {
     char str_local[30];
-    char str_local1[NM_UTILS_INET_ADDRSTRLEN];
+    char str_local1[NM_INET_ADDRSTRLEN];
     char str_remote[30];
-    char str_remote1[NM_UTILS_INET_ADDRSTRLEN];
+    char str_remote1[NM_INET_ADDRSTRLEN];
     char str_ttl[30];
     char str_tos[30];
     char str_parent_ifindex[30];
     char str_input_flags[30];
     char str_output_flags[30];
     char str_input_key[30];
-    char str_input_key1[NM_UTILS_INET_ADDRSTRLEN];
+    char str_input_key1[NM_INET_ADDRSTRLEN];
     char str_output_key[30];
-    char str_output_key1[NM_UTILS_INET_ADDRSTRLEN];
+    char str_output_key1[NM_INET_ADDRSTRLEN];
 
     if (!nm_utils_to_string_buffer_init_null(lnk, &buf, &len))
         return buf;
@@ -6222,13 +6225,11 @@ nm_platform_lnk_gre_to_string(const NMPlatformLnkGre *lnk, char *buf, gsize len)
         "%s"    /* okey */
         "",
         lnk->is_tap ? "tap" : "",
-        lnk->remote ? nm_sprintf_buf(str_remote,
-                                     " remote %s",
-                                     _nm_utils_inet4_ntop(lnk->remote, str_remote1))
-                    : "",
-        lnk->local
-            ? nm_sprintf_buf(str_local, " local %s", _nm_utils_inet4_ntop(lnk->local, str_local1))
+        lnk->remote
+            ? nm_sprintf_buf(str_remote, " remote %s", nm_inet4_ntop(lnk->remote, str_remote1))
             : "",
+        lnk->local ? nm_sprintf_buf(str_local, " local %s", nm_inet4_ntop(lnk->local, str_local1))
+                   : "",
         lnk->parent_ifindex ? nm_sprintf_buf(str_parent_ifindex, " dev %d", lnk->parent_ifindex)
                             : "",
         lnk->ttl ? nm_sprintf_buf(str_ttl, " ttl %u", lnk->ttl) : " ttl inherit",
@@ -6241,12 +6242,12 @@ nm_platform_lnk_gre_to_string(const NMPlatformLnkGre *lnk, char *buf, gsize len)
         NM_FLAGS_HAS(lnk->input_flags, GRE_KEY) || lnk->input_key
             ? nm_sprintf_buf(str_input_key,
                              " ikey %s",
-                             _nm_utils_inet4_ntop(lnk->input_key, str_input_key1))
+                             nm_inet4_ntop(lnk->input_key, str_input_key1))
             : "",
         NM_FLAGS_HAS(lnk->output_flags, GRE_KEY) || lnk->output_key
             ? nm_sprintf_buf(str_output_key,
                              " okey %s",
-                             _nm_utils_inet4_ntop(lnk->output_key, str_output_key1))
+                             nm_inet4_ntop(lnk->output_key, str_output_key1))
             : "");
     return buf;
 }
@@ -6275,9 +6276,9 @@ const char *
 nm_platform_lnk_ip6tnl_to_string(const NMPlatformLnkIp6Tnl *lnk, char *buf, gsize len)
 {
     char  str_local[30];
-    char  str_local1[NM_UTILS_INET_ADDRSTRLEN];
+    char  str_local1[NM_INET_ADDRSTRLEN];
     char  str_remote[30];
-    char  str_remote1[NM_UTILS_INET_ADDRSTRLEN];
+    char  str_remote1[NM_INET_ADDRSTRLEN];
     char  str_ttl[30];
     char  str_tclass[30];
     char  str_flow[30];
@@ -6294,32 +6295,32 @@ nm_platform_lnk_ip6tnl_to_string(const NMPlatformLnkIp6Tnl *lnk, char *buf, gsiz
     else
         str_type = "ip6tnl";
 
-    g_snprintf(
-        buf,
-        len,
-        "%s" /* type */
-        "%s" /* remote */
-        "%s" /* local */
-        "%s" /* parent_ifindex */
-        "%s" /* ttl */
-        "%s" /* tclass */
-        "%s" /* encap limit */
-        "%s" /* flow label */
-        "%s" /* proto */
-        " flags 0x%x"
-        "",
-        str_type,
-        nm_sprintf_buf(str_remote, " remote %s", _nm_utils_inet6_ntop(&lnk->remote, str_remote1)),
-        nm_sprintf_buf(str_local, " local %s", _nm_utils_inet6_ntop(&lnk->local, str_local1)),
-        lnk->parent_ifindex ? nm_sprintf_buf(str_parent_ifindex, " dev %d", lnk->parent_ifindex)
-                            : "",
-        lnk->ttl ? nm_sprintf_buf(str_ttl, " ttl %u", lnk->ttl) : " ttl inherit",
-        lnk->tclass == 1 ? " tclass inherit"
-                         : nm_sprintf_buf(str_tclass, " tclass 0x%x", lnk->tclass),
-        nm_sprintf_buf(str_encap, " encap-limit %u", lnk->encap_limit),
-        nm_sprintf_buf(str_flow, " flow-label 0x05%x", lnk->flow_label),
-        nm_sprintf_buf(str_proto, " proto %u", lnk->proto),
-        (guint) lnk->flags);
+    g_snprintf(buf,
+               len,
+               "%s" /* type */
+               "%s" /* remote */
+               "%s" /* local */
+               "%s" /* parent_ifindex */
+               "%s" /* ttl */
+               "%s" /* tclass */
+               "%s" /* encap limit */
+               "%s" /* flow label */
+               "%s" /* proto */
+               " flags 0x%x"
+               "",
+               str_type,
+               nm_sprintf_buf(str_remote, " remote %s", nm_inet6_ntop(&lnk->remote, str_remote1)),
+               nm_sprintf_buf(str_local, " local %s", nm_inet6_ntop(&lnk->local, str_local1)),
+               lnk->parent_ifindex
+                   ? nm_sprintf_buf(str_parent_ifindex, " dev %d", lnk->parent_ifindex)
+                   : "",
+               lnk->ttl ? nm_sprintf_buf(str_ttl, " ttl %u", lnk->ttl) : " ttl inherit",
+               lnk->tclass == 1 ? " tclass inherit"
+                                : nm_sprintf_buf(str_tclass, " tclass 0x%x", lnk->tclass),
+               nm_sprintf_buf(str_encap, " encap-limit %u", lnk->encap_limit),
+               nm_sprintf_buf(str_flow, " flow-label 0x05%x", lnk->flow_label),
+               nm_sprintf_buf(str_proto, " proto %u", lnk->proto),
+               (guint) lnk->flags);
     return buf;
 }
 
@@ -6327,9 +6328,9 @@ const char *
 nm_platform_lnk_ipip_to_string(const NMPlatformLnkIpIp *lnk, char *buf, gsize len)
 {
     char str_local[30];
-    char str_local1[NM_UTILS_INET_ADDRSTRLEN];
+    char str_local1[NM_INET_ADDRSTRLEN];
     char str_remote[30];
-    char str_remote1[NM_UTILS_INET_ADDRSTRLEN];
+    char str_remote1[NM_INET_ADDRSTRLEN];
     char str_ttl[30];
     char str_tos[30];
     char str_parent_ifindex[30];
@@ -6348,13 +6349,11 @@ nm_platform_lnk_ipip_to_string(const NMPlatformLnkIpIp *lnk, char *buf, gsize le
         "%s" /* tos */
         "%s" /* path_mtu_discovery */
         "",
-        lnk->remote ? nm_sprintf_buf(str_remote,
-                                     " remote %s",
-                                     _nm_utils_inet4_ntop(lnk->remote, str_remote1))
-                    : "",
-        lnk->local
-            ? nm_sprintf_buf(str_local, " local %s", _nm_utils_inet4_ntop(lnk->local, str_local1))
+        lnk->remote
+            ? nm_sprintf_buf(str_remote, " remote %s", nm_inet4_ntop(lnk->remote, str_remote1))
             : "",
+        lnk->local ? nm_sprintf_buf(str_local, " local %s", nm_inet4_ntop(lnk->local, str_local1))
+                   : "",
         lnk->parent_ifindex ? nm_sprintf_buf(str_parent_ifindex, " dev %d", lnk->parent_ifindex)
                             : "",
         lnk->ttl ? nm_sprintf_buf(str_ttl, " ttl %u", lnk->ttl) : " ttl inherit",
@@ -6417,9 +6416,9 @@ const char *
 nm_platform_lnk_sit_to_string(const NMPlatformLnkSit *lnk, char *buf, gsize len)
 {
     char str_local[30];
-    char str_local1[NM_UTILS_INET_ADDRSTRLEN];
+    char str_local1[NM_INET_ADDRSTRLEN];
     char str_remote[30];
-    char str_remote1[NM_UTILS_INET_ADDRSTRLEN];
+    char str_remote1[NM_INET_ADDRSTRLEN];
     char str_ttl[30];
     char str_tos[30];
     char str_flags[30];
@@ -6442,13 +6441,11 @@ nm_platform_lnk_sit_to_string(const NMPlatformLnkSit *lnk, char *buf, gsize len)
         "%s" /* flags */
         "%s" /* proto */
         "",
-        lnk->remote ? nm_sprintf_buf(str_remote,
-                                     " remote %s",
-                                     _nm_utils_inet4_ntop(lnk->remote, str_remote1))
-                    : "",
-        lnk->local
-            ? nm_sprintf_buf(str_local, " local %s", _nm_utils_inet4_ntop(lnk->local, str_local1))
+        lnk->remote
+            ? nm_sprintf_buf(str_remote, " remote %s", nm_inet4_ntop(lnk->remote, str_remote1))
             : "",
+        lnk->local ? nm_sprintf_buf(str_local, " local %s", nm_inet4_ntop(lnk->local, str_local1))
+                   : "",
         lnk->parent_ifindex ? nm_sprintf_buf(str_parent_ifindex, " dev %d", lnk->parent_ifindex)
                             : "",
         lnk->ttl ? nm_sprintf_buf(str_ttl, " ttl %u", lnk->ttl) : " ttl inherit",
@@ -6502,19 +6499,112 @@ const char *
 nm_platform_lnk_vlan_to_string(const NMPlatformLnkVlan *lnk, char *buf, gsize len)
 {
     char *b;
+    char  protocol[32];
 
     if (!nm_utils_to_string_buffer_init_null(lnk, &buf, &len))
         return buf;
 
     b = buf;
 
+    switch (lnk->protocol) {
+    case ETH_P_8021AD:
+        nm_sprintf_buf(protocol, "802.1ad");
+        break;
+    case ETH_P_8021Q:
+        nm_sprintf_buf(protocol, "802.1Q");
+        break;
+    default:
+        nm_sprintf_buf(protocol, "0x%04hx", lnk->protocol);
+        break;
+    }
+
     nm_strbuf_append(&b, &len, "vlan %u", lnk->id);
+    nm_strbuf_append(&b, &len, " protocol %s", protocol);
     if (lnk->flags)
         nm_strbuf_append(&b, &len, " flags 0x%x", lnk->flags);
     return buf;
 }
 
 const char *
+nm_platform_lnk_vti_to_string(const NMPlatformLnkVti *lnk, char *buf, gsize len)
+{
+    char str_local[30 + NM_INET_ADDRSTRLEN];
+    char str_local1[NM_INET_ADDRSTRLEN];
+    char str_remote[30 + NM_INET_ADDRSTRLEN];
+    char str_remote1[NM_INET_ADDRSTRLEN];
+    char str_ikey[30];
+    char str_okey[30];
+    char str_fwmark[30];
+    char str_parent_ifindex[30];
+
+    if (!nm_utils_to_string_buffer_init_null(lnk, &buf, &len))
+        return buf;
+
+    g_snprintf(
+        buf,
+        len,
+        "vti"
+        "%s" /* remote */
+        "%s" /* local */
+        "%s" /* parent_ifindex */
+        "%s" /* ikey */
+        "%s" /* okey */
+        "%s" /* fwmark */
+        "",
+        lnk->remote
+            ? nm_sprintf_buf(str_remote, " remote %s", nm_inet4_ntop(lnk->remote, str_remote1))
+            : "",
+        lnk->local ? nm_sprintf_buf(str_local, " local %s", nm_inet4_ntop(lnk->local, str_local1))
+                   : "",
+        lnk->parent_ifindex ? nm_sprintf_buf(str_parent_ifindex, " dev %d", lnk->parent_ifindex)
+                            : "",
+        lnk->ikey ? nm_sprintf_buf(str_ikey, " ikey %u", lnk->ikey) : "",
+        lnk->okey ? nm_sprintf_buf(str_okey, " okey %u", lnk->okey) : "",
+        lnk->fwmark ? nm_sprintf_buf(str_fwmark, " fwmark 0x%x", lnk->fwmark) : "");
+    return buf;
+}
+
+const char *
+nm_platform_lnk_vti6_to_string(const NMPlatformLnkVti6 *lnk, char *buf, gsize len)
+{
+    char str_local[30 + NM_INET_ADDRSTRLEN];
+    char str_local1[NM_INET_ADDRSTRLEN];
+    char str_remote[30 + NM_INET_ADDRSTRLEN];
+    char str_remote1[NM_INET_ADDRSTRLEN];
+    char str_ikey[30];
+    char str_okey[30];
+    char str_fwmark[30];
+    char str_parent_ifindex[30];
+
+    if (!nm_utils_to_string_buffer_init_null(lnk, &buf, &len))
+        return buf;
+
+    g_snprintf(
+        buf,
+        len,
+        "vti6"
+        "%s" /* remote */
+        "%s" /* local */
+        "%s" /* parent_ifindex */
+        "%s" /* ikey */
+        "%s" /* okey */
+        "%s" /* fwmark */
+        "",
+        IN6_IS_ADDR_UNSPECIFIED(&lnk->remote)
+            ? ""
+            : nm_sprintf_buf(str_remote, " remote %s", nm_inet6_ntop(&lnk->remote, str_remote1)),
+        IN6_IS_ADDR_UNSPECIFIED(&lnk->local)
+            ? ""
+            : nm_sprintf_buf(str_local, " local %s", nm_inet6_ntop(&lnk->local, str_local1)),
+        lnk->parent_ifindex ? nm_sprintf_buf(str_parent_ifindex, " dev %d", lnk->parent_ifindex)
+                            : "",
+        lnk->ikey ? nm_sprintf_buf(str_ikey, " ikey %u", lnk->ikey) : "",
+        lnk->okey ? nm_sprintf_buf(str_okey, " okey %u", lnk->okey) : "",
+        lnk->fwmark ? nm_sprintf_buf(str_fwmark, " fwmark 0x%x", lnk->fwmark) : "");
+    return buf;
+}
+
+const char *
 nm_platform_lnk_vrf_to_string(const NMPlatformLnkVrf *lnk, char *buf, gsize len)
 {
     char *b;
@@ -6535,13 +6625,13 @@ nm_platform_lnk_vxlan_to_string(const NMPlatformLnkVxlan *lnk, char *buf, gsize
     char str_group6[100];
     char str_local[100];
     char str_local6[100];
-    char str_dev[25];
+    char str_dev[30];
     char str_limit[25];
     char str_src_port[35];
     char str_dst_port[25];
     char str_tos[25];
     char str_ttl[25];
-    char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    char sbuf[NM_INET_ADDRSTRLEN];
 
     if (!nm_utils_to_string_buffer_init_null(lnk, &buf, &len))
         return buf;
@@ -6553,7 +6643,7 @@ nm_platform_lnk_vxlan_to_string(const NMPlatformLnkVxlan *lnk, char *buf, gsize
                    sizeof(str_group),
                    " %s %s",
                    IN_MULTICAST(ntohl(lnk->group)) ? "group" : "remote",
-                   _nm_utils_inet4_ntop(lnk->group, sbuf));
+                   nm_inet4_ntop(lnk->group, sbuf));
     }
     if (IN6_IS_ADDR_UNSPECIFIED(&lnk->group6))
         str_group6[0] = '\0';
@@ -6563,16 +6653,13 @@ nm_platform_lnk_vxlan_to_string(const NMPlatformLnkVxlan *lnk, char *buf, gsize
                    " %s%s %s",
                    IN6_IS_ADDR_MULTICAST(&lnk->group6) ? "group" : "remote",
                    str_group[0] ? "6" : "", /* usually, a vxlan has either v4 or v6 only. */
-                   _nm_utils_inet6_ntop(&lnk->group6, sbuf));
+                   nm_inet6_ntop(&lnk->group6, sbuf));
     }
 
     if (lnk->local == 0)
         str_local[0] = '\0';
     else {
-        g_snprintf(str_local,
-                   sizeof(str_local),
-                   " local %s",
-                   _nm_utils_inet4_ntop(lnk->local, sbuf));
+        g_snprintf(str_local, sizeof(str_local), " local %s", nm_inet4_ntop(lnk->local, sbuf));
     }
     if (IN6_IS_ADDR_UNSPECIFIED(&lnk->local6))
         str_local6[0] = '\0';
@@ -6581,7 +6668,7 @@ nm_platform_lnk_vxlan_to_string(const NMPlatformLnkVxlan *lnk, char *buf, gsize
                    sizeof(str_local6),
                    " local%s %s",
                    str_local[0] ? "6" : "", /* usually, a vxlan has either v4 or v6 only. */
-                   _nm_utils_inet6_ntop(&lnk->local6, sbuf));
+                   nm_inet6_ntop(&lnk->local6, sbuf));
     }
 
     g_snprintf(
@@ -6609,7 +6696,7 @@ nm_platform_lnk_vxlan_to_string(const NMPlatformLnkVxlan *lnk, char *buf, gsize
         str_group6,
         str_local,
         str_local6,
-        lnk->parent_ifindex ? nm_sprintf_buf(str_dev, " dev %d", lnk->parent_ifindex) : "",
+        _to_string_dev(str_dev, lnk->parent_ifindex),
         lnk->src_port_min || lnk->src_port_max
             ? nm_sprintf_buf(str_src_port, " srcport %u %u", lnk->src_port_min, lnk->src_port_max)
             : "",
@@ -6631,9 +6718,9 @@ nm_platform_wireguard_peer_to_string(const NMPWireGuardPeer *peer, char *buf, gs
 {
     char         *buf0           = buf;
     gs_free char *public_key_b64 = NULL;
-    char          s_sockaddr[NM_UTILS_INET_ADDRSTRLEN + 100];
+    char          s_sockaddr[NM_INET_ADDRSTRLEN + 100];
     char          s_endpoint[20 + sizeof(s_sockaddr)];
-    char          s_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char          s_addr[NM_INET_ADDRSTRLEN];
     char          s_keepalive[100];
     guint         i;
 
@@ -6677,7 +6764,7 @@ nm_platform_wireguard_peer_to_string(const NMPWireGuardPeer *peer, char *buf, gs
         nm_strbuf_append(&buf,
                          &len,
                          " %s/%u",
-                         nm_utils_inet_ntop(allowed_ip->family, &allowed_ip->addr, s_addr),
+                         nm_inet_ntop(allowed_ip->family, &allowed_ip->addr, s_addr),
                          allowed_ip->mask);
     }
 
@@ -6713,246 +6800,6 @@ nm_platform_lnk_wireguard_to_string(const NMPlatformLnkWireGuard *lnk, char *buf
     return buf;
 }
 
-/**
- * nm_platform_ip4_address_to_string:
- * @route: pointer to NMPlatformIP4Address address structure
- * @buf: (allow-none): an optional buffer. If %NULL, a static buffer is used.
- * @len: the size of the @buf. If @buf is %NULL, this argument is ignored.
- *
- * A method for converting an address struct into a string representation.
- *
- * Example output: ""
- *
- * Returns: a string representation of the address.
- */
-const char *
-nm_platform_ip4_address_to_string(const NMPlatformIP4Address *address, char *buf, gsize len)
-{
-    char        s_flags[TO_STRING_IFA_FLAGS_BUF_SIZE];
-    char        s_address[INET_ADDRSTRLEN];
-    char        s_peer[INET_ADDRSTRLEN];
-    char        str_dev[TO_STRING_DEV_BUF_SIZE];
-    char        str_label[32];
-    char        str_lft[30], str_pref[30], str_time[50], s_source[50];
-    char       *str_peer = NULL;
-    const char *str_lft_p, *str_pref_p, *str_time_p;
-    gint32      now = nm_utils_get_monotonic_timestamp_sec();
-    in_addr_t   broadcast_address;
-    char        str_broadcast[INET_ADDRSTRLEN];
-
-    if (!nm_utils_to_string_buffer_init_null(address, &buf, &len))
-        return buf;
-
-    inet_ntop(AF_INET, &address->address, s_address, sizeof(s_address));
-
-    if (address->peer_address != address->address) {
-        inet_ntop(AF_INET, &address->peer_address, s_peer, sizeof(s_peer));
-        str_peer = g_strconcat(" ptp ", s_peer, NULL);
-    }
-
-    _to_string_dev(NULL, address->ifindex, str_dev, sizeof(str_dev));
-
-    if (*address->label)
-        g_snprintf(str_label, sizeof(str_label), " label %s", address->label);
-    else
-        str_label[0] = 0;
-
-    str_lft_p = _lifetime_to_string(address->timestamp,
-                                    address->lifetime ?: NM_PLATFORM_LIFETIME_PERMANENT,
-                                    now,
-                                    str_lft,
-                                    sizeof(str_lft)),
-    str_pref_p =
-        (address->lifetime == address->preferred)
-            ? str_lft_p
-            : (_lifetime_to_string(address->timestamp,
-                                   address->lifetime ? MIN(address->preferred, address->lifetime)
-                                                     : NM_PLATFORM_LIFETIME_PERMANENT,
-                                   now,
-                                   str_pref,
-                                   sizeof(str_pref)));
-    str_time_p = _lifetime_summary_to_string(now,
-                                             address->timestamp,
-                                             address->preferred,
-                                             address->lifetime,
-                                             str_time,
-                                             sizeof(str_time));
-
-    broadcast_address = nm_platform_ip4_broadcast_address_from_addr(address);
-
-    g_snprintf(
-        buf,
-        len,
-        "%s/%d"
-        "%s%s" /* broadcast */
-        " lft %s"
-        " pref %s"
-        "%s" /* time */
-        "%s" /* peer  */
-        "%s" /* dev */
-        "%s" /* flags */
-        "%s" /* label */
-        " src %s"
-        "%s" /* a_acd_not_ready */
-        "%s" /* a_force_commit */
-        "%s" /* a_no_auto_noprefixroute */
-        "",
-        s_address,
-        address->plen,
-        broadcast_address != 0u || address->use_ip4_broadcast_address
-            ? (address->use_ip4_broadcast_address ? " brd " : " brd* ")
-            : "",
-        broadcast_address != 0u || address->use_ip4_broadcast_address
-            ? _nm_utils_inet4_ntop(broadcast_address, str_broadcast)
-            : "",
-        str_lft_p,
-        str_pref_p,
-        str_time_p,
-        str_peer ?: "",
-        str_dev,
-        _to_string_ifa_flags(address->n_ifa_flags, s_flags, sizeof(s_flags)),
-        str_label,
-        nmp_utils_ip_config_source_to_string(address->addr_source, s_source, sizeof(s_source)),
-        address->a_acd_not_ready ? " ip4acd-not-ready" : "",
-        address->a_force_commit ? " force-commit" : "",
-        address->a_no_auto_noprefixroute ? " no-auto-noprefixroute" : "");
-    g_free(str_peer);
-    return buf;
-}
-
-NM_UTILS_FLAGS2STR_DEFINE(nm_platform_link_flags2str,
-                          unsigned,
-                          NM_UTILS_FLAGS2STR(IFF_LOOPBACK, "loopback"),
-                          NM_UTILS_FLAGS2STR(IFF_BROADCAST, "broadcast"),
-                          NM_UTILS_FLAGS2STR(IFF_POINTOPOINT, "pointopoint"),
-                          NM_UTILS_FLAGS2STR(IFF_MULTICAST, "multicast"),
-                          NM_UTILS_FLAGS2STR(IFF_NOARP, "noarp"),
-                          NM_UTILS_FLAGS2STR(IFF_ALLMULTI, "allmulti"),
-                          NM_UTILS_FLAGS2STR(IFF_PROMISC, "promisc"),
-                          NM_UTILS_FLAGS2STR(IFF_MASTER, "master"),
-                          NM_UTILS_FLAGS2STR(IFF_SLAVE, "slave"),
-                          NM_UTILS_FLAGS2STR(IFF_DEBUG, "debug"),
-                          NM_UTILS_FLAGS2STR(IFF_DYNAMIC, "dynamic"),
-                          NM_UTILS_FLAGS2STR(IFF_AUTOMEDIA, "automedia"),
-                          NM_UTILS_FLAGS2STR(IFF_PORTSEL, "portsel"),
-                          NM_UTILS_FLAGS2STR(IFF_NOTRAILERS, "notrailers"),
-                          NM_UTILS_FLAGS2STR(IFF_UP, "up"),
-                          NM_UTILS_FLAGS2STR(IFF_RUNNING, "running"),
-                          NM_UTILS_FLAGS2STR(IFF_LOWER_UP, "lowerup"),
-                          NM_UTILS_FLAGS2STR(IFF_DORMANT, "dormant"),
-                          NM_UTILS_FLAGS2STR(IFF_ECHO, "echo"), );
-
-NM_UTILS_ENUM2STR_DEFINE(nm_platform_link_inet6_addrgenmode2str,
-                         guint8,
-                         NM_UTILS_ENUM2STR(NM_IN6_ADDR_GEN_MODE_NONE, "none"),
-                         NM_UTILS_ENUM2STR(NM_IN6_ADDR_GEN_MODE_EUI64, "eui64"),
-                         NM_UTILS_ENUM2STR(NM_IN6_ADDR_GEN_MODE_STABLE_PRIVACY, "stable-privacy"),
-                         NM_UTILS_ENUM2STR(NM_IN6_ADDR_GEN_MODE_RANDOM, "random"), );
-
-G_STATIC_ASSERT(IFA_F_SECONDARY == IFA_F_TEMPORARY);
-
-NM_UTILS_FLAGS2STR_DEFINE(nm_platform_addr_flags2str,
-                          unsigned,
-                          NM_UTILS_FLAGS2STR(IFA_F_SECONDARY, "secondary"),
-                          NM_UTILS_FLAGS2STR(IFA_F_NODAD, "nodad"),
-                          NM_UTILS_FLAGS2STR(IFA_F_OPTIMISTIC, "optimistic"),
-                          NM_UTILS_FLAGS2STR(IFA_F_DADFAILED, "dadfailed"),
-                          NM_UTILS_FLAGS2STR(IFA_F_HOMEADDRESS, "homeaddress"),
-                          NM_UTILS_FLAGS2STR(IFA_F_DEPRECATED, "deprecated"),
-                          NM_UTILS_FLAGS2STR(IFA_F_TENTATIVE, "tentative"),
-                          NM_UTILS_FLAGS2STR(IFA_F_PERMANENT, "permanent"),
-                          NM_UTILS_FLAGS2STR(IFA_F_MANAGETEMPADDR, "mngtmpaddr"),
-                          NM_UTILS_FLAGS2STR(IFA_F_NOPREFIXROUTE, "noprefixroute"),
-                          NM_UTILS_FLAGS2STR(IFA_F_MCAUTOJOIN, "mcautojoin"),
-                          NM_UTILS_FLAGS2STR(IFA_F_STABLE_PRIVACY, "stable-privacy"), );
-
-NM_UTILS_ENUM2STR_DEFINE(nm_platform_route_scope2str,
-                         int,
-                         NM_UTILS_ENUM2STR(RT_SCOPE_NOWHERE, "nowhere"),
-                         NM_UTILS_ENUM2STR(RT_SCOPE_HOST, "host"),
-                         NM_UTILS_ENUM2STR(RT_SCOPE_LINK, "link"),
-                         NM_UTILS_ENUM2STR(RT_SCOPE_SITE, "site"),
-                         NM_UTILS_ENUM2STR(RT_SCOPE_UNIVERSE, "global"), );
-
-/**
- * nm_platform_ip6_address_to_string:
- * @route: pointer to NMPlatformIP6Address address structure
- * @buf: (allow-none): an optional buffer. If %NULL, a static buffer is used.
- * @len: the size of the @buf. If @buf is %NULL, this argument is ignored.
- *
- * A method for converting an address struct into a string representation.
- *
- * Example output: "2001:db8:0:f101::1/64 lft 4294967295 pref 4294967295 time 16922666 on dev em1"
- *
- * Returns: a string representation of the address.
- */
-const char *
-nm_platform_ip6_address_to_string(const NMPlatformIP6Address *address, char *buf, gsize len)
-{
-    char        s_flags[TO_STRING_IFA_FLAGS_BUF_SIZE];
-    char        s_address[INET6_ADDRSTRLEN];
-    char        s_peer[INET6_ADDRSTRLEN];
-    char        str_lft[30], str_pref[30], str_time[50], s_source[50];
-    char        str_dev[TO_STRING_DEV_BUF_SIZE];
-    char       *str_peer = NULL;
-    const char *str_lft_p, *str_pref_p, *str_time_p;
-    gint32      now = nm_utils_get_monotonic_timestamp_sec();
-
-    if (!nm_utils_to_string_buffer_init_null(address, &buf, &len))
-        return buf;
-
-    inet_ntop(AF_INET6, &address->address, s_address, sizeof(s_address));
-
-    if (!IN6_IS_ADDR_UNSPECIFIED(&address->peer_address)) {
-        inet_ntop(AF_INET6, &address->peer_address, s_peer, sizeof(s_peer));
-        str_peer = g_strconcat(" ptp ", s_peer, NULL);
-    }
-
-    _to_string_dev(NULL, address->ifindex, str_dev, sizeof(str_dev));
-
-    str_lft_p = _lifetime_to_string(address->timestamp,
-                                    address->lifetime ?: NM_PLATFORM_LIFETIME_PERMANENT,
-                                    now,
-                                    str_lft,
-                                    sizeof(str_lft)),
-    str_pref_p =
-        (address->lifetime == address->preferred)
-            ? str_lft_p
-            : (_lifetime_to_string(address->timestamp,
-                                   address->lifetime ? MIN(address->preferred, address->lifetime)
-                                                     : NM_PLATFORM_LIFETIME_PERMANENT,
-                                   now,
-                                   str_pref,
-                                   sizeof(str_pref)));
-    str_time_p = _lifetime_summary_to_string(now,
-                                             address->timestamp,
-                                             address->preferred,
-                                             address->lifetime,
-                                             str_time,
-                                             sizeof(str_time));
-
-    g_snprintf(
-        buf,
-        len,
-        "%s/%d lft %s pref %s%s%s%s%s src %s"
-        "%s" /* a_force_commit */
-        "%s" /* a_no_auto_noprefixroute */
-        "",
-        s_address,
-        address->plen,
-        str_lft_p,
-        str_pref_p,
-        str_time_p,
-        str_peer ?: "",
-        str_dev,
-        _to_string_ifa_flags(address->n_ifa_flags, s_flags, sizeof(s_flags)),
-        nmp_utils_ip_config_source_to_string(address->addr_source, s_source, sizeof(s_source)),
-        address->a_force_commit ? " force-commit" : "",
-        address->a_no_auto_noprefixroute ? " no-auto-noprefixroute" : "");
-    g_free(str_peer);
-    return buf;
-}
-
 static NM_UTILS_FLAGS2STR_DEFINE(_rtm_flags_to_string,
                                  unsigned,
                                  NM_UTILS_FLAGS2STR(RTNH_F_DEAD, "dead"),
@@ -6990,6 +6837,10 @@ _rtm_flags_to_string_full(char *buf, gsize buf_size, unsigned rtm_flags)
 /**
  * nm_platform_ip4_route_to_string:
  * @route: pointer to NMPlatformIP4Route route structure
+ * @extra_nexthops: (allow-none): the route might be a ECMP multihop route
+ *   (with n_nexthops > 1). In that case, provide the list of extra hops
+ *   to print too. It is allowed for a multihop route to omit the extra hops
+ *   by passing NULL.
  * @buf: (allow-none): an optional buffer. If %NULL, a static buffer is used.
  * @len: the size of the @buf. If @buf is %NULL, this argument is ignored.
  *
@@ -7000,30 +6851,40 @@ _rtm_flags_to_string_full(char *buf, gsize buf_size, unsigned rtm_flags)
  * Returns: a string representation of the route.
  */
 const char *
-nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsize len)
-{
-    char s_network[INET_ADDRSTRLEN];
-    char s_gateway[INET_ADDRSTRLEN];
-    char s_pref_src[INET_ADDRSTRLEN];
-    char str_dev[TO_STRING_DEV_BUF_SIZE];
-    char str_mss[32];
-    char str_table[30];
-    char str_scope[30];
-    char s_source[50];
-    char str_tos[32];
-    char str_window[32];
-    char str_cwnd[32];
-    char str_initcwnd[32];
-    char str_initrwnd[32];
-    char str_rto_min[32];
-    char str_mtu[32];
-    char str_rtm_flags[_RTM_FLAGS_TO_STRING_MAXLEN];
-    char str_type[30];
-    char str_metric[30];
+nm_platform_ip4_route_to_string_full(const NMPlatformIP4Route     *route,
+                                     const NMPlatformIP4RtNextHop *extra_nexthops,
+                                     char                         *buf,
+                                     gsize                         len)
+{
+    char *buf0;
+    char  s_network[INET_ADDRSTRLEN];
+    char  s_gateway[INET_ADDRSTRLEN];
+    char  s_pref_src[INET_ADDRSTRLEN];
+    char  str_dev[30];
+    char  str_mss[32];
+    char  str_table[30];
+    char  str_scope[30];
+    char  s_source[50];
+    char  str_tos[32];
+    char  str_window[32];
+    char  str_cwnd[32];
+    char  str_initcwnd[32];
+    char  str_initrwnd[32];
+    char  str_rto_min[32];
+    char  str_mtu[32];
+    char  str_rtm_flags[_RTM_FLAGS_TO_STRING_MAXLEN];
+    char  str_type[30];
+    char  str_metric[30];
+    char  weight_str[20];
+    guint n_nexthops;
 
     if (!nm_utils_to_string_buffer_init_null(route, &buf, &len))
         return buf;
 
+    buf0 = buf;
+
+    n_nexthops = nm_platform_ip4_route_get_n_nexthops(route);
+
     inet_ntop(AF_INET, &route->network, s_network, sizeof(s_network));
 
     if (route->gateway == 0)
@@ -7031,16 +6892,15 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz
     else
         inet_ntop(AF_INET, &route->gateway, s_gateway, sizeof(s_gateway));
 
-    _to_string_dev(NULL, route->ifindex, str_dev, sizeof(str_dev));
-
-    g_snprintf(
-        buf,
-        len,
+    nm_strbuf_append(
+        &buf,
+        &len,
         "type %s " /* type */
         "%s"       /* table */
         "%s/%d"
         "%s%s" /* gateway */
-        "%s"
+        "%s%s" /* weight */
+        "%s"   /* dev/ifindex */
         " metric %s"
         "%s"         /* mss */
         " rt-src %s" /* protocol */
@@ -7068,9 +6928,13 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz
                    : ""),
         s_network,
         route->plen,
-        s_gateway[0] ? " via " : "",
-        s_gateway,
-        str_dev,
+        n_nexthops <= 1 && s_gateway[0] ? " via " : "",
+        n_nexthops <= 1 ? s_gateway : "",
+        NM_PRINT_FMT_QUOTED2(n_nexthops <= 1 && route->weight != 0,
+                             " weight ",
+                             nm_sprintf_buf(weight_str, "%u", route->weight),
+                             ""),
+        n_nexthops <= 1 ? _to_string_dev(str_dev, route->ifindex) : "",
         route->metric_any
             ? (route->metric ? nm_sprintf_buf(str_metric, "??+%u", route->metric) : "??")
             : nm_sprintf_buf(str_metric, "%u", route->metric),
@@ -7120,7 +6984,50 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz
                                                        route->mtu)
                                       : "",
         route->r_force_commit ? " force-commit" : "");
-    return buf;
+
+    if ((n_nexthops == 1 && route->ifindex > 0) || n_nexthops == 0) {
+        /* A plain single hop route. Nothing extra to remark. */
+    } else {
+        nm_strbuf_append(&buf, &len, " n_nexthops %u", n_nexthops);
+        if (n_nexthops > 1) {
+            nm_strbuf_append(&buf,
+                             &len,
+                             " nexthop"
+                             "%s%s"       /* gateway */
+                             " weight %s" /* weight */
+                             "%s"         /* dev/ifindex */
+                             "",
+                             s_gateway[0] ? " via " : "",
+                             s_gateway,
+                             nm_sprintf_buf(weight_str, "%u", route->weight),
+                             _to_string_dev(str_dev, route->ifindex));
+            if (!extra_nexthops)
+                nm_strbuf_append_str(&buf, &len, " nexthops [...]");
+            else {
+                guint i;
+
+                for (i = 1; i < n_nexthops; i++) {
+                    const NMPlatformIP4RtNextHop *nexthop = &extra_nexthops[i - 1];
+
+                    nm_strbuf_append(
+                        &buf,
+                        &len,
+                        " nexthop"
+                        "%s"         /* ifindex */
+                        "%s%s"       /* gateway */
+                        " weight %s" /* weight */
+                        "",
+                        NM_PRINT_FMT_QUOTED2(nexthop->gateway != 0 || nexthop->ifindex <= 0,
+                                             " via ",
+                                             nm_inet4_ntop(nexthop->gateway, s_gateway),
+                                             ""),
+                        _to_string_dev(str_dev, nexthop->ifindex),
+                        nm_sprintf_buf(weight_str, "%u", nexthop->weight));
+                }
+            }
+        }
+    }
+    return buf0;
 }
 
 /**
@@ -7147,7 +7054,7 @@ nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsiz
     char str_table[30];
     char str_pref[40];
     char str_pref2[30];
-    char str_dev[TO_STRING_DEV_BUF_SIZE];
+    char str_dev[30];
     char str_mss[32];
     char s_source[50];
     char str_window[32];
@@ -7174,8 +7081,6 @@ nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsiz
     else
         inet_ntop(AF_INET6, &route->pref_src, s_pref_src, sizeof(s_pref_src));
 
-    _to_string_dev(NULL, route->ifindex, str_dev, sizeof(str_dev));
-
     g_snprintf(
         buf,
         len,
@@ -7213,7 +7118,7 @@ nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsiz
         route->plen,
         s_gateway[0] ? " via " : "",
         s_gateway,
-        str_dev,
+        _to_string_dev(str_dev, route->ifindex),
         route->metric_any
             ? (route->metric ? nm_sprintf_buf(str_metric, "??+%u", route->metric) : "??")
             : nm_sprintf_buf(str_metric, "%u", route->metric),
@@ -7225,7 +7130,7 @@ nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsiz
         route->src_plen || !IN6_IS_ADDR_UNSPECIFIED(&route->src)
             ? nm_sprintf_buf(s_src_all,
                              " src %s/%u",
-                             _nm_utils_inet6_ntop(&route->src, s_src),
+                             nm_inet6_ntop(&route->src, s_src),
                              (unsigned) route->src_plen)
             : "",
         _rtm_flags_to_string_full(str_rtm_flags, sizeof(str_rtm_flags), route->r_rtm_flags),
@@ -7279,7 +7184,7 @@ _routing_rule_addr_to_string(char          **buf,
                              guint8          plen,
                              gboolean        is_src)
 {
-    char     s_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char     s_addr[NM_INET_ADDRSTRLEN];
     gboolean is_zero;
     gsize    addr_size;
 
@@ -7300,7 +7205,7 @@ _routing_rule_addr_to_string(char          **buf,
 
     nm_strbuf_append_str(buf, len, is_src ? " from " : " to ");
 
-    nm_strbuf_append_str(buf, len, nm_utils_inet_ntop(addr_family, addr, s_addr));
+    nm_strbuf_append_str(buf, len, nm_inet_ntop(addr_family, addr, s_addr));
 
     if (plen != (addr_size * 8))
         nm_strbuf_append(buf, len, "/%u", plen);
@@ -7495,7 +7400,7 @@ nm_platform_routing_rule_to_string(const NMPlatformRoutingRule *routing_rule, ch
 const char *
 nm_platform_qdisc_to_string(const NMPlatformQdisc *qdisc, char *buf, gsize len)
 {
-    char        str_dev[TO_STRING_DEV_BUF_SIZE];
+    char        str_dev[30];
     const char *buf0;
 
     if (!nm_utils_to_string_buffer_init_null(qdisc, &buf, &len))
@@ -7507,7 +7412,7 @@ nm_platform_qdisc_to_string(const NMPlatformQdisc *qdisc, char *buf, gsize len)
                      &len,
                      "%s%s family %u handle %x parent %x info %x",
                      qdisc->kind,
-                     _to_string_dev(NULL, qdisc->ifindex, str_dev, sizeof(str_dev)),
+                     _to_string_dev(str_dev, qdisc->ifindex),
                      qdisc->addr_family,
                      qdisc->handle,
                      qdisc->parent,
@@ -7584,9 +7489,7 @@ nm_platform_qdisc_hash_update(const NMPlatformQdisc *obj, NMHashState *h)
 }
 
 int
-nm_platform_qdisc_cmp_full(const NMPlatformQdisc *a,
-                           const NMPlatformQdisc *b,
-                           gboolean               compare_handle)
+nm_platform_qdisc_cmp(const NMPlatformQdisc *a, const NMPlatformQdisc *b, gboolean compare_handle)
 {
     NM_CMP_SELF(a, b);
     NM_CMP_FIELD(a, b, ifindex);
@@ -7623,16 +7526,10 @@ nm_platform_qdisc_cmp_full(const NMPlatformQdisc *a,
     return 0;
 }
 
-int
-nm_platform_qdisc_cmp(const NMPlatformQdisc *a, const NMPlatformQdisc *b)
-{
-    return nm_platform_qdisc_cmp_full(a, b, TRUE);
-}
-
 const char *
 nm_platform_tfilter_to_string(const NMPlatformTfilter *tfilter, char *buf, gsize len)
 {
-    char  str_dev[TO_STRING_DEV_BUF_SIZE];
+    char  str_dev[30];
     char  act_buf[300];
     char *p;
     gsize l;
@@ -7673,7 +7570,7 @@ nm_platform_tfilter_to_string(const NMPlatformTfilter *tfilter, char *buf, gsize
                len,
                "%s%s family %u handle %x parent %x info %x%s",
                tfilter->kind,
-               _to_string_dev(NULL, tfilter->ifindex, str_dev, sizeof(str_dev)),
+               _to_string_dev(str_dev, tfilter->ifindex),
                tfilter->addr_family,
                tfilter->handle,
                tfilter->parent,
@@ -7741,7 +7638,7 @@ static NM_UTILS_FLAGS2STR_DEFINE(_mptcp_flags_to_string,
 const char *
 nm_platform_mptcp_addr_to_string(const NMPlatformMptcpAddr *mptcp_addr, char *buf, gsize len)
 {
-    char str_addr[30 + NM_UTILS_INET_ADDRSTRLEN];
+    char str_addr[30 + NM_INET_ADDRSTRLEN];
     char str_port[30];
     char str_id[30];
     char str_flags[200];
@@ -7754,7 +7651,7 @@ nm_platform_mptcp_addr_to_string(const NMPlatformMptcpAddr *mptcp_addr, char *bu
     if (mptcp_addr->addr_family == 0)
         nm_sprintf_buf(str_addr, "no-addr");
     else if (NM_IN_SET(mptcp_addr->addr_family, AF_INET, AF_INET6))
-        nm_utils_inet_ntop(mptcp_addr->addr_family, &mptcp_addr->addr, str_addr);
+        nm_inet_ntop(mptcp_addr->addr_family, &mptcp_addr->addr, str_addr);
     else
         nm_sprintf_buf(str_addr, "af %d", mptcp_addr->addr_family);
 
@@ -8363,7 +8260,7 @@ nm_platform_lnk_tun_cmp(const NMPlatformLnkTun *a, const NMPlatformLnkTun *b)
 void
 nm_platform_lnk_vlan_hash_update(const NMPlatformLnkVlan *obj, NMHashState *h)
 {
-    nm_hash_update_vals(h, obj->id, obj->flags);
+    nm_hash_update_vals(h, obj->id, obj->protocol, obj->flags);
 }
 
 int
@@ -8371,6 +8268,7 @@ nm_platform_lnk_vlan_cmp(const NMPlatformLnkVlan *a, const NMPlatformLnkVlan *b)
 {
     NM_CMP_SELF(a, b);
     NM_CMP_FIELD(a, b, id);
+    NM_CMP_FIELD(a, b, protocol);
     NM_CMP_FIELD(a, b, flags);
     return 0;
 }
@@ -8390,6 +8288,56 @@ nm_platform_lnk_vrf_cmp(const NMPlatformLnkVrf *a, const NMPlatformLnkVrf *b)
 }
 
 void
+nm_platform_lnk_vti_hash_update(const NMPlatformLnkVti *obj, NMHashState *h)
+{
+    nm_hash_update_vals(h,
+                        obj->local,
+                        obj->remote,
+                        obj->parent_ifindex,
+                        obj->ikey,
+                        obj->okey,
+                        obj->fwmark);
+}
+
+int
+nm_platform_lnk_vti_cmp(const NMPlatformLnkVti *a, const NMPlatformLnkVti *b)
+{
+    NM_CMP_SELF(a, b);
+    NM_CMP_FIELD(a, b, parent_ifindex);
+    NM_CMP_FIELD(a, b, local);
+    NM_CMP_FIELD(a, b, remote);
+    NM_CMP_FIELD(a, b, ikey);
+    NM_CMP_FIELD(a, b, okey);
+    NM_CMP_FIELD(a, b, fwmark);
+    return 0;
+}
+
+void
+nm_platform_lnk_vti6_hash_update(const NMPlatformLnkVti6 *obj, NMHashState *h)
+{
+    nm_hash_update_vals(h,
+                        obj->local,
+                        obj->remote,
+                        obj->parent_ifindex,
+                        obj->ikey,
+                        obj->okey,
+                        obj->fwmark);
+}
+
+int
+nm_platform_lnk_vti6_cmp(const NMPlatformLnkVti6 *a, const NMPlatformLnkVti6 *b)
+{
+    NM_CMP_SELF(a, b);
+    NM_CMP_FIELD(a, b, parent_ifindex);
+    NM_CMP_FIELD_MEMCMP(a, b, local);
+    NM_CMP_FIELD_MEMCMP(a, b, remote);
+    NM_CMP_FIELD(a, b, ikey);
+    NM_CMP_FIELD(a, b, okey);
+    NM_CMP_FIELD(a, b, fwmark);
+    return 0;
+}
+
+void
 nm_platform_lnk_vxlan_hash_update(const NMPlatformLnkVxlan *obj, NMHashState *h)
 {
     nm_hash_update_vals(h,
@@ -8458,275 +8406,18 @@ nm_platform_lnk_wireguard_cmp(const NMPlatformLnkWireGuard *a, const NMPlatformL
     return 0;
 }
 
-static int
-_address_pretty_sort_get_prio_4(in_addr_t addr)
-{
-    if (nm_utils_ip4_address_is_link_local(addr))
-        return 0;
-    return 1;
-}
-
-int
-nm_platform_ip4_address_pretty_sort_cmp(const NMPlatformIP4Address *a1,
-                                        const NMPlatformIP4Address *a2)
-{
-    in_addr_t n1;
-    in_addr_t n2;
-
-    nm_assert(a1);
-    nm_assert(a2);
-
-    /* Sort by address type. For example link local will
-     * be sorted *after* a global address. */
-    NM_CMP_DIRECT(_address_pretty_sort_get_prio_4(a2->address),
-                  _address_pretty_sort_get_prio_4(a1->address));
-
-    /* Sort the addresses based on their source. */
-    NM_CMP_DIRECT(a2->addr_source, a1->addr_source);
-
-    NM_CMP_DIRECT((a2->label[0] == '\0'), (a1->label[0] == '\0'));
-
-    /* Finally, sort addresses lexically. We compare only the
-     * network part so that the order of addresses in the same
-     * subnet (and thus also the primary/secondary role) is
-     * preserved.
-     */
-    n1 = nm_utils_ip4_address_clear_host_address(a1->address, a1->plen);
-    n2 = nm_utils_ip4_address_clear_host_address(a2->address, a2->plen);
-    NM_CMP_DIRECT_MEMCMP(&n1, &n2, sizeof(guint32));
-    return 0;
-}
-
-static int
-_address_pretty_sort_get_prio_6(const struct in6_addr *addr)
-{
-    if (IN6_IS_ADDR_V4MAPPED(addr))
-        return 0;
-    if (IN6_IS_ADDR_V4COMPAT(addr))
-        return 1;
-    if (IN6_IS_ADDR_UNSPECIFIED(addr))
-        return 2;
-    if (IN6_IS_ADDR_LOOPBACK(addr))
-        return 3;
-    if (IN6_IS_ADDR_LINKLOCAL(addr))
-        return 4;
-    if (IN6_IS_ADDR_SITELOCAL(addr))
-        return 5;
-    return 6;
-}
-
-static int
-_address_cmp_expiry(const NMPlatformIPAddress *a, const NMPlatformIPAddress *b)
-{
-    guint32 lifetime_a;
-    guint32 lifetime_b;
-    guint32 preferred_a;
-    guint32 preferred_b;
-    gint32  now = 0;
-
-    lifetime_a =
-        nmp_utils_lifetime_get(a->timestamp, a->lifetime, a->preferred, &now, &preferred_a);
-    lifetime_b =
-        nmp_utils_lifetime_get(b->timestamp, b->lifetime, b->preferred, &now, &preferred_b);
-
-    NM_CMP_DIRECT(lifetime_a, lifetime_b);
-    NM_CMP_DIRECT(preferred_a, preferred_b);
-    return 0;
-}
-
-int
-nm_platform_ip6_address_pretty_sort_cmp(const NMPlatformIP6Address *a1,
-                                        const NMPlatformIP6Address *a2,
-                                        gboolean                    prefer_temp)
-{
-    gboolean ipv6_privacy1;
-    gboolean ipv6_privacy2;
-
-    nm_assert(a1);
-    nm_assert(a2);
-
-    /* tentative addresses are always sorted back... */
-    /* sort tentative addresses after non-tentative. */
-    NM_CMP_DIRECT(NM_FLAGS_HAS(a1->n_ifa_flags, IFA_F_TENTATIVE),
-                  NM_FLAGS_HAS(a2->n_ifa_flags, IFA_F_TENTATIVE));
-
-    /* Sort by address type. For example link local will
-     * be sorted *after* site local or global. */
-    NM_CMP_DIRECT(_address_pretty_sort_get_prio_6(&a2->address),
-                  _address_pretty_sort_get_prio_6(&a1->address));
-
-    ipv6_privacy1 = NM_FLAGS_ANY(a1->n_ifa_flags, IFA_F_MANAGETEMPADDR | IFA_F_SECONDARY);
-    ipv6_privacy2 = NM_FLAGS_ANY(a2->n_ifa_flags, IFA_F_MANAGETEMPADDR | IFA_F_SECONDARY);
-    if (ipv6_privacy1 || ipv6_privacy2) {
-        gboolean public1 = TRUE;
-        gboolean public2 = TRUE;
-
-        if (ipv6_privacy1) {
-            if (a1->n_ifa_flags & IFA_F_SECONDARY)
-                public1 = prefer_temp;
-            else
-                public1 = !prefer_temp;
-        }
-        if (ipv6_privacy2) {
-            if (a2->n_ifa_flags & IFA_F_SECONDARY)
-                public2 = prefer_temp;
-            else
-                public2 = !prefer_temp;
-        }
-
-        NM_CMP_DIRECT(public2, public1);
-    }
-
-    /* Sort the addresses based on their source. */
-    NM_CMP_DIRECT(a2->addr_source, a1->addr_source);
-
-    /* sort permanent addresses before non-permanent. */
-    NM_CMP_DIRECT(NM_FLAGS_HAS(a2->n_ifa_flags, IFA_F_PERMANENT),
-                  NM_FLAGS_HAS(a1->n_ifa_flags, IFA_F_PERMANENT));
-
-    /* finally sort addresses lexically */
-    NM_CMP_DIRECT_IN6ADDR(&a1->address, &a2->address);
-    NM_CMP_DIRECT_MEMCMP(a1, a2, sizeof(*a1));
-    return 0;
-}
-
 void
-nm_platform_ip4_address_hash_update(const NMPlatformIP4Address *obj, NMHashState *h)
+nm_platform_ip4_rt_nexthop_hash_update(const NMPlatformIP4RtNextHop *obj,
+                                       gboolean                      for_id,
+                                       NMHashState                  *h)
 {
-    nm_hash_update_vals(h,
-                        obj->ifindex,
-                        obj->addr_source,
-                        obj->use_ip4_broadcast_address ? obj->broadcast_address : ((in_addr_t) 0u),
-                        obj->timestamp,
-                        obj->lifetime,
-                        obj->preferred,
-                        obj->n_ifa_flags,
-                        obj->plen,
-                        obj->address,
-                        obj->peer_address,
-                        NM_HASH_COMBINE_BOOLS(guint8,
-                                              obj->use_ip4_broadcast_address,
-                                              obj->a_acd_not_ready,
-                                              obj->a_force_commit,
-                                              obj->a_no_auto_noprefixroute));
-    nm_hash_update_strarr(h, obj->label);
-}
+    guint8 w;
 
-int
-nm_platform_ip4_address_cmp(const NMPlatformIP4Address *a,
-                            const NMPlatformIP4Address *b,
-                            NMPlatformIPAddressCmpType  cmp_type)
-{
-    NM_CMP_SELF(a, b);
-
-    NM_CMP_FIELD(a, b, ifindex);
-    NM_CMP_FIELD(a, b, plen);
-    NM_CMP_FIELD(a, b, address);
-
-    switch (cmp_type) {
-    case NM_PLATFORM_IP_ADDRESS_CMP_TYPE_ID:
-        /* for IPv4 addresses, you can add the same local address with differing peer-address
-         * (IFA_ADDRESS), provided that their net-part differs. */
-        NM_CMP_DIRECT_IN4ADDR_SAME_PREFIX(a->peer_address, b->peer_address, a->plen);
-        return 0;
-    case NM_PLATFORM_IP_ADDRESS_CMP_TYPE_SEMANTICALLY:
-    case NM_PLATFORM_IP_ADDRESS_CMP_TYPE_FULL:
-        NM_CMP_FIELD(a, b, peer_address);
-        NM_CMP_FIELD_STR(a, b, label);
-        if (cmp_type == NM_PLATFORM_IP_ADDRESS_CMP_TYPE_SEMANTICALLY) {
-            NM_CMP_RETURN(_address_cmp_expiry((const NMPlatformIPAddress *) a,
-                                              (const NMPlatformIPAddress *) b));
-
-            /* Most flags are set by kernel. We only compare the ones that
-             * NetworkManager actively sets.
-             *
-             * NM actively only sets IFA_F_NOPREFIXROUTE (and IFA_F_MANAGETEMPADDR for IPv6),
-             * where nm_platform_ip_address_sync() always sets IFA_F_NOPREFIXROUTE.
-             * There are thus no flags to compare for IPv4. */
-
-            NM_CMP_DIRECT(nm_platform_ip4_broadcast_address_from_addr(a),
-                          nm_platform_ip4_broadcast_address_from_addr(b));
-        } else {
-            NM_CMP_FIELD(a, b, timestamp);
-            NM_CMP_FIELD(a, b, lifetime);
-            NM_CMP_FIELD(a, b, preferred);
-            NM_CMP_FIELD(a, b, n_ifa_flags);
-            NM_CMP_FIELD(a, b, addr_source);
-            NM_CMP_FIELD_UNSAFE(a, b, use_ip4_broadcast_address);
-            if (a->use_ip4_broadcast_address)
-                NM_CMP_FIELD(a, b, broadcast_address);
-            NM_CMP_FIELD_UNSAFE(a, b, a_acd_not_ready);
-            NM_CMP_FIELD_UNSAFE(a, b, a_force_commit);
-            NM_CMP_FIELD_UNSAFE(a, b, a_no_auto_noprefixroute);
-        }
-        return 0;
-    }
-    return nm_assert_unreachable_val(0);
-}
-
-void
-nm_platform_ip6_address_hash_update(const NMPlatformIP6Address *obj, NMHashState *h)
-{
-    nm_hash_update_vals(
-        h,
-        obj->ifindex,
-        obj->addr_source,
-        obj->timestamp,
-        obj->lifetime,
-        obj->preferred,
-        obj->n_ifa_flags,
-        obj->plen,
-        obj->address,
-        obj->peer_address,
-        NM_HASH_COMBINE_BOOLS(guint8, obj->a_force_commit, obj->a_no_auto_noprefixroute));
-}
-
-int
-nm_platform_ip6_address_cmp(const NMPlatformIP6Address *a,
-                            const NMPlatformIP6Address *b,
-                            NMPlatformIPAddressCmpType  cmp_type)
-{
-    const struct in6_addr *p_a, *p_b;
-
-    NM_CMP_SELF(a, b);
+    nm_assert(obj);
 
-    NM_CMP_FIELD(a, b, ifindex);
-    NM_CMP_FIELD_IN6ADDR(a, b, address);
+    w = for_id ? NM_MAX(obj->weight, 1u) : obj->weight;
 
-    switch (cmp_type) {
-    case NM_PLATFORM_IP_ADDRESS_CMP_TYPE_ID:
-        /* for IPv6 addresses, the prefix length is not part of the primary identifier. */
-        return 0;
-    case NM_PLATFORM_IP_ADDRESS_CMP_TYPE_SEMANTICALLY:
-    case NM_PLATFORM_IP_ADDRESS_CMP_TYPE_FULL:
-        NM_CMP_FIELD(a, b, plen);
-        p_a = nm_platform_ip6_address_get_peer(a);
-        p_b = nm_platform_ip6_address_get_peer(b);
-        NM_CMP_DIRECT_MEMCMP(p_a, p_b, sizeof(*p_a));
-        if (cmp_type == NM_PLATFORM_IP_ADDRESS_CMP_TYPE_SEMANTICALLY) {
-            NM_CMP_RETURN(_address_cmp_expiry((const NMPlatformIPAddress *) a,
-                                              (const NMPlatformIPAddress *) b));
-
-            /* Most flags are set by kernel. We only compare the ones that
-             * NetworkManager actively sets.
-             *
-             * NM actively only sets IFA_F_NOPREFIXROUTE and IFA_F_MANAGETEMPADDR,
-             * where nm_platform_ip_address_sync() always sets IFA_F_NOPREFIXROUTE.
-             * We thus only care about IFA_F_MANAGETEMPADDR. */
-            NM_CMP_DIRECT(a->n_ifa_flags & IFA_F_MANAGETEMPADDR,
-                          b->n_ifa_flags & IFA_F_MANAGETEMPADDR);
-        } else {
-            NM_CMP_FIELD(a, b, timestamp);
-            NM_CMP_FIELD(a, b, lifetime);
-            NM_CMP_FIELD(a, b, preferred);
-            NM_CMP_FIELD(a, b, n_ifa_flags);
-            NM_CMP_FIELD(a, b, addr_source);
-            NM_CMP_FIELD_UNSAFE(a, b, a_force_commit);
-            NM_CMP_FIELD_UNSAFE(a, b, a_no_auto_noprefixroute);
-        }
-        return 0;
-    }
-    return nm_assert_unreachable_val(0);
+    nm_hash_update_vals(h, obj->ifindex, obj->gateway, w);
 }
 
 void
@@ -8736,48 +8427,48 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
 {
     switch (cmp_type) {
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID:
-        nm_hash_update_vals(
-            h,
-            nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(obj)),
-            nm_utils_ip4_address_clear_host_address(obj->network, obj->plen),
-            obj->plen,
-            obj->metric,
-            obj->tos,
-            NM_HASH_COMBINE_BOOLS(guint8, obj->metric_any, obj->table_any));
-        break;
+    case NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID:
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID:
         nm_hash_update_vals(
             h,
-            obj->type_coerced,
             nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(obj)),
-            nm_utils_ip4_address_clear_host_address(obj->network, obj->plen),
+            nm_ip4_addr_clear_host_address(obj->network, obj->plen),
             obj->plen,
             obj->metric,
             obj->tos,
-            /* on top of WEAK_ID: */
-            obj->ifindex,
-            nmp_utils_ip_config_source_round_trip_rtprot(obj->rt_source),
-            _ip_route_scope_inv_get_normalized(obj),
-            obj->gateway,
-            obj->mss,
-            obj->pref_src,
-            obj->window,
-            obj->cwnd,
-            obj->initcwnd,
-            obj->initrwnd,
-            obj->mtu,
-            obj->rto_min,
-            obj->r_rtm_flags & RTNH_F_ONLINK,
-            NM_HASH_COMBINE_BOOLS(guint16,
-                                  obj->metric_any,
-                                  obj->table_any,
-                                  obj->quickack,
-                                  obj->lock_window,
-                                  obj->lock_cwnd,
-                                  obj->lock_initcwnd,
-                                  obj->lock_initrwnd,
-                                  obj->lock_mtu,
-                                  obj->lock_mss));
+            NM_HASH_COMBINE_BOOLS(guint8, obj->metric_any, obj->table_any));
+        if (NM_IN_SET(cmp_type,
+                      NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID,
+                      NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID)) {
+            nm_hash_update_vals(h,
+                                obj->type_coerced,
+                                nmp_utils_ip_config_source_round_trip_rtprot(obj->rt_source),
+                                _ip_route_scope_inv_get_normalized(obj),
+                                obj->mss,
+                                obj->pref_src,
+                                obj->window,
+                                obj->cwnd,
+                                obj->initcwnd,
+                                obj->initrwnd,
+                                obj->mtu,
+                                obj->rto_min,
+                                obj->r_rtm_flags & RTNH_F_ONLINK,
+                                NM_HASH_COMBINE_BOOLS(guint16,
+                                                      obj->quickack,
+                                                      obj->lock_window,
+                                                      obj->lock_cwnd,
+                                                      obj->lock_initcwnd,
+                                                      obj->lock_initrwnd,
+                                                      obj->lock_mtu,
+                                                      obj->lock_mss));
+            if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) {
+                nm_hash_update_vals(h,
+                                    obj->ifindex,
+                                    nm_platform_ip4_route_get_n_nexthops(obj),
+                                    obj->gateway,
+                                    (guint8) MAX(obj->weight, 1u));
+            }
+        }
         break;
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY:
         nm_hash_update_vals(
@@ -8785,10 +8476,12 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
             obj->type_coerced,
             nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(obj)),
             obj->ifindex,
-            nm_utils_ip4_address_clear_host_address(obj->network, obj->plen),
+            nm_ip4_addr_clear_host_address(obj->network, obj->plen),
             obj->plen,
             obj->metric,
+            nm_platform_ip4_route_get_n_nexthops(obj),
             obj->gateway,
+            (guint8) MAX(obj->weight, 1u),
             nmp_utils_ip_config_source_round_trip_rtprot(obj->rt_source),
             _ip_route_scope_inv_get_normalized(obj),
             obj->tos,
@@ -8821,6 +8514,8 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
                             obj->plen,
                             obj->metric,
                             obj->gateway,
+                            obj->n_nexthops,
+                            obj->weight,
                             obj->rt_source,
                             obj->scope_inv,
                             obj->tos,
@@ -8849,30 +8544,55 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
 }
 
 int
+nm_platform_ip4_rt_nexthop_cmp(const NMPlatformIP4RtNextHop *a,
+                               const NMPlatformIP4RtNextHop *b,
+                               gboolean                      for_id)
+{
+    guint8 w_a;
+    guint8 w_b;
+
+    /* Note that weight zero is not valid (in kernel). We thus treat
+     * weight zero usually the same as 1.
+     *
+     * Not here for cmp/hash_update functions. These functions check for the exact
+     * bit-pattern, and not the it means at other places. */
+    NM_CMP_SELF(a, b);
+    NM_CMP_FIELD(a, b, ifindex);
+    NM_CMP_FIELD(a, b, gateway);
+
+    w_a = for_id ? NM_MAX(a->weight, 1u) : a->weight;
+    w_b = for_id ? NM_MAX(b->weight, 1u) : b->weight;
+    NM_CMP_DIRECT(w_a, w_b);
+
+    return 0;
+}
+
+int
 nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
                           const NMPlatformIP4Route *b,
                           NMPlatformIPRouteCmpType  cmp_type)
 {
     NM_CMP_SELF(a, b);
     switch (cmp_type) {
+    case NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID:
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID:
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID:
         NM_CMP_FIELD_UNSAFE(a, b, table_any);
         NM_CMP_DIRECT(nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(a)),
                       nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(b)));
-        NM_CMP_DIRECT_IN4ADDR_SAME_PREFIX(a->network, b->network, MIN(a->plen, b->plen));
+        NM_CMP_DIRECT_IP4_ADDR_SAME_PREFIX(a->network, b->network, MIN(a->plen, b->plen));
         NM_CMP_FIELD(a, b, plen);
         NM_CMP_FIELD_UNSAFE(a, b, metric_any);
         NM_CMP_FIELD(a, b, metric);
         NM_CMP_FIELD(a, b, tos);
-        if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) {
-            NM_CMP_FIELD(a, b, ifindex);
+        if (NM_IN_SET(cmp_type,
+                      NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID,
+                      NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID)) {
             NM_CMP_FIELD(a, b, type_coerced);
             NM_CMP_DIRECT(nmp_utils_ip_config_source_round_trip_rtprot(a->rt_source),
                           nmp_utils_ip_config_source_round_trip_rtprot(b->rt_source));
             NM_CMP_DIRECT(_ip_route_scope_inv_get_normalized(a),
                           _ip_route_scope_inv_get_normalized(b));
-            NM_CMP_FIELD(a, b, gateway);
             NM_CMP_FIELD(a, b, mss);
             NM_CMP_FIELD(a, b, pref_src);
             NM_CMP_FIELD(a, b, window);
@@ -8881,7 +8601,13 @@ nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
             NM_CMP_FIELD(a, b, initrwnd);
             NM_CMP_FIELD(a, b, mtu);
             NM_CMP_FIELD(a, b, rto_min);
+
+            /* Note that for NetworkManager, the onlink flag is only part of the entire route.
+             * For kernel, each next hop has it's own onlink flag (rtnh_flags). This means,
+             * we can only merge ECMP routes, if they agree with their onlink flag, and then
+             * all next hops are onlink (or not). */
             NM_CMP_DIRECT(a->r_rtm_flags & RTNH_F_ONLINK, b->r_rtm_flags & RTNH_F_ONLINK);
+
             NM_CMP_FIELD_UNSAFE(a, b, quickack);
             NM_CMP_FIELD_UNSAFE(a, b, lock_window);
             NM_CMP_FIELD_UNSAFE(a, b, lock_cwnd);
@@ -8889,6 +8615,13 @@ nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
             NM_CMP_FIELD_UNSAFE(a, b, lock_initrwnd);
             NM_CMP_FIELD_UNSAFE(a, b, lock_mtu);
             NM_CMP_FIELD_UNSAFE(a, b, lock_mss);
+            if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) {
+                NM_CMP_FIELD(a, b, ifindex);
+                NM_CMP_FIELD(a, b, gateway);
+                NM_CMP_DIRECT(NM_MAX(a->weight, 1u), NM_MAX(b->weight, 1u));
+                NM_CMP_DIRECT(nm_platform_ip4_route_get_n_nexthops(a),
+                              nm_platform_ip4_route_get_n_nexthops(b));
+            }
         }
         break;
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY:
@@ -8902,13 +8635,22 @@ nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
             NM_CMP_FIELD(a, b, table_coerced);
         NM_CMP_FIELD(a, b, ifindex);
         if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
-            NM_CMP_DIRECT_IN4ADDR_SAME_PREFIX(a->network, b->network, MIN(a->plen, b->plen));
+            NM_CMP_DIRECT_IP4_ADDR_SAME_PREFIX(a->network, b->network, MIN(a->plen, b->plen));
         else
             NM_CMP_FIELD(a, b, network);
         NM_CMP_FIELD(a, b, plen);
         NM_CMP_FIELD_UNSAFE(a, b, metric_any);
         NM_CMP_FIELD(a, b, metric);
+        if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) {
+            NM_CMP_DIRECT(nm_platform_ip4_route_get_n_nexthops(a),
+                          nm_platform_ip4_route_get_n_nexthops(b));
+        } else
+            NM_CMP_FIELD(a, b, n_nexthops);
         NM_CMP_FIELD(a, b, gateway);
+        if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
+            NM_CMP_DIRECT(NM_MAX(a->weight, 1u), NM_MAX(b->weight, 1u));
+        else
+            NM_CMP_FIELD(a, b, weight);
         if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) {
             NM_CMP_DIRECT(nmp_utils_ip_config_source_round_trip_rtprot(a->rt_source),
                           nmp_utils_ip_config_source_round_trip_rtprot(b->rt_source));
@@ -8958,22 +8700,25 @@ nm_platform_ip6_route_hash_update(const NMPlatformIP6Route *obj,
         nm_hash_update_vals(
             h,
             nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(obj)),
-            *nm_utils_ip6_address_clear_host_address(&a1, &obj->network, obj->plen),
+            *nm_ip6_addr_clear_host_address(&a1, &obj->network, obj->plen),
             obj->plen,
             obj->metric,
-            *nm_utils_ip6_address_clear_host_address(&a2, &obj->src, obj->src_plen),
+            *nm_ip6_addr_clear_host_address(&a2, &obj->src, obj->src_plen),
             obj->src_plen,
             NM_HASH_COMBINE_BOOLS(guint8, obj->metric_any, obj->table_any));
         break;
+    case NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID:
+        nm_assert_not_reached();
+        /* fall-through */
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID:
         nm_hash_update_vals(
             h,
             obj->type_coerced,
             nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(obj)),
-            *nm_utils_ip6_address_clear_host_address(&a1, &obj->network, obj->plen),
+            *nm_ip6_addr_clear_host_address(&a1, &obj->network, obj->plen),
             obj->plen,
             obj->metric,
-            *nm_utils_ip6_address_clear_host_address(&a2, &obj->src, obj->src_plen),
+            *nm_ip6_addr_clear_host_address(&a2, &obj->src, obj->src_plen),
             obj->src_plen,
             NM_HASH_COMBINE_BOOLS(guint8, obj->metric_any, obj->table_any),
             /* on top of WEAK_ID: */
@@ -8986,12 +8731,12 @@ nm_platform_ip6_route_hash_update(const NMPlatformIP6Route *obj,
             obj->type_coerced,
             nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(obj)),
             obj->ifindex,
-            *nm_utils_ip6_address_clear_host_address(&a1, &obj->network, obj->plen),
+            *nm_ip6_addr_clear_host_address(&a1, &obj->network, obj->plen),
             obj->plen,
             obj->metric,
             obj->gateway,
             obj->pref_src,
-            *nm_utils_ip6_address_clear_host_address(&a2, &obj->src, obj->src_plen),
+            *nm_ip6_addr_clear_host_address(&a2, &obj->src, obj->src_plen),
             obj->src_plen,
             nmp_utils_ip_config_source_round_trip_rtprot(obj->rt_source),
             obj->mss,
@@ -9057,16 +8802,19 @@ nm_platform_ip6_route_cmp(const NMPlatformIP6Route *a,
 {
     NM_CMP_SELF(a, b);
     switch (cmp_type) {
+    case NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID:
+        nm_assert_not_reached();
+        /* fall-through */
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID:
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID:
         NM_CMP_FIELD_UNSAFE(a, b, table_any);
         NM_CMP_DIRECT(nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(a)),
                       nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(b)));
-        NM_CMP_DIRECT_IN6ADDR_SAME_PREFIX(&a->network, &b->network, MIN(a->plen, b->plen));
+        NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->network, &b->network, MIN(a->plen, b->plen));
         NM_CMP_FIELD(a, b, plen);
         NM_CMP_FIELD_UNSAFE(a, b, metric_any);
         NM_CMP_FIELD(a, b, metric);
-        NM_CMP_DIRECT_IN6ADDR_SAME_PREFIX(&a->src, &b->src, MIN(a->src_plen, b->src_plen));
+        NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->src, &b->src, MIN(a->src_plen, b->src_plen));
         NM_CMP_FIELD(a, b, src_plen);
         if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) {
             NM_CMP_FIELD(a, b, ifindex);
@@ -9085,7 +8833,7 @@ nm_platform_ip6_route_cmp(const NMPlatformIP6Route *a,
             NM_CMP_FIELD(a, b, table_coerced);
         NM_CMP_FIELD(a, b, ifindex);
         if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
-            NM_CMP_DIRECT_IN6ADDR_SAME_PREFIX(&a->network, &b->network, MIN(a->plen, b->plen));
+            NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->network, &b->network, MIN(a->plen, b->plen));
         else
             NM_CMP_FIELD_IN6ADDR(a, b, network);
         NM_CMP_FIELD(a, b, plen);
@@ -9094,7 +8842,7 @@ nm_platform_ip6_route_cmp(const NMPlatformIP6Route *a,
         NM_CMP_FIELD_IN6ADDR(a, b, gateway);
         NM_CMP_FIELD_IN6ADDR(a, b, pref_src);
         if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) {
-            NM_CMP_DIRECT_IN6ADDR_SAME_PREFIX(&a->src, &b->src, MIN(a->src_plen, b->src_plen));
+            NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->src, &b->src, MIN(a->src_plen, b->src_plen));
             NM_CMP_FIELD(a, b, src_plen);
             NM_CMP_DIRECT(nmp_utils_ip_config_source_round_trip_rtprot(a->rt_source),
                           nmp_utils_ip_config_source_round_trip_rtprot(b->rt_source));
@@ -9463,9 +9211,9 @@ nm_platform_ip4_address_generate_device_route(const NMPlatformIP4Address *addr,
     if (addr->plen == 0)
         return NULL;
 
-    network_4 = nm_utils_ip4_address_clear_host_address(addr->peer_address, addr->plen);
+    network_4 = nm_ip4_addr_clear_host_address(addr->peer_address, addr->plen);
 
-    if (nm_utils_ip4_address_is_zeronet(network_4)) {
+    if (nm_ip4_addr_is_zeronet(network_4)) {
         /* Kernel doesn't add device-routes for destinations that
          * start with 0.x.y.z. Skip them. */
         return NULL;
@@ -9570,7 +9318,10 @@ log_ip4_route(NMPlatform                *self,
 
     _LOG3D("signal: route   4 %7s: %s",
            nm_platform_signal_change_type_to_string(change_type),
-           nm_platform_ip4_route_to_string(route, sbuf, sizeof(sbuf)));
+           nmp_object_to_string(NMP_OBJECT_UP_CAST(route),
+                                NMP_OBJECT_TO_STRING_PUBLIC,
+                                sbuf,
+                                sizeof(sbuf)));
 }
 
 static void
@@ -9585,7 +9336,10 @@ log_ip6_route(NMPlatform                *self,
 
     _LOG3D("signal: route   6 %7s: %s",
            nm_platform_signal_change_type_to_string(change_type),
-           nm_platform_ip6_route_to_string(route, sbuf, sizeof(sbuf)));
+           nmp_object_to_string(NMP_OBJECT_UP_CAST(route),
+                                NMP_OBJECT_TO_STRING_PUBLIC,
+                                sbuf,
+                                sizeof(sbuf)));
 }
 
 static void
@@ -9750,36 +9504,91 @@ nm_platform_netns_push(NMPlatform *self, NMPNetns **netns)
 
 /*****************************************************************************/
 
-const _NMPlatformVTableAddressUnion nm_platform_vtable_address = {
-    .v4 =
-        {
-            .is_ip4         = TRUE,
-            .obj_type       = NMP_OBJECT_TYPE_IP4_ADDRESS,
-            .addr_family    = AF_INET,
-            .sizeof_address = sizeof(NMPlatformIP4Address),
-            .address_cmp =
-                (int (*)(const NMPlatformIPXAddress *a,
-                         const NMPlatformIPXAddress *b,
-                         NMPlatformIPAddressCmpType  cmp_type)) nm_platform_ip4_address_cmp,
-            .address_to_string = (const char *(*) (const NMPlatformIPXAddress *address,
-                                                   char                       *buf,
-                                                   gsize len)) nm_platform_ip4_address_to_string,
-        },
-    .v6 =
-        {
-            .is_ip4         = FALSE,
-            .obj_type       = NMP_OBJECT_TYPE_IP6_ADDRESS,
-            .addr_family    = AF_INET6,
-            .sizeof_address = sizeof(NMPlatformIP6Address),
-            .address_cmp =
-                (int (*)(const NMPlatformIPXAddress *a,
-                         const NMPlatformIPXAddress *b,
-                         NMPlatformIPAddressCmpType  cmp_type)) nm_platform_ip6_address_cmp,
-            .address_to_string = (const char *(*) (const NMPlatformIPXAddress *address,
-                                                   char                       *buf,
-                                                   gsize len)) nm_platform_ip6_address_to_string,
-        },
-};
+typedef struct {
+    struct in6_addr address;
+    CList           lst;
+    gint64          timestamp_nsec;
+    int             ifindex;
+} IP6DadFailedAddr;
+
+static void
+ip6_dadfailed_addr_free(IP6DadFailedAddr *addr)
+{
+    c_list_unlink_stale(&addr->lst);
+    nm_g_slice_free(addr);
+}
+
+static void
+ip6_dadfailed_prune_old(NMPlatform *self, gint64 now_nsec)
+{
+    NMPlatformPrivate *priv = NM_PLATFORM_GET_PRIVATE(self);
+    IP6DadFailedAddr  *addr;
+    IP6DadFailedAddr  *safe;
+
+    c_list_for_each_entry_safe (addr, safe, &priv->ip6_dadfailed_lst_head, lst) {
+        if (addr->timestamp_nsec + (10 * NM_UTILS_NSEC_PER_SEC) > now_nsec)
+            break;
+        ip6_dadfailed_addr_free(addr);
+    }
+}
+
+gboolean
+nm_platform_ip6_dadfailed_check(NMPlatform *self, int ifindex, const struct in6_addr *ip6)
+{
+    NMPlatformPrivate *priv = NM_PLATFORM_GET_PRIVATE(self);
+    IP6DadFailedAddr  *addr;
+
+    ip6_dadfailed_prune_old(self, nm_utils_get_monotonic_timestamp_nsec());
+
+    c_list_for_each_entry_prev (addr, &priv->ip6_dadfailed_lst_head, lst) {
+        if (addr->ifindex == ifindex && IN6_ARE_ADDR_EQUAL(&addr->address, ip6)) {
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+
+/*
+ * If an IPv6 address fails DAD and has infinite lifetime, kernel just
+ * sets the DADFAILED flag. However when the address has a finite
+ * lifetime kernel deletes it immediately and the RTM_DELLINK netlink
+ * message contains the DADFAILED flag. In the second case, we remove
+ * the address from the platform cache and there is no way for
+ * platform's clients to check whether DAD failed. To work around
+ * this, we store all deleted-with-DADFAILED addresses and provide a
+ * mechanism to access them.
+ */
+void
+nm_platform_ip6_dadfailed_set(NMPlatform            *self,
+                              int                    ifindex,
+                              const struct in6_addr *ip6,
+                              gboolean               failed)
+{
+    NMPlatformPrivate *priv     = NM_PLATFORM_GET_PRIVATE(self);
+    gint64             now_nsec = nm_utils_get_monotonic_timestamp_nsec();
+    IP6DadFailedAddr  *addr;
+    IP6DadFailedAddr  *safe;
+
+    ip6_dadfailed_prune_old(self, now_nsec);
+
+    if (failed) {
+        addr  = g_slice_new(IP6DadFailedAddr);
+        *addr = (IP6DadFailedAddr){
+            .address        = *ip6,
+            .ifindex        = ifindex,
+            .timestamp_nsec = now_nsec,
+        };
+        c_list_link_tail(&priv->ip6_dadfailed_lst_head, &addr->lst);
+    } else {
+        c_list_for_each_entry_safe (addr, safe, &priv->ip6_dadfailed_lst_head, lst) {
+            if (addr->ifindex == ifindex && IN6_ARE_ADDR_EQUAL(&addr->address, ip6)) {
+                ip6_dadfailed_addr_free(addr);
+            }
+        }
+    }
+}
+
+/*****************************************************************************/
 
 const _NMPlatformVTableRouteUnion nm_platform_vtable_route = {
     .v4 =
@@ -9819,6 +9628,20 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps
     NMPlatformPrivate *priv = NM_PLATFORM_GET_PRIVATE(self);
 
     switch (prop_id) {
+    case PROP_MULTI_IDX:
+        /* construct-only */
+        {
+            NMDedupMultiIndex *multi_idx;
+
+            multi_idx = g_value_get_pointer(value);
+            if (!multi_idx)
+                multi_idx = nm_dedup_multi_index_new();
+            else
+                multi_idx = nm_dedup_multi_index_ref(multi_idx);
+
+            priv->multi_idx = multi_idx;
+            break;
+        }
     case PROP_NETNS_SUPPORT:
         /* construct-only */
         if (g_value_get_boolean(value)) {
@@ -9865,9 +9688,10 @@ constructor(GType type, guint n_construct_params, GObjectConstructParam *constru
     self = NM_PLATFORM(object);
     priv = NM_PLATFORM_GET_PRIVATE(self);
 
-    priv->multi_idx = nm_dedup_multi_index_new();
+    nm_assert(priv->multi_idx);
 
     priv->cache = nmp_cache_new(priv->multi_idx, priv->use_udev);
+    c_list_init(&priv->ip6_dadfailed_lst_head);
 
     return object;
 }
@@ -9877,6 +9701,7 @@ finalize(GObject *object)
 {
     NMPlatform        *self = NM_PLATFORM(object);
     NMPlatformPrivate *priv = NM_PLATFORM_GET_PRIVATE(self);
+    IP6DadFailedAddr  *addr;
 
     nm_clear_g_source(&priv->ip4_dev_route_blacklist_check_id);
     nm_clear_g_source(&priv->ip4_dev_route_blacklist_gc_timeout_id);
@@ -9885,6 +9710,10 @@ finalize(GObject *object)
     nm_dedup_multi_index_unref(priv->multi_idx);
     nmp_cache_free(priv->cache);
 
+    while ((addr = c_list_first_entry(&priv->ip6_dadfailed_lst_head, IP6DadFailedAddr, lst))) {
+        ip6_dadfailed_addr_free(addr);
+    }
+
     G_OBJECT_CLASS(nm_platform_parent_class)->finalize(object);
 }
 
@@ -9903,6 +9732,14 @@ nm_platform_class_init(NMPlatformClass *platform_class)
 
     g_object_class_install_property(
         object_class,
+        PROP_MULTI_IDX,
+        g_param_spec_pointer(NM_PLATFORM_MULTI_IDX,
+                             "",
+                             "",
+                             G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+    g_object_class_install_property(
+        object_class,
         PROP_NETNS_SUPPORT,
         g_param_spec_boolean(NM_PLATFORM_NETNS_SUPPORT,
                              "",
diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h
index 30d0b506..1cd9c6c6 100644
--- a/src/libnm-platform/nm-platform.h
+++ b/src/libnm-platform/nm-platform.h
@@ -8,9 +8,10 @@
 
 #include "libnm-platform/nmp-base.h"
 #include "libnm-base/nm-base.h"
+#include "nmp-plobj.h"
 
 #define NM_TYPE_PLATFORM (nm_platform_get_type())
-#define NM_PLATFORM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_PLATFORM, NMPlatform))
+#define NM_PLATFORM(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_PLATFORM, NMPlatform))
 #define NM_PLATFORM_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_PLATFORM, NMPlatformClass))
 #define NM_IS_PLATFORM(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_PLATFORM))
@@ -22,16 +23,11 @@
 
 /*****************************************************************************/
 
+#define NM_PLATFORM_CACHE_TC      "cache-tc"
+#define NM_PLATFORM_LOG_WITH_PTR  "log-with-ptr"
+#define NM_PLATFORM_MULTI_IDX     "multi-idx"
 #define NM_PLATFORM_NETNS_SUPPORT "netns-support"
 #define NM_PLATFORM_USE_UDEV      "use-udev"
-#define NM_PLATFORM_LOG_WITH_PTR  "log-with-ptr"
-#define NM_PLATFORM_CACHE_TC      "cache-tc"
-
-/*****************************************************************************/
-
-/* IFNAMSIZ is both defined in <linux/if.h> and <net/if.h>. In the past, these
- * headers conflicted, so we cannot simply include either of them in a header-file.*/
-#define NMP_IFNAMSIZ 16
 
 /*****************************************************************************/
 
@@ -41,24 +37,8 @@ struct udev_device;
 
 typedef gboolean (*NMPObjectPredicateFunc)(const NMPObject *obj, gpointer user_data);
 
-/* workaround for older libnl version, that does not define these flags. */
-#ifndef IFA_F_MANAGETEMPADDR
-#define IFA_F_MANAGETEMPADDR 0x100
-#endif
-#ifndef IFA_F_NOPREFIXROUTE
-#define IFA_F_NOPREFIXROUTE 0x200
-#endif
-
 #define NM_RT_SCOPE_LINK 253 /* RT_SCOPE_LINK */
 
-/* Define of the IN6_ADDR_GEN_MODE_* values to workaround old kernel headers
- * that don't define it. */
-#define NM_IN6_ADDR_GEN_MODE_UNKNOWN        255 /* no corresponding value.  */
-#define NM_IN6_ADDR_GEN_MODE_EUI64          0   /* IN6_ADDR_GEN_MODE_EUI64 */
-#define NM_IN6_ADDR_GEN_MODE_NONE           1   /* IN6_ADDR_GEN_MODE_NONE */
-#define NM_IN6_ADDR_GEN_MODE_STABLE_PRIVACY 2   /* IN6_ADDR_GEN_MODE_STABLE_PRIVACY */
-#define NM_IN6_ADDR_GEN_MODE_RANDOM         3   /* IN6_ADDR_GEN_MODE_RANDOM */
-
 #define NM_IFF_MULTI_QUEUE 0x0100 /* IFF_MULTI_QUEUE */
 
 #define NM_MPTCP_PM_ADDR_FLAG_SIGNAL   ((guint32) (1 << 0))
@@ -101,14 +81,6 @@ typedef enum {
 } NMPNlmFlags;
 
 typedef enum {
-    NM_PLATFORM_IP_ADDRESS_CMP_TYPE_ID,
-
-    NM_PLATFORM_IP_ADDRESS_CMP_TYPE_SEMANTICALLY,
-
-    NM_PLATFORM_IP_ADDRESS_CMP_TYPE_FULL,
-} NMPlatformIPAddressCmpType;
-
-typedef enum {
     /* compare fields which kernel considers as similar routes.
      * It is a looser comparisong then NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID
      * and means that `ip route add` would fail to add two routes
@@ -131,6 +103,11 @@ typedef enum {
      */
     NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID,
 
+    /* IPv4 route can have multiple hops. This is the ID, by which multiple
+     * routes are merged according to the next hop. This is basically NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID
+     * which ignores the next hops. */
+    NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID,
+
     /* compare all fields as they make sense for kernel. For example,
      * a route destination 192.168.1.5/24 is not accepted by kernel and
      * we treat it identical to 192.168.1.0/24. Semantically these
@@ -171,50 +148,11 @@ G_STATIC_ASSERT(_nm_alignof(NMPLinkAddress) == 1);
 gconstpointer nmp_link_address_get(const NMPLinkAddress *addr, size_t *length);
 GBytes       *nmp_link_address_get_as_bytes(const NMPLinkAddress *addr);
 
-typedef enum {
-
-    /* match-flags are strictly inclusive. That means,
-     * by default nothing is matched, but if you enable a particular
-     * flag, a candidate that matches passes the check.
-     *
-     * In other words: adding more flags can only extend the result
-     * set of matching objects.
-     *
-     * Also, the flags form partitions. Like, an address can be either of
-     * ADDRTYPE_NORMAL or ADDRTYPE_LINKLOCAL, but never both. Same for
-     * the ADDRSTATE match types.
-     */
-    NM_PLATFORM_MATCH_WITH_NONE = 0,
-
-    NM_PLATFORM_MATCH_WITH_ADDRTYPE_NORMAL    = (1LL << 0),
-    NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL = (1LL << 1),
-    NM_PLATFORM_MATCH_WITH_ADDRTYPE__ANY =
-        NM_PLATFORM_MATCH_WITH_ADDRTYPE_NORMAL | NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL,
-
-    NM_PLATFORM_MATCH_WITH_ADDRSTATE_NORMAL     = (1LL << 2),
-    NM_PLATFORM_MATCH_WITH_ADDRSTATE_TENTATIVE  = (1LL << 3),
-    NM_PLATFORM_MATCH_WITH_ADDRSTATE_DADFAILED  = (1LL << 4),
-    NM_PLATFORM_MATCH_WITH_ADDRSTATE_DEPRECATED = (1LL << 5),
-    NM_PLATFORM_MATCH_WITH_ADDRSTATE__ANY =
-        NM_PLATFORM_MATCH_WITH_ADDRSTATE_NORMAL | NM_PLATFORM_MATCH_WITH_ADDRSTATE_TENTATIVE
-        | NM_PLATFORM_MATCH_WITH_ADDRSTATE_DADFAILED | NM_PLATFORM_MATCH_WITH_ADDRSTATE_DEPRECATED,
-} NMPlatformMatchFlags;
-
 #define NM_PLATFORM_LINK_OTHER_NETNS (-1)
 
-struct _NMPlatformObject {
-    /* the object type has no fields of its own, it is only used to having
-     * a special pointer type that can be used to indicate "any" type. */
-    char _dummy_don_t_use_me;
-};
-
-#define __NMPlatformObjWithIfindex_COMMON \
-    int ifindex;                          \
-    ;
-
 struct _NMPlatformObjWithIfindex {
     __NMPlatformObjWithIfindex_COMMON;
-};
+} _nm_alignas(NMPlatformObject);
 
 struct _NMPlatformLink {
     __NMPlatformObjWithIfindex_COMMON;
@@ -271,7 +209,7 @@ struct _NMPlatformLink {
     bool connected : 1;
 
     bool initialized : 1;
-};
+} _nm_alignas(NMPlatformObject);
 
 typedef enum {
     NM_PLATFORM_SIGNAL_ID_NONE,
@@ -295,129 +233,6 @@ typedef enum {
     NM_PLATFORM_SIGNAL_REMOVED,
 } NMPlatformSignalChangeType;
 
-#define NM_PLATFORM_IP_ADDRESS_CAST(address) \
-    NM_CONSTCAST(NMPlatformIPAddress,        \
-                 (address),                  \
-                 NMPlatformIPXAddress,       \
-                 NMPlatformIP4Address,       \
-                 NMPlatformIP6Address)
-
-#define __NMPlatformIPAddress_COMMON                                                         \
-    __NMPlatformObjWithIfindex_COMMON;                                                       \
-    NMIPConfigSource addr_source;                                                            \
-                                                                                             \
-    /* Timestamp in seconds in the reference system of nm_utils_get_monotonic_timestamp_*().
-     *
-     * The rules are:
-     * 1 @lifetime==0: @timestamp and @preferred is irrelevant (but mostly set to 0 too). Such addresses
-     *   are permanent. This rule is so that unset addresses (calloc) are permanent by default.
-     * 2 @lifetime==@preferred==NM_PLATFORM_LIFETIME_PERMANENT: @timestamp is irrelevant (but mostly
-     *   set to 0). Such addresses are permanent.
-     * 3 Non permanent addresses should (almost) always have @timestamp > 0. 0 is not a valid timestamp
-     *   and never returned by nm_utils_get_monotonic_timestamp_sec(). In this case @valid/@preferred
-     *   is anchored at @timestamp.
-     * 4 Non permanent addresses with @timestamp == 0 are implicitly anchored at *now*, thus the time
-     *   moves as time goes by. This is usually not useful, except e.g. nm_platform_ip[46]_address_add().
-     *
-     * Non permanent addresses from DHCP/RA might have the @timestamp set to the moment of when the
-     * lease was received. Addresses from kernel might have the @timestamp based on the last modification
-     * time of the addresses. But don't rely on this behaviour, the @timestamp is only defined for anchoring
-     * @lifetime and @preferred.
-     */ \
-    guint32 timestamp;                                                                       \
-    guint32 lifetime;  /* seconds since timestamp */                                         \
-    guint32 preferred; /* seconds since timestamp */                                         \
-                                                                                             \
-    /* ifa_flags in 'struct ifaddrmsg' from <linux/if_addr.h>, extended to 32 bit by
-     * IFA_FLAGS attribute. */         \
-    guint32 n_ifa_flags;                                                                     \
-                                                                                             \
-    bool use_ip4_broadcast_address : 1;                                                      \
-                                                                                             \
-    /* Meta flags not honored by NMPlatform (netlink code). Instead, they can be
-     * used by the upper layers which use NMPlatformIPRoute to track addresses that
-     * should be configured. */             \
-    bool a_force_commit : 1;                                                                 \
-                                                                                             \
-    /* nm_platform_ip_address_sync() likes to add IFA_F_NOPREFIXROUTE flag for all
-     * addresses, regardless of a_ifi_flags property. By setting this boolean, that
-     * automatism can be suppressed, and the noprefixroute flag does not get added
-     * automatically. */           \
-    bool a_no_auto_noprefixroute : 1;                                                        \
-                                                                                             \
-    /* Don't have a bitfield as last field in __NMPlatformIPAddress_COMMON. It would then
-     * be unclear how the following fields get merged. We could also use a zero bitfield,
-     * but instead we just have there the uint8 field. */    \
-    guint8 plen;                                                                             \
-    ;
-
-/**
- * NMPlatformIPAddress:
- *
- * Common parts of NMPlatformIP4Address and NMPlatformIP6Address.
- **/
-typedef struct {
-    __NMPlatformIPAddress_COMMON;
-    _nm_alignas(NMIPAddr) guint8 address_ptr[];
-} NMPlatformIPAddress;
-
-/**
- * NMPlatformIP4Address:
- * @timestamp: timestamp as returned by nm_utils_get_monotonic_timestamp_sec()
- **/
-struct _NMPlatformIP4Address {
-    __NMPlatformIPAddress_COMMON;
-
-    /* The local address IFA_LOCAL. */
-    _nm_alignas(NMIPAddr) in_addr_t address;
-
-    /* The IFA_ADDRESS PTP peer address. This field is rather important, because
-     * it constitutes the identifier for the IPv4 address (e.g. you can add two
-     * addresses that only differ by their peer's network-part.
-     *
-     * Beware that for most cases, NetworkManager doesn't want to set an explicit
-     * peer-address. However, that corresponds to setting the peer address to @address
-     * itself. Leaving peer-address unset/zero, means explicitly setting the peer
-     * address to 0.0.0.0, which you probably don't want.
-     * */
-    in_addr_t peer_address; /* PTP peer address */
-
-    /* IFA_BROADCAST.
-     *
-     * This parameter is ignored unless use_ip4_broadcast_address is TRUE.
-     * See nm_platform_ip4_broadcast_address_from_addr(). */
-    in_addr_t broadcast_address;
-
-    char label[NMP_IFNAMSIZ];
-
-    /* Whether the address is ready to be configured. By default, an address is, but this
-     * flag may indicate that the address is just for tracking purpose only, but the ACD
-     * state is not yet ready for the address to be configured. */
-    bool a_acd_not_ready : 1;
-};
-
-/**
- * NMPlatformIP6Address:
- * @timestamp: timestamp as returned by nm_utils_get_monotonic_timestamp_sec()
- **/
-struct _NMPlatformIP6Address {
-    __NMPlatformIPAddress_COMMON;
-    _nm_alignas(NMIPAddr) struct in6_addr address;
-    struct in6_addr peer_address;
-};
-
-typedef union {
-    NMPlatformIPAddress  ax;
-    NMPlatformIP4Address a4;
-    NMPlatformIP6Address a6;
-} NMPlatformIPXAddress;
-
-#undef __NMPlatformIPAddress_COMMON
-
-#define NM_PLATFORM_IP4_ADDRESS_INIT(...) (&((const NMPlatformIP4Address){__VA_ARGS__}))
-
-#define NM_PLATFORM_IP6_ADDRESS_INIT(...) (&((const NMPlatformIP6Address){__VA_ARGS__}))
-
 /* Default value for adding an IPv4 route. This is also what iproute2 does.
  * Note that contrary to IPv6, you can add routes with metric 0 and it is even
  * the default.
@@ -545,16 +360,16 @@ typedef union {
      */                                                                          \
     guint8 type_coerced;                                                                  \
                                                                                           \
-    /* Don't have a bitfield as last field in __NMPlatformIPAddress_COMMON. It would then
+    /* Don't have a bitfield as last field in __NMPlatformIPRoute_COMMON. It would then
      * be unclear how the following fields get merged. We could also use a zero bitfield,
-     * but instead we just have there the uint8 field. */ \
+     * but instead we just have there the uint8 field. */   \
     guint8 plen;                                                                          \
     ;
 
 typedef struct {
     __NMPlatformIPRoute_COMMON;
     _nm_alignas(NMIPAddr) guint8 network_ptr[];
-} NMPlatformIPRoute;
+} _nm_alignas(NMPlatformObject) NMPlatformIPRoute;
 
 #define NM_PLATFORM_IP_ROUTE_CAST(route) \
     NM_CONSTCAST(NMPlatformIPRoute,      \
@@ -567,9 +382,27 @@ typedef struct {
 
 struct _NMPlatformIP4Route {
     __NMPlatformIPRoute_COMMON;
+
     in_addr_t network;
 
-    /* RTA_GATEWAY. The gateway is part of the primary key for a route */
+    /* If n_nexthops is zero, the the address has no next hops. That applies
+     *    to certain route types like blackhole.
+     * If n_nexthops is 1, then the fields "ifindex", "gateway" and "weight"
+     *   are the first next-hop. There are no further nexthops.
+     * If n_nexthops is greater than 1, the first next hop is in the fields
+     *   "ifindex", "gateway", "weight", and the (n_nexthops-1) hops are in
+     *   NMPObjectIP4Route.extra_nexthops field (outside the NMPlatformIP4Route
+     *   struct).
+     *
+     * For convenience, if ifindex > 0 and n_nexthops == 0, we assume that n_nexthops
+     * is in fact 1. If ifindex is <= 0, n_nexthops must be zero.
+     * See nm_platform_ip4_route_get_n_nexthops(). */
+    guint n_nexthops;
+
+    /* RTA_GATEWAY. The gateway is part of the primary key for a route.
+     * If n_nexthops is zero, this value is undefined (should be zero).
+     * If n_nexthops is greater or equal to one, this is the gateway of
+     * the first hop. */
     in_addr_t gateway;
 
     /* RTA_PREFSRC (called "src" by iproute2).
@@ -578,6 +411,20 @@ struct _NMPlatformIP4Route {
      * pref_src must match, unless set to 0.0.0.0 to match any. */
     in_addr_t pref_src;
 
+    /* This is the weight of for the first next-hop, in case of n_nexthops > 1.
+     *
+     * If n_nexthops is zero, this value is undefined (should be zero).
+     * If n_nexthops is 1, this also doesn't matter, but it's usually set to
+     * zero.
+     * If n_nexthops is greater or equal to one, this is the weight of
+     * the first hop.
+     *
+     * Note that upper layers (nm_utils_ip_route_attribute_to_platform()) use this flag to indicate
+     * whether this is a multihop route. Single-hop, non-ECMP routes will have a weight of zero.
+     *
+     * The valid range for weight in kernel is 1-256. */
+    guint16 weight;
+
     /* rtm_tos (iproute2: tos)
      *
      * For IPv4, tos is part of the weak-id (like metric).
@@ -597,7 +444,7 @@ struct _NMPlatformIP4Route {
      * For IPv6 routes, the scope is ignored and kernel always assumes global scope.
      * Hence, this field is only in NMPlatformIP4Route. */
     guint8 scope_inv;
-};
+} _nm_alignas(NMPlatformObject);
 
 struct _NMPlatformIP6Route {
     __NMPlatformIPRoute_COMMON;
@@ -630,7 +477,7 @@ struct _NMPlatformIP6Route {
      * The type is guint8 to keep the struct size small. But the values are compatible with
      * the NMIcmpv6RouterPref enum. */
     guint8 rt_pref;
-};
+} _nm_alignas(NMPlatformObject);
 
 typedef union {
     NMPlatformIPRoute  rx;
@@ -684,7 +531,7 @@ typedef struct {
     guint8             ip_proto;                   /* FRA_IP_PROTO */
 
     bool uid_range_has : 1; /* has(FRA_UID_RANGE) */
-} NMPlatformRoutingRule;
+} _nm_alignas(NMPlatformObject) NMPlatformRoutingRule;
 
 #define NM_PLATFORM_FQ_CODEL_MEMORY_LIMIT_UNSET (~((guint32) 0))
 
@@ -752,7 +599,7 @@ typedef struct {
         NMPlatformQdiscSfq     sfq;
         NMPlatformQdiscTbf     tbf;
     };
-} NMPlatformQdisc;
+} _nm_alignas(NMPlatformObject) NMPlatformQdisc;
 
 typedef struct {
     char sdata[32];
@@ -792,28 +639,7 @@ typedef struct {
     guint32          parent;
     guint32          info;
     NMPlatformAction action;
-} NMPlatformTfilter;
-
-typedef struct {
-    bool          is_ip4;
-    NMPObjectType obj_type;
-    gint8         addr_family;
-    guint8        sizeof_address;
-    int (*address_cmp)(const NMPlatformIPXAddress *a,
-                       const NMPlatformIPXAddress *b,
-                       NMPlatformIPAddressCmpType  cmp_type);
-    const char *(*address_to_string)(const NMPlatformIPXAddress *address, char *buf, gsize len);
-} NMPlatformVTableAddress;
-
-typedef union {
-    struct {
-        NMPlatformVTableAddress v6;
-        NMPlatformVTableAddress v4;
-    };
-    NMPlatformVTableAddress vx[2];
-} _NMPlatformVTableAddressUnion;
-
-extern const _NMPlatformVTableAddressUnion nm_platform_vtable_address;
+} _nm_alignas(NMPlatformObject) NMPlatformTfilter;
 
 typedef struct {
     bool          is_ip4;
@@ -843,6 +669,25 @@ typedef struct {
 } NMPlatformVFVlan;
 
 typedef struct {
+    int       ifindex;
+    in_addr_t gateway;
+    /* The valid range for weight is 1-256. Single hop routes in kernel
+     * don't have a weight, we assign them weight zero (to indicate the
+     * weight is missing).
+     *
+     * Upper layers (nm_utils_ip_route_attribute_to_platform()) care about
+     * the distinction of unset weight (no-ECMP). They express no-ECMP as
+     * zero.
+     */
+    guint16 weight;
+
+    /* FIXME: each next hop in kernel also has a rtnh_flags (for example to
+     * set RTNH_F_ONLINK). As the next hop is part of the identifier of an
+     * IPv4 route, so is their flags. We must also track the flag, otherwise
+     * two routes that look different for kernel, get merged by platform cache. */
+} NMPlatformIP4RtNextHop;
+
+typedef struct {
     guint             num_vlans;
     guint32           index;
     guint32           min_tx_rate;
@@ -887,7 +732,7 @@ typedef struct {
     bool        mcast_snooping : 1;
     bool        stp_state : 1;
     bool        vlan_stats_enabled : 1;
-} NMPlatformLnkBridge;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkBridge;
 
 extern const NMPlatformLnkBridge nm_platform_lnk_bridge_default;
 
@@ -929,7 +774,7 @@ typedef struct {
     bool        tlb_dynamic_lb_has : 1;
     bool        updelay_has : 1;
     bool        use_carrier : 1;
-} NMPlatformLnkBond;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkBond;
 
 typedef struct {
     int       parent_ifindex;
@@ -943,12 +788,12 @@ typedef struct {
     guint8    tos;
     bool      path_mtu_discovery : 1;
     bool      is_tap : 1;
-} NMPlatformLnkGre;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkGre;
 
 typedef struct {
     int         p_key;
     const char *mode;
-} NMPlatformLnkInfiniband;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkInfiniband;
 
 typedef struct {
     struct in6_addr local;
@@ -968,7 +813,7 @@ typedef struct {
     guint16 output_flags;
     bool    is_tap : 1;
     bool    is_gre : 1;
-} NMPlatformLnkIp6Tnl;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkIp6Tnl;
 
 typedef struct {
     int       parent_ifindex;
@@ -977,9 +822,28 @@ typedef struct {
     guint8    ttl;
     guint8    tos;
     bool      path_mtu_discovery : 1;
-} NMPlatformLnkIpIp;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkIpIp;
+
+typedef struct {
+    int       parent_ifindex;
+    in_addr_t local;
+    in_addr_t remote;
+    guint32   ikey;
+    guint32   okey;
+    guint32   fwmark;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkVti;
+
+typedef struct {
+    int             parent_ifindex;
+    struct in6_addr local;
+    struct in6_addr remote;
+    guint32         ikey;
+    guint32         okey;
+    guint32         fwmark;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkVti6;
 
 typedef struct {
+    int     parent_ifindex;
     guint64 sci; /* host byte order */
     guint64 cipher_suite;
     guint32 window;
@@ -992,13 +856,13 @@ typedef struct {
     bool    es : 1;
     bool    scb : 1;
     bool    replay_protect : 1;
-} NMPlatformLnkMacsec;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkMacsec;
 
 typedef struct {
     guint mode;
     bool  no_promisc : 1;
     bool  tap : 1;
-} NMPlatformLnkMacvlan;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkMacvlan;
 
 typedef struct {
     int       parent_ifindex;
@@ -1009,7 +873,7 @@ typedef struct {
     guint8    tos;
     guint8    proto;
     bool      path_mtu_discovery : 1;
-} NMPlatformLnkSit;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkSit;
 
 typedef struct {
     guint32 owner;
@@ -1024,17 +888,17 @@ typedef struct {
     bool vnet_hdr : 1;
     bool multi_queue : 1;
     bool persist : 1;
-} NMPlatformLnkTun;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkTun;
 
 typedef struct {
-    /* rtnl_link_vlan_get_id(), IFLA_VLAN_ID */
     guint16      id;
+    guint16      protocol;
     _NMVlanFlags flags;
-} NMPlatformLnkVlan;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkVlan;
 
 typedef struct {
     guint32 table;
-} NMPlatformLnkVrf;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkVrf;
 
 typedef struct {
     struct in6_addr group6;
@@ -1055,7 +919,7 @@ typedef struct {
     bool            rsc : 1;
     bool            l2miss : 1;
     bool            l3miss : 1;
-} NMPlatformLnkVxlan;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkVxlan;
 
 #define NMP_WIREGUARD_PUBLIC_KEY_LEN    32
 #define NMP_WIREGUARD_SYMMETRIC_KEY_LEN 32
@@ -1065,7 +929,7 @@ typedef struct {
     guint16 listen_port;
     guint8  private_key[NMP_WIREGUARD_PUBLIC_KEY_LEN];
     guint8  public_key[NMP_WIREGUARD_PUBLIC_KEY_LEN];
-} NMPlatformLnkWireGuard;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkWireGuard;
 
 typedef enum {
     NM_PLATFORM_WIREGUARD_CHANGE_FLAG_NONE            = 0,
@@ -1370,10 +1234,7 @@ typedef struct {
                                    struct in6_addr address,
                                    guint8          plen);
 
-    int (*ip_route_add)(NMPlatform              *self,
-                        NMPNlmFlags              flags,
-                        int                      addr_family,
-                        const NMPlatformIPRoute *route);
+    int (*ip_route_add)(NMPlatform *self, NMPNlmFlags flags, NMPObject *obj_stack);
     int (*ip_route_get)(NMPlatform   *self,
                         int           addr_family,
                         gconstpointer address,
@@ -1426,29 +1287,6 @@ GType nm_platform_get_type(void);
 
 /*****************************************************************************/
 
-static inline in_addr_t
-nm_platform_ip4_broadcast_address_create(in_addr_t address, guint8 plen)
-{
-    return address | ~_nm_utils_ip4_prefix_to_netmask(plen);
-}
-
-static inline in_addr_t
-nm_platform_ip4_broadcast_address_from_addr(const NMPlatformIP4Address *addr)
-{
-    nm_assert(addr);
-
-    if (addr->use_ip4_broadcast_address)
-        return addr->broadcast_address;
-
-    /* the set broadcast-address gets ignored, and we determine a default brd base
-     * on the peer IFA_ADDRESS. */
-    if (addr->peer_address != 0u && addr->plen < 31 /* RFC3021 */)
-        return nm_platform_ip4_broadcast_address_create(addr->peer_address, addr->plen);
-    return 0u;
-}
-
-/*****************************************************************************/
-
 /**
  * nm_platform_route_table_coerce:
  * @table: the route table, in its original value as received
@@ -1589,9 +1427,9 @@ nm_platform_ip4_address_get_scope(in_addr_t addr)
     /* For IPv4 addresses, we can set any scope we want (for any address).
      * However, there are scopes that make sense based on the address,
      * so choose those. */
-    return nm_utils_ip4_address_is_loopback(addr)     ? (254 /* RT_SCOPE_HOST */)
-           : nm_utils_ip4_address_is_link_local(addr) ? (253 /* RT_SCOPE_LINK */)
-                                                      : (0 /* RT_SCOPE_UNIVERSE */);
+    return nm_ip4_addr_is_loopback(addr)     ? (254 /* RT_SCOPE_HOST */)
+           : nm_ip4_addr_is_link_local(addr) ? (253 /* RT_SCOPE_LINK */)
+                                             : (0 /* RT_SCOPE_UNIVERSE */);
 }
 
 static inline guint8
@@ -1612,7 +1450,7 @@ nm_platform_ip_address_get_scope(int addr_family, gconstpointer addr)
     /* Note that this function returns the scope as we configure
      * it in kernel (for IPv4) or as kernel chooses it (for IPv6).
      *
-     * That means, rfc1918 private addresses nm_utils_ip_is_site_local() are
+     * That means, rfc1918 private addresses nm_ip_addr_is_site_local() are
      * considered RT_SCOPE_UNIVERSE.
      *
      * Also, the deprecated IN6_IS_ADDR_SITELOCAL() addresses (fec0::/10)
@@ -1851,28 +1689,13 @@ nm_platform_link_sit_add(NMPlatform             *self,
 }
 
 static inline int
-nm_platform_link_vlan_add(NMPlatform            *self,
-                          const char            *name,
-                          int                    parent,
-                          int                    vlanid,
-                          guint32                vlanflags,
-                          const NMPlatformLink **out_link)
+nm_platform_link_vlan_add(NMPlatform              *self,
+                          const char              *name,
+                          int                      parent,
+                          const NMPlatformLnkVlan *props,
+                          const NMPlatformLink   **out_link)
 {
-    g_return_val_if_fail(parent >= 0, -NME_BUG);
-    g_return_val_if_fail(vlanid >= 0, -NME_BUG);
-
-    return nm_platform_link_add(self,
-                                NM_LINK_TYPE_VLAN,
-                                name,
-                                parent,
-                                NULL,
-                                0,
-                                0,
-                                &((NMPlatformLnkVlan){
-                                    .id    = vlanid,
-                                    .flags = vlanflags,
-                                }),
-                                out_link);
+    return nm_platform_link_add(self, NM_LINK_TYPE_VLAN, name, parent, NULL, 0, 0, props, out_link);
 }
 
 static inline int
@@ -1885,6 +1708,24 @@ nm_platform_link_vrf_add(NMPlatform             *self,
 }
 
 static inline int
+nm_platform_link_vti_add(NMPlatform             *self,
+                         const char             *name,
+                         const NMPlatformLnkVti *props,
+                         const NMPlatformLink  **out_link)
+{
+    return nm_platform_link_add(self, NM_LINK_TYPE_VTI, name, 0, NULL, 0, 0, props, out_link);
+}
+
+static inline int
+nm_platform_link_vti6_add(NMPlatform              *self,
+                          const char              *name,
+                          const NMPlatformLnkVti6 *props,
+                          const NMPlatformLink   **out_link)
+{
+    return nm_platform_link_add(self, NM_LINK_TYPE_VTI6, name, 0, NULL, 0, 0, props, out_link);
+}
+
+static inline int
 nm_platform_link_vxlan_add(NMPlatform               *self,
                            const char               *name,
                            const NMPlatformLnkVxlan *props,
@@ -2173,6 +2014,10 @@ const NMPlatformLnkVlan *
 nm_platform_link_get_lnk_vlan(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
 const NMPlatformLnkVrf *
 nm_platform_link_get_lnk_vrf(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
+const NMPlatformLnkVti *
+nm_platform_link_get_lnk_vti(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
+const NMPlatformLnkVti6 *
+nm_platform_link_get_lnk_vti6(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
 const NMPlatformLnkVxlan *
 nm_platform_link_get_lnk_vxlan(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
 const NMPlatformLnkWireGuard *
@@ -2238,9 +2083,6 @@ guint16  nm_platform_wpan_get_short_addr(NMPlatform *self, int ifindex);
 gboolean nm_platform_wpan_set_short_addr(NMPlatform *self, int ifindex, guint16 short_addr);
 gboolean nm_platform_wpan_set_channel(NMPlatform *self, int ifindex, guint8 page, guint8 channel);
 
-void nm_platform_ip4_address_set_addr(NMPlatformIP4Address *addr, in_addr_t address, guint8 plen);
-const struct in6_addr *nm_platform_ip6_address_get_peer(const NMPlatformIP6Address *addr);
-
 const NMPObject *nm_platform_ip_address_get(NMPlatform                                 *self,
                                             int                                         addr_family,
                                             int                                         ifindex,
@@ -2252,6 +2094,9 @@ const NMPlatformIP4Address *nm_platform_ip4_address_get(NMPlatform *self,
                                                         guint8      plen,
                                                         in_addr_t   peer_address);
 
+const NMPlatformIP6Address *
+nm_platform_ip6_address_get(NMPlatform *self, int ifindex, const struct in6_addr *address);
+
 int      nm_platform_link_sit_add(NMPlatform             *self,
                                   const char             *name,
                                   const NMPlatformLnkSit *props,
@@ -2274,9 +2119,6 @@ int nm_platform_link_wireguard_change(NMPlatform                               *
                                       guint                                     peers_len,
                                       NMPlatformWireGuardChangeFlags            change_flags);
 
-const NMPlatformIP6Address *
-nm_platform_ip6_address_get(NMPlatform *self, int ifindex, const struct in6_addr *address);
-
 gboolean nm_platform_object_delete(NMPlatform *self, const NMPObject *route);
 
 gboolean nm_platform_ip4_address_add(NMPlatform *self,
@@ -2328,11 +2170,17 @@ nm_platform_ip_address_delete(NMPlatform                                       *
     }
 }
 
-gboolean nm_platform_ip_address_sync(NMPlatform *self,
-                                     int         addr_family,
-                                     int         ifindex,
-                                     GPtrArray  *known_addresses,
-                                     GPtrArray  *addresses_prune);
+typedef enum {
+    NMP_IP_ADDRESS_SYNC_FLAGS_NONE               = 0,
+    NMP_IP_ADDRESS_SYNC_FLAGS_WITH_NOPREFIXROUTE = (1 << 0),
+} NMPIPAddressSyncFlags;
+
+gboolean nm_platform_ip_address_sync(NMPlatform           *self,
+                                     int                   addr_family,
+                                     int                   ifindex,
+                                     GPtrArray            *known_addresses,
+                                     GPtrArray            *addresses_prune,
+                                     NMPIPAddressSyncFlags flags);
 
 GPtrArray *
 nm_platform_ip_address_get_prune_list(NMPlatform            *self,
@@ -2343,17 +2191,6 @@ nm_platform_ip_address_get_prune_list(NMPlatform            *self,
 
 gboolean nm_platform_ip_address_flush(NMPlatform *self, int addr_family, int ifindex);
 
-static inline gpointer
-nm_platform_ip_address_get_peer_address(int addr_family, const NMPlatformIPAddress *addr)
-{
-    nm_assert_addr_family(addr_family);
-    nm_assert(addr);
-
-    if (NM_IS_IPv4(addr_family))
-        return &((NMPlatformIP4Address *) addr)->peer_address;
-    return &((NMPlatformIP6Address *) addr)->peer_address;
-}
-
 void nm_platform_ip_route_normalize(int addr_family, NMPlatformIPRoute *route);
 
 static inline guint32
@@ -2365,6 +2202,23 @@ nm_platform_ip4_route_get_effective_metric(const NMPlatformIP4Route *r)
                          : r->metric;
 }
 
+static inline guint
+nm_platform_ip4_route_get_n_nexthops(const NMPlatformIP4Route *r)
+{
+    /* The first hop of the "n_nexthops" is in NMPlatformIP4Route
+     * itself. Thus, if the caller only sets ifindex and leaves
+     * n_nexthops at zero, the number of next hops is still 1
+     * (for convenience of the user who wants to initialize a
+     * single hop route). */
+    if (r->n_nexthops >= 1) {
+        nm_assert(r->ifindex > 0);
+        return r->n_nexthops;
+    }
+    if (r->ifindex > 0)
+        return 1;
+    return 0;
+}
+
 static inline guint32
 nm_platform_ip6_route_get_effective_metric(const NMPlatformIP6Route *r)
 {
@@ -2398,7 +2252,10 @@ nm_platform_ip_route_get_gateway(int addr_family, const NMPlatformIPRoute *route
 }
 
 int nm_platform_ip_route_add(NMPlatform *self, NMPNlmFlags flags, const NMPObject *route);
-int nm_platform_ip4_route_add(NMPlatform *self, NMPNlmFlags flags, const NMPlatformIP4Route *route);
+int nm_platform_ip4_route_add(NMPlatform                   *self,
+                              NMPNlmFlags                   flags,
+                              const NMPlatformIP4Route     *route,
+                              const NMPlatformIP4RtNextHop *extra_nexthops);
 int nm_platform_ip6_route_add(NMPlatform *self, NMPNlmFlags flags, const NMPlatformIP6Route *route);
 
 GPtrArray *nm_platform_ip_route_get_prune_list(NMPlatform            *self,
@@ -2449,14 +2306,23 @@ const char *nm_platform_lnk_sit_to_string(const NMPlatformLnkSit *lnk, char *buf
 const char *nm_platform_lnk_tun_to_string(const NMPlatformLnkTun *lnk, char *buf, gsize len);
 const char *nm_platform_lnk_vlan_to_string(const NMPlatformLnkVlan *lnk, char *buf, gsize len);
 const char *nm_platform_lnk_vrf_to_string(const NMPlatformLnkVrf *lnk, char *buf, gsize len);
+const char *nm_platform_lnk_vti_to_string(const NMPlatformLnkVti *lnk, char *buf, gsize len);
+const char *nm_platform_lnk_vti6_to_string(const NMPlatformLnkVti6 *lnk, char *buf, gsize len);
 const char *nm_platform_lnk_vxlan_to_string(const NMPlatformLnkVxlan *lnk, char *buf, gsize len);
 const char *
 nm_platform_lnk_wireguard_to_string(const NMPlatformLnkWireGuard *lnk, char *buf, gsize len);
-const char *
-nm_platform_ip4_address_to_string(const NMPlatformIP4Address *address, char *buf, gsize len);
-const char *
-nm_platform_ip6_address_to_string(const NMPlatformIP6Address *address, char *buf, gsize len);
-const char *nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsize len);
+
+const char *nm_platform_ip4_route_to_string_full(const NMPlatformIP4Route     *route,
+                                                 const NMPlatformIP4RtNextHop *extra_nexthops,
+                                                 char                         *buf,
+                                                 gsize                         len);
+
+static inline const char *
+nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsize len)
+{
+    return nm_platform_ip4_route_to_string_full(route, NULL, buf, len);
+}
+
 const char *nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsize len);
 const char *
 nm_platform_routing_rule_to_string(const NMPlatformRoutingRule *routing_rule, char *buf, gsize len);
@@ -2492,79 +2358,52 @@ int nm_platform_lnk_sit_cmp(const NMPlatformLnkSit *a, const NMPlatformLnkSit *b
 int nm_platform_lnk_tun_cmp(const NMPlatformLnkTun *a, const NMPlatformLnkTun *b);
 int nm_platform_lnk_vlan_cmp(const NMPlatformLnkVlan *a, const NMPlatformLnkVlan *b);
 int nm_platform_lnk_vrf_cmp(const NMPlatformLnkVrf *a, const NMPlatformLnkVrf *b);
+int nm_platform_lnk_vti_cmp(const NMPlatformLnkVti *a, const NMPlatformLnkVti *b);
+int nm_platform_lnk_vti6_cmp(const NMPlatformLnkVti6 *a, const NMPlatformLnkVti6 *b);
 int nm_platform_lnk_vxlan_cmp(const NMPlatformLnkVxlan *a, const NMPlatformLnkVxlan *b);
 int nm_platform_lnk_wireguard_cmp(const NMPlatformLnkWireGuard *a, const NMPlatformLnkWireGuard *b);
-int nm_platform_ip4_address_cmp(const NMPlatformIP4Address *a,
-                                const NMPlatformIP4Address *b,
-                                NMPlatformIPAddressCmpType  cmp_type);
-int nm_platform_ip6_address_cmp(const NMPlatformIP6Address *a,
-                                const NMPlatformIP6Address *b,
-                                NMPlatformIPAddressCmpType  cmp_type);
-
-static inline int
-nm_platform_ip4_address_cmp_full(const NMPlatformIP4Address *a, const NMPlatformIP4Address *b)
-{
-    return nm_platform_ip4_address_cmp(a, b, NM_PLATFORM_IP_ADDRESS_CMP_TYPE_FULL);
-}
-
-static inline int
-nm_platform_ip6_address_cmp_full(const NMPlatformIP6Address *a, const NMPlatformIP6Address *b)
-{
-    return nm_platform_ip6_address_cmp(a, b, NM_PLATFORM_IP_ADDRESS_CMP_TYPE_FULL);
-}
-
-int nm_platform_ip4_address_pretty_sort_cmp(const NMPlatformIP4Address *a1,
-                                            const NMPlatformIP4Address *a2);
-
-int nm_platform_ip6_address_pretty_sort_cmp(const NMPlatformIP6Address *a1,
-                                            const NMPlatformIP6Address *a2,
-                                            gboolean                    prefer_temp);
 
 GHashTable *nm_platform_ip4_address_addr_to_hash(NMPlatform *self, int ifindex);
 
 int nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
                               const NMPlatformIP4Route *b,
                               NMPlatformIPRouteCmpType  cmp_type);
+int nm_platform_ip4_rt_nexthop_cmp(const NMPlatformIP4RtNextHop *a,
+                                   const NMPlatformIP4RtNextHop *b,
+                                   gboolean                      for_id);
 int nm_platform_ip6_route_cmp(const NMPlatformIP6Route *a,
                               const NMPlatformIP6Route *b,
                               NMPlatformIPRouteCmpType  cmp_type);
 
-static inline int
-nm_platform_ip4_route_cmp_full(const NMPlatformIP4Route *a, const NMPlatformIP4Route *b)
-{
-    return nm_platform_ip4_route_cmp(a, b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL);
-}
-
-static inline int
-nm_platform_ip6_route_cmp_full(const NMPlatformIP6Route *a, const NMPlatformIP6Route *b)
-{
-    return nm_platform_ip6_route_cmp(a, b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL);
-}
-
 int nm_platform_routing_rule_cmp(const NMPlatformRoutingRule *a,
                                  const NMPlatformRoutingRule *b,
                                  NMPlatformRoutingRuleCmpType cmp_type);
 
-static inline int
-nm_platform_routing_rule_cmp_full(const NMPlatformRoutingRule *a, const NMPlatformRoutingRule *b)
-{
-    return nm_platform_routing_rule_cmp(a, b, NM_PLATFORM_ROUTING_RULE_CMP_TYPE_FULL);
-}
+int
+nm_platform_qdisc_cmp(const NMPlatformQdisc *a, const NMPlatformQdisc *b, gboolean compare_handle);
 
-int nm_platform_qdisc_cmp(const NMPlatformQdisc *a, const NMPlatformQdisc *b);
-int nm_platform_qdisc_cmp_full(const NMPlatformQdisc *a,
-                               const NMPlatformQdisc *b,
-                               gboolean               compare_handle);
 int nm_platform_tfilter_cmp(const NMPlatformTfilter *a, const NMPlatformTfilter *b);
 
 int nm_platform_mptcp_addr_cmp(const NMPlatformMptcpAddr *a, const NMPlatformMptcpAddr *b);
 
 void nm_platform_link_hash_update(const NMPlatformLink *obj, NMHashState *h);
-void nm_platform_ip4_address_hash_update(const NMPlatformIP4Address *obj, NMHashState *h);
-void nm_platform_ip6_address_hash_update(const NMPlatformIP6Address *obj, NMHashState *h);
 void nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
                                        NMPlatformIPRouteCmpType  cmp_type,
                                        NMHashState              *h);
+
+static inline guint
+nm_platform_ip4_route_hash(const NMPlatformIP4Route *obj, NMPlatformIPRouteCmpType cmp_type)
+{
+    NMHashState h;
+
+    nm_hash_init(&h, 1118769853u);
+    nm_platform_ip4_route_hash_update(obj, cmp_type, &h);
+    return nm_hash_complete(&h);
+}
+
+void nm_platform_ip4_rt_nexthop_hash_update(const NMPlatformIP4RtNextHop *obj,
+                                            gboolean                      for_id,
+                                            NMHashState                  *h);
 void nm_platform_ip6_route_hash_update(const NMPlatformIP6Route *obj,
                                        NMPlatformIPRouteCmpType  cmp_type,
                                        NMHashState              *h);
@@ -2583,6 +2422,8 @@ void nm_platform_lnk_sit_hash_update(const NMPlatformLnkSit *obj, NMHashState *h
 void nm_platform_lnk_tun_hash_update(const NMPlatformLnkTun *obj, NMHashState *h);
 void nm_platform_lnk_vlan_hash_update(const NMPlatformLnkVlan *obj, NMHashState *h);
 void nm_platform_lnk_vrf_hash_update(const NMPlatformLnkVrf *obj, NMHashState *h);
+void nm_platform_lnk_vti_hash_update(const NMPlatformLnkVti *obj, NMHashState *h);
+void nm_platform_lnk_vti6_hash_update(const NMPlatformLnkVti6 *obj, NMHashState *h);
 void nm_platform_lnk_vxlan_hash_update(const NMPlatformLnkVxlan *obj, NMHashState *h);
 void nm_platform_lnk_wireguard_hash_update(const NMPlatformLnkWireGuard *obj, NMHashState *h);
 
@@ -2596,13 +2437,6 @@ gboolean nm_platform_mptcp_addr_index_addr_equal(gconstpointer data_a, gconstpoi
 
 #define NM_PLATFORM_LINK_FLAGS2STR_MAX_LEN ((gsize) 162)
 
-const char *nm_platform_link_flags2str(unsigned flags, char *buf, gsize len);
-const char *nm_platform_link_inet6_addrgenmode2str(guint8 mode, char *buf, gsize len);
-const char *nm_platform_addr_flags2str(unsigned flags, char *buf, gsize len);
-const char *nm_platform_route_scope2str(int scope, char *buf, gsize len);
-
-int nm_platform_ip_address_cmp_expiry(const NMPlatformIPAddress *a, const NMPlatformIPAddress *b);
-
 gboolean nm_platform_ethtool_set_wake_on_lan(NMPlatform              *self,
                                              int                      ifindex,
                                              _NMSettingWiredWakeOnLan wol,
@@ -2653,21 +2487,6 @@ struct _NMDedupMultiIndex *nm_platform_get_multi_idx(NMPlatform *self);
 
 /*****************************************************************************/
 
-NMPlatformIP4Route *nm_platform_ip4_address_generate_device_route(const NMPlatformIP4Address *addr,
-                                                                  int                 ifindex,
-                                                                  guint32             route_table,
-                                                                  guint32             route_metric,
-                                                                  gboolean            force_commit,
-                                                                  NMPlatformIP4Route *dst);
-
-/*****************************************************************************/
-
-gboolean nm_platform_ip_address_match(int                        addr_family,
-                                      const NMPlatformIPAddress *addr,
-                                      NMPlatformMatchFlags       match_flag);
-
-/*****************************************************************************/
-
 guint16 nm_platform_genl_get_family_id(NMPlatform *self, NMPGenlFamilyType family_type);
 
 int
@@ -2675,4 +2494,35 @@ nm_platform_mptcp_addr_update(NMPlatform *self, NMOptionBool add, const NMPlatfo
 
 GPtrArray *nm_platform_mptcp_addrs_dump(NMPlatform *self);
 
+gboolean nm_platform_ip6_dadfailed_check(NMPlatform *self, int ifindex, const struct in6_addr *ip6);
+void     nm_platform_ip6_dadfailed_set(NMPlatform            *self,
+                                       int                    ifindex,
+                                       const struct in6_addr *ip6,
+                                       gboolean               failed);
+
+/*****************************************************************************/
+
+static inline NMPlatformIP4Address *
+nm_platform_ip4_address_init_loopback_addr1(NMPlatformIP4Address *a)
+{
+    *a = ((NMPlatformIP4Address){
+        .address      = NM_IPV4LO_ADDR1,
+        .peer_address = NM_IPV4LO_ADDR1,
+        .ifindex      = NM_LOOPBACK_IFINDEX,
+        .plen         = NM_IPV4LO_PREFIXLEN,
+    });
+    return a;
+}
+
+static inline NMPlatformIP6Address *
+nm_platform_ip6_address_init_loopback(NMPlatformIP6Address *a)
+{
+    *a = ((NMPlatformIP6Address){
+        .address = IN6ADDR_LOOPBACK_INIT,
+        .ifindex = NM_LOOPBACK_IFINDEX,
+        .plen    = 128,
+    });
+    return a;
+}
+
 #endif /* __NETWORKMANAGER_PLATFORM_H__ */
diff --git a/src/libnm-platform/nmp-base.c b/src/libnm-platform/nmp-base.c
new file mode 100644
index 00000000..c23190b3
--- /dev/null
+++ b/src/libnm-platform/nmp-base.c
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2015 - 2018 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nmp-base.h"
+
+#include <linux/rtnetlink.h>
+#include <linux/if.h>
+#include "nm-compat-headers/linux/if_addr.h"
+
+/*****************************************************************************/
+
+NM_UTILS_FLAGS2STR_DEFINE(nm_platform_link_flags2str,
+                          unsigned,
+                          NM_UTILS_FLAGS2STR(IFF_LOOPBACK, "loopback"),
+                          NM_UTILS_FLAGS2STR(IFF_BROADCAST, "broadcast"),
+                          NM_UTILS_FLAGS2STR(IFF_POINTOPOINT, "pointopoint"),
+                          NM_UTILS_FLAGS2STR(IFF_MULTICAST, "multicast"),
+                          NM_UTILS_FLAGS2STR(IFF_NOARP, "noarp"),
+                          NM_UTILS_FLAGS2STR(IFF_ALLMULTI, "allmulti"),
+                          NM_UTILS_FLAGS2STR(IFF_PROMISC, "promisc"),
+                          NM_UTILS_FLAGS2STR(IFF_MASTER, "master"),
+                          NM_UTILS_FLAGS2STR(IFF_SLAVE, "slave"),
+                          NM_UTILS_FLAGS2STR(IFF_DEBUG, "debug"),
+                          NM_UTILS_FLAGS2STR(IFF_DYNAMIC, "dynamic"),
+                          NM_UTILS_FLAGS2STR(IFF_AUTOMEDIA, "automedia"),
+                          NM_UTILS_FLAGS2STR(IFF_PORTSEL, "portsel"),
+                          NM_UTILS_FLAGS2STR(IFF_NOTRAILERS, "notrailers"),
+                          NM_UTILS_FLAGS2STR(IFF_UP, "up"),
+                          NM_UTILS_FLAGS2STR(IFF_RUNNING, "running"),
+                          NM_UTILS_FLAGS2STR(IFF_LOWER_UP, "lowerup"),
+                          NM_UTILS_FLAGS2STR(IFF_DORMANT, "dormant"),
+                          NM_UTILS_FLAGS2STR(IFF_ECHO, "echo"), );
+
+NM_UTILS_ENUM2STR_DEFINE(nm_platform_link_inet6_addrgenmode2str,
+                         guint8,
+                         NM_UTILS_ENUM2STR(NM_IN6_ADDR_GEN_MODE_NONE, "none"),
+                         NM_UTILS_ENUM2STR(NM_IN6_ADDR_GEN_MODE_EUI64, "eui64"),
+                         NM_UTILS_ENUM2STR(NM_IN6_ADDR_GEN_MODE_STABLE_PRIVACY, "stable-privacy"),
+                         NM_UTILS_ENUM2STR(NM_IN6_ADDR_GEN_MODE_RANDOM, "random"), );
+
+NM_UTILS_FLAGS2STR_DEFINE(nm_platform_addr_flags2str,
+                          unsigned,
+                          NM_UTILS_FLAGS2STR(IFA_F_SECONDARY, "secondary"),
+                          NM_UTILS_FLAGS2STR(IFA_F_NODAD, "nodad"),
+                          NM_UTILS_FLAGS2STR(IFA_F_OPTIMISTIC, "optimistic"),
+                          NM_UTILS_FLAGS2STR(IFA_F_DADFAILED, "dadfailed"),
+                          NM_UTILS_FLAGS2STR(IFA_F_HOMEADDRESS, "homeaddress"),
+                          NM_UTILS_FLAGS2STR(IFA_F_DEPRECATED, "deprecated"),
+                          NM_UTILS_FLAGS2STR(IFA_F_TENTATIVE, "tentative"),
+                          NM_UTILS_FLAGS2STR(IFA_F_PERMANENT, "permanent"),
+                          NM_UTILS_FLAGS2STR(IFA_F_MANAGETEMPADDR, "mngtmpaddr"),
+                          NM_UTILS_FLAGS2STR(IFA_F_NOPREFIXROUTE, "noprefixroute"),
+                          NM_UTILS_FLAGS2STR(IFA_F_MCAUTOJOIN, "mcautojoin"),
+                          NM_UTILS_FLAGS2STR(IFA_F_STABLE_PRIVACY, "stable-privacy"), );
+
+G_STATIC_ASSERT(IFA_F_SECONDARY == IFA_F_TEMPORARY);
+
+NM_UTILS_ENUM2STR_DEFINE(nm_platform_route_scope2str,
+                         int,
+                         NM_UTILS_ENUM2STR(RT_SCOPE_NOWHERE, "nowhere"),
+                         NM_UTILS_ENUM2STR(RT_SCOPE_HOST, "host"),
+                         NM_UTILS_ENUM2STR(RT_SCOPE_LINK, "link"),
+                         NM_UTILS_ENUM2STR(RT_SCOPE_SITE, "site"),
+                         NM_UTILS_ENUM2STR(RT_SCOPE_UNIVERSE, "global"), );
diff --git a/src/libnm-platform/nmp-base.h b/src/libnm-platform/nmp-base.h
index a6ee3df1..80d254b2 100644
--- a/src/libnm-platform/nmp-base.h
+++ b/src/libnm-platform/nmp-base.h
@@ -11,6 +11,22 @@
 
 /*****************************************************************************/
 
+/* IFNAMSIZ is both defined in <linux/if.h> and <net/if.h>. In the past, these
+ * headers conflicted, so we cannot simply include either of them in a header-file.*/
+#define NMP_IFNAMSIZ 16
+
+/*****************************************************************************/
+
+/* Define of the IN6_ADDR_GEN_MODE_* values to workaround old kernel headers
+ * that don't define it. */
+#define NM_IN6_ADDR_GEN_MODE_UNKNOWN        255 /* no corresponding value.  */
+#define NM_IN6_ADDR_GEN_MODE_EUI64          0   /* IN6_ADDR_GEN_MODE_EUI64 */
+#define NM_IN6_ADDR_GEN_MODE_NONE           1   /* IN6_ADDR_GEN_MODE_NONE */
+#define NM_IN6_ADDR_GEN_MODE_STABLE_PRIVACY 2   /* IN6_ADDR_GEN_MODE_STABLE_PRIVACY */
+#define NM_IN6_ADDR_GEN_MODE_RANDOM         3   /* IN6_ADDR_GEN_MODE_RANDOM */
+
+/*****************************************************************************/
+
 typedef enum {
     NM_PLATFORM_LINK_DUPLEX_UNKNOWN,
     NM_PLATFORM_LINK_DUPLEX_HALF,
@@ -145,6 +161,8 @@ typedef enum _nm_packed {
     NMP_OBJECT_TYPE_LNK_TUN,
     NMP_OBJECT_TYPE_LNK_VLAN,
     NMP_OBJECT_TYPE_LNK_VRF,
+    NMP_OBJECT_TYPE_LNK_VTI,
+    NMP_OBJECT_TYPE_LNK_VTI6,
     NMP_OBJECT_TYPE_LNK_VXLAN,
     NMP_OBJECT_TYPE_LNK_WIREGUARD,
     NMP_OBJECT_TYPE_LNK_BOND,
@@ -189,4 +207,11 @@ typedef enum {
     NM_IP_ROUTE_TABLE_SYNC_MODE_ALL_PRUNE,
 } NMIPRouteTableSyncMode;
 
+/*****************************************************************************/
+
+const char *nm_platform_link_flags2str(unsigned flags, char *buf, gsize len);
+const char *nm_platform_link_inet6_addrgenmode2str(guint8 mode, char *buf, gsize len);
+const char *nm_platform_addr_flags2str(unsigned flags, char *buf, gsize len);
+const char *nm_platform_route_scope2str(int scope, char *buf, gsize len);
+
 #endif /* __NMP_FWD_H__ */
diff --git a/src/libnm-platform/nmp-global-tracker.c b/src/libnm-platform/nmp-global-tracker.c
index 12869cb2..09f1e217 100644
--- a/src/libnm-platform/nmp-global-tracker.c
+++ b/src/libnm-platform/nmp-global-tracker.c
@@ -800,7 +800,7 @@ nmp_global_tracker_sync_mptcp_addrs(NMPGlobalTracker *self, gboolean reapply)
 
         /* Now, drop all duplicates addresses. Only keep the first one. */
         for (i = 0, j = 0; i < entries->len; i++) {
-            const MptcpSyncData       *d          = nm_g_array_index_p(entries, MptcpSyncData, i);
+            const MptcpSyncData       *d          = &nm_g_array_index(entries, MptcpSyncData, i);
             const NMPlatformMptcpAddr *mptcp_addr = NMP_OBJECT_CAST_MPTCP_ADDR(d->obj_data->obj);
 
             obj_data = g_hash_table_lookup(entries_hash_by_addr, (gpointer) mptcp_addr);
@@ -826,7 +826,7 @@ nmp_global_tracker_sync_mptcp_addrs(NMPGlobalTracker *self, gboolean reapply)
                 nm_assert_not_reached();
 
             if (i != j)
-                *(nm_g_array_index_p(entries, MptcpSyncData, j)) = *d;
+                (nm_g_array_index(entries, MptcpSyncData, j)) = *d;
             j++;
 
             if (j >= MPTCP_PM_ADDR_MAX) {
@@ -931,7 +931,7 @@ keep_and_next:
 
     if (entries) {
         for (i = 0; i < entries->len; i++) {
-            const MptcpSyncData       *d          = nm_g_array_index_p(entries, MptcpSyncData, i);
+            const MptcpSyncData       *d          = &nm_g_array_index(entries, MptcpSyncData, i);
             const NMPlatformMptcpAddr *mptcp_addr = NMP_OBJECT_CAST_MPTCP_ADDR(d->obj_data->obj);
             const NMPObject           *kobj;
 
diff --git a/src/libnm-platform/nmp-netns.c b/src/libnm-platform/nmp-netns.c
index 2563e8f6..c18f67ab 100644
--- a/src/libnm-platform/nmp-netns.c
+++ b/src/libnm-platform/nmp-netns.c
@@ -66,26 +66,28 @@ __ns_types_to_str(int ns_types, int ns_types_already_set, char *buf, gsize len)
 
 #define _NMLOG_DOMAIN      LOGD_PLATFORM
 #define _NMLOG_PREFIX_NAME "netns"
-#define _NMLOG(level, netns, ...)                                     \
-    G_STMT_START                                                      \
-    {                                                                 \
-        NMLogLevel _level = (level);                                  \
-                                                                      \
-        if (nm_logging_enabled(_level, _NMLOG_DOMAIN)) {              \
-            NMPNetns *_netns = (netns);                               \
-            char      _sbuf[20];                                      \
-                                                                      \
-            _nm_log(_level,                                           \
-                    _NMLOG_DOMAIN,                                    \
-                    0,                                                \
-                    NULL,                                             \
-                    NULL,                                             \
-                    "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),      \
-                    _NMLOG_PREFIX_NAME,                               \
-                    (_netns ? nm_sprintf_buf(_sbuf, "[%p]", _netns)   \
-                            : "") _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
-        }                                                             \
-    }                                                                 \
+#define _NMLOG(level, netns, ...)                                               \
+    G_STMT_START                                                                \
+    {                                                                           \
+        NMLogLevel _level = (level);                                            \
+                                                                                \
+        if (nm_logging_enabled(_level, _NMLOG_DOMAIN)) {                        \
+            NMPNetns *_netns = (netns);                                         \
+            char      _sbuf[32];                                                \
+                                                                                \
+            _nm_log(_level,                                                     \
+                    _NMLOG_DOMAIN,                                              \
+                    0,                                                          \
+                    NULL,                                                       \
+                    NULL,                                                       \
+                    "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                \
+                    _NMLOG_PREFIX_NAME,                                         \
+                    (_netns ? nm_sprintf_buf(_sbuf,                             \
+                                             "[" NM_HASH_OBFUSCATE_PTR_FMT "]", \
+                                             NM_HASH_OBFUSCATE_PTR(_netns))     \
+                            : "") _NM_UTILS_MACRO_REST(__VA_ARGS__));           \
+        }                                                                       \
+    }                                                                           \
     G_STMT_END
 
 /*****************************************************************************/
@@ -194,7 +196,7 @@ _stack_current_netns(GArray *netns_stack, int ns_types)
     for (j = netns_stack->len; ns_types && j >= 1;) {
         NetnsInfo *info;
 
-        info = &g_array_index(netns_stack, NetnsInfo, --j);
+        info = &nm_g_array_index(netns_stack, NetnsInfo, --j);
 
         if (NM_FLAGS_ALL(info->ns_types, ns_types))
             return info->netns;
@@ -218,7 +220,7 @@ _stack_current_ns_types(GArray *netns_stack, NMPNetns *netns, int ns_types)
     for (j = netns_stack->len; ns_types && j >= 1;) {
         NetnsInfo *info;
 
-        info = &g_array_index(netns_stack, NetnsInfo, --j);
+        info = &nm_g_array_index(netns_stack, NetnsInfo, --j);
         if (info->netns != netns) {
             ns_types = NM_FLAGS_UNSET(ns_types, info->ns_types);
             continue;
@@ -240,7 +242,7 @@ static NetnsInfo *
 _stack_peek(GArray *netns_stack)
 {
     if (netns_stack->len > 0)
-        return &g_array_index(netns_stack, NetnsInfo, (netns_stack->len - 1));
+        return &nm_g_array_last(netns_stack, NetnsInfo);
     return NULL;
 }
 
@@ -248,7 +250,7 @@ static NetnsInfo *
 _stack_bottom(GArray *netns_stack)
 {
     if (netns_stack->len > 0)
-        return &g_array_index(netns_stack, NetnsInfo, 0);
+        return &nm_g_array_first(netns_stack, NetnsInfo);
     return NULL;
 }
 
@@ -262,9 +264,7 @@ _stack_push(GArray *netns_stack, NMPNetns *netns, int ns_types)
     nm_assert(NM_FLAGS_ANY(ns_types, _CLONE_NS_ALL));
     nm_assert(!NM_FLAGS_ANY(ns_types, ~_CLONE_NS_ALL));
 
-    g_array_set_size(netns_stack, netns_stack->len + 1);
-
-    info  = &g_array_index(netns_stack, NetnsInfo, (netns_stack->len - 1));
+    info  = nm_g_array_append_new(netns_stack, NetnsInfo);
     *info = (NetnsInfo){
         .netns    = g_object_ref(netns),
         .ns_types = ns_types,
@@ -280,7 +280,7 @@ _stack_pop(GArray *netns_stack)
     nm_assert(netns_stack);
     nm_assert(netns_stack->len > 1);
 
-    info = &g_array_index(netns_stack, NetnsInfo, (netns_stack->len - 1));
+    info = &nm_g_array_last(netns_stack, NetnsInfo);
 
     nm_assert(NMP_IS_NETNS(info->netns));
     nm_assert(info->count == 1);
diff --git a/src/libnm-platform/nmp-netns.h b/src/libnm-platform/nmp-netns.h
index b18bd03e..baeae4f1 100644
--- a/src/libnm-platform/nmp-netns.h
+++ b/src/libnm-platform/nmp-netns.h
@@ -11,7 +11,7 @@
 /*****************************************************************************/
 
 #define NMP_TYPE_NETNS            (nmp_netns_get_type())
-#define NMP_NETNS(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NMP_TYPE_NETNS, NMPNetns))
+#define NMP_NETNS(obj)            (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NMP_TYPE_NETNS, NMPNetns))
 #define NMP_NETNS_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NMP_TYPE_NETNS, NMPNetnsClass))
 #define NMP_IS_NETNS(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMP_TYPE_NETNS))
 #define NMP_IS_NETNS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NMP_TYPE_NETNS))
diff --git a/src/libnm-platform/nmp-object.c b/src/libnm-platform/nmp-object.c
index db7bd314..9ba027ba 100644
--- a/src/libnm-platform/nmp-object.c
+++ b/src/libnm-platform/nmp-object.c
@@ -216,7 +216,7 @@ nm_sock_addr_union_cpy_untrusted(NMSockAddrUnion *dst,
 const char *
 nm_sock_addr_union_to_string(const NMSockAddrUnion *sa, char *buf, gsize len)
 {
-    char s_addr[NM_UTILS_INET_ADDRSTRLEN];
+    char s_addr[NM_INET_ADDRSTRLEN];
     char s_scope_id[40];
 
     if (!nm_utils_to_string_buffer_init_null(sa, &buf, &len))
@@ -232,14 +232,14 @@ nm_sock_addr_union_to_string(const NMSockAddrUnion *sa, char *buf, gsize len)
         g_snprintf(buf,
                    len,
                    "%s:%u",
-                   _nm_utils_inet4_ntop(sa->in.sin_addr.s_addr, s_addr),
+                   nm_inet4_ntop(sa->in.sin_addr.s_addr, s_addr),
                    (guint) htons(sa->in.sin_port));
         break;
     case AF_INET6:
         g_snprintf(buf,
                    len,
                    "[%s%s]:%u",
-                   _nm_utils_inet6_ntop(&sa->in6.sin6_addr, s_addr),
+                   nm_inet6_ntop(&sa->in6.sin6_addr, s_addr),
                    (sa->in6.sin6_scope_id != 0
                         ? nm_sprintf_buf(s_scope_id, "%u", sa->in6.sin6_scope_id)
                         : ""),
@@ -735,6 +735,12 @@ _vt_cmd_obj_dispose_link(NMPObject *obj)
 }
 
 static void
+_vt_cmd_obj_dispose_ip4_route(NMPObject *obj)
+{
+    nm_clear_g_free((gpointer *) &obj->_ip4_route.extra_nexthops);
+}
+
+static void
 _vt_cmd_obj_dispose_lnk_vlan(NMPObject *obj)
 {
     g_free((gpointer) obj->_lnk_vlan.ingress_qos_map);
@@ -762,17 +768,23 @@ _vt_cmd_obj_dispose_lnk_wireguard(NMPObject *obj)
     _wireguard_clear(&obj->_lnk_wireguard);
 }
 
+static gsize
+_NMP_OBJECT_STRUCT_SIZE(const NMPClass *klass)
+{
+    nm_assert(klass);
+    nm_assert(klass->sizeof_public > 0);
+    nm_assert(klass->sizeof_public <= klass->sizeof_data);
+
+    return klass->sizeof_data + G_STRUCT_OFFSET(NMPObject, object);
+}
+
 static NMPObject *
 _nmp_object_new_from_class(const NMPClass *klass)
 {
     NMPObject *obj;
 
-    nm_assert(klass);
-    nm_assert(klass->sizeof_data > 0);
-    nm_assert(klass->sizeof_public > 0 && klass->sizeof_public <= klass->sizeof_data);
-
-    obj         = g_slice_alloc0(klass->sizeof_data + G_STRUCT_OFFSET(NMPObject, object));
-    obj->_class = klass;
+    obj                    = g_slice_alloc0(_NMP_OBJECT_STRUCT_SIZE(klass));
+    obj->_class            = klass;
     obj->parent._ref_count = 1;
     return obj;
 }
@@ -805,9 +817,8 @@ static NMPObject *
 _nmp_object_stackinit_from_class(NMPObject *obj, const NMPClass *klass)
 {
     nm_assert(obj);
-    nm_assert(klass);
 
-    memset(obj, 0, sizeof(NMPObject));
+    memset(obj, 0, _NMP_OBJECT_STRUCT_SIZE(klass));
     obj->_class            = klass;
     obj->parent._ref_count = NM_OBJ_REF_COUNT_STACKINIT;
     return obj;
@@ -843,14 +854,12 @@ nmp_object_stackinit_id(NMPObject *obj, const NMPObject *src)
     if (klass->cmd_plobj_id_copy)
         klass->cmd_plobj_id_copy(&obj->object, &src->object);
     else {
-        /* This object must not implement cmd_obj_copy().
-         * If it would, it would mean that we require a deep copy
-         * of the data. As @obj is stack-allocated, it cannot track
-         * ownership. The caller must not use nmp_object_stackinit_id()
-         * with an object of such a type. */
-        nm_assert(!klass->cmd_obj_copy);
-
-        /* plain memcpy of the public part suffices. */
+        /* plain memcpy.
+         *
+         * Note that for NMPObjectIP4Route this also copies extra_nexthops
+         * pointer, aliasing it without taking ownership. That is potentially
+         * dangerous, but when using a stack allocated instance, you must
+         * always take care of ownership. */
         memcpy(&obj->object, &src->object, klass->sizeof_data);
     }
     return obj;
@@ -907,30 +916,33 @@ nmp_object_to_string(const NMPObject      *obj,
 
     klass = NMP_OBJECT_GET_CLASS(obj);
 
-    if (klass->cmd_obj_to_string)
+    if (klass->cmd_obj_to_string) {
+        nm_assert(!klass->cmd_plobj_to_string);
+        nm_assert(!klass->cmd_plobj_to_string_id);
         return klass->cmd_obj_to_string(obj, to_string_mode, buf, buf_size);
+    }
+
+    nm_assert(klass->cmd_plobj_to_string);
 
     switch (to_string_mode) {
     case NMP_OBJECT_TO_STRING_ID:
-        if (!klass->cmd_plobj_to_string_id) {
-            g_snprintf(buf, buf_size, NM_HASH_OBFUSCATE_PTR_FMT, NM_HASH_OBFUSCATE_PTR(obj));
-            return buf;
-        }
-        return klass->cmd_plobj_to_string_id(&obj->object, buf, buf_size);
+        if (klass->cmd_plobj_to_string_id)
+            return klass->cmd_plobj_to_string_id(&obj->object, buf, buf_size);
+        g_snprintf(buf, buf_size, NM_HASH_OBFUSCATE_PTR_FMT, NM_HASH_OBFUSCATE_PTR(obj));
+        return buf;
     case NMP_OBJECT_TO_STRING_ALL:
-        g_snprintf(
-            buf,
-            buf_size,
-            "[%s," NM_HASH_OBFUSCATE_PTR_FMT ",%u,%calive,%cvisible; %s]",
-            klass->obj_type_name,
-            NM_HASH_OBFUSCATE_PTR(obj),
-            obj->parent._ref_count,
-            nmp_object_is_alive(obj) ? '+' : '-',
-            nmp_object_is_visible(obj) ? '+' : '-',
-            NMP_OBJECT_GET_CLASS(obj)->cmd_plobj_to_string(&obj->object, buf2, sizeof(buf2)));
+        g_snprintf(buf,
+                   buf_size,
+                   "[%s," NM_HASH_OBFUSCATE_PTR_FMT ",%u,%calive,%cvisible; %s]",
+                   klass->obj_type_name,
+                   NM_HASH_OBFUSCATE_PTR(obj),
+                   obj->parent._ref_count,
+                   nmp_object_is_alive(obj) ? '+' : '-',
+                   nmp_object_is_visible(obj) ? '+' : '-',
+                   klass->cmd_plobj_to_string(&obj->object, buf2, sizeof(buf2)));
         return buf;
     case NMP_OBJECT_TO_STRING_PUBLIC:
-        NMP_OBJECT_GET_CLASS(obj)->cmd_plobj_to_string(&obj->object, buf, buf_size);
+        klass->cmd_plobj_to_string(&obj->object, buf, buf_size);
         return buf;
     default:
         g_return_val_if_reached("ERROR");
@@ -948,7 +960,8 @@ _vt_cmd_obj_to_string_link(const NMPObject      *obj,
 
     switch (to_string_mode) {
     case NMP_OBJECT_TO_STRING_ID:
-        return klass->cmd_plobj_to_string_id(&obj->object, buf, buf_size);
+        g_snprintf(buf, buf_size, "%d", obj->link.ifindex);
+        return buf;
     case NMP_OBJECT_TO_STRING_ALL:
         nm_strbuf_append(&b,
                          &buf_size,
@@ -961,7 +974,7 @@ _vt_cmd_obj_to_string_link(const NMPObject      *obj,
                          nmp_object_is_visible(obj) ? '+' : '-',
                          obj->_link.netlink.is_in_netlink ? '+' : '-',
                          NM_HASH_OBFUSCATE_PTR(obj->_link.udev.device));
-        NMP_OBJECT_GET_CLASS(obj)->cmd_plobj_to_string(&obj->object, b, buf_size);
+        nm_platform_link_to_string(&obj->link, b, buf_size);
         nm_strbuf_seek_end(&b, &buf_size);
         if (obj->_link.netlink.lnk) {
             nm_strbuf_append_str(&b, &buf_size, "; ");
@@ -971,7 +984,7 @@ _vt_cmd_obj_to_string_link(const NMPObject      *obj,
         nm_strbuf_append_c(&b, &buf_size, ']');
         return buf;
     case NMP_OBJECT_TO_STRING_PUBLIC:
-        NMP_OBJECT_GET_CLASS(obj)->cmd_plobj_to_string(&obj->object, b, buf_size);
+        nm_platform_link_to_string(&obj->link, b, buf_size);
         if (obj->_link.netlink.lnk) {
             nm_strbuf_seek_end(&b, &buf_size);
             nm_strbuf_append_str(&b, &buf_size, "; ");
@@ -984,6 +997,41 @@ _vt_cmd_obj_to_string_link(const NMPObject      *obj,
 }
 
 static const char *
+_vt_cmd_obj_to_string_ip4_route(const NMPObject      *obj,
+                                NMPObjectToStringMode to_string_mode,
+                                char                 *buf,
+                                gsize                 buf_size)
+{
+    const NMPClass *klass;
+    char            buf2[NM_UTILS_TO_STRING_BUFFER_SIZE];
+
+    klass = NMP_OBJECT_GET_CLASS(obj);
+
+    switch (to_string_mode) {
+    case NMP_OBJECT_TO_STRING_PUBLIC:
+    case NMP_OBJECT_TO_STRING_ID:
+        nm_platform_ip4_route_to_string_full(&obj->ip4_route,
+                                             obj->_ip4_route.extra_nexthops,
+                                             buf,
+                                             buf_size);
+        return buf;
+    case NMP_OBJECT_TO_STRING_ALL:
+        g_snprintf(buf,
+                   buf_size,
+                   "[%s," NM_HASH_OBFUSCATE_PTR_FMT ",%u,%calive,%cvisible; %s]",
+                   klass->obj_type_name,
+                   NM_HASH_OBFUSCATE_PTR(obj),
+                   obj->parent._ref_count,
+                   nmp_object_is_alive(obj) ? '+' : '-',
+                   nmp_object_is_visible(obj) ? '+' : '-',
+                   nmp_object_to_string(obj, NMP_OBJECT_TO_STRING_PUBLIC, buf2, sizeof(buf2)));
+        return buf;
+    default:
+        g_return_val_if_reached("ERROR");
+    }
+}
+
+static const char *
 _vt_cmd_obj_to_string_lnk_vlan(const NMPObject      *obj,
                                NMPObjectToStringMode to_string_mode,
                                char                 *buf,
@@ -1013,7 +1061,7 @@ _vt_cmd_obj_to_string_lnk_vlan(const NMPObject      *obj,
                    nmp_object_to_string(obj, NMP_OBJECT_TO_STRING_PUBLIC, buf2, sizeof(buf2)));
         return buf;
     case NMP_OBJECT_TO_STRING_PUBLIC:
-        NMP_OBJECT_GET_CLASS(obj)->cmd_plobj_to_string(&obj->object, buf, buf_size);
+        nm_platform_lnk_vlan_to_string(&obj->lnk_vlan, buf, buf_size);
 
         b = buf;
         l = strlen(b);
@@ -1092,8 +1140,7 @@ _vt_cmd_obj_to_string_lnk_wireguard(const NMPObject      *obj,
 
         return buf;
     case NMP_OBJECT_TO_STRING_PUBLIC:
-        NMP_OBJECT_GET_CLASS(obj)->cmd_plobj_to_string(&obj->object, buf, buf_size);
-
+        nm_platform_lnk_wireguard_to_string(&obj->lnk_wireguard, buf, buf_size);
         return buf;
     default:
         g_return_val_if_reached("ERROR");
@@ -1106,41 +1153,38 @@ _vt_cmd_obj_to_string_lnk_wireguard(const NMPObject      *obj,
                                                          gsize                   buf_len) \
     {                                                                                     \
         plat_type *const obj = (plat_type *) _obj;                                        \
-        _nm_unused char  buf1[NM_UTILS_INET_ADDRSTRLEN];                                  \
-        _nm_unused char  buf2[NM_UTILS_INET_ADDRSTRLEN];                                  \
+        _nm_unused char  buf1[NM_INET_ADDRSTRLEN];                                        \
+        _nm_unused char  buf2[NM_INET_ADDRSTRLEN];                                        \
                                                                                           \
         g_snprintf(buf, buf_len, __VA_ARGS__);                                            \
         return buf;                                                                       \
     }                                                                                     \
     _NM_DUMMY_STRUCT_FOR_TRAILING_SEMICOLON
 
-_vt_cmd_plobj_to_string_id(link, NMPlatformLink, "%d", obj->ifindex);
-
-_vt_cmd_plobj_to_string_id(ip4_address,
-                           NMPlatformIP4Address,
-                           "%d: %s/%d%s%s",
-                           obj->ifindex,
-                           _nm_utils_inet4_ntop(obj->address, buf1),
-                           obj->plen,
-                           obj->peer_address != obj->address ? "," : "",
-                           obj->peer_address != obj->address ? _nm_utils_inet4_ntop(
-                               nm_utils_ip4_address_clear_host_address(obj->peer_address,
-                                                                       obj->plen),
-                               buf2)
-                                                             : "");
+_vt_cmd_plobj_to_string_id(
+    ip4_address,
+    NMPlatformIP4Address,
+    "%d: %s/%d%s%s",
+    obj->ifindex,
+    nm_inet4_ntop(obj->address, buf1),
+    obj->plen,
+    obj->peer_address != obj->address ? "," : "",
+    obj->peer_address != obj->address
+        ? nm_inet4_ntop(nm_ip4_addr_clear_host_address(obj->peer_address, obj->plen), buf2)
+        : "");
 
 _vt_cmd_plobj_to_string_id(ip6_address,
                            NMPlatformIP6Address,
                            "%d: %s",
                            obj->ifindex,
-                           _nm_utils_inet6_ntop(&obj->address, buf1));
+                           nm_inet6_ntop(&obj->address, buf1));
 
 _vt_cmd_plobj_to_string_id(qdisc, NMPlatformQdisc, "%d: %d", obj->ifindex, obj->parent);
 
 _vt_cmd_plobj_to_string_id(tfilter, NMPlatformTfilter, "%d: %d", obj->ifindex, obj->parent);
 
 void
-nmp_object_hash_update(const NMPObject *obj, NMHashState *h)
+nmp_object_hash_update_full(const NMPObject *obj, gboolean for_id, NMHashState *h)
 {
     const NMPClass *klass;
 
@@ -1148,20 +1192,44 @@ nmp_object_hash_update(const NMPObject *obj, NMHashState *h)
 
     klass = NMP_OBJECT_GET_CLASS(obj);
 
+    nm_assert((!!klass->cmd_plobj_id_cmp) == (!!klass->cmd_plobj_id_hash_update));
+    nm_assert((!!klass->cmd_obj_cmp) == (!!klass->cmd_obj_hash_update));
+    nm_assert((!!klass->cmd_plobj_cmp) == (!!klass->cmd_plobj_hash_update));
+
+    nm_assert((!!klass->cmd_obj_hash_update) ^ (!!klass->cmd_plobj_hash_update));
+    nm_assert((!klass->cmd_obj_hash_update) || (!klass->cmd_plobj_id_hash_update));
+
     nm_hash_update_val(h, klass->obj_type);
+
+    if (for_id) {
+        if (klass->cmd_obj_hash_update)
+            klass->cmd_obj_hash_update(obj, TRUE, h);
+        else if (klass->cmd_plobj_id_hash_update)
+            klass->cmd_plobj_id_hash_update(&obj->object, h);
+        else {
+            /* The klass doesn't implement ID compare. It means, to use pointer
+             * equality. */
+            nm_hash_update_val(h, obj);
+        }
+        return;
+    }
+
     if (klass->cmd_obj_hash_update)
-        klass->cmd_obj_hash_update(obj, h);
-    else if (klass->cmd_plobj_hash_update)
-        klass->cmd_plobj_hash_update(&obj->object, h);
+        klass->cmd_obj_hash_update(obj, FALSE, h);
     else
-        nm_hash_update_val(h, obj);
+        klass->cmd_plobj_hash_update(&obj->object, h);
 }
 
 static void
-_vt_cmd_obj_hash_update_link(const NMPObject *obj, NMHashState *h)
+_vt_cmd_obj_hash_update_link(const NMPObject *obj, gboolean for_id, NMHashState *h)
 {
     nm_assert(NMP_OBJECT_GET_TYPE(obj) == NMP_OBJECT_TYPE_LINK);
 
+    if (for_id) {
+        nm_hash_update_val(h, obj->link.ifindex);
+        return;
+    }
+
     nm_platform_link_hash_update(&obj->link, h);
     nm_hash_update_vals(h, obj->_link.netlink.is_in_netlink, obj->_link.udev.device);
     if (obj->_link.netlink.lnk)
@@ -1169,10 +1237,30 @@ _vt_cmd_obj_hash_update_link(const NMPObject *obj, NMHashState *h)
 }
 
 static void
-_vt_cmd_obj_hash_update_lnk_vlan(const NMPObject *obj, NMHashState *h)
+_vt_cmd_obj_hash_update_ip4_route(const NMPObject *obj, gboolean for_id, NMHashState *h)
+{
+    guint i;
+
+    nm_assert(NMP_OBJECT_GET_TYPE(obj) == NMP_OBJECT_TYPE_IP4_ROUTE);
+
+    nm_platform_ip4_route_hash_update(&obj->ip4_route,
+                                      for_id ? NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID
+                                             : NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL,
+                                      h);
+    for (i = 1u; i < obj->ip4_route.n_nexthops; i++)
+        nm_platform_ip4_rt_nexthop_hash_update(&obj->_ip4_route.extra_nexthops[i - 1u], for_id, h);
+}
+
+static void
+_vt_cmd_obj_hash_update_lnk_vlan(const NMPObject *obj, gboolean for_id, NMHashState *h)
 {
     nm_assert(NMP_OBJECT_GET_TYPE(obj) == NMP_OBJECT_TYPE_LNK_VLAN);
 
+    if (for_id) {
+        nm_hash_update_val(h, obj);
+        return;
+    }
+
     nm_platform_lnk_vlan_hash_update(&obj->lnk_vlan, h);
     _vlan_xgress_qos_mappings_hash_update(obj->_lnk_vlan.n_ingress_qos_map,
                                           obj->_lnk_vlan.ingress_qos_map,
@@ -1183,12 +1271,17 @@ _vt_cmd_obj_hash_update_lnk_vlan(const NMPObject *obj, NMHashState *h)
 }
 
 static void
-_vt_cmd_obj_hash_update_lnk_wireguard(const NMPObject *obj, NMHashState *h)
+_vt_cmd_obj_hash_update_lnk_wireguard(const NMPObject *obj, gboolean for_id, NMHashState *h)
 {
     guint i;
 
     nm_assert(NMP_OBJECT_GET_TYPE(obj) == NMP_OBJECT_TYPE_LNK_WIREGUARD);
 
+    if (for_id) {
+        nm_hash_update_val(h, obj);
+        return;
+    }
+
     nm_platform_lnk_wireguard_hash_update(&obj->lnk_wireguard, h);
 
     nm_hash_update_val(h, obj->_lnk_wireguard.peers_len);
@@ -1199,47 +1292,85 @@ _vt_cmd_obj_hash_update_lnk_wireguard(const NMPObject *obj, NMHashState *h)
 int
 nmp_object_cmp_full(const NMPObject *obj1, const NMPObject *obj2, NMPObjectCmpFlags flags)
 {
-    const NMPClass *klass1;
+    const NMPClass *klass;
     const NMPClass *klass2;
     NMPObject       obj_stackcopy;
 
+    nm_assert(
+        !NM_FLAGS_ANY(flags, ~(NMP_OBJECT_CMP_FLAGS_ID | NMP_OBJECT_CMP_FLAGS_IGNORE_IFINDEX)));
+
+    /* The ID flag (currently) cannot be combined with other flags. That's partly because
+     * it's not implemented, but also because some objects use only pointer equality. So it's
+     * not clear how that combines with other flags (well, they'd be ignored). */
+    nm_assert(!NM_FLAGS_HAS(flags, NMP_OBJECT_CMP_FLAGS_ID) || flags == NMP_OBJECT_CMP_FLAGS_ID);
+
     NM_CMP_SELF(obj1, obj2);
 
     g_return_val_if_fail(NMP_OBJECT_IS_VALID(obj1), -1);
     g_return_val_if_fail(NMP_OBJECT_IS_VALID(obj2), 1);
 
-    klass1 = NMP_OBJECT_GET_CLASS(obj1);
+    klass = NMP_OBJECT_GET_CLASS(obj1);
+
+    nm_assert(klass);
+
+    nm_assert((!!klass->cmd_plobj_id_cmp) == (!!klass->cmd_plobj_id_hash_update));
+    nm_assert((!!klass->cmd_obj_cmp) == (!!klass->cmd_obj_hash_update));
+    nm_assert((!!klass->cmd_plobj_cmp) == (!!klass->cmd_plobj_hash_update));
+
+    nm_assert((!!klass->cmd_obj_cmp) ^ (!!klass->cmd_plobj_cmp));
+    nm_assert((!klass->cmd_obj_cmp) || (!klass->cmd_plobj_id_cmp));
+
     klass2 = NMP_OBJECT_GET_CLASS(obj2);
 
-    if (klass1 != klass2) {
-        nm_assert(klass1->obj_type != klass2->obj_type);
-        return klass1->obj_type < klass2->obj_type ? -1 : 1;
+    if (klass != klass2) {
+        nm_assert(klass2);
+        NM_CMP_DIRECT(klass->obj_type, klass2->obj_type);
+        return nm_assert_unreachable_val(0);
+    }
+
+    if (NM_FLAGS_HAS(flags, NMP_OBJECT_CMP_FLAGS_ID)) {
+        if (klass->cmd_obj_cmp)
+            return klass->cmd_obj_cmp(obj1, obj2, TRUE);
+        if (klass->cmd_plobj_id_cmp)
+            return klass->cmd_plobj_id_cmp(&obj1->object, &obj2->object);
+
+        /* the klass doesn't implement ID cmp(). That means, different objects
+         * never compare equal, but the cmp() according to their pointer value. */
+        NM_CMP_DIRECT_PTR(obj1, obj2);
+        return nm_assert_unreachable_val(0);
     }
 
     if (NM_FLAGS_HAS(flags, NMP_OBJECT_CMP_FLAGS_IGNORE_IFINDEX)) {
-        if (!NM_IN_SET(klass1,
-                       nmp_class_from_type(NMP_OBJECT_TYPE_IP4_ADDRESS),
-                       nmp_class_from_type(NMP_OBJECT_TYPE_IP6_ADDRESS),
-                       nmp_class_from_type(NMP_OBJECT_TYPE_IP4_ROUTE),
-                       nmp_class_from_type(NMP_OBJECT_TYPE_IP6_ROUTE))) {
+        if (!NM_IN_SET(klass->obj_type,
+                       NMP_OBJECT_TYPE_IP4_ADDRESS,
+                       NMP_OBJECT_TYPE_IP6_ADDRESS,
+                       NMP_OBJECT_TYPE_IP4_ROUTE,
+                       NMP_OBJECT_TYPE_IP6_ROUTE)) {
             /* This flag is currently only implemented for certain types.
              * That is, because we just create a stack copy, and that naive
              * approach only knows for types where we know that it works. */
         } else if (obj1->obj_with_ifindex.ifindex != obj2->obj_with_ifindex.ifindex) {
-            nmp_object_stackinit(&obj_stackcopy, klass1->obj_type, &obj2->obj_with_ifindex);
+            nmp_object_stackinit(&obj_stackcopy, klass->obj_type, &obj2->obj_with_ifindex);
             obj_stackcopy.obj_with_ifindex.ifindex = obj1->obj_with_ifindex.ifindex;
-            obj2                                   = &obj_stackcopy;
+            if (klass->obj_type == NMP_OBJECT_TYPE_IP4_ROUTE)
+                obj_stackcopy._ip4_route.extra_nexthops = obj2->_ip4_route.extra_nexthops;
+            obj2 = &obj_stackcopy;
         }
     }
 
-    if (klass1->cmd_obj_cmp)
-        return klass1->cmd_obj_cmp(obj1, obj2);
-    return klass1->cmd_plobj_cmp(&obj1->object, &obj2->object);
+    if (klass->cmd_obj_cmp)
+        return klass->cmd_obj_cmp(obj1, obj2, FALSE);
+    return klass->cmd_plobj_cmp(&obj1->object, &obj2->object);
 }
 
 static int
-_vt_cmd_obj_cmp_link(const NMPObject *obj1, const NMPObject *obj2)
+_vt_cmd_obj_cmp_link(const NMPObject *obj1, const NMPObject *obj2, gboolean for_id)
 {
+    if (for_id) {
+        NM_CMP_FIELD(obj1, obj2, link.ifindex);
+        return 0;
+    }
+
     NM_CMP_RETURN(nm_platform_link_cmp(&obj1->link, &obj2->link));
     NM_CMP_DIRECT(obj1->_link.netlink.is_in_netlink, obj2->_link.netlink.is_in_netlink);
     NM_CMP_RETURN(nmp_object_cmp(obj1->_link.netlink.lnk, obj2->_link.netlink.lnk));
@@ -1261,10 +1392,37 @@ _vt_cmd_obj_cmp_link(const NMPObject *obj1, const NMPObject *obj2)
 }
 
 static int
-_vt_cmd_obj_cmp_lnk_vlan(const NMPObject *obj1, const NMPObject *obj2)
+_vt_cmd_obj_cmp_ip4_route(const NMPObject *obj1, const NMPObject *obj2, gboolean for_id)
+{
+    int   c;
+    guint i;
+
+    c = nm_platform_ip4_route_cmp(&obj1->ip4_route,
+                                  &obj2->ip4_route,
+                                  for_id ? NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID
+                                         : NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL);
+    NM_CMP_RETURN_DIRECT(c);
+
+    for (i = 1u; i < obj1->ip4_route.n_nexthops; i++) {
+        c = nm_platform_ip4_rt_nexthop_cmp(&obj1->_ip4_route.extra_nexthops[i - 1u],
+                                           &obj2->_ip4_route.extra_nexthops[i - 1u],
+                                           for_id);
+        NM_CMP_RETURN_DIRECT(c);
+    }
+
+    return 0;
+}
+
+static int
+_vt_cmd_obj_cmp_lnk_vlan(const NMPObject *obj1, const NMPObject *obj2, gboolean for_id)
 {
     int c;
 
+    if (for_id) {
+        NM_CMP_DIRECT_PTR(obj1, obj2);
+        return nm_assert_unreachable_val(0);
+    }
+
     c = nm_platform_lnk_vlan_cmp(&obj1->lnk_vlan, &obj2->lnk_vlan);
     if (c)
         return c;
@@ -1287,10 +1445,15 @@ _vt_cmd_obj_cmp_lnk_vlan(const NMPObject *obj1, const NMPObject *obj2)
 }
 
 static int
-_vt_cmd_obj_cmp_lnk_wireguard(const NMPObject *obj1, const NMPObject *obj2)
+_vt_cmd_obj_cmp_lnk_wireguard(const NMPObject *obj1, const NMPObject *obj2, gboolean for_id)
 {
     guint i;
 
+    if (for_id) {
+        NM_CMP_DIRECT_PTR(obj1, obj2);
+        return nm_assert_unreachable_val(0);
+    }
+
     NM_CMP_RETURN(nm_platform_lnk_wireguard_cmp(&obj1->lnk_wireguard, &obj2->lnk_wireguard));
 
     NM_CMP_FIELD(obj1, obj2, _lnk_wireguard.peers_len);
@@ -1354,6 +1517,28 @@ _vt_cmd_obj_copy_link(NMPObject *dst, const NMPObject *src)
 }
 
 static void
+_vt_cmd_obj_copy_ip4_route(NMPObject *dst, const NMPObject *src)
+{
+    nm_assert(dst != src);
+
+    if (src->ip4_route.n_nexthops <= 1) {
+        nm_clear_g_free((gpointer *) &dst->_ip4_route.extra_nexthops);
+    } else if (src->ip4_route.n_nexthops != dst->ip4_route.n_nexthops
+               || !nm_memeq_n(src->_ip4_route.extra_nexthops,
+                              src->ip4_route.n_nexthops - 1u,
+                              dst->_ip4_route.extra_nexthops,
+                              dst->ip4_route.n_nexthops - 1u,
+                              sizeof(NMPlatformIP4RtNextHop))) {
+        nm_clear_g_free((gpointer *) &dst->_ip4_route.extra_nexthops);
+        dst->_ip4_route.extra_nexthops =
+            nm_memdup(src->_ip4_route.extra_nexthops,
+                      sizeof(NMPlatformIP4RtNextHop) * (src->ip4_route.n_nexthops - 1u));
+    }
+
+    dst->ip4_route = src->ip4_route;
+}
+
+static void
 _vt_cmd_obj_copy_lnk_vlan(NMPObject *dst, const NMPObject *src)
 {
     dst->lnk_vlan = src->lnk_vlan;
@@ -1451,39 +1636,6 @@ nmp_object_clone(const NMPObject *obj, gboolean id_only)
     return dst;
 }
 
-int
-nmp_object_id_cmp(const NMPObject *obj1, const NMPObject *obj2)
-{
-    const NMPClass *klass, *klass2;
-
-    NM_CMP_SELF(obj1, obj2);
-
-    g_return_val_if_fail(NMP_OBJECT_IS_VALID(obj1), FALSE);
-    g_return_val_if_fail(NMP_OBJECT_IS_VALID(obj2), FALSE);
-
-    klass = NMP_OBJECT_GET_CLASS(obj1);
-    nm_assert(!klass->cmd_plobj_id_hash_update == !klass->cmd_plobj_id_cmp);
-
-    klass2 = NMP_OBJECT_GET_CLASS(obj2);
-    nm_assert(klass);
-    if (klass != klass2) {
-        nm_assert(klass2);
-        NM_CMP_DIRECT(klass->obj_type, klass2->obj_type);
-        /* resort to pointer comparison */
-        NM_CMP_DIRECT_PTR(klass, klass2);
-        return 0;
-    }
-
-    if (!klass->cmd_plobj_id_cmp) {
-        /* the klass doesn't implement ID cmp(). That means, different objects
-         * never compare equal, but the cmp() according to their pointer value. */
-        NM_CMP_DIRECT_PTR(obj1, obj2);
-        return 0;
-    }
-
-    return klass->cmd_plobj_id_cmp(&obj1->object, &obj2->object);
-}
-
 #define _vt_cmd_plobj_id_cmp(type, plat_type, cmd)                        \
     static int _vt_cmd_plobj_id_cmp_##type(const NMPlatformObject *_obj1, \
                                            const NMPlatformObject *_obj2) \
@@ -1499,8 +1651,6 @@ nmp_object_id_cmp(const NMPObject *obj1, const NMPObject *obj2)
     }                                                                     \
     _NM_DUMMY_STRUCT_FOR_TRAILING_SEMICOLON
 
-_vt_cmd_plobj_id_cmp(link, NMPlatformLink, { NM_CMP_FIELD(obj1, obj2, ifindex); });
-
 static int
 _vt_cmd_plobj_id_cmp_ip4_address(const NMPlatformObject *obj1, const NMPlatformObject *obj2)
 {
@@ -1528,14 +1678,6 @@ _vt_cmd_plobj_id_cmp(tfilter, NMPlatformTfilter, {
 });
 
 static int
-_vt_cmd_plobj_id_cmp_ip4_route(const NMPlatformObject *obj1, const NMPlatformObject *obj2)
-{
-    return nm_platform_ip4_route_cmp((const NMPlatformIP4Route *) obj1,
-                                     (const NMPlatformIP4Route *) obj2,
-                                     NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID);
-}
-
-static int
 _vt_cmd_plobj_id_cmp_ip6_route(const NMPlatformObject *obj1, const NMPlatformObject *obj2)
 {
     return nm_platform_ip6_route_cmp((const NMPlatformIP6Route *) obj1,
@@ -1582,28 +1724,6 @@ _vt_cmd_plobj_id_cmp(mptcp_addr, NMPlatformMptcpAddr, {
     NM_CMP_FIELD(obj1, obj2, port);
 });
 
-void
-nmp_object_id_hash_update(const NMPObject *obj, NMHashState *h)
-{
-    const NMPClass *klass;
-
-    g_return_if_fail(NMP_OBJECT_IS_VALID(obj));
-
-    klass = NMP_OBJECT_GET_CLASS(obj);
-
-    nm_assert(!klass->cmd_plobj_id_hash_update == !klass->cmd_plobj_id_cmp);
-
-    if (!klass->cmd_plobj_id_hash_update) {
-        /* The klass doesn't implement ID compare. It means, to use pointer
-         * equality. */
-        nm_hash_update_val(h, obj);
-        return;
-    }
-
-    nm_hash_update_val(h, klass->obj_type);
-    klass->cmd_plobj_id_hash_update(&obj->object, h);
-}
-
 guint
 nmp_object_id_hash(const NMPObject *obj)
 {
@@ -1627,8 +1747,6 @@ nmp_object_id_hash(const NMPObject *obj)
     }                                                                                             \
     _NM_DUMMY_STRUCT_FOR_TRAILING_SEMICOLON
 
-_vt_cmd_plobj_id_hash_update(link, NMPlatformLink, { nm_hash_update_val(h, obj->ifindex); });
-
 _vt_cmd_plobj_id_hash_update(ip4_address, NMPlatformIP4Address, {
     nm_hash_update_vals(
         h,
@@ -1636,7 +1754,7 @@ _vt_cmd_plobj_id_hash_update(ip4_address, NMPlatformIP4Address, {
         obj->plen,
         obj->address,
         /* for IPv4 we must also consider the net-part of the peer-address (IFA_ADDRESS) */
-        nm_utils_ip4_address_clear_host_address(obj->peer_address, obj->plen));
+        nm_ip4_addr_clear_host_address(obj->peer_address, obj->plen));
 });
 
 _vt_cmd_plobj_id_hash_update(ip6_address, NMPlatformIP6Address, {
@@ -1647,10 +1765,6 @@ _vt_cmd_plobj_id_hash_update(ip6_address, NMPlatformIP6Address, {
         obj->address);
 });
 
-_vt_cmd_plobj_id_hash_update(ip4_route, NMPlatformIP4Route, {
-    nm_platform_ip4_route_hash_update(obj, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID, h);
-});
-
 _vt_cmd_plobj_id_hash_update(ip6_route, NMPlatformIP6Route, {
     nm_platform_ip6_route_hash_update(obj, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID, h);
 });
@@ -1674,19 +1788,19 @@ _vt_cmd_plobj_id_hash_update(mptcp_addr, NMPlatformMptcpAddr, {
 });
 
 static void
-_vt_cmd_plobj_hash_update_ip4_route(const NMPlatformObject *obj, NMHashState *h)
+_vt_cmd_plobj_hash_update_ip6_route(const NMPlatformObject *obj, NMHashState *h)
 {
-    return nm_platform_ip4_route_hash_update((const NMPlatformIP4Route *) obj,
+    return nm_platform_ip6_route_hash_update((const NMPlatformIP6Route *) obj,
                                              NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL,
                                              h);
 }
 
-static void
-_vt_cmd_plobj_hash_update_ip6_route(const NMPlatformObject *obj, NMHashState *h)
+static int
+_vt_cmd_plobj_cmp_ip6_route(const NMPlatformObject *obj1, const NMPlatformObject *obj2)
 {
-    return nm_platform_ip6_route_hash_update((const NMPlatformIP6Route *) obj,
-                                             NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL,
-                                             h);
+    return nm_platform_ip6_route_cmp((const NMPlatformIP6Route *) obj1,
+                                     (const NMPlatformIP6Route *) obj2,
+                                     NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL);
 }
 
 static void
@@ -1697,6 +1811,24 @@ _vt_cmd_plobj_hash_update_routing_rule(const NMPlatformObject *obj, NMHashState
                                                 h);
 }
 
+static inline int
+_vt_cmd_plobj_cmp_routing_rule(const NMPlatformObject *obj1, const NMPlatformObject *obj2)
+{
+    return nm_platform_routing_rule_cmp((const NMPlatformRoutingRule *) obj1,
+                                        (const NMPlatformRoutingRule *) obj2,
+                                        NM_PLATFORM_ROUTING_RULE_CMP_TYPE_FULL);
+}
+
+static int
+_vt_cmd_plobj_cmp_qdisc(const NMPlatformObject *obj1, const NMPlatformObject *obj2)
+{
+    return nm_platform_qdisc_cmp((const NMPlatformQdisc *) obj1,
+                                 (const NMPlatformQdisc *) obj2,
+                                 TRUE);
+}
+
+/*****************************************************************************/
+
 guint
 nmp_object_indirect_id_hash(gconstpointer a)
 {
@@ -1859,7 +1991,7 @@ _vt_dedup_obj_destroy(NMDedupMultiObj *obj)
     klass = o->_class;
     if (klass->cmd_obj_dispose)
         klass->cmd_obj_dispose(o);
-    g_slice_free1(klass->sizeof_data + G_STRUCT_OFFSET(NMPObject, object), o);
+    g_slice_free1(_NMP_OBJECT_STRUCT_SIZE(klass), o);
 }
 
 static const NMDedupMultiObj *
@@ -2827,6 +2959,7 @@ nmp_cache_update_netlink_route(NMPCache         *cache,
                                NMPObject        *obj_hand_over,
                                gboolean          is_dump,
                                guint16           nlmsgflags,
+                               gboolean          route_is_alive,
                                const NMPObject **out_obj_old,
                                const NMPObject **out_obj_new,
                                const NMPObject **out_obj_replace,
@@ -2845,30 +2978,60 @@ nmp_cache_update_netlink_route(NMPCache         *cache,
     nm_assert(cache);
     nm_assert(NMP_OBJECT_IS_VALID(obj_hand_over));
     nm_assert(!NMP_OBJECT_IS_STACKINIT(obj_hand_over));
-    /* A link object from netlink must have the udev related fields unset.
-     * We could implement to handle that, but there is no need to support such
-     * a use-case */
     nm_assert(NM_IN_SET(NMP_OBJECT_GET_TYPE(obj_hand_over),
                         NMP_OBJECT_TYPE_IP4_ROUTE,
                         NMP_OBJECT_TYPE_IP6_ROUTE));
     nm_assert(nm_dedup_multi_index_obj_find(cache->multi_idx, obj_hand_over) != obj_hand_over);
 
+    if (NM_FLAGS_HAS(nlmsgflags, NLM_F_REPLACE)) {
+        /* This means, that the message indicates that another route was replaced.
+         * Since we don't cache all routes (see "route_is_alive"), we cannot know
+         * with certainty which route was replaced.
+         *
+         * Even if we would cache *all* routes (which we cannot, if kernel adds new
+         * routing features that modify the known nmp_object_id_equal()), it would
+         * be hard to find the right route that was replaced. Well, probably we
+         * would have to keep NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID sorted by order
+         * of notifications, which is hard. The code below actually makes an effort
+         * to do that, but it's not actually used, because we just resync.
+         *
+         * The only proper solution for this would be to improve kernel with [1]
+         * and [2].
+         *
+         * [1] https://bugzilla.redhat.com/show_bug.cgi?id=1337855
+         * [2] https://bugzilla.redhat.com/show_bug.cgi?id=1337860
+         *
+         * We need to resync.
+         */
+        if (NMP_OBJECT_GET_TYPE(obj_hand_over) == NMP_OBJECT_TYPE_IP4_ROUTE
+            && !nmp_cache_lookup_all(cache, NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID, obj_hand_over)) {
+            /* For IPv4, we can do a small optimization. We skip the resync, if we have
+             * no conflicting routes (by weak-id).
+             *
+             * This optimization does not work for IPv6 (maybe should be fixed).
+             */
+        } else {
+            entry_replace   = NULL;
+            resync_required = TRUE;
+            goto out;
+        }
+    }
+
     entry_old = _lookup_entry(cache, obj_hand_over);
     entry_new = NULL;
 
     NM_SET_OUT(out_obj_old, nmp_object_ref(nm_dedup_multi_entry_get_obj(entry_old)));
 
-    if (!entry_old) {
-        if (!nmp_object_is_alive(obj_hand_over))
-            goto update_done;
+    is_alive = route_is_alive && nmp_object_is_alive(obj_hand_over);
 
-        _idxcache_update(cache, NULL, obj_hand_over, is_dump, &entry_new);
-        ops_type = NMP_CACHE_OPS_ADDED;
+    if (!entry_old) {
+        if (is_alive) {
+            _idxcache_update(cache, NULL, obj_hand_over, is_dump, &entry_new);
+            ops_type = NMP_CACHE_OPS_ADDED;
+        }
         goto update_done;
     }
 
-    is_alive = nmp_object_is_alive(obj_hand_over);
-
     if (!is_alive) {
         /* the update would make @entry_old invalid. Remove it. */
         _idxcache_update(cache, entry_old, NULL, FALSE, NULL);
@@ -2901,19 +3064,11 @@ update_done:
     if (is_dump)
         goto out;
 
-    if (!entry_new) {
-        if (NM_FLAGS_HAS(nlmsgflags, NLM_F_REPLACE)
-            && nmp_cache_lookup_all(cache, NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID, obj_hand_over)) {
-            /* hm. @obj_hand_over was not added, meaning it was not alive.
-             * However, we track some other objects with the same weak-id.
-             * It's unclear what that means. To be sure, resync. */
-            resync_required = TRUE;
-        }
+    if (!entry_new)
         goto out;
-    }
 
-    /* FIXME: for routes, we only maintain the order correctly for the BY_WEAK_ID
-     * index. For all other indexes their order becomes messed up. */
+    /* For routes, we only maintain the order correctly for the BY_WEAK_ID
+     * index. For all other indexes, their order is not preserved. */
     entry_cur =
         _lookup_entry_with_idx_type(cache, NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID, entry_new->obj);
     if (!entry_cur) {
@@ -3138,29 +3293,23 @@ typedef int (*CmdPlobjCmpFunc)(const NMPlatformObject *obj1, const NMPlatformObj
 const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
     [NMP_OBJECT_TYPE_LINK - 1] =
         {
-            .parent                   = DEDUP_MULTI_OBJ_CLASS_INIT(),
-            .obj_type                 = NMP_OBJECT_TYPE_LINK,
-            .sizeof_data              = sizeof(NMPObjectLink),
-            .sizeof_public            = sizeof(NMPlatformLink),
-            .obj_type_name            = "link",
-            .rtm_gettype              = RTM_GETLINK,
-            .signal_type_id           = NM_PLATFORM_SIGNAL_ID_LINK,
-            .signal_type              = NM_PLATFORM_SIGNAL_LINK_CHANGED,
-            .supported_cache_ids      = _supported_cache_ids_link,
-            .cmd_obj_hash_update      = _vt_cmd_obj_hash_update_link,
-            .cmd_obj_cmp              = _vt_cmd_obj_cmp_link,
-            .cmd_obj_copy             = _vt_cmd_obj_copy_link,
-            .cmd_obj_dispose          = _vt_cmd_obj_dispose_link,
-            .cmd_obj_is_alive         = _vt_cmd_obj_is_alive_link,
-            .cmd_obj_is_visible       = _vt_cmd_obj_is_visible_link,
-            .cmd_obj_to_string        = _vt_cmd_obj_to_string_link,
-            .cmd_plobj_id_copy        = _vt_cmd_plobj_id_copy_link,
-            .cmd_plobj_id_cmp         = _vt_cmd_plobj_id_cmp_link,
-            .cmd_plobj_id_hash_update = _vt_cmd_plobj_id_hash_update_link,
-            .cmd_plobj_to_string_id   = _vt_cmd_plobj_to_string_id_link,
-            .cmd_plobj_to_string      = (CmdPlobjToStringFunc) nm_platform_link_to_string,
-            .cmd_plobj_hash_update    = (CmdPlobjHashUpdateFunc) nm_platform_link_hash_update,
-            .cmd_plobj_cmp            = (CmdPlobjCmpFunc) nm_platform_link_cmp,
+            .parent              = DEDUP_MULTI_OBJ_CLASS_INIT(),
+            .obj_type            = NMP_OBJECT_TYPE_LINK,
+            .sizeof_data         = sizeof(NMPObjectLink),
+            .sizeof_public       = sizeof(NMPlatformLink),
+            .obj_type_name       = "link",
+            .rtm_gettype         = RTM_GETLINK,
+            .signal_type_id      = NM_PLATFORM_SIGNAL_ID_LINK,
+            .signal_type         = NM_PLATFORM_SIGNAL_LINK_CHANGED,
+            .supported_cache_ids = _supported_cache_ids_link,
+            .cmd_obj_hash_update = _vt_cmd_obj_hash_update_link,
+            .cmd_obj_cmp         = _vt_cmd_obj_cmp_link,
+            .cmd_obj_copy        = _vt_cmd_obj_copy_link,
+            .cmd_obj_dispose     = _vt_cmd_obj_dispose_link,
+            .cmd_obj_is_alive    = _vt_cmd_obj_is_alive_link,
+            .cmd_obj_is_visible  = _vt_cmd_obj_is_visible_link,
+            .cmd_obj_to_string   = _vt_cmd_obj_to_string_link,
+            .cmd_plobj_id_copy   = _vt_cmd_plobj_id_copy_link,
         },
     [NMP_OBJECT_TYPE_IP4_ADDRESS - 1] =
         {
@@ -3206,23 +3355,22 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
         },
     [NMP_OBJECT_TYPE_IP4_ROUTE - 1] =
         {
-            .parent                   = DEDUP_MULTI_OBJ_CLASS_INIT(),
-            .obj_type                 = NMP_OBJECT_TYPE_IP4_ROUTE,
-            .sizeof_data              = sizeof(NMPObjectIP4Route),
-            .sizeof_public            = sizeof(NMPlatformIP4Route),
-            .obj_type_name            = "ip4-route",
-            .addr_family              = AF_INET,
-            .rtm_gettype              = RTM_GETROUTE,
-            .signal_type_id           = NM_PLATFORM_SIGNAL_ID_IP4_ROUTE,
-            .signal_type              = NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED,
-            .supported_cache_ids      = _supported_cache_ids_ipx_route,
-            .cmd_obj_is_alive         = _vt_cmd_obj_is_alive_ipx_route,
-            .cmd_plobj_id_cmp         = _vt_cmd_plobj_id_cmp_ip4_route,
-            .cmd_plobj_id_hash_update = _vt_cmd_plobj_id_hash_update_ip4_route,
-            .cmd_plobj_to_string_id   = (CmdPlobjToStringIdFunc) nm_platform_ip4_route_to_string,
-            .cmd_plobj_to_string      = (CmdPlobjToStringFunc) nm_platform_ip4_route_to_string,
-            .cmd_plobj_hash_update    = _vt_cmd_plobj_hash_update_ip4_route,
-            .cmd_plobj_cmp            = (CmdPlobjCmpFunc) nm_platform_ip4_route_cmp_full,
+            .parent              = DEDUP_MULTI_OBJ_CLASS_INIT(),
+            .obj_type            = NMP_OBJECT_TYPE_IP4_ROUTE,
+            .sizeof_data         = sizeof(NMPObjectIP4Route),
+            .sizeof_public       = sizeof(NMPlatformIP4Route),
+            .obj_type_name       = "ip4-route",
+            .addr_family         = AF_INET,
+            .rtm_gettype         = RTM_GETROUTE,
+            .signal_type_id      = NM_PLATFORM_SIGNAL_ID_IP4_ROUTE,
+            .signal_type         = NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED,
+            .supported_cache_ids = _supported_cache_ids_ipx_route,
+            .cmd_obj_is_alive    = _vt_cmd_obj_is_alive_ipx_route,
+            .cmd_obj_hash_update = _vt_cmd_obj_hash_update_ip4_route,
+            .cmd_obj_cmp         = _vt_cmd_obj_cmp_ip4_route,
+            .cmd_obj_copy        = _vt_cmd_obj_copy_ip4_route,
+            .cmd_obj_dispose     = _vt_cmd_obj_dispose_ip4_route,
+            .cmd_obj_to_string   = _vt_cmd_obj_to_string_ip4_route,
         },
     [NMP_OBJECT_TYPE_IP6_ROUTE - 1] =
         {
@@ -3242,7 +3390,7 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
             .cmd_plobj_to_string_id   = (CmdPlobjToStringIdFunc) nm_platform_ip6_route_to_string,
             .cmd_plobj_to_string      = (CmdPlobjToStringFunc) nm_platform_ip6_route_to_string,
             .cmd_plobj_hash_update    = _vt_cmd_plobj_hash_update_ip6_route,
-            .cmd_plobj_cmp            = (CmdPlobjCmpFunc) nm_platform_ip6_route_cmp_full,
+            .cmd_plobj_cmp            = _vt_cmd_plobj_cmp_ip6_route,
         },
     [NMP_OBJECT_TYPE_ROUTING_RULE - 1] =
         {
@@ -3261,7 +3409,7 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
             .cmd_plobj_to_string_id   = (CmdPlobjToStringIdFunc) nm_platform_routing_rule_to_string,
             .cmd_plobj_to_string      = (CmdPlobjToStringFunc) nm_platform_routing_rule_to_string,
             .cmd_plobj_hash_update    = _vt_cmd_plobj_hash_update_routing_rule,
-            .cmd_plobj_cmp            = (CmdPlobjCmpFunc) nm_platform_routing_rule_cmp_full,
+            .cmd_plobj_cmp            = _vt_cmd_plobj_cmp_routing_rule,
         },
     [NMP_OBJECT_TYPE_QDISC - 1] =
         {
@@ -3280,7 +3428,7 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
             .cmd_plobj_to_string_id   = _vt_cmd_plobj_to_string_id_qdisc,
             .cmd_plobj_to_string      = (CmdPlobjToStringFunc) nm_platform_qdisc_to_string,
             .cmd_plobj_hash_update    = (CmdPlobjHashUpdateFunc) nm_platform_qdisc_hash_update,
-            .cmd_plobj_cmp            = (CmdPlobjCmpFunc) nm_platform_qdisc_cmp,
+            .cmd_plobj_cmp            = _vt_cmd_plobj_cmp_qdisc,
         },
     [NMP_OBJECT_TYPE_TFILTER - 1] =
         {
@@ -3460,20 +3608,17 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
         },
     [NMP_OBJECT_TYPE_LNK_VLAN - 1] =
         {
-            .parent                = DEDUP_MULTI_OBJ_CLASS_INIT(),
-            .obj_type              = NMP_OBJECT_TYPE_LNK_VLAN,
-            .sizeof_data           = sizeof(NMPObjectLnkVlan),
-            .sizeof_public         = sizeof(NMPlatformLnkVlan),
-            .obj_type_name         = "vlan",
-            .lnk_link_type         = NM_LINK_TYPE_VLAN,
-            .cmd_obj_hash_update   = _vt_cmd_obj_hash_update_lnk_vlan,
-            .cmd_obj_cmp           = _vt_cmd_obj_cmp_lnk_vlan,
-            .cmd_obj_copy          = _vt_cmd_obj_copy_lnk_vlan,
-            .cmd_obj_dispose       = _vt_cmd_obj_dispose_lnk_vlan,
-            .cmd_obj_to_string     = _vt_cmd_obj_to_string_lnk_vlan,
-            .cmd_plobj_to_string   = (CmdPlobjToStringFunc) nm_platform_lnk_vlan_to_string,
-            .cmd_plobj_hash_update = (CmdPlobjHashUpdateFunc) nm_platform_lnk_vlan_hash_update,
-            .cmd_plobj_cmp         = (CmdPlobjCmpFunc) nm_platform_lnk_vlan_cmp,
+            .parent              = DEDUP_MULTI_OBJ_CLASS_INIT(),
+            .obj_type            = NMP_OBJECT_TYPE_LNK_VLAN,
+            .sizeof_data         = sizeof(NMPObjectLnkVlan),
+            .sizeof_public       = sizeof(NMPlatformLnkVlan),
+            .obj_type_name       = "vlan",
+            .lnk_link_type       = NM_LINK_TYPE_VLAN,
+            .cmd_obj_hash_update = _vt_cmd_obj_hash_update_lnk_vlan,
+            .cmd_obj_cmp         = _vt_cmd_obj_cmp_lnk_vlan,
+            .cmd_obj_copy        = _vt_cmd_obj_copy_lnk_vlan,
+            .cmd_obj_dispose     = _vt_cmd_obj_dispose_lnk_vlan,
+            .cmd_obj_to_string   = _vt_cmd_obj_to_string_lnk_vlan,
         },
     [NMP_OBJECT_TYPE_LNK_VRF - 1] =
         {
@@ -3487,6 +3632,30 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
             .cmd_plobj_hash_update = (CmdPlobjHashUpdateFunc) nm_platform_lnk_vrf_hash_update,
             .cmd_plobj_cmp         = (CmdPlobjCmpFunc) nm_platform_lnk_vrf_cmp,
         },
+    [NMP_OBJECT_TYPE_LNK_VTI - 1] =
+        {
+            .parent                = DEDUP_MULTI_OBJ_CLASS_INIT(),
+            .obj_type              = NMP_OBJECT_TYPE_LNK_VTI,
+            .sizeof_data           = sizeof(NMPObjectLnkVti),
+            .sizeof_public         = sizeof(NMPlatformLnkVti),
+            .obj_type_name         = "vti",
+            .lnk_link_type         = NM_LINK_TYPE_VTI,
+            .cmd_plobj_to_string   = (CmdPlobjToStringFunc) nm_platform_lnk_vti_to_string,
+            .cmd_plobj_hash_update = (CmdPlobjHashUpdateFunc) nm_platform_lnk_vti_hash_update,
+            .cmd_plobj_cmp         = (CmdPlobjCmpFunc) nm_platform_lnk_vti_cmp,
+        },
+    [NMP_OBJECT_TYPE_LNK_VTI6 - 1] =
+        {
+            .parent                = DEDUP_MULTI_OBJ_CLASS_INIT(),
+            .obj_type              = NMP_OBJECT_TYPE_LNK_VTI6,
+            .sizeof_data           = sizeof(NMPObjectLnkVti6),
+            .sizeof_public         = sizeof(NMPlatformLnkVti6),
+            .obj_type_name         = "vti6",
+            .lnk_link_type         = NM_LINK_TYPE_VTI6,
+            .cmd_plobj_to_string   = (CmdPlobjToStringFunc) nm_platform_lnk_vti6_to_string,
+            .cmd_plobj_hash_update = (CmdPlobjHashUpdateFunc) nm_platform_lnk_vti6_hash_update,
+            .cmd_plobj_cmp         = (CmdPlobjCmpFunc) nm_platform_lnk_vti6_cmp,
+        },
     [NMP_OBJECT_TYPE_LNK_VXLAN - 1] =
         {
             .parent                = DEDUP_MULTI_OBJ_CLASS_INIT(),
@@ -3501,20 +3670,17 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
         },
     [NMP_OBJECT_TYPE_LNK_WIREGUARD - 1] =
         {
-            .parent                = DEDUP_MULTI_OBJ_CLASS_INIT(),
-            .obj_type              = NMP_OBJECT_TYPE_LNK_WIREGUARD,
-            .sizeof_data           = sizeof(NMPObjectLnkWireGuard),
-            .sizeof_public         = sizeof(NMPlatformLnkWireGuard),
-            .obj_type_name         = "wireguard",
-            .lnk_link_type         = NM_LINK_TYPE_WIREGUARD,
-            .cmd_obj_hash_update   = _vt_cmd_obj_hash_update_lnk_wireguard,
-            .cmd_obj_cmp           = _vt_cmd_obj_cmp_lnk_wireguard,
-            .cmd_obj_copy          = _vt_cmd_obj_copy_lnk_wireguard,
-            .cmd_obj_dispose       = _vt_cmd_obj_dispose_lnk_wireguard,
-            .cmd_obj_to_string     = _vt_cmd_obj_to_string_lnk_wireguard,
-            .cmd_plobj_to_string   = (CmdPlobjToStringFunc) nm_platform_lnk_wireguard_to_string,
-            .cmd_plobj_hash_update = (CmdPlobjHashUpdateFunc) nm_platform_lnk_wireguard_hash_update,
-            .cmd_plobj_cmp         = (CmdPlobjCmpFunc) nm_platform_lnk_wireguard_cmp,
+            .parent              = DEDUP_MULTI_OBJ_CLASS_INIT(),
+            .obj_type            = NMP_OBJECT_TYPE_LNK_WIREGUARD,
+            .sizeof_data         = sizeof(NMPObjectLnkWireGuard),
+            .sizeof_public       = sizeof(NMPlatformLnkWireGuard),
+            .obj_type_name       = "wireguard",
+            .lnk_link_type       = NM_LINK_TYPE_WIREGUARD,
+            .cmd_obj_hash_update = _vt_cmd_obj_hash_update_lnk_wireguard,
+            .cmd_obj_cmp         = _vt_cmd_obj_cmp_lnk_wireguard,
+            .cmd_obj_copy        = _vt_cmd_obj_copy_lnk_wireguard,
+            .cmd_obj_dispose     = _vt_cmd_obj_dispose_lnk_wireguard,
+            .cmd_obj_to_string   = _vt_cmd_obj_to_string_lnk_wireguard,
         },
     [NMP_OBJECT_TYPE_LNK_BOND - 1] =
         {
diff --git a/src/libnm-platform/nmp-object.h b/src/libnm-platform/nmp-object.h
index 25156fe2..0d5f84b3 100644
--- a/src/libnm-platform/nmp-object.h
+++ b/src/libnm-platform/nmp-object.h
@@ -183,25 +183,26 @@ typedef struct {
     /* Only for NMPObjectLnk* types. */
     NMLinkType lnk_link_type;
 
-    void (*cmd_obj_hash_update)(const NMPObject *obj, NMHashState *h);
-    int (*cmd_obj_cmp)(const NMPObject *obj1, const NMPObject *obj2);
-    void (*cmd_obj_copy)(NMPObject *dst, const NMPObject *src);
-    void (*cmd_obj_dispose)(NMPObject *obj);
     gboolean (*cmd_obj_is_alive)(const NMPObject *obj);
     gboolean (*cmd_obj_is_visible)(const NMPObject *obj);
+    void (*cmd_obj_copy)(NMPObject *dst, const NMPObject *src);
+    void (*cmd_obj_dispose)(NMPObject *obj);
+
+    void (*cmd_obj_hash_update)(const NMPObject *obj, gboolean for_id, NMHashState *h);
+    int (*cmd_obj_cmp)(const NMPObject *obj1, const NMPObject *obj2, gboolean for_id);
     const char *(*cmd_obj_to_string)(const NMPObject      *obj,
                                      NMPObjectToStringMode to_string_mode,
                                      char                 *buf,
                                      gsize                 buf_size);
 
     /* functions that operate on NMPlatformObject */
+    void (*cmd_plobj_hash_update)(const NMPlatformObject *obj, NMHashState *h);
+    int (*cmd_plobj_cmp)(const NMPlatformObject *obj1, const NMPlatformObject *obj2);
     void (*cmd_plobj_id_copy)(NMPlatformObject *dst, const NMPlatformObject *src);
     int (*cmd_plobj_id_cmp)(const NMPlatformObject *obj1, const NMPlatformObject *obj2);
     void (*cmd_plobj_id_hash_update)(const NMPlatformObject *obj, NMHashState *h);
     const char *(*cmd_plobj_to_string_id)(const NMPlatformObject *obj, char *buf, gsize buf_size);
     const char *(*cmd_plobj_to_string)(const NMPlatformObject *obj, char *buf, gsize len);
-    void (*cmd_plobj_hash_update)(const NMPlatformObject *obj, NMHashState *h);
-    int (*cmd_plobj_cmp)(const NMPlatformObject *obj1, const NMPlatformObject *obj2);
 } NMPClass;
 
 extern const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX];
@@ -296,6 +297,14 @@ typedef struct {
 } NMPObjectLnkVrf;
 
 typedef struct {
+    NMPlatformLnkVti _public;
+} NMPObjectLnkVti;
+
+typedef struct {
+    NMPlatformLnkVti6 _public;
+} NMPObjectLnkVti6;
+
+typedef struct {
     NMPlatformLnkVxlan _public;
 } NMPObjectLnkVxlan;
 
@@ -313,6 +322,13 @@ typedef struct {
 
 typedef struct {
     NMPlatformIP4Route _public;
+
+    /* The first hop is embedded in _public (in the
+     * ifindex, gateway and weight fields).
+     * Only if _public.n_nexthops is greater than 1, then
+     * this contains the remaining(!!) (_public.n_nexthops - 1)
+     * extra hops for ECMP multihop routes. */
+    const NMPlatformIP4RtNextHop *extra_nexthops;
 } NMPObjectIP4Route;
 
 typedef struct {
@@ -388,6 +404,12 @@ struct _NMPObject {
         NMPlatformLnkVrf lnk_vrf;
         NMPObjectLnkVrf  _lnk_vrf;
 
+        NMPlatformLnkVti lnk_vti;
+        NMPObjectLnkVti  _lnk_vti;
+
+        NMPlatformLnkVti6 lnk_vti6;
+        NMPObjectLnkVti6  _lnk_vti6;
+
         NMPlatformLnkVxlan lnk_vxlan;
         NMPObjectLnkVxlan  _lnk_vxlan;
 
@@ -419,7 +441,7 @@ struct _NMPObject {
         NMPlatformMptcpAddr mptcp_addr;
         NMPObjectMptcpAddr  _mptcp_addr;
     };
-};
+} _nm_alignas(NMDedupMultiObj);
 
 /*****************************************************************************/
 
@@ -446,9 +468,12 @@ NMP_OBJECT_UP_CAST(const NMPlatformObject *plobj)
 {
     NMPObject *obj;
 
-    obj = plobj ? (NMPObject *) (&(((char *) plobj)[-((int) G_STRUCT_OFFSET(NMPObject, object))]))
+    obj = plobj ? NM_CAST_ALIGN(NMPObject,
+                                &(((char *) plobj)[-((int) G_STRUCT_OFFSET(NMPObject, object))]))
                 : NULL;
+
     nm_assert(!obj || (obj->parent._ref_count > 0 && NMP_CLASS_IS_VALID(obj->_class)));
+
     return obj;
 }
 #define NMP_OBJECT_UP_CAST(plobj) (NMP_OBJECT_UP_CAST((const NMPlatformObject *) (plobj)))
@@ -517,6 +542,8 @@ _NMP_OBJECT_TYPE_IS_OBJ_WITH_IFINDEX(NMPObjectType obj_type)
     case NMP_OBJECT_TYPE_LNK_TUN:
     case NMP_OBJECT_TYPE_LNK_VLAN:
     case NMP_OBJECT_TYPE_LNK_VRF:
+    case NMP_OBJECT_TYPE_LNK_VTI:
+    case NMP_OBJECT_TYPE_LNK_VTI6:
     case NMP_OBJECT_TYPE_LNK_VXLAN:
     case NMP_OBJECT_TYPE_LNK_WIREGUARD:
 
@@ -534,6 +561,8 @@ _NMP_OBJECT_TYPE_IS_OBJ_WITH_IFINDEX(NMPObjectType obj_type)
     return FALSE;
 }
 
+#define NMP_OBJECT_TYPE_NAME(obj_type) (nmp_class_from_type(obj_type)->obj_type_name)
+
 #define NMP_OBJECT_CAST_OBJECT(obj)                                       \
     ({                                                                    \
         typeof(obj) _obj = (obj);                                         \
@@ -717,14 +746,35 @@ const char *nmp_object_to_string(const NMPObject      *obj,
                                  NMPObjectToStringMode to_string_mode,
                                  char                 *buf,
                                  gsize                 buf_size);
-void        nmp_object_hash_update(const NMPObject *obj, NMHashState *h);
+
+void nmp_object_hash_update_full(const NMPObject *obj, gboolean for_id, NMHashState *h);
+
+static inline void
+nmp_object_hash_update(const NMPObject *obj, NMHashState *h)
+{
+    return nmp_object_hash_update_full(obj, FALSE, h);
+}
 
 typedef enum {
     NMP_OBJECT_CMP_FLAGS_NONE = 0,
 
+    /* Only compare for the ID. This is what nmp_object_id_cmp() does.
+     *
+     * In most cases, the identity of an object is a (non-strict) subset
+     * of the attributes of the object.
+     *
+     * However, for some objects (like NMPObjectLnk) there is on concept
+     * of identity. They implement object identity based on pointer equality
+     * (in that case, the ID is not a subset of the object's attributes).
+     *
+     * That's why this flag (currently) cannot be meaningfully combined with
+     * other flags.
+     */
+    NMP_OBJECT_CMP_FLAGS_ID = NM_BIT(0),
+
     /* Warning: this flag is currently only implemented for certain object types
      * (address and routes). */
-    NMP_OBJECT_CMP_FLAGS_IGNORE_IFINDEX = (1llu << 0),
+    NMP_OBJECT_CMP_FLAGS_IGNORE_IFINDEX = NM_BIT(1),
 } NMPObjectCmpFlags;
 
 int nmp_object_cmp_full(const NMPObject *obj1, const NMPObject *obj2, NMPObjectCmpFlags flags);
@@ -744,8 +794,18 @@ nmp_object_equal(const NMPObject *obj1, const NMPObject *obj2)
 void       nmp_object_copy(NMPObject *dst, const NMPObject *src, gboolean id_only);
 NMPObject *nmp_object_clone(const NMPObject *obj, gboolean id_only);
 
-int   nmp_object_id_cmp(const NMPObject *obj1, const NMPObject *obj2);
-void  nmp_object_id_hash_update(const NMPObject *obj, NMHashState *h);
+static inline int
+nmp_object_id_cmp(const NMPObject *obj1, const NMPObject *obj2)
+{
+    return nmp_object_cmp_full(obj1, obj2, NMP_OBJECT_CMP_FLAGS_ID);
+}
+
+static inline void
+nmp_object_id_hash_update(const NMPObject *obj, NMHashState *h)
+{
+    return nmp_object_hash_update_full(obj, TRUE, h);
+}
+
 guint nmp_object_id_hash(const NMPObject *obj);
 
 static inline gboolean
@@ -915,6 +975,7 @@ NMPCacheOpsType nmp_cache_update_netlink_route(NMPCache         *cache,
                                                NMPObject        *obj_hand_over,
                                                gboolean          is_dump,
                                                guint16           nlmsgflags,
+                                               gboolean          route_is_alive,
                                                const NMPObject **out_obj_old,
                                                const NMPObject **out_obj_new,
                                                const NMPObject **out_obj_replace,
diff --git a/src/libnm-platform/nmp-plobj.c b/src/libnm-platform/nmp-plobj.c
new file mode 100644
index 00000000..c27aa1fc
--- /dev/null
+++ b/src/libnm-platform/nmp-plobj.c
@@ -0,0 +1,637 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2015 - 2018 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nmp-plobj.h"
+
+#include "nm-compat-headers/linux/if_addr.h"
+
+#include "libnm-glib-aux/nm-time-utils.h"
+#include "nm-platform-utils.h"
+
+/*****************************************************************************/
+
+#define TO_STRING_IFA_FLAGS_BUF_SIZE 256
+
+static const char *
+_to_string_ifa_flags(guint32 ifa_flags, char *buf, gsize size)
+{
+#define S_FLAGS_PREFIX " flags "
+    nm_assert(buf && size >= TO_STRING_IFA_FLAGS_BUF_SIZE && size > NM_STRLEN(S_FLAGS_PREFIX));
+
+    if (!ifa_flags)
+        buf[0] = '\0';
+    else {
+        nm_platform_addr_flags2str(ifa_flags,
+                                   &buf[NM_STRLEN(S_FLAGS_PREFIX)],
+                                   size - NM_STRLEN(S_FLAGS_PREFIX));
+        if (buf[NM_STRLEN(S_FLAGS_PREFIX)] == '\0')
+            buf[0] = '\0';
+        else
+            memcpy(buf, S_FLAGS_PREFIX, NM_STRLEN(S_FLAGS_PREFIX));
+    }
+    return buf;
+}
+
+#define _to_string_dev(arr, ifindex)                                                   \
+    ({                                                                                 \
+        const int _ifindex = (ifindex);                                                \
+                                                                                       \
+        _ifindex ? nm_sprintf_buf((arr), " dev %d", ifindex) : nm_str_truncate((arr)); \
+    })
+
+static const char *
+_lifetime_to_string(guint32 timestamp, guint32 lifetime, gint32 now, char *buf, size_t buf_size)
+{
+    if (lifetime == NM_PLATFORM_LIFETIME_PERMANENT)
+        return "forever";
+
+    g_snprintf(buf,
+               buf_size,
+               "%usec",
+               nmp_utils_lifetime_rebase_relative_time_on_now(timestamp, lifetime, now));
+    return buf;
+}
+
+static const char *
+_lifetime_summary_to_string(gint32  now,
+                            guint32 timestamp,
+                            guint32 preferred,
+                            guint32 lifetime,
+                            char   *buf,
+                            size_t  buf_size)
+{
+    g_snprintf(buf,
+               buf_size,
+               " lifetime %d-%u[%u,%u]",
+               (signed) now,
+               (unsigned) timestamp,
+               (unsigned) preferred,
+               (unsigned) lifetime);
+    return buf;
+}
+
+static int
+_address_cmp_expiry(const NMPlatformIPAddress *a, const NMPlatformIPAddress *b)
+{
+    guint32 lifetime_a;
+    guint32 lifetime_b;
+    guint32 preferred_a;
+    guint32 preferred_b;
+    gint32  now = 0;
+
+    lifetime_a =
+        nmp_utils_lifetime_get(a->timestamp, a->lifetime, a->preferred, &now, &preferred_a);
+    lifetime_b =
+        nmp_utils_lifetime_get(b->timestamp, b->lifetime, b->preferred, &now, &preferred_b);
+
+    NM_CMP_DIRECT(lifetime_a, lifetime_b);
+    NM_CMP_DIRECT(preferred_a, preferred_b);
+    return 0;
+}
+
+/*****************************************************************************/
+
+void
+nm_platform_ip4_address_hash_update(const NMPlatformIP4Address *obj, NMHashState *h)
+{
+    nm_hash_update_vals(h,
+                        obj->ifindex,
+                        obj->addr_source,
+                        obj->use_ip4_broadcast_address ? obj->broadcast_address : ((in_addr_t) 0u),
+                        obj->timestamp,
+                        obj->lifetime,
+                        obj->preferred,
+                        obj->n_ifa_flags,
+                        obj->plen,
+                        obj->address,
+                        obj->peer_address,
+                        NM_HASH_COMBINE_BOOLS(guint8,
+                                              obj->use_ip4_broadcast_address,
+                                              obj->a_acd_not_ready,
+                                              obj->a_force_commit));
+    nm_hash_update_strarr(h, obj->label);
+}
+
+int
+nm_platform_ip4_address_cmp(const NMPlatformIP4Address *a,
+                            const NMPlatformIP4Address *b,
+                            NMPlatformIPAddressCmpType  cmp_type)
+{
+    NM_CMP_SELF(a, b);
+
+    NM_CMP_FIELD(a, b, ifindex);
+    NM_CMP_FIELD(a, b, plen);
+    NM_CMP_FIELD(a, b, address);
+
+    switch (cmp_type) {
+    case NM_PLATFORM_IP_ADDRESS_CMP_TYPE_ID:
+        /* for IPv4 addresses, you can add the same local address with differing peer-address
+         * (IFA_ADDRESS), provided that their net-part differs. */
+        NM_CMP_DIRECT_IP4_ADDR_SAME_PREFIX(a->peer_address, b->peer_address, a->plen);
+        return 0;
+    case NM_PLATFORM_IP_ADDRESS_CMP_TYPE_SEMANTICALLY:
+    case NM_PLATFORM_IP_ADDRESS_CMP_TYPE_FULL:
+        NM_CMP_FIELD(a, b, peer_address);
+        NM_CMP_FIELD_STR(a, b, label);
+        if (cmp_type == NM_PLATFORM_IP_ADDRESS_CMP_TYPE_SEMANTICALLY) {
+            NM_CMP_RETURN(_address_cmp_expiry((const NMPlatformIPAddress *) a,
+                                              (const NMPlatformIPAddress *) b));
+
+            /* Most flags are set by kernel. We only compare the ones that
+             * NetworkManager actively sets.
+             *
+             * NM actively only sets IFA_F_NOPREFIXROUTE (and IFA_F_MANAGETEMPADDR for IPv6),
+             * where nm_platform_ip_address_sync() sets IFA_F_NOPREFIXROUTE depending on
+             * NMP_IP_ADDRESS_SYNC_FLAGS_WITH_NOPREFIXROUTE.
+             * There are thus no flags to compare for IPv4. */
+
+            NM_CMP_DIRECT(nm_platform_ip4_broadcast_address_from_addr(a),
+                          nm_platform_ip4_broadcast_address_from_addr(b));
+        } else {
+            NM_CMP_FIELD(a, b, timestamp);
+            NM_CMP_FIELD(a, b, lifetime);
+            NM_CMP_FIELD(a, b, preferred);
+            NM_CMP_FIELD(a, b, n_ifa_flags);
+            NM_CMP_FIELD(a, b, addr_source);
+            NM_CMP_FIELD_UNSAFE(a, b, use_ip4_broadcast_address);
+            if (a->use_ip4_broadcast_address)
+                NM_CMP_FIELD(a, b, broadcast_address);
+            NM_CMP_FIELD_UNSAFE(a, b, a_acd_not_ready);
+            NM_CMP_FIELD_UNSAFE(a, b, a_force_commit);
+        }
+        return 0;
+    }
+    return nm_assert_unreachable_val(0);
+}
+
+/*****************************************************************************/
+
+void
+nm_platform_ip6_address_hash_update(const NMPlatformIP6Address *obj, NMHashState *h)
+{
+    nm_hash_update_vals(h,
+                        obj->ifindex,
+                        obj->addr_source,
+                        obj->timestamp,
+                        obj->lifetime,
+                        obj->preferred,
+                        obj->n_ifa_flags,
+                        obj->plen,
+                        obj->address,
+                        obj->peer_address,
+                        NM_HASH_COMBINE_BOOLS(guint8, obj->a_force_commit));
+}
+
+int
+nm_platform_ip6_address_cmp(const NMPlatformIP6Address *a,
+                            const NMPlatformIP6Address *b,
+                            NMPlatformIPAddressCmpType  cmp_type)
+{
+    const struct in6_addr *p_a, *p_b;
+
+    NM_CMP_SELF(a, b);
+
+    NM_CMP_FIELD(a, b, ifindex);
+    NM_CMP_FIELD_IN6ADDR(a, b, address);
+
+    switch (cmp_type) {
+    case NM_PLATFORM_IP_ADDRESS_CMP_TYPE_ID:
+        /* for IPv6 addresses, the prefix length is not part of the primary identifier. */
+        return 0;
+    case NM_PLATFORM_IP_ADDRESS_CMP_TYPE_SEMANTICALLY:
+    case NM_PLATFORM_IP_ADDRESS_CMP_TYPE_FULL:
+        NM_CMP_FIELD(a, b, plen);
+        p_a = nm_platform_ip6_address_get_peer(a);
+        p_b = nm_platform_ip6_address_get_peer(b);
+        NM_CMP_DIRECT_MEMCMP(p_a, p_b, sizeof(*p_a));
+        if (cmp_type == NM_PLATFORM_IP_ADDRESS_CMP_TYPE_SEMANTICALLY) {
+            NM_CMP_RETURN(_address_cmp_expiry((const NMPlatformIPAddress *) a,
+                                              (const NMPlatformIPAddress *) b));
+
+            /* Most flags are set by kernel. We only compare the ones that
+             * NetworkManager actively sets.
+             *
+             * NM actively only sets IFA_F_NOPREFIXROUTE and IFA_F_MANAGETEMPADDR,
+             * where nm_platform_ip_address_sync() sets IFA_F_NOPREFIXROUTE depending on
+             * NMP_IP_ADDRESS_SYNC_FLAGS_WITH_NOPREFIXROUTE.
+             * We thus only care about IFA_F_MANAGETEMPADDR. */
+            NM_CMP_DIRECT(a->n_ifa_flags & IFA_F_MANAGETEMPADDR,
+                          b->n_ifa_flags & IFA_F_MANAGETEMPADDR);
+        } else {
+            NM_CMP_FIELD(a, b, timestamp);
+            NM_CMP_FIELD(a, b, lifetime);
+            NM_CMP_FIELD(a, b, preferred);
+            NM_CMP_FIELD(a, b, n_ifa_flags);
+            NM_CMP_FIELD(a, b, addr_source);
+            NM_CMP_FIELD_UNSAFE(a, b, a_force_commit);
+        }
+        return 0;
+    }
+    return nm_assert_unreachable_val(0);
+}
+
+/*****************************************************************************/
+
+static int
+_address_pretty_sort_get_prio_4(in_addr_t addr)
+{
+    if (nm_ip4_addr_is_link_local(addr))
+        return 0;
+    return 1;
+}
+
+static int
+_address_pretty_sort_get_prio_6(const struct in6_addr *addr)
+{
+    if (IN6_IS_ADDR_V4MAPPED(addr))
+        return 0;
+    if (IN6_IS_ADDR_V4COMPAT(addr))
+        return 1;
+    if (IN6_IS_ADDR_UNSPECIFIED(addr))
+        return 2;
+    if (IN6_IS_ADDR_LOOPBACK(addr))
+        return 3;
+    if (IN6_IS_ADDR_LINKLOCAL(addr))
+        return 4;
+    if (IN6_IS_ADDR_SITELOCAL(addr))
+        return 5;
+    return 6;
+}
+
+int
+nm_platform_ip4_address_pretty_sort_cmp(const NMPlatformIP4Address *a1,
+                                        const NMPlatformIP4Address *a2)
+{
+    in_addr_t n1;
+    in_addr_t n2;
+
+    nm_assert(a1);
+    nm_assert(a2);
+
+    /* Sort by address type. For example link local will
+     * be sorted *after* a global address. */
+    NM_CMP_DIRECT(_address_pretty_sort_get_prio_4(a2->address),
+                  _address_pretty_sort_get_prio_4(a1->address));
+
+    /* Sort the addresses based on their source. */
+    NM_CMP_DIRECT(a2->addr_source, a1->addr_source);
+
+    NM_CMP_DIRECT((a2->label[0] == '\0'), (a1->label[0] == '\0'));
+
+    /* Finally, sort addresses lexically. We compare only the
+     * network part so that the order of addresses in the same
+     * subnet (and thus also the primary/secondary role) is
+     * preserved.
+     */
+    n1 = nm_ip4_addr_clear_host_address(a1->address, a1->plen);
+    n2 = nm_ip4_addr_clear_host_address(a2->address, a2->plen);
+    NM_CMP_DIRECT_MEMCMP(&n1, &n2, sizeof(guint32));
+    return 0;
+}
+
+int
+nm_platform_ip6_address_pretty_sort_cmp(const NMPlatformIP6Address *a1,
+                                        const NMPlatformIP6Address *a2,
+                                        gboolean                    prefer_temp)
+{
+    gboolean ipv6_privacy1;
+    gboolean ipv6_privacy2;
+
+    nm_assert(a1);
+    nm_assert(a2);
+
+    /* tentative addresses are always sorted back... */
+    /* sort tentative addresses after non-tentative. */
+    NM_CMP_DIRECT(NM_FLAGS_HAS(a1->n_ifa_flags, IFA_F_TENTATIVE),
+                  NM_FLAGS_HAS(a2->n_ifa_flags, IFA_F_TENTATIVE));
+
+    /* Sort by address type. For example link local will
+     * be sorted *after* site local or global. */
+    NM_CMP_DIRECT(_address_pretty_sort_get_prio_6(&a2->address),
+                  _address_pretty_sort_get_prio_6(&a1->address));
+
+    ipv6_privacy1 = NM_FLAGS_ANY(a1->n_ifa_flags, IFA_F_MANAGETEMPADDR | IFA_F_SECONDARY);
+    ipv6_privacy2 = NM_FLAGS_ANY(a2->n_ifa_flags, IFA_F_MANAGETEMPADDR | IFA_F_SECONDARY);
+    if (ipv6_privacy1 || ipv6_privacy2) {
+        gboolean public1 = TRUE;
+        gboolean public2 = TRUE;
+
+        if (ipv6_privacy1) {
+            if (a1->n_ifa_flags & IFA_F_SECONDARY)
+                public1 = prefer_temp;
+            else
+                public1 = !prefer_temp;
+        }
+        if (ipv6_privacy2) {
+            if (a2->n_ifa_flags & IFA_F_SECONDARY)
+                public2 = prefer_temp;
+            else
+                public2 = !prefer_temp;
+        }
+
+        NM_CMP_DIRECT(public2, public1);
+    }
+
+    /* Sort the addresses based on their source. */
+    NM_CMP_DIRECT(a2->addr_source, a1->addr_source);
+
+    /* sort permanent addresses before non-permanent. */
+    NM_CMP_DIRECT(NM_FLAGS_HAS(a2->n_ifa_flags, IFA_F_PERMANENT),
+                  NM_FLAGS_HAS(a1->n_ifa_flags, IFA_F_PERMANENT));
+
+    /* finally sort addresses lexically */
+    NM_CMP_DIRECT_IN6ADDR(&a1->address, &a2->address);
+    NM_CMP_DIRECT_MEMCMP(a1, a2, sizeof(*a1));
+    return 0;
+}
+
+void
+nm_platform_ip4_address_set_addr(NMPlatformIP4Address *addr, in_addr_t address, guint8 plen)
+{
+    nm_assert(plen <= 32);
+
+    addr->address      = address;
+    addr->peer_address = address;
+    addr->plen         = plen;
+}
+
+const struct in6_addr *
+nm_platform_ip6_address_get_peer(const NMPlatformIP6Address *addr)
+{
+    if (IN6_IS_ADDR_UNSPECIFIED(&addr->peer_address)
+        || IN6_ARE_ADDR_EQUAL(&addr->peer_address, &addr->address))
+        return &addr->address;
+    return &addr->peer_address;
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_platform_ip4_address_to_string:
+ * @route: pointer to NMPlatformIP4Address address structure
+ * @buf: (allow-none): an optional buffer. If %NULL, a static buffer is used.
+ * @len: the size of the @buf. If @buf is %NULL, this argument is ignored.
+ *
+ * A method for converting an address struct into a string representation.
+ *
+ * Example output: ""
+ *
+ * Returns: a string representation of the address.
+ */
+const char *
+nm_platform_ip4_address_to_string(const NMPlatformIP4Address *address, char *buf, gsize len)
+{
+    char        s_flags[TO_STRING_IFA_FLAGS_BUF_SIZE];
+    char        s_address[INET_ADDRSTRLEN];
+    char        s_peer[INET_ADDRSTRLEN];
+    char        str_dev[30];
+    char        str_label[32];
+    char        str_lft[30];
+    char        str_pref[30];
+    char        str_time[50];
+    char        s_source[50];
+    char       *str_peer = NULL;
+    const char *str_lft_p, *str_pref_p, *str_time_p;
+    gint32      now = nm_utils_get_monotonic_timestamp_sec();
+    in_addr_t   broadcast_address;
+    char        str_broadcast[INET_ADDRSTRLEN];
+
+    if (!nm_utils_to_string_buffer_init_null(address, &buf, &len))
+        return buf;
+
+    inet_ntop(AF_INET, &address->address, s_address, sizeof(s_address));
+
+    if (address->peer_address != address->address) {
+        inet_ntop(AF_INET, &address->peer_address, s_peer, sizeof(s_peer));
+        str_peer = g_strconcat(" ptp ", s_peer, NULL);
+    }
+
+    if (*address->label)
+        g_snprintf(str_label, sizeof(str_label), " label %s", address->label);
+    else
+        str_label[0] = 0;
+
+    str_lft_p = _lifetime_to_string(address->timestamp,
+                                    address->lifetime ?: NM_PLATFORM_LIFETIME_PERMANENT,
+                                    now,
+                                    str_lft,
+                                    sizeof(str_lft)),
+    str_pref_p =
+        (address->lifetime == address->preferred)
+            ? str_lft_p
+            : (_lifetime_to_string(address->timestamp,
+                                   address->lifetime ? MIN(address->preferred, address->lifetime)
+                                                     : NM_PLATFORM_LIFETIME_PERMANENT,
+                                   now,
+                                   str_pref,
+                                   sizeof(str_pref)));
+    str_time_p = _lifetime_summary_to_string(now,
+                                             address->timestamp,
+                                             address->preferred,
+                                             address->lifetime,
+                                             str_time,
+                                             sizeof(str_time));
+
+    broadcast_address = nm_platform_ip4_broadcast_address_from_addr(address);
+
+    g_snprintf(
+        buf,
+        len,
+        "%s/%d"
+        "%s%s" /* broadcast */
+        " lft %s"
+        " pref %s"
+        "%s" /* time */
+        "%s" /* peer  */
+        "%s" /* dev */
+        "%s" /* flags */
+        "%s" /* label */
+        " src %s"
+        "%s" /* a_acd_not_ready */
+        "%s" /* a_force_commit */
+        "",
+        s_address,
+        address->plen,
+        broadcast_address != 0u || address->use_ip4_broadcast_address
+            ? (address->use_ip4_broadcast_address ? " brd " : " brd* ")
+            : "",
+        broadcast_address != 0u || address->use_ip4_broadcast_address
+            ? nm_inet4_ntop(broadcast_address, str_broadcast)
+            : "",
+        str_lft_p,
+        str_pref_p,
+        str_time_p,
+        str_peer ?: "",
+        _to_string_dev(str_dev, address->ifindex),
+        _to_string_ifa_flags(address->n_ifa_flags, s_flags, sizeof(s_flags)),
+        str_label,
+        nmp_utils_ip_config_source_to_string(address->addr_source, s_source, sizeof(s_source)),
+        address->a_acd_not_ready ? " ip4acd-not-ready" : "",
+        address->a_force_commit ? " force-commit" : "");
+    g_free(str_peer);
+    return buf;
+}
+
+/**
+ * nm_platform_ip6_address_to_string:
+ * @route: pointer to NMPlatformIP6Address address structure
+ * @buf: (allow-none): an optional buffer. If %NULL, a static buffer is used.
+ * @len: the size of the @buf. If @buf is %NULL, this argument is ignored.
+ *
+ * A method for converting an address struct into a string representation.
+ *
+ * Example output: "2001:db8:0:f101::1/64 lft 4294967295 pref 4294967295 time 16922666 on dev em1"
+ *
+ * Returns: a string representation of the address.
+ */
+const char *
+nm_platform_ip6_address_to_string(const NMPlatformIP6Address *address, char *buf, gsize len)
+{
+    char        s_flags[TO_STRING_IFA_FLAGS_BUF_SIZE];
+    char        s_address[INET6_ADDRSTRLEN];
+    char        s_peer[INET6_ADDRSTRLEN];
+    char        str_lft[30];
+    char        str_pref[30];
+    char        str_time[50];
+    char        s_source[50];
+    char        str_dev[30];
+    char       *str_peer = NULL;
+    const char *str_lft_p, *str_pref_p, *str_time_p;
+    gint32      now = nm_utils_get_monotonic_timestamp_sec();
+
+    if (!nm_utils_to_string_buffer_init_null(address, &buf, &len))
+        return buf;
+
+    inet_ntop(AF_INET6, &address->address, s_address, sizeof(s_address));
+
+    if (!IN6_IS_ADDR_UNSPECIFIED(&address->peer_address)) {
+        inet_ntop(AF_INET6, &address->peer_address, s_peer, sizeof(s_peer));
+        str_peer = g_strconcat(" ptp ", s_peer, NULL);
+    }
+
+    str_lft_p = _lifetime_to_string(address->timestamp,
+                                    address->lifetime ?: NM_PLATFORM_LIFETIME_PERMANENT,
+                                    now,
+                                    str_lft,
+                                    sizeof(str_lft)),
+    str_pref_p =
+        (address->lifetime == address->preferred)
+            ? str_lft_p
+            : (_lifetime_to_string(address->timestamp,
+                                   address->lifetime ? MIN(address->preferred, address->lifetime)
+                                                     : NM_PLATFORM_LIFETIME_PERMANENT,
+                                   now,
+                                   str_pref,
+                                   sizeof(str_pref)));
+    str_time_p = _lifetime_summary_to_string(now,
+                                             address->timestamp,
+                                             address->preferred,
+                                             address->lifetime,
+                                             str_time,
+                                             sizeof(str_time));
+
+    g_snprintf(
+        buf,
+        len,
+        "%s/%d lft %s pref %s%s%s%s%s src %s"
+        "%s" /* a_force_commit */
+        "",
+        s_address,
+        address->plen,
+        str_lft_p,
+        str_pref_p,
+        str_time_p,
+        str_peer ?: "",
+        _to_string_dev(str_dev, address->ifindex),
+        _to_string_ifa_flags(address->n_ifa_flags, s_flags, sizeof(s_flags)),
+        nmp_utils_ip_config_source_to_string(address->addr_source, s_source, sizeof(s_source)),
+        address->a_force_commit ? " force-commit" : "");
+    g_free(str_peer);
+    return buf;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_platform_ip_address_match(int                        addr_family,
+                             const NMPlatformIPAddress *address,
+                             NMPlatformMatchFlags       match_flag)
+{
+    nm_assert(!NM_FLAGS_ANY(
+        match_flag,
+        ~(NM_PLATFORM_MATCH_WITH_ADDRTYPE__ANY | NM_PLATFORM_MATCH_WITH_ADDRSTATE__ANY)));
+    nm_assert(NM_FLAGS_ANY(match_flag, NM_PLATFORM_MATCH_WITH_ADDRTYPE__ANY));
+    nm_assert(NM_FLAGS_ANY(match_flag, NM_PLATFORM_MATCH_WITH_ADDRSTATE__ANY));
+
+    if (addr_family == AF_INET) {
+        if (nm_ip4_addr_is_link_local(((NMPlatformIP4Address *) address)->address)) {
+            if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL))
+                return FALSE;
+        } else {
+            if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRTYPE_NORMAL))
+                return FALSE;
+        }
+    } else {
+        if (IN6_IS_ADDR_LINKLOCAL(address->address_ptr)) {
+            if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL))
+                return FALSE;
+        } else {
+            if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRTYPE_NORMAL))
+                return FALSE;
+        }
+    }
+
+    if (NM_FLAGS_HAS(address->n_ifa_flags, IFA_F_DADFAILED)) {
+        if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRSTATE_DADFAILED))
+            return FALSE;
+    } else if (NM_FLAGS_HAS(address->n_ifa_flags, IFA_F_TENTATIVE)
+               && !NM_FLAGS_HAS(address->n_ifa_flags, IFA_F_OPTIMISTIC)) {
+        if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRSTATE_TENTATIVE))
+            return FALSE;
+    } else if (NM_FLAGS_HAS(address->n_ifa_flags, IFA_F_DEPRECATED)) {
+        if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRSTATE_DEPRECATED))
+            return FALSE;
+    } else {
+        if (!NM_FLAGS_HAS(match_flag, NM_PLATFORM_MATCH_WITH_ADDRSTATE_NORMAL))
+            return FALSE;
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+const _NMPlatformVTableAddressUnion nm_platform_vtable_address = {
+    .v4 =
+        {
+            .is_ip4         = TRUE,
+            .obj_type       = NMP_OBJECT_TYPE_IP4_ADDRESS,
+            .addr_family    = AF_INET,
+            .sizeof_address = sizeof(NMPlatformIP4Address),
+            .address_cmp =
+                (int (*)(const NMPlatformIPXAddress *a,
+                         const NMPlatformIPXAddress *b,
+                         NMPlatformIPAddressCmpType  cmp_type)) nm_platform_ip4_address_cmp,
+            .address_to_string = (const char *(*) (const NMPlatformIPXAddress *address,
+                                                   char                       *buf,
+                                                   gsize len)) nm_platform_ip4_address_to_string,
+        },
+    .v6 =
+        {
+            .is_ip4         = FALSE,
+            .obj_type       = NMP_OBJECT_TYPE_IP6_ADDRESS,
+            .addr_family    = AF_INET6,
+            .sizeof_address = sizeof(NMPlatformIP6Address),
+            .address_cmp =
+                (int (*)(const NMPlatformIPXAddress *a,
+                         const NMPlatformIPXAddress *b,
+                         NMPlatformIPAddressCmpType  cmp_type)) nm_platform_ip6_address_cmp,
+            .address_to_string = (const char *(*) (const NMPlatformIPXAddress *address,
+                                                   char                       *buf,
+                                                   gsize len)) nm_platform_ip6_address_to_string,
+        },
+};
diff --git a/src/libnm-platform/nmp-plobj.h b/src/libnm-platform/nmp-plobj.h
new file mode 100644
index 00000000..e149150d
--- /dev/null
+++ b/src/libnm-platform/nmp-plobj.h
@@ -0,0 +1,296 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2015 - 2018 Red Hat, Inc.
+ */
+
+#ifndef __NMP_PLOBJ_H__
+#define __NMP_PLOBJ_H__
+
+#include "libnm-base/nm-base.h"
+
+#include "nmp-base.h"
+
+/*****************************************************************************/
+
+/* This is used with _nm_align() on the platform structs. Note that
+ * "align" attribute can only increase the natural alignment, unless
+ * also "packed" is specified. That's what we want.
+ * https://gcc.gnu.org/onlinedocs/gcc/Common-Type-Attributes.html#Common-Type-Attributes.
+ */
+#define _NMPlatformObject_Align (MAX(_nm_alignof(void *), _nm_alignof(gint64)))
+
+struct _NMPlatformObject {
+    /* the object type has no fields of its own, it is only used to having
+     * a special pointer type that can be used to indicate "any" type. */
+    char _dummy_don_t_use_me;
+} _nm_align(_NMPlatformObject_Align);
+
+/*****************************************************************************/
+
+#define __NMPlatformObjWithIfindex_COMMON \
+    int ifindex;                          \
+    ;
+
+/*****************************************************************************/
+
+#define __NMPlatformIPAddress_COMMON                                                         \
+    __NMPlatformObjWithIfindex_COMMON;                                                       \
+    NMIPConfigSource addr_source;                                                            \
+                                                                                             \
+    /* Timestamp in seconds in the reference system of nm_utils_get_monotonic_timestamp_*().
+     *
+     * The rules are:
+     * 1 @lifetime==0: @timestamp and @preferred is irrelevant (but mostly set to 0 too). Such addresses
+     *   are permanent. This rule is so that unset addresses (calloc) are permanent by default.
+     * 2 @lifetime==@preferred==NM_PLATFORM_LIFETIME_PERMANENT: @timestamp is irrelevant (but mostly
+     *   set to 0). Such addresses are permanent.
+     * 3 Non permanent addresses should (almost) always have @timestamp > 0. 0 is not a valid timestamp
+     *   and never returned by nm_utils_get_monotonic_timestamp_sec(). In this case @valid/@preferred
+     *   is anchored at @timestamp.
+     * 4 Non permanent addresses with @timestamp == 0 are implicitly anchored at *now*, thus the time
+     *   moves as time goes by. This is usually not useful, except e.g. nm_platform_ip[46]_address_add().
+     *
+     * Non permanent addresses from DHCP/RA might have the @timestamp set to the moment of when the
+     * lease was received. Addresses from kernel might have the @timestamp based on the last modification
+     * time of the addresses. But don't rely on this behaviour, the @timestamp is only defined for anchoring
+     * @lifetime and @preferred.
+     */ \
+    guint32 timestamp;                                                                       \
+    guint32 lifetime;  /* seconds since timestamp */                                         \
+    guint32 preferred; /* seconds since timestamp */                                         \
+                                                                                             \
+    /* ifa_flags in 'struct ifaddrmsg' from <linux/if_addr.h>, extended to 32 bit by
+     * IFA_FLAGS attribute. */         \
+    guint32 n_ifa_flags;                                                                     \
+                                                                                             \
+    bool use_ip4_broadcast_address : 1;                                                      \
+                                                                                             \
+    /* Meta flags not honored by NMPlatform (netlink code). Instead, they can be
+     * used by the upper layers which use NMPlatformIPRoute to track addresses that
+     * should be configured. */             \
+    bool a_force_commit : 1;                                                                 \
+                                                                                             \
+    /* Don't have a bitfield as last field in __NMPlatformIPAddress_COMMON. It would then
+     * be unclear how the following fields get merged. We could also use a zero bitfield,
+     * but instead we just have there the uint8 field. */    \
+    guint8 plen;                                                                             \
+    ;
+
+/**
+ * NMPlatformIPAddress:
+ *
+ * Common parts of NMPlatformIP4Address and NMPlatformIP6Address.
+ **/
+typedef struct {
+    __NMPlatformIPAddress_COMMON;
+    _nm_alignas(NMIPAddr) guint8 address_ptr[];
+} _nm_alignas(NMPlatformObject) NMPlatformIPAddress;
+
+/**
+ * NMPlatformIP4Address:
+ * @timestamp: timestamp as returned by nm_utils_get_monotonic_timestamp_sec()
+ **/
+struct _NMPlatformIP4Address {
+    __NMPlatformIPAddress_COMMON;
+
+    /* The local address IFA_LOCAL. */
+    _nm_alignas(NMIPAddr) in_addr_t address;
+
+    /* The IFA_ADDRESS PTP peer address. This field is rather important, because
+     * it constitutes the identifier for the IPv4 address (e.g. you can add two
+     * addresses that only differ by their peer's network-part.
+     *
+     * Beware that for most cases, NetworkManager doesn't want to set an explicit
+     * peer-address. However, that corresponds to setting the peer address to @address
+     * itself. Leaving peer-address unset/zero, means explicitly setting the peer
+     * address to 0.0.0.0, which you probably don't want.
+     * */
+    in_addr_t peer_address; /* PTP peer address */
+
+    /* IFA_BROADCAST.
+     *
+     * This parameter is ignored unless use_ip4_broadcast_address is TRUE.
+     * See nm_platform_ip4_broadcast_address_from_addr(). */
+    in_addr_t broadcast_address;
+
+    char label[NMP_IFNAMSIZ];
+
+    /* Whether the address is ready to be configured. By default, an address is, but this
+     * flag may indicate that the address is just for tracking purpose only, but the ACD
+     * state is not yet ready for the address to be configured. */
+    bool a_acd_not_ready : 1;
+} _nm_alignas(NMPlatformObject);
+
+/**
+ * NMPlatformIP6Address:
+ * @timestamp: timestamp as returned by nm_utils_get_monotonic_timestamp_sec()
+ **/
+struct _NMPlatformIP6Address {
+    __NMPlatformIPAddress_COMMON;
+    _nm_alignas(NMIPAddr) struct in6_addr address;
+    struct in6_addr peer_address;
+} _nm_alignas(NMPlatformObject);
+
+typedef union {
+    NMPlatformIPAddress  ax;
+    NMPlatformIP4Address a4;
+    NMPlatformIP6Address a6;
+} NMPlatformIPXAddress;
+
+#undef __NMPlatformIPAddress_COMMON
+
+/*****************************************************************************/
+
+typedef enum {
+    NM_PLATFORM_IP_ADDRESS_CMP_TYPE_ID,
+
+    NM_PLATFORM_IP_ADDRESS_CMP_TYPE_SEMANTICALLY,
+
+    NM_PLATFORM_IP_ADDRESS_CMP_TYPE_FULL,
+} NMPlatformIPAddressCmpType;
+
+#define NM_PLATFORM_IP_ADDRESS_CAST(address) \
+    NM_CONSTCAST(NMPlatformIPAddress,        \
+                 (address),                  \
+                 NMPlatformIPXAddress,       \
+                 NMPlatformIP4Address,       \
+                 NMPlatformIP6Address)
+
+#define NM_PLATFORM_IP4_ADDRESS_INIT(...) (&((const NMPlatformIP4Address){__VA_ARGS__}))
+
+#define NM_PLATFORM_IP6_ADDRESS_INIT(...) (&((const NMPlatformIP6Address){__VA_ARGS__}))
+
+/*****************************************************************************/
+
+typedef struct {
+    bool          is_ip4;
+    NMPObjectType obj_type;
+    gint8         addr_family;
+    guint8        sizeof_address;
+    int (*address_cmp)(const NMPlatformIPXAddress *a,
+                       const NMPlatformIPXAddress *b,
+                       NMPlatformIPAddressCmpType  cmp_type);
+    const char *(*address_to_string)(const NMPlatformIPXAddress *address, char *buf, gsize len);
+} NMPlatformVTableAddress;
+
+typedef union {
+    struct {
+        NMPlatformVTableAddress v6;
+        NMPlatformVTableAddress v4;
+    };
+    NMPlatformVTableAddress vx[2];
+} _NMPlatformVTableAddressUnion;
+
+extern const _NMPlatformVTableAddressUnion nm_platform_vtable_address;
+
+void nm_platform_ip4_address_hash_update(const NMPlatformIP4Address *obj, NMHashState *h);
+
+int nm_platform_ip4_address_cmp(const NMPlatformIP4Address *a,
+                                const NMPlatformIP4Address *b,
+                                NMPlatformIPAddressCmpType  cmp_type);
+
+static inline int
+nm_platform_ip4_address_cmp_full(const NMPlatformIP4Address *a, const NMPlatformIP4Address *b)
+{
+    return nm_platform_ip4_address_cmp(a, b, NM_PLATFORM_IP_ADDRESS_CMP_TYPE_FULL);
+}
+
+void nm_platform_ip6_address_hash_update(const NMPlatformIP6Address *obj, NMHashState *h);
+
+int nm_platform_ip6_address_cmp(const NMPlatformIP6Address *a,
+                                const NMPlatformIP6Address *b,
+                                NMPlatformIPAddressCmpType  cmp_type);
+
+static inline int
+nm_platform_ip6_address_cmp_full(const NMPlatformIP6Address *a, const NMPlatformIP6Address *b)
+{
+    return nm_platform_ip6_address_cmp(a, b, NM_PLATFORM_IP_ADDRESS_CMP_TYPE_FULL);
+}
+
+int nm_platform_ip4_address_pretty_sort_cmp(const NMPlatformIP4Address *a1,
+                                            const NMPlatformIP4Address *a2);
+
+int nm_platform_ip6_address_pretty_sort_cmp(const NMPlatformIP6Address *a1,
+                                            const NMPlatformIP6Address *a2,
+                                            gboolean                    prefer_temp);
+
+static inline in_addr_t
+nm_platform_ip4_broadcast_address_from_addr(const NMPlatformIP4Address *addr)
+{
+    nm_assert(addr);
+
+    if (addr->use_ip4_broadcast_address)
+        return addr->broadcast_address;
+
+    /* the set broadcast-address gets ignored, and we determine a default brd base
+     * on the peer IFA_ADDRESS. */
+    if (addr->peer_address != 0u && addr->plen < 31 /* RFC3021 */)
+        return nm_ip4_addr_get_broadcast_address(addr->peer_address, addr->plen);
+    return 0u;
+}
+
+void nm_platform_ip4_address_set_addr(NMPlatformIP4Address *addr, in_addr_t address, guint8 plen);
+
+const struct in6_addr *nm_platform_ip6_address_get_peer(const NMPlatformIP6Address *addr);
+
+static inline gpointer
+nm_platform_ip_address_get_peer_address(int addr_family, const NMPlatformIPAddress *addr)
+{
+    nm_assert_addr_family(addr_family);
+    nm_assert(addr);
+
+    if (NM_IS_IPv4(addr_family))
+        return &((NMPlatformIP4Address *) addr)->peer_address;
+    return &((NMPlatformIP6Address *) addr)->peer_address;
+}
+
+const char *
+nm_platform_ip4_address_to_string(const NMPlatformIP4Address *address, char *buf, gsize len);
+const char *
+nm_platform_ip6_address_to_string(const NMPlatformIP6Address *address, char *buf, gsize len);
+
+int nm_platform_ip_address_cmp_expiry(const NMPlatformIPAddress *a, const NMPlatformIPAddress *b);
+
+NMPlatformIP4Route *nm_platform_ip4_address_generate_device_route(const NMPlatformIP4Address *addr,
+                                                                  int                 ifindex,
+                                                                  guint32             route_table,
+                                                                  guint32             route_metric,
+                                                                  gboolean            force_commit,
+                                                                  NMPlatformIP4Route *dst);
+
+typedef enum {
+
+    /* match-flags are strictly inclusive. That means,
+     * by default nothing is matched, but if you enable a particular
+     * flag, a candidate that matches passes the check.
+     *
+     * In other words: adding more flags can only extend the result
+     * set of matching objects.
+     *
+     * Also, the flags form partitions. Like, an address can be either of
+     * ADDRTYPE_NORMAL or ADDRTYPE_LINKLOCAL, but never both. Same for
+     * the ADDRSTATE match types.
+     */
+    NM_PLATFORM_MATCH_WITH_NONE = 0,
+
+    NM_PLATFORM_MATCH_WITH_ADDRTYPE_NORMAL    = (1LL << 0),
+    NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL = (1LL << 1),
+    NM_PLATFORM_MATCH_WITH_ADDRTYPE__ANY =
+        NM_PLATFORM_MATCH_WITH_ADDRTYPE_NORMAL | NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL,
+
+    NM_PLATFORM_MATCH_WITH_ADDRSTATE_NORMAL     = (1LL << 2),
+    NM_PLATFORM_MATCH_WITH_ADDRSTATE_TENTATIVE  = (1LL << 3),
+    NM_PLATFORM_MATCH_WITH_ADDRSTATE_DADFAILED  = (1LL << 4),
+    NM_PLATFORM_MATCH_WITH_ADDRSTATE_DEPRECATED = (1LL << 5),
+    NM_PLATFORM_MATCH_WITH_ADDRSTATE__ANY =
+        NM_PLATFORM_MATCH_WITH_ADDRSTATE_NORMAL | NM_PLATFORM_MATCH_WITH_ADDRSTATE_TENTATIVE
+        | NM_PLATFORM_MATCH_WITH_ADDRSTATE_DADFAILED | NM_PLATFORM_MATCH_WITH_ADDRSTATE_DEPRECATED,
+} NMPlatformMatchFlags;
+
+gboolean nm_platform_ip_address_match(int                        addr_family,
+                                      const NMPlatformIPAddress *addr,
+                                      NMPlatformMatchFlags       match_flag);
+
+/*****************************************************************************/
+
+#endif /* __NMP_PLOBJ_H__ */
diff --git a/src/libnm-platform/tests/test-nm-platform.c b/src/libnm-platform/tests/test-nm-platform.c
index cd54df92..90a29045 100644
--- a/src/libnm-platform/tests/test-nm-platform.c
+++ b/src/libnm-platform/tests/test-nm-platform.c
@@ -6,16 +6,67 @@
 #include "libnm-platform/nm-netlink.h"
 #include "libnm-platform/nmp-netns.h"
 #include "libnm-platform/nm-platform-utils.h"
+#include "libnm-platform/nmp-object.h"
 
 #include "libnm-glib-aux/nm-test-utils.h"
 
 /*****************************************************************************/
 
-void
-_nm_logging_clear_platform_logging_cache(void)
-{
-    /* this symbols is required by nm-log-core library. */
-}
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectIP4Address));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectIP4Route));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectIP6Address));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectIP6Route));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLink));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkBond));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkBridge));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkGre));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkInfiniband));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkIp6Tnl));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkIpIp));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkMacsec));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkMacvlan));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkSit));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkTun));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkVlan));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkVrf));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkVti));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkVti6));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkVxlan));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkWireGuard));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectQdisc));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectRoutingRule));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectTfilter));
+
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformIP4Address));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformIP4Route));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformIP6Address));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformIP6Route));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformIPAddress));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformIPAddress));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformIPRoute));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformIPXAddress));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformIPXRoute));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLink));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkBond));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkBridge));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkGre));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkInfiniband));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkIp6Tnl));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkIpIp));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkMacsec));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkMacvlan));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkSit));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkTun));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkVlan));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkVrf));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkVti));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkVti6));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkVxlan));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkWireGuard));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformObjWithIfindex));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformQdisc));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformRoutingRule));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformTfilter));
 
 /*****************************************************************************/
 
@@ -140,6 +191,57 @@ test_nmp_link_mode_all_advertised_modes_bits(void)
 
 /*****************************************************************************/
 
+static void
+test_nmpclass_consistency(void)
+{
+    NMPObjectType obj_type;
+    NMPObjectType obj_type2;
+
+    G_STATIC_ASSERT(G_N_ELEMENTS(_nmp_classes) == NMP_OBJECT_TYPE_MAX);
+
+    for (obj_type = 1; obj_type <= NMP_OBJECT_TYPE_MAX; obj_type++) {
+        const NMPClass *klass = nmp_class_from_type(obj_type);
+        gboolean        is_lnk;
+
+        g_assert(klass);
+        g_assert(klass == &_nmp_classes[obj_type - 1]);
+
+        g_assert_cmpint(klass->obj_type, ==, obj_type);
+        g_assert(klass->obj_type_name);
+
+        g_assert((!!klass->cmd_obj_cmp) == (!!klass->cmd_obj_hash_update));
+        g_assert((!!klass->cmd_plobj_cmp) == (!!klass->cmd_plobj_hash_update));
+        g_assert((!!klass->cmd_plobj_id_cmp) == (!!klass->cmd_plobj_id_hash_update));
+
+        g_assert((!!klass->cmd_obj_cmp) != (!!klass->cmd_plobj_cmp));
+        g_assert((!!klass->cmd_obj_hash_update) != (!!klass->cmd_plobj_hash_update));
+
+        g_assert((!klass->cmd_obj_cmp) || (!klass->cmd_plobj_id_cmp));
+        g_assert((!klass->cmd_obj_hash_update) || (!klass->cmd_plobj_id_hash_update));
+
+        g_assert_cmpint(klass->sizeof_public, >, 0);
+        g_assert_cmpint(klass->sizeof_data, >=, klass->sizeof_public);
+
+        g_assert((!!klass->cmd_obj_to_string) != (!!klass->cmd_plobj_to_string));
+        g_assert(!klass->cmd_plobj_to_string_id || klass->cmd_plobj_to_string);
+
+        is_lnk = (obj_type >= NMP_OBJECT_TYPE_LNK_BRIDGE && obj_type <= NMP_OBJECT_TYPE_LNK_BOND);
+        if (klass->lnk_link_type == NM_LINK_TYPE_NONE) {
+            G_STATIC_ASSERT(NM_LINK_TYPE_NONE == 0);
+            g_assert(!is_lnk);
+        } else
+            g_assert(is_lnk);
+
+        for (obj_type2 = 1; obj_type2 < obj_type; obj_type2++) {
+            const NMPClass *klass2 = nmp_class_from_type(obj_type2);
+
+            g_assert_cmpstr(klass->obj_type_name, !=, klass2->obj_type_name);
+        }
+    }
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE();
 
 int
@@ -150,6 +252,7 @@ main(int argc, char **argv)
     g_test_add_func("/nm-platform/test_use_symbols", test_use_symbols);
     g_test_add_func("/nm-platform/test_nmp_link_mode_all_advertised_modes_bits",
                     test_nmp_link_mode_all_advertised_modes_bits);
+    g_test_add_func("/nm-platform/test_nmpclass_consistency", test_nmpclass_consistency);
 
     return g_test_run();
 }
diff --git a/src/libnm-platform/wifi/nm-wifi-utils-nl80211.h b/src/libnm-platform/wifi/nm-wifi-utils-nl80211.h
index 4783c572..094c0ff0 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils-nl80211.h
+++ b/src/libnm-platform/wifi/nm-wifi-utils-nl80211.h
@@ -12,7 +12,7 @@
 
 #define NM_TYPE_WIFI_UTILS_NL80211 (nm_wifi_utils_nl80211_get_type())
 #define NM_WIFI_UTILS_NL80211(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_UTILS_NL80211, NMWifiUtilsNl80211))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_UTILS_NL80211, NMWifiUtilsNl80211))
 #define NM_WIFI_UTILS_NL80211_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_UTILS_NL80211, NMWifiUtilsNl80211Class))
 #define NM_IS_WIFI_UTILS_NL80211(obj) \
diff --git a/src/libnm-platform/wifi/nm-wifi-utils-wext.c b/src/libnm-platform/wifi/nm-wifi-utils-wext.c
index 678d71fe..eac3c929 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils-wext.c
+++ b/src/libnm-platform/wifi/nm-wifi-utils-wext.c
@@ -90,7 +90,7 @@ dispose(GObject *object)
 {
     NMWifiUtilsWext *wext = NM_WIFI_UTILS_WEXT(object);
 
-    wext->fd = nm_close(wext->fd);
+    nm_clear_fd(&wext->fd);
 }
 
 static gboolean
diff --git a/src/libnm-platform/wifi/nm-wifi-utils-wext.h b/src/libnm-platform/wifi/nm-wifi-utils-wext.h
index d6f3453c..28a74eba 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils-wext.h
+++ b/src/libnm-platform/wifi/nm-wifi-utils-wext.h
@@ -10,7 +10,7 @@
 
 #define NM_TYPE_WIFI_UTILS_WEXT (nm_wifi_utils_wext_get_type())
 #define NM_WIFI_UTILS_WEXT(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_UTILS_WEXT, NMWifiUtilsWext))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_UTILS_WEXT, NMWifiUtilsWext))
 #define NM_WIFI_UTILS_WEXT_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_UTILS_WEXT, NMWifiUtilsWextClass))
 #define NM_IS_WIFI_UTILS_WEXT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WIFI_UTILS_WEXT))
diff --git a/src/libnm-platform/wifi/nm-wifi-utils.h b/src/libnm-platform/wifi/nm-wifi-utils.h
index fd636418..aa5a34cd 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils.h
+++ b/src/libnm-platform/wifi/nm-wifi-utils.h
@@ -13,7 +13,7 @@
 typedef struct NMWifiUtils NMWifiUtils;
 
 #define NM_TYPE_WIFI_UTILS (nm_wifi_utils_get_type())
-#define NM_WIFI_UTILS(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_UTILS, NMWifiUtils))
+#define NM_WIFI_UTILS(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_UTILS, NMWifiUtils))
 #define NM_WIFI_UTILS_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_UTILS, NMWifiUtilsClass))
 #define NM_IS_WIFI_UTILS(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WIFI_UTILS))
diff --git a/src/libnm-platform/wpan/nm-wpan-utils.h b/src/libnm-platform/wpan/nm-wpan-utils.h
index eea5587d..67c5f412 100644
--- a/src/libnm-platform/wpan/nm-wpan-utils.h
+++ b/src/libnm-platform/wpan/nm-wpan-utils.h
@@ -11,7 +11,7 @@
 typedef struct NMWpanUtils NMWpanUtils;
 
 #define NM_TYPE_WPAN_UTILS (nm_wpan_utils_get_type())
-#define NM_WPAN_UTILS(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WPAN_UTILS, NMWpanUtils))
+#define NM_WPAN_UTILS(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WPAN_UTILS, NMWpanUtils))
 #define NM_WPAN_UTILS_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WPAN_UTILS, NMWpanUtilsClass))
 #define NM_IS_WPAN_UTILS(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WPAN_UTILS))