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/nm-linux-platform.c282
-rw-r--r--src/libnm-platform/nm-linux-platform.h2
-rw-r--r--src/libnm-platform/nm-netlink.c65
-rw-r--r--src/libnm-platform/nm-netlink.h52
-rw-r--r--src/libnm-platform/nm-platform-utils.c18
-rw-r--r--src/libnm-platform/nm-platform.c747
-rw-r--r--src/libnm-platform/nm-platform.h99
-rw-r--r--src/libnm-platform/nmp-netns.c20
-rw-r--r--src/libnm-platform/nmp-object.c93
-rw-r--r--src/libnm-platform/nmp-object.h99
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils-wext.c2
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils.c2
12 files changed, 844 insertions, 637 deletions
diff --git a/src/libnm-platform/nm-linux-platform.c b/src/libnm-platform/nm-linux-platform.c
index bcf94200..78df53e7 100644
--- a/src/libnm-platform/nm-linux-platform.c
+++ b/src/libnm-platform/nm-linux-platform.c
@@ -80,7 +80,7 @@ enum { TCA_DEF_UNSPEC, TCA_DEF_TM, TCA_DEF_PARMS, TCA_DEF_DATA, TCA_DEF_PAD, __T
 /*****************************************************************************/
 
 #ifndef IFLA_PROMISCUITY
-    #define IFLA_PROMISCUITY 30
+#define IFLA_PROMISCUITY 30
 #endif
 #define IFLA_NUM_TX_QUEUES 31
 #define IFLA_NUM_RX_QUEUES 32
@@ -114,7 +114,7 @@ enum { TCA_DEF_UNSPEC, TCA_DEF_TM, TCA_DEF_PARMS, TCA_DEF_DATA, TCA_DEF_PAD, __T
 #define IFLA_IPTUN_PMTUDISC    10
 #define __IFLA_IPTUN_MAX       19
 #ifndef IFLA_IPTUN_MAX
-    #define IFLA_IPTUN_MAX (__IFLA_IPTUN_MAX - 1)
+#define IFLA_IPTUN_MAX (__IFLA_IPTUN_MAX - 1)
 #endif
 
 #define IFLA_TUN_UNSPEC              0
@@ -136,7 +136,7 @@ G_STATIC_ASSERT(RTA_MAX == (__RTA_MAX - 1));
 #define RTA_MAX (MAX((__RTA_MAX - 1), RTA_PREF))
 
 #ifndef MACVLAN_FLAG_NOPROMISC
-    #define MACVLAN_FLAG_NOPROMISC 1
+#define MACVLAN_FLAG_NOPROMISC 1
 #endif
 
 #define IP6_FLOWINFO_TCLASS_MASK    0x0FF00000
@@ -145,11 +145,13 @@ G_STATIC_ASSERT(RTA_MAX == (__RTA_MAX - 1));
 
 #define IFLA_BR_VLAN_STATS_ENABLED 41
 
+#define IFLA_PERM_ADDRESS 54
+
 /*****************************************************************************/
 
 /* Appeared in the kernel prior to 3.13 dated 19 January, 2014 */
 #ifndef ARPHRD_6LOWPAN
-    #define ARPHRD_6LOWPAN 825
+#define ARPHRD_6LOWPAN 825
 #endif
 
 /*****************************************************************************/
@@ -267,8 +269,8 @@ struct _ifla_vf_vlan_info {
 
 /* Appeared in the kernel 4.0 dated April 12, 2015 */
 #ifndef BRIDGE_VLAN_INFO_RANGE_BEGIN
-    #define BRIDGE_VLAN_INFO_RANGE_BEGIN (1 << 3) /* VLAN is start of vlan range */
-    #define BRIDGE_VLAN_INFO_RANGE_END   (1 << 4) /* VLAN is end of vlan range */
+#define BRIDGE_VLAN_INFO_RANGE_BEGIN (1 << 3) /* VLAN is start of vlan range */
+#define BRIDGE_VLAN_INFO_RANGE_END   (1 << 4) /* VLAN is end of vlan range */
 #endif
 
 /*****************************************************************************/
@@ -582,25 +584,25 @@ wait_for_nl_response_to_string(WaitForNlResponseResult seq_result,
 
     switch (seq_result) {
     case WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN:
-        nm_utils_strbuf_append_str(&buf, &buf_size, "unknown");
+        nm_strbuf_append_str(&buf, &buf_size, "unknown");
         break;
     case WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK:
-        nm_utils_strbuf_append_str(&buf, &buf_size, "success");
+        nm_strbuf_append_str(&buf, &buf_size, "success");
         break;
     case WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_UNKNOWN:
-        nm_utils_strbuf_append_str(&buf, &buf_size, "failure");
+        nm_strbuf_append_str(&buf, &buf_size, "failure");
         break;
     default:
         if (seq_result < 0) {
-            nm_utils_strbuf_append(&buf,
-                                   &buf_size,
-                                   "failure %d (%s%s%s)",
-                                   -((int) seq_result),
-                                   nm_strerror_native(-((int) seq_result)),
-                                   errmsg ? " - " : "",
-                                   errmsg ?: "");
+            nm_strbuf_append(&buf,
+                             &buf_size,
+                             "failure %d (%s%s%s)",
+                             -((int) seq_result),
+                             nm_strerror_native(-((int) seq_result)),
+                             errmsg ? " - " : "",
+                             errmsg ?: "");
         } else
-            nm_utils_strbuf_append(&buf, &buf_size, "internal failure %d", (int) seq_result);
+            nm_strbuf_append(&buf, &buf_size, "internal failure %d", (int) seq_result);
         break;
     }
     return buf0;
@@ -1277,15 +1279,6 @@ _parse_af_inet6(NMPlatform *        platform,
         token_valid = TRUE;
     }
 
-    /* Hack to detect support addrgenmode of the kernel. We only parse
-     * netlink messages that we receive from kernel, hence this check
-     * is valid. */
-    if (!_nm_platform_kernel_support_detected(NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL)) {
-        /* IFLA_INET6_ADDR_GEN_MODE was added in kernel 3.17, dated 5 October, 2014. */
-        _nm_platform_kernel_support_init(NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL,
-                                         tb[IFLA_INET6_ADDR_GEN_MODE] ? 1 : -1);
-    }
-
     if (tb[IFLA_INET6_ADDR_GEN_MODE]) {
         i6_addr_gen_mode_inv = _nm_platform_uint8_inv(nla_get_u8(tb[IFLA_INET6_ADDR_GEN_MODE]));
         if (i6_addr_gen_mode_inv == 0) {
@@ -2305,7 +2298,7 @@ _wireguard_get_device_cb(struct nl_msg *msg, void *arg)
 {
     static const struct nla_policy policy[] = {
         [WGDEVICE_A_IFINDEX]     = {.type = NLA_U32},
-        [WGDEVICE_A_IFNAME]      = {.type = NLA_NUL_STRING, .maxlen = IFNAMSIZ},
+        [WGDEVICE_A_IFNAME]      = {.type = NLA_STRING, .maxlen = IFNAMSIZ},
         [WGDEVICE_A_PRIVATE_KEY] = {},
         [WGDEVICE_A_PUBLIC_KEY]  = {},
         [WGDEVICE_A_FLAGS]       = {.type = NLA_U32},
@@ -2935,6 +2928,7 @@ _new_from_nl_link(NMPlatform *     platform,
         [IFLA_NET_NS_PID]    = {.type = NLA_U32},
         [IFLA_NET_NS_FD]     = {.type = NLA_U32},
         [IFLA_LINK_NETNSID]  = {},
+        [IFLA_PERM_ADDRESS]  = {.type = NLA_UNSPEC},
     };
     const struct ifinfomsg *ifi;
     struct nlattr *         tb[G_N_ELEMENTS(policy)];
@@ -2945,12 +2939,13 @@ _new_from_nl_link(NMPlatform *     platform,
     gboolean *                completed_from_cache     = cache ? &completed_from_cache_val : NULL;
     const NMPObject *         link_cached              = NULL;
     const NMPObject *         lnk_data                 = NULL;
-    gboolean                  address_complete_from_cache   = TRUE;
-    gboolean                  broadcast_complete_from_cache = TRUE;
-    gboolean                  lnk_data_complete_from_cache  = TRUE;
-    gboolean                  need_ext_data                 = FALSE;
-    gboolean                  af_inet6_token_valid          = FALSE;
-    gboolean                  af_inet6_addr_gen_mode_valid  = FALSE;
+    gboolean                  address_complete_from_cache      = TRUE;
+    gboolean                  perm_address_complete_from_cache = TRUE;
+    gboolean                  broadcast_complete_from_cache    = TRUE;
+    gboolean                  lnk_data_complete_from_cache     = TRUE;
+    gboolean                  need_ext_data                    = FALSE;
+    gboolean                  af_inet6_token_valid             = FALSE;
+    gboolean                  af_inet6_addr_gen_mode_valid     = FALSE;
 
     if (!nlmsg_valid_hdr(nlh, sizeof(*ifi)))
         return NULL;
@@ -3053,6 +3048,20 @@ _new_from_nl_link(NMPlatform *     platform,
         address_complete_from_cache = FALSE;
     }
 
+    if (tb[IFLA_PERM_ADDRESS]) {
+        if (!_nm_platform_kernel_support_detected(
+                NM_PLATFORM_KERNEL_SUPPORT_TYPE_IFLA_PERM_ADDRESS)) {
+            /* support for IFLA_PERM_ADDRESS was added in f74877a5457d34d604dba6dbbb13c4c05bac8b93,
+             * kernel 5.6, 30 March 2020.
+             *
+             * We can only detect support if the attribute is present. A missing attribute
+             * is not conclusive. */
+            _nm_platform_kernel_support_init(NM_PLATFORM_KERNEL_SUPPORT_TYPE_IFLA_PERM_ADDRESS, 1);
+        }
+        _nmp_link_address_set(&obj->link.l_perm_address, tb[IFLA_PERM_ADDRESS]);
+        perm_address_complete_from_cache = FALSE;
+    }
+
     if (tb[IFLA_BROADCAST]) {
         _nmp_link_address_set(&obj->link.l_broadcast, tb[IFLA_BROADCAST]);
         broadcast_complete_from_cache = FALSE;
@@ -3135,8 +3144,8 @@ _new_from_nl_link(NMPlatform *     platform,
 
     if (completed_from_cache
         && (lnk_data_complete_from_cache || need_ext_data || address_complete_from_cache
-            || broadcast_complete_from_cache || !af_inet6_token_valid
-            || !af_inet6_addr_gen_mode_valid || !tb[IFLA_STATS64])) {
+            || perm_address_complete_from_cache || broadcast_complete_from_cache
+            || !af_inet6_token_valid || !af_inet6_addr_gen_mode_valid || !tb[IFLA_STATS64])) {
         _lookup_cached_link(cache, obj->link.ifindex, completed_from_cache, &link_cached);
         if (link_cached && link_cached->_link.netlink.is_in_netlink) {
             if (lnk_data_complete_from_cache && link_cached->link.type == obj->link.type
@@ -3160,6 +3169,8 @@ _new_from_nl_link(NMPlatform *     platform,
 
             if (address_complete_from_cache)
                 obj->link.l_address = link_cached->link.l_address;
+            if (perm_address_complete_from_cache)
+                obj->link.l_perm_address = link_cached->link.l_perm_address;
             if (broadcast_complete_from_cache)
                 obj->link.l_broadcast = link_cached->link.l_broadcast;
             if (!af_inet6_token_valid)
@@ -3572,13 +3583,6 @@ rta_multipath_done:;
     obj->ip_route.lock_mtu      = NM_FLAGS_HAS(lock, 1 << RTAX_MTU);
 
     if (!is_v4) {
-        if (!_nm_platform_kernel_support_detected(NM_PLATFORM_KERNEL_SUPPORT_TYPE_RTA_PREF)) {
-            /* Detect support for RTA_PREF by inspecting the netlink message.
-             * RTA_PREF was added in kernel 4.1, dated 21 June, 2015. */
-            _nm_platform_kernel_support_init(NM_PLATFORM_KERNEL_SUPPORT_TYPE_RTA_PREF,
-                                             tb[RTA_PREF] ? 1 : -1);
-        }
-
         if (tb[RTA_PREF])
             obj->ip6_route.rt_pref = nla_get_u8(tb[RTA_PREF]);
     }
@@ -3876,6 +3880,9 @@ _new_from_nl_qdisc(NMPlatform *platform, struct nlmsghdr *nlh, gboolean id_only)
     const struct tcmsg *tcm;
     nm_auto_nmpobj NMPObject *obj = NULL;
 
+    if (!nm_platform_get_cache_tc(platform))
+        return NULL;
+
     if (nlmsg_parse_arr(nlh, sizeof(*tcm), tb, policy) < 0)
         return NULL;
 
@@ -3976,7 +3983,7 @@ _new_from_nl_qdisc(NMPlatform *platform, struct nlmsghdr *nlh, gboolean id_only)
 }
 
 static NMPObject *
-_new_from_nl_tfilter(struct nlmsghdr *nlh, gboolean id_only)
+_new_from_nl_tfilter(NMPlatform *platform, struct nlmsghdr *nlh, gboolean id_only)
 {
     static const struct nla_policy policy[] = {
         [TCA_KIND] = {.type = NLA_STRING},
@@ -3985,6 +3992,9 @@ _new_from_nl_tfilter(struct nlmsghdr *nlh, gboolean id_only)
     NMPObject *         obj = NULL;
     const struct tcmsg *tcm;
 
+    if (!nm_platform_get_cache_tc(platform))
+        return NULL;
+
     if (nlmsg_parse_arr(nlh, sizeof(*tcm), tb, policy) < 0)
         return NULL;
 
@@ -4055,7 +4065,7 @@ nmp_object_new_from_nl(NMPlatform *    platform,
     case RTM_NEWTFILTER:
     case RTM_DELTFILTER:
     case RTM_GETTFILTER:
-        return _new_from_nl_tfilter(msghdr, id_only);
+        return _new_from_nl_tfilter(platform, msghdr, id_only);
     default:
         return NULL;
     }
@@ -4064,7 +4074,7 @@ nmp_object_new_from_nl(NMPlatform *    platform,
 /*****************************************************************************/
 
 static gboolean
-_nl_msg_new_link_set_afspec(struct nl_msg *msg, int addr_gen_mode, NMUtilsIPv6IfaceId *iid)
+_nl_msg_new_link_set_afspec(struct nl_msg *msg, int addr_gen_mode, const NMUtilsIPv6IfaceId *iid)
 {
     struct nlattr *af_spec;
     struct nlattr *af_attr;
@@ -4082,11 +4092,9 @@ _nl_msg_new_link_set_afspec(struct nl_msg *msg, int addr_gen_mode, NMUtilsIPv6If
             NLA_PUT_U8(msg, IFLA_INET6_ADDR_GEN_MODE, addr_gen_mode);
 
         if (iid) {
-            struct in6_addr i6_token = {.s6_addr = {
-                                            0,
-                                        }};
+            struct in6_addr i6_token = IN6ADDR_ANY_INIT;
 
-            nm_utils_ipv6_addr_set_interface_identifier(&i6_token, *iid);
+            nm_utils_ipv6_addr_set_interface_identifier(&i6_token, iid);
             NLA_PUT(msg, IFLA_INET6_TOKEN, sizeof(struct in6_addr), &i6_token);
         }
 
@@ -5811,13 +5819,13 @@ delayed_action_to_string_full(DelayedActionType action_type,
     char *                                    buf0 = buf;
     const DelayedActionWaitForNlResponseData *data;
 
-    nm_utils_strbuf_append_str(&buf, &buf_size, delayed_action_to_string(action_type));
+    nm_strbuf_append_str(&buf, &buf_size, delayed_action_to_string(action_type));
     switch (action_type) {
     case DELAYED_ACTION_TYPE_MASTER_CONNECTED:
-        nm_utils_strbuf_append(&buf, &buf_size, " (master-ifindex %d)", GPOINTER_TO_INT(user_data));
+        nm_strbuf_append(&buf, &buf_size, " (master-ifindex %d)", GPOINTER_TO_INT(user_data));
         break;
     case DELAYED_ACTION_TYPE_REFRESH_LINK:
-        nm_utils_strbuf_append(&buf, &buf_size, " (ifindex %d)", GPOINTER_TO_INT(user_data));
+        nm_strbuf_append(&buf, &buf_size, " (ifindex %d)", GPOINTER_TO_INT(user_data));
         break;
     case DELAYED_ACTION_TYPE_WAIT_FOR_NL_RESPONSE:
         data = user_data;
@@ -5826,7 +5834,7 @@ delayed_action_to_string_full(DelayedActionType action_type,
             gint64 timeout = data->timeout_abs_ns - nm_utils_get_monotonic_timestamp_nsec();
             char   b[255];
 
-            nm_utils_strbuf_append(
+            nm_strbuf_append(
                 &buf,
                 &buf_size,
                 " (seq %u, timeout in %s%" G_GINT64_FORMAT ".%09" G_GINT64_FORMAT
@@ -5841,7 +5849,7 @@ delayed_action_to_string_full(DelayedActionType action_type,
                     ? wait_for_nl_response_to_string(data->seq_result, NULL, b, sizeof(b))
                     : "");
         } else
-            nm_utils_strbuf_append_str(&buf, &buf_size, " (any)");
+            nm_strbuf_append_str(&buf, &buf_size, " (any)");
         break;
     default:
         nm_assert(!user_data);
@@ -5934,7 +5942,7 @@ delayed_action_wait_for_nl_response_complete_check(NMPlatform *            platf
     guint                   i;
     guint32                 next_seq_number     = 0;
     gint64                  next_timeout_abs_ns = 0;
-    int                     now_ns              = 0;
+    gint64                  now_ns              = 0;
 
     for (i = 0; i < priv->delayed_action.list_wait_for_nl_response->len;) {
         const DelayedActionWaitForNlResponseData *data =
@@ -6904,27 +6912,6 @@ event_valid_msg(NMPlatform *platform, struct nl_msg *msg, gboolean handle_events
 
     msghdr = nlmsg_hdr(msg);
 
-    if (!_nm_platform_kernel_support_detected(NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS)
-        && msghdr->nlmsg_type == RTM_NEWADDR) {
-        /* IFA_FLAGS is set for IPv4 and IPv6 addresses. It was added first to IPv6,
-         * but if we encounter an IPv4 address with IFA_FLAGS, we surely have support. */
-        if (nlmsg_valid_hdr(msghdr, sizeof(struct ifaddrmsg))
-            && NM_IN_SET(((struct ifaddrmsg *) nlmsg_data(msghdr))->ifa_family,
-                         AF_INET,
-                         AF_INET6)) {
-            /* see if the nl_msg contains the IFA_FLAGS attribute. If it does,
-             * we assume, that the kernel supports extended flags, IFA_F_MANAGETEMPADDR
-             * and IFA_F_NOPREFIXROUTE for IPv6. They were added together in kernel 3.14,
-             * dated 30 March, 2014.
-             *
-             * For IPv4, IFA_F_NOPREFIXROUTE was added later, but there is no easy
-             * way to detect kernel support. */
-            _nm_platform_kernel_support_init(
-                NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS,
-                !!nlmsg_find_attr(msghdr, sizeof(struct ifaddrmsg), IFA_FLAGS) ? 1 : -1);
-        }
-    }
-
     if (!handle_events)
         return;
 
@@ -7487,19 +7474,14 @@ link_change_flags(NMPlatform *platform, int ifindex, unsigned flags_mask, unsign
 }
 
 static int
-link_set_user_ipv6ll_enabled(NMPlatform *platform, int ifindex, gboolean enabled)
+link_set_inet6_addr_gen_mode(NMPlatform *platform, int ifindex, guint8 mode)
 {
     nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
-    guint8 mode = enabled ? NM_IN6_ADDR_GEN_MODE_NONE : NM_IN6_ADDR_GEN_MODE_EUI64;
+    char                         sbuf[100];
 
     _LOGD("link: change %d: user-ipv6ll: set IPv6 address generation mode to %s",
           ifindex,
-          nm_platform_link_inet6_addrgenmode2str(mode, NULL, 0));
-
-    if (!nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL)) {
-        _LOGD("link: change %d: user-ipv6ll: not supported", ifindex);
-        return -NME_PL_OPNOTSUPP;
-    }
+          nm_platform_link_inet6_addrgenmode2str(mode, sbuf, sizeof(sbuf)));
 
     nlmsg = _nl_msg_new_link(RTM_NEWLINK, 0, ifindex, NULL);
     if (!nlmsg || !_nl_msg_new_link_set_afspec(nlmsg, mode, NULL))
@@ -7509,7 +7491,7 @@ link_set_user_ipv6ll_enabled(NMPlatform *platform, int ifindex, gboolean enabled
 }
 
 static gboolean
-link_set_token(NMPlatform *platform, int ifindex, NMUtilsIPv6IfaceId iid)
+link_set_token(NMPlatform *platform, int ifindex, const NMUtilsIPv6IfaceId *iid)
 {
     nm_auto_nlmsg struct nl_msg *nlmsg = NULL;
     char                         sbuf[NM_UTILS_INET_ADDRSTRLEN];
@@ -7519,7 +7501,7 @@ link_set_token(NMPlatform *platform, int ifindex, NMUtilsIPv6IfaceId iid)
           nm_utils_inet6_interface_identifier_to_token(iid, sbuf));
 
     nlmsg = _nl_msg_new_link(RTM_NEWLINK, 0, ifindex, NULL);
-    if (!nlmsg || !_nl_msg_new_link_set_afspec(nlmsg, -1, &iid))
+    if (!nlmsg || !_nl_msg_new_link_set_afspec(nlmsg, -1, iid))
         g_return_val_if_reached(FALSE);
 
     return (do_change_link(platform, CHANGE_LINK_TYPE_UNSPEC, ifindex, nlmsg, NULL) >= 0);
@@ -7625,14 +7607,21 @@ nla_put_failure:
 }
 
 static gboolean
-link_get_permanent_address(NMPlatform *platform, int ifindex, guint8 *buf, size_t *length)
+link_get_permanent_address_ethtool(NMPlatform *platform, int ifindex, NMPLinkAddress *out_address)
 {
     nm_auto_pop_netns NMPNetns *netns = NULL;
+    guint8                      buffer[_NM_UTILS_HWADDR_LEN_MAX];
+    gsize                       len;
 
     if (!nm_platform_netns_push(platform, &netns))
         return FALSE;
 
-    return nmp_utils_ethtool_get_permanent_address(ifindex, buf, length);
+    if (!nmp_utils_ethtool_get_permanent_address(ifindex, buffer, &len))
+        return FALSE;
+    nm_assert(len <= _NM_UTILS_HWADDR_LEN_MAX);
+    memcpy(out_address->data, buffer, len);
+    out_address->len = len;
+    return TRUE;
 }
 
 static int
@@ -8867,7 +8856,79 @@ qdisc_add(NMPlatform *platform, NMPNlmFlags flags, const NMPlatformQdisc *qdisc)
     return -NME_UNSPEC;
 }
 
-/*****************************************************************************/
+static int
+tc_delete(NMPlatform *platform, int nlmsgtype, int ifindex, guint32 parent, gboolean log_error)
+{
+    WaitForNlResponseResult      seq_result = WAIT_FOR_NL_RESPONSE_RESULT_UNKNOWN;
+    gs_free char *               errmsg     = NULL;
+    int                          nle;
+    char                         s_buf[256];
+    const char *                 log_tag;
+    nm_auto_nlmsg struct nl_msg *msg = NULL;
+    const struct tcmsg           tcm = {
+        .tcm_ifindex = ifindex,
+        .tcm_parent  = parent,
+    };
+
+    switch (nlmsgtype) {
+    case RTM_DELQDISC:
+        log_tag = "do-delete-qdisc";
+        break;
+    case RTM_DELTFILTER:
+        log_tag = "do-delete-tfilter";
+        break;
+    default:
+        nm_assert_not_reached();
+        log_tag = "do-delete-tc";
+    }
+
+    msg = nlmsg_alloc_simple(nlmsgtype, NMP_NLM_FLAG_F_ECHO);
+
+    if (nlmsg_append_struct(msg, &tcm) < 0)
+        goto nla_put_failure;
+
+    event_handler_read_netlink(platform, FALSE);
+
+    nle = _nl_send_nlmsg(platform,
+                         msg,
+                         &seq_result,
+                         &errmsg,
+                         DELAYED_ACTION_RESPONSE_TYPE_VOID,
+                         NULL);
+    if (nle < 0) {
+        _NMLOG(log_error ? LOGL_ERR : LOGL_DEBUG,
+               "%s: failed sending netlink request \"%s\" (%d)",
+               log_tag,
+               nm_strerror(nle),
+               -nle);
+        return -NME_PL_NETLINK;
+    }
+
+    delayed_action_handle_all(platform, FALSE);
+
+    nm_assert(seq_result);
+
+    _NMLOG((seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK || !log_error) ? LOGL_DEBUG
+                                                                                 : LOGL_WARN,
+           "%s: %s",
+           log_tag,
+           wait_for_nl_response_to_string(seq_result, errmsg, s_buf, sizeof(s_buf)));
+
+    if (seq_result == WAIT_FOR_NL_RESPONSE_RESULT_RESPONSE_OK)
+        return 0;
+    if (seq_result < 0)
+        return seq_result;
+    return -NME_UNSPEC;
+
+nla_put_failure:
+    g_return_val_if_reached(-NME_UNSPEC);
+}
+
+static int
+qdisc_delete(NMPlatform *platform, int ifindex, guint32 parent, gboolean log_error)
+{
+    return tc_delete(platform, RTM_DELQDISC, ifindex, parent, log_error);
+}
 
 static int
 tfilter_add(NMPlatform *platform, NMPNlmFlags flags, const NMPlatformTfilter *tfilter)
@@ -8910,6 +8971,12 @@ tfilter_add(NMPlatform *platform, NMPNlmFlags flags, const NMPlatformTfilter *tf
     return -NME_UNSPEC;
 }
 
+static int
+tfilter_delete(NMPlatform *platform, int ifindex, guint32 parent, gboolean log_error)
+{
+    return tc_delete(platform, RTM_DELTFILTER, ifindex, parent, log_error);
+}
+
 /*****************************************************************************/
 
 static gboolean
@@ -9405,7 +9472,7 @@ constructed(GObject *_object)
         priv->udev_client = nm_udev_client_new(NM_MAKE_STRV("net"), handle_udev_event, platform);
     }
 
-    _LOGD("create (%s netns, %s, %s udev)",
+    _LOGD("create (%s netns, %s, %s udev, %s tc-cache)",
           !platform->_netns ? "ignore" : "use",
           !platform->_netns && nmp_netns_is_initial()
               ? "initial netns"
@@ -9416,7 +9483,8 @@ constructed(GObject *_object)
                                        nmp_netns_get_current(),
                                        nmp_netns_get_current() == nmp_netns_get_initial() ? "/main"
                                                                                           : "")),
-          nm_platform_get_use_udev(platform) ? "use" : "no");
+          nm_platform_get_use_udev(platform) ? "use" : "no",
+          nm_platform_get_cache_tc(platform) ? "use" : "no");
 
     priv->genl = nl_socket_alloc();
     g_assert(priv->genl);
@@ -9462,10 +9530,14 @@ constructed(GObject *_object)
                                     RTNLGRP_IPV6_IFADDR,
                                     RTNLGRP_IPV6_ROUTE,
                                     RTNLGRP_LINK,
-                                    RTNLGRP_TC,
                                     0);
     g_assert(!nle);
 
+    if (nm_platform_get_cache_tc(platform)) {
+        nle = nl_socket_add_memberships(priv->nlh, RTNLGRP_TC, 0);
+        nm_assert(!nle);
+    }
+
     fd = nl_socket_get_fd(priv->nlh);
 
     _LOGD("Netlink socket for events established: port=%u, fd=%d",
@@ -9549,7 +9621,7 @@ path_is_read_only_fs(const char *path)
 }
 
 NMPlatform *
-nm_linux_platform_new(gboolean log_with_ptr, gboolean netns_support)
+nm_linux_platform_new(gboolean log_with_ptr, gboolean netns_support, gboolean cache_tc)
 {
     gboolean use_udev = FALSE;
 
@@ -9563,6 +9635,8 @@ nm_linux_platform_new(gboolean log_with_ptr, gboolean netns_support)
                         use_udev,
                         NM_PLATFORM_NETNS_SUPPORT,
                         netns_support,
+                        NM_PLATFORM_CACHE_TC,
+                        cache_tc,
                         NULL);
 }
 
@@ -9639,16 +9713,16 @@ nm_linux_platform_class_init(NMLinuxPlatformClass *klass)
 
     platform_class->link_change_flags = link_change_flags;
 
-    platform_class->link_set_user_ipv6ll_enabled = link_set_user_ipv6ll_enabled;
+    platform_class->link_set_inet6_addr_gen_mode = link_set_inet6_addr_gen_mode;
     platform_class->link_set_token               = link_set_token;
 
-    platform_class->link_set_address            = link_set_address;
-    platform_class->link_get_permanent_address  = link_get_permanent_address;
-    platform_class->link_set_mtu                = link_set_mtu;
-    platform_class->link_set_name               = link_set_name;
-    platform_class->link_set_sriov_params_async = link_set_sriov_params_async;
-    platform_class->link_set_sriov_vfs          = link_set_sriov_vfs;
-    platform_class->link_set_bridge_vlans       = link_set_bridge_vlans;
+    platform_class->link_set_address                   = link_set_address;
+    platform_class->link_get_permanent_address_ethtool = link_get_permanent_address_ethtool;
+    platform_class->link_set_mtu                       = link_set_mtu;
+    platform_class->link_set_name                      = link_set_name;
+    platform_class->link_set_sriov_params_async        = link_set_sriov_params_async;
+    platform_class->link_set_sriov_vfs                 = link_set_sriov_vfs;
+    platform_class->link_set_bridge_vlans              = link_set_bridge_vlans;
 
     platform_class->link_get_physical_port_id = link_get_physical_port_id;
     platform_class->link_get_dev_id           = link_get_dev_id;
@@ -9704,8 +9778,10 @@ nm_linux_platform_class_init(NMLinuxPlatformClass *klass)
 
     platform_class->routing_rule_add = routing_rule_add;
 
-    platform_class->qdisc_add   = qdisc_add;
-    platform_class->tfilter_add = tfilter_add;
+    platform_class->qdisc_add      = qdisc_add;
+    platform_class->qdisc_delete   = qdisc_delete;
+    platform_class->tfilter_add    = tfilter_add;
+    platform_class->tfilter_delete = tfilter_delete;
 
     platform_class->process_events = process_events;
 }
diff --git a/src/libnm-platform/nm-linux-platform.h b/src/libnm-platform/nm-linux-platform.h
index 26c31e3e..85e06934 100644
--- a/src/libnm-platform/nm-linux-platform.h
+++ b/src/libnm-platform/nm-linux-platform.h
@@ -23,6 +23,6 @@ typedef struct _NMLinuxPlatformClass NMLinuxPlatformClass;
 
 GType nm_linux_platform_get_type(void);
 
-NMPlatform *nm_linux_platform_new(gboolean log_with_ptr, gboolean netns_support);
+NMPlatform *nm_linux_platform_new(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 e92c4dfe..56cde50c 100644
--- a/src/libnm-platform/nm-netlink.c
+++ b/src/libnm-platform/nm-netlink.c
@@ -13,7 +13,7 @@
 /*****************************************************************************/
 
 #ifndef SOL_NETLINK
-    #define SOL_NETLINK 270
+#define SOL_NETLINK 270
 #endif
 
 /*****************************************************************************/
@@ -24,7 +24,7 @@
 #define NL_NO_AUTO_ACK       (1 << 5)
 
 #ifndef NETLINK_EXT_ACK
-    #define NETLINK_EXT_ACK 11
+#define NETLINK_EXT_ACK 11
 #endif
 
 struct nl_msg {
@@ -162,28 +162,28 @@ nl_nlmsghdr_to_str(const struct nlmsghdr *hdr, char *buf, gsize len)
     }
 
     if (s)
-        nm_utils_strbuf_append_str(&buf, &len, s);
+        nm_strbuf_append_str(&buf, &len, s);
     else
-        nm_utils_strbuf_append(&buf, &len, "(%u)", (unsigned) hdr->nlmsg_type);
+        nm_strbuf_append(&buf, &len, "(%u)", (unsigned) hdr->nlmsg_type);
 
     flags = hdr->nlmsg_flags;
 
     if (!flags) {
-        nm_utils_strbuf_append_str(&buf, &len, ", flags 0");
+        nm_strbuf_append_str(&buf, &len, ", flags 0");
         goto flags_done;
     }
 
-#define _F(f, n)                                                   \
-    G_STMT_START                                                   \
-    {                                                              \
-        if (NM_FLAGS_ALL(flags, f)) {                              \
-            flags &= ~(f);                                         \
-            nm_utils_strbuf_append(&buf, &len, "%s%s", prefix, n); \
-            if (!flags)                                            \
-                goto flags_done;                                   \
-            prefix = ",";                                          \
-        }                                                          \
-    }                                                              \
+#define _F(f, n)                                             \
+    G_STMT_START                                             \
+    {                                                        \
+        if (NM_FLAGS_ALL(flags, f)) {                        \
+            flags &= ~(f);                                   \
+            nm_strbuf_append(&buf, &len, "%s%s", prefix, n); \
+            if (!flags)                                      \
+                goto flags_done;                             \
+            prefix = ",";                                    \
+        }                                                    \
+    }                                                        \
     G_STMT_END
 
     prefix       = ", flags ";
@@ -225,11 +225,11 @@ nl_nlmsghdr_to_str(const struct nlmsghdr *hdr, char *buf, gsize len)
 
     if (flags_before != flags)
         prefix = ";";
-    nm_utils_strbuf_append(&buf, &len, "%s0x%04x", prefix, flags);
+    nm_strbuf_append(&buf, &len, "%s0x%04x", prefix, flags);
 
 flags_done:
 
-    nm_utils_strbuf_append(&buf, &len, ", seq %u", (unsigned) hdr->nlmsg_seq);
+    nm_strbuf_append(&buf, &len, ", seq %u", (unsigned) hdr->nlmsg_seq);
 
     return b;
 }
@@ -585,21 +585,21 @@ nla_nest_end(struct nl_msg *msg, struct nlattr *start)
     return _nest_end(msg, start, 0);
 }
 
-static const uint16_t nla_attr_minlen[NLA_TYPE_MAX + 1] = {
+static const uint8_t nla_attr_minlen[NLA_TYPE_MAX + 1] = {
     [NLA_U8]     = sizeof(uint8_t),
     [NLA_U16]    = sizeof(uint16_t),
     [NLA_U32]    = sizeof(uint32_t),
     [NLA_U64]    = sizeof(uint64_t),
     [NLA_STRING] = 1,
-    [NLA_FLAG]   = 0,
 };
 
 static int
 validate_nla(const struct nlattr *nla, int maxtype, const struct nla_policy *policy)
 {
     const struct nla_policy *pt;
-    unsigned int             minlen = 0;
-    int                      type   = nla_type(nla);
+    uint8_t                  minlen;
+    uint16_t                 len;
+    int                      type = nla_type(nla);
 
     if (type < 0 || type > maxtype)
         return 0;
@@ -609,25 +609,30 @@ validate_nla(const struct nlattr *nla, int maxtype, const struct nla_policy *pol
     if (pt->type > NLA_TYPE_MAX)
         g_return_val_if_reached(-NME_BUG);
 
-    if (pt->minlen)
+    if (pt->minlen > 0)
         minlen = pt->minlen;
-    else if (pt->type != NLA_UNSPEC)
+    else
         minlen = nla_attr_minlen[pt->type];
 
-    if (nla_len(nla) < minlen)
+    len = nla_len(nla);
+
+    if (len < minlen)
         return -NME_UNSPEC;
 
-    if (pt->maxlen && nla_len(nla) > pt->maxlen)
+    if (pt->maxlen > 0 && len > pt->maxlen)
         return -NME_UNSPEC;
 
-    if (pt->type == NLA_STRING) {
-        const char *data;
+    switch (pt->type) {
+    case NLA_STRING:
+    {
+        const char *data = nla_data(nla);
 
         nm_assert(minlen > 0);
 
-        data = nla_data(nla);
-        if (data[nla_len(nla) - 1] != '\0')
+        if (data[len - 1u] != '\0')
             return -NME_UNSPEC;
+        break;
+    }
     }
 
     return 0;
diff --git a/src/libnm-platform/nm-netlink.h b/src/libnm-platform/nm-netlink.h
index ab355f74..00d817cd 100644
--- a/src/libnm-platform/nm-netlink.h
+++ b/src/libnm-platform/nm-netlink.h
@@ -21,7 +21,7 @@
 #define NLMSGERR_ATTR_MAX    3
 
 #ifndef NLM_F_ACK_TLVS
-    #define NLM_F_ACK_TLVS 0x200
+#define NLM_F_ACK_TLVS 0x200
 #endif
 
 /*****************************************************************************/
@@ -35,15 +35,7 @@ enum {
     NLA_U64,    /* 64 bit integer */
     NLA_STRING, /* NUL terminated character string */
     NLA_FLAG,   /* Flag */
-    NLA_MSECS,  /* Micro seconds (64bit) */
     NLA_NESTED, /* Nested attributes */
-    NLA_NESTED_COMPAT,
-    NLA_NUL_STRING,
-    NLA_BINARY,
-    NLA_S8,
-    NLA_S16,
-    NLA_S32,
-    NLA_S64,
     __NLA_TYPE_MAX,
 };
 
@@ -63,10 +55,10 @@ const char *nl_nlmsghdr_to_str(const struct nlmsghdr *hdr, char *buf, gsize len)
 
 struct nla_policy {
     /* Type of attribute or NLA_UNSPEC */
-    uint16_t type;
+    uint8_t type;
 
     /* Minimal length of payload required */
-    uint16_t minlen;
+    uint8_t minlen;
 
     /* Maximal length of payload allowed */
     uint16_t maxlen;
@@ -75,18 +67,24 @@ struct nla_policy {
 /*****************************************************************************/
 
 /* static asserts that @tb and @policy are suitable arguments to nla_parse(). */
-#define _nl_static_assert_tb(tb, policy)                                                      \
-    G_STMT_START                                                                              \
-    {                                                                                         \
-        G_STATIC_ASSERT_EXPR(G_N_ELEMENTS(tb) > 0);                                           \
-                                                                                              \
+#if _NM_CC_SUPPORT_GENERIC
+#define _nl_static_assert_tb(tb, policy)                                                   \
+    G_STMT_START                                                                           \
+    {                                                                                      \
+        G_STATIC_ASSERT_EXPR(G_N_ELEMENTS(tb) > 0);                                        \
+                                                                                           \
         /* We allow @policy to be either a C array or NULL. The sizeof()
-         * must either match the expected array size or the sizeof(NULL),
-         * but not both. */                      \
-        G_STATIC_ASSERT_EXPR((sizeof(policy) == G_N_ELEMENTS(tb) * sizeof(struct nla_policy)) \
-                             ^ (sizeof(policy) == sizeof(NULL)));                             \
-    }                                                                                         \
+         * must either match the expected array size or we check that
+         * "policy" has typeof(NULL). This isn't a perfect compile time check,
+         * but good enough. */                   \
+        G_STATIC_ASSERT_EXPR(                                                              \
+            _Generic((policy), typeof(NULL) : 1, default                                   \
+                     : (sizeof(policy) == G_N_ELEMENTS(tb) * sizeof(struct nla_policy)))); \
+    }                                                                                      \
     G_STMT_END
+#else
+#define _nl_static_assert_tb(tb, policy) G_STATIC_ASSERT_EXPR(G_N_ELEMENTS(tb) > 0)
+#endif
 
 /*****************************************************************************/
 
@@ -112,28 +110,24 @@ nla_padlen(int payload)
 
 struct nlattr *nla_reserve(struct nl_msg *msg, int attrtype, int attrlen);
 
-static inline int
+static inline uint16_t
 nla_len(const struct nlattr *nla)
 {
     nm_assert(nla);
     nm_assert(nla->nla_len >= NLA_HDRLEN);
 
-    return ((int) nla->nla_len) - NLA_HDRLEN;
+    return nla->nla_len - ((uint16_t) NLA_HDRLEN);
 }
 
 static inline int
 nla_type(const struct nlattr *nla)
 {
-    nm_assert(nla_len(nla) >= 0);
-
     return nla->nla_type & NLA_TYPE_MASK;
 }
 
 static inline void *
 nla_data(const struct nlattr *nla)
 {
-    nm_assert(nla_len(nla) >= 0);
-
     return &(((char *) nla)[NLA_HDRLEN]);
 }
 
@@ -219,9 +213,7 @@ nla_get_be64(const struct nlattr *nla)
 static inline char *
 nla_get_string(const struct nlattr *nla)
 {
-    nm_assert(nla_len(nla) >= 0);
-
-    return (char *) nla_data(nla);
+    return nla_data(nla);
 }
 
 size_t nla_strlcpy(char *dst, const struct nlattr *nla, size_t dstsize);
diff --git a/src/libnm-platform/nm-platform-utils.c b/src/libnm-platform/nm-platform-utils.c
index ce847451..f0522736 100644
--- a/src/libnm-platform/nm-platform-utils.c
+++ b/src/libnm-platform/nm-platform-utils.c
@@ -308,14 +308,14 @@ _ethtool_edata_to_string(gpointer edata, gsize edata_size, char *sbuf, gsize sbu
 /*****************************************************************************/
 
 #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 27)
-    #define ethtool_cmd_speed(pedata) ((pedata)->speed)
-
-    #define ethtool_cmd_speed_set(pedata, speed) \
-        G_STMT_START                             \
-        {                                        \
-            (pedata)->speed = (guint16) (speed); \
-        }                                        \
-        G_STMT_END
+#define ethtool_cmd_speed(pedata) ((pedata)->speed)
+
+#define ethtool_cmd_speed_set(pedata, speed) \
+    G_STMT_START                             \
+    {                                        \
+        (pedata)->speed = (guint16) (speed); \
+    }                                        \
+    G_STMT_END
 #endif
 
 static int
@@ -554,7 +554,7 @@ _ASSERT_ethtool_feature_infos(void)
         for (k = 0; k < inf->n_kernel_names; k++) {
             const char *name = inf->kernel_names[k];
 
-            g_assert(nm_utils_strv_find_first((char **) inf->kernel_names, k, name) < 0);
+            g_assert(nm_strv_find_first(inf->kernel_names, k, name) < 0);
 
             /* these offload features are only informational and cannot be set from user-space
              * (NETIF_F_NEVER_CHANGE). We should not track them in _ethtool_feature_infos. */
diff --git a/src/libnm-platform/nm-platform.c b/src/libnm-platform/nm-platform.c
index b7a65df5..5d4ec037 100644
--- a/src/libnm-platform/nm-platform.c
+++ b/src/libnm-platform/nm-platform.c
@@ -56,6 +56,7 @@ G_STATIC_ASSERT(_nm_alignof(NMPlatformIPAddress) == _nm_alignof(NMPlatformIPXAdd
 
 G_STATIC_ASSERT(sizeof(((NMPLinkAddress *) NULL)->data) == _NM_UTILS_HWADDR_LEN_MAX);
 G_STATIC_ASSERT(sizeof(((NMPlatformLink *) NULL)->l_address.data) == _NM_UTILS_HWADDR_LEN_MAX);
+G_STATIC_ASSERT(sizeof(((NMPlatformLink *) NULL)->l_perm_address.data) == _NM_UTILS_HWADDR_LEN_MAX);
 G_STATIC_ASSERT(sizeof(((NMPlatformLink *) NULL)->l_broadcast.data) == _NM_UTILS_HWADDR_LEN_MAX);
 
 static const char *
@@ -175,12 +176,14 @@ enum {
     PROP_NETNS_SUPPORT,
     PROP_USE_UDEV,
     PROP_LOG_WITH_PTR,
+    PROP_CACHE_TC,
     LAST_PROP,
 };
 
 typedef struct _NMPlatformPrivate {
     bool use_udev : 1;
     bool log_with_ptr : 1;
+    bool cache_tc : 1;
 
     guint              ip4_dev_route_blacklist_check_id;
     guint              ip4_dev_route_blacklist_gc_timeout_id;
@@ -211,6 +214,12 @@ nm_platform_get_log_with_ptr(NMPlatform *self)
     return NM_PLATFORM_GET_PRIVATE(self)->log_with_ptr;
 }
 
+gboolean
+nm_platform_get_cache_tc(NMPlatform *self)
+{
+    return NM_PLATFORM_GET_PRIVATE(self)->cache_tc;
+}
+
 /*****************************************************************************/
 
 guint
@@ -299,24 +308,6 @@ static const struct {
     const char *name;
     const char *desc;
 } _nm_platform_kernel_support_info[_NM_PLATFORM_KERNEL_SUPPORT_NUM] = {
-    [NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS] =
-        {
-            .compile_time_default = TRUE,
-            .name                 = "EXTENDED_IFA_FLAGS",
-            .desc                 = "IPv6 temporary addresses support",
-        },
-    [NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL] =
-        {
-            .compile_time_default = TRUE,
-            .name                 = "USER_IPV6LL",
-            .desc                 = "IFLA_INET6_ADDR_GEN_MODE support",
-        },
-    [NM_PLATFORM_KERNEL_SUPPORT_TYPE_RTA_PREF] =
-        {
-            .compile_time_default = (RTA_MAX >= 20 /* RTA_PREF */),
-            .name                 = "RTA_PREF",
-            .desc                 = "ability to set router preference for IPv6 routes",
-        },
     [NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_L3MDEV] =
         {
             .compile_time_default = (FRA_MAX >= 19 /* FRA_L3MDEV */),
@@ -348,6 +339,12 @@ static const struct {
             .name                 = "IFLA_BR_VLAN_STATS_ENABLE",
             .desc                 = "IFLA_BR_VLAN_STATS_ENABLE bridge link attribute",
         },
+    [NM_PLATFORM_KERNEL_SUPPORT_TYPE_IFLA_PERM_ADDRESS] =
+        {
+            .compile_time_default = (IFLA_MAX >= 54 /* IFLA_PERM_ADDRESS */),
+            .name                 = "IFLA_PERM_ADDRESS",
+            .desc                 = "IFLA_PERM_ADDRESS netlink attribute",
+        },
 };
 
 int
@@ -1229,25 +1226,25 @@ nm_platform_link_add(NMPlatform *           self,
 
                switch (type) {
                case NM_LINK_TYPE_BRIDGE:
-                   nm_utils_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
                    nm_platform_lnk_bridge_to_string((const NMPlatformLnkBridge *) extra_data,
                                                     buf_p,
                                                     buf_len);
                    break;
                case NM_LINK_TYPE_VLAN:
-                   nm_utils_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
                    nm_platform_lnk_vlan_to_string((const NMPlatformLnkVlan *) extra_data,
                                                   buf_p,
                                                   buf_len);
                    break;
                case NM_LINK_TYPE_VRF:
-                   nm_utils_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
                    nm_platform_lnk_vrf_to_string((const NMPlatformLnkVrf *) extra_data,
                                                  buf_p,
                                                  buf_len);
                    break;
                case NM_LINK_TYPE_VXLAN:
-                   nm_utils_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
                    nm_platform_lnk_vxlan_to_string((const NMPlatformLnkVxlan *) extra_data,
                                                    buf_p,
                                                    buf_len);
@@ -1257,13 +1254,13 @@ nm_platform_link_add(NMPlatform *           self,
                    break;
                case NM_LINK_TYPE_GRE:
                case NM_LINK_TYPE_GRETAP:
-                   nm_utils_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
                    nm_platform_lnk_gre_to_string((const NMPlatformLnkGre *) extra_data,
                                                  buf_p,
                                                  buf_len);
                    break;
                case NM_LINK_TYPE_SIT:
-                   nm_utils_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
                    nm_platform_lnk_sit_to_string((const NMPlatformLnkSit *) extra_data,
                                                  buf_p,
                                                  buf_len);
@@ -1271,26 +1268,26 @@ nm_platform_link_add(NMPlatform *           self,
                case NM_LINK_TYPE_IP6TNL:
                case NM_LINK_TYPE_IP6GRE:
                case NM_LINK_TYPE_IP6GRETAP:
-                   nm_utils_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
                    nm_platform_lnk_ip6tnl_to_string((const NMPlatformLnkIp6Tnl *) extra_data,
                                                     buf_p,
                                                     buf_len);
                    break;
                case NM_LINK_TYPE_IPIP:
-                   nm_utils_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
                    nm_platform_lnk_ipip_to_string((const NMPlatformLnkIpIp *) extra_data,
                                                   buf_p,
                                                   buf_len);
                    break;
                case NM_LINK_TYPE_MACSEC:
-                   nm_utils_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
                    nm_platform_lnk_macsec_to_string((const NMPlatformLnkMacsec *) extra_data,
                                                     buf_p,
                                                     buf_len);
                    break;
                case NM_LINK_TYPE_MACVLAN:
                case NM_LINK_TYPE_MACVTAP:
-                   nm_utils_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
                    nm_platform_lnk_macvlan_to_string((const NMPlatformLnkMacvlan *) extra_data,
                                                      buf_p,
                                                      buf_len);
@@ -1611,7 +1608,7 @@ nm_platform_link_uses_arp(NMPlatform *self, int ifindex)
  * Returns: %TRUE a tokenized identifier was available
  */
 gboolean
-nm_platform_link_set_ipv6_token(NMPlatform *self, int ifindex, NMUtilsIPv6IfaceId iid)
+nm_platform_link_set_ipv6_token(NMPlatform *self, int ifindex, const NMUtilsIPv6IfaceId *iid)
 {
     _CHECK_SELF(self, klass, FALSE);
 
@@ -1649,47 +1646,20 @@ nm_platform_link_get_udev_device(NMPlatform *self, int ifindex)
     return obj_cache ? obj_cache->_link.udev.device : NULL;
 }
 
-/**
- * nm_platform_link_get_user_ip6vll_enabled:
- * @self: platform instance
- * @ifindex: Interface index
- *
- * Check whether NM handles IPv6LL address creation for the link.  If the
- * platform or OS doesn't support changing the IPv6LL address mode, this call
- * will fail and return %FALSE.
- *
- * Returns: %TRUE if NM handles the IPv6LL address for @ifindex
- */
-gboolean
-nm_platform_link_get_user_ipv6ll_enabled(NMPlatform *self, int ifindex)
+int
+nm_platform_link_get_inet6_addr_gen_mode(NMPlatform *self, int ifindex)
 {
-    const NMPlatformLink *pllink;
-
-    pllink = nm_platform_link_get(self, ifindex);
-    if (pllink && pllink->inet6_addr_gen_mode_inv)
-        return _nm_platform_uint8_inv(pllink->inet6_addr_gen_mode_inv) == NM_IN6_ADDR_GEN_MODE_NONE;
-    return FALSE;
+    return _nm_platform_link_get_inet6_addr_gen_mode(nm_platform_link_get(self, ifindex));
 }
 
-/**
- * nm_platform_link_set_user_ip6vll_enabled:
- * @self: platform instance
- * @ifindex: Interface index
- *
- * Set whether NM handles IPv6LL address creation for the link.  If the
- * platform or OS doesn't support changing the IPv6LL address mode, this call
- * will fail and return %FALSE.
- *
- * Returns: the negative nm-error on failure.
- */
 int
-nm_platform_link_set_user_ipv6ll_enabled(NMPlatform *self, int ifindex, gboolean enabled)
+nm_platform_link_set_inet6_addr_gen_mode(NMPlatform *self, int ifindex, guint8 mode)
 {
     _CHECK_SELF(self, klass, -NME_BUG);
 
     g_return_val_if_fail(ifindex > 0, -NME_BUG);
 
-    return klass->link_set_user_ipv6ll_enabled(self, ifindex, enabled);
+    return klass->link_set_inet6_addr_gen_mode(self, ifindex, mode);
 }
 
 /**
@@ -1736,7 +1706,7 @@ nm_platform_link_get_address(NMPlatform *self, int ifindex, size_t *length)
 }
 
 /**
- * nm_platform_link_get_permanent_address:
+ * nm_platform_link_get_permanent_address_ethtool:
  * @self: platform instance
  * @ifindex: Interface index
  * @buf: buffer of at least %_NM_UTILS_HWADDR_LEN_MAX bytes, on success
@@ -1747,23 +1717,48 @@ nm_platform_link_get_address(NMPlatform *self, int ifindex, size_t *length)
  * address.
  */
 gboolean
-nm_platform_link_get_permanent_address(NMPlatform *self, int ifindex, guint8 *buf, size_t *length)
+nm_platform_link_get_permanent_address_ethtool(NMPlatform *    self,
+                                               int             ifindex,
+                                               NMPLinkAddress *out_address)
 {
     _CHECK_SELF(self, klass, FALSE);
 
-    if (length)
-        *length = 0;
+    if (out_address)
+        out_address->len = 0;
 
     g_return_val_if_fail(ifindex > 0, FALSE);
-    g_return_val_if_fail(buf, FALSE);
-    g_return_val_if_fail(length, FALSE);
+    g_return_val_if_fail(out_address, FALSE);
 
-    if (klass->link_get_permanent_address)
-        return klass->link_get_permanent_address(self, ifindex, buf, length);
+    if (klass->link_get_permanent_address_ethtool)
+        return klass->link_get_permanent_address_ethtool(self, ifindex, out_address);
     return FALSE;
 }
 
 gboolean
+nm_platform_link_get_permanent_address(NMPlatform *          self,
+                                       const NMPlatformLink *plink,
+                                       NMPLinkAddress *      out_address)
+{
+    _CHECK_SELF(self, klass, FALSE);
+    nm_assert(out_address);
+
+    if (!plink)
+        return FALSE;
+    if (plink->l_perm_address.len > 0) {
+        *out_address = plink->l_perm_address;
+        return TRUE;
+    }
+    if (nm_platform_kernel_support_get_full(NM_PLATFORM_KERNEL_SUPPORT_TYPE_IFLA_PERM_ADDRESS,
+                                            FALSE)
+        == NM_OPTION_BOOL_TRUE) {
+        /* kernel supports the netlink API IFLA_PERM_ADDRESS, but we don't have the
+         * address cached. There is no need to fallback to ethtool ioctl. */
+        return FALSE;
+    }
+    return nm_platform_link_get_permanent_address_ethtool(self, plink->ifindex, out_address);
+}
+
+gboolean
 nm_platform_link_supports_carrier_detect(NMPlatform *self, int ifindex)
 {
     _CHECK_SELF(self, klass, FALSE);
@@ -2336,13 +2331,13 @@ nm_platform_link_wireguard_change(NMPlatform *                              self
             gsize len = sizeof(buf_peers);
             guint i;
 
-            nm_utils_strbuf_append_str(&b, &len, " { ");
+            nm_strbuf_append_str(&b, &len, " { ");
             for (i = 0; i < peers_len; i++) {
-                nm_utils_strbuf_append_str(&b, &len, " { ");
+                nm_strbuf_append_str(&b, &len, " { ");
                 nm_platform_wireguard_peer_to_string(&peers[i], b, len);
-                nm_utils_strbuf_seek_end(&b, &len);
+                nm_strbuf_seek_end(&b, &len);
                 if (peer_flags) {
-                    nm_utils_strbuf_append(
+                    nm_strbuf_append(
                         &b,
                         &len,
                         " (%s)",
@@ -2350,9 +2345,9 @@ nm_platform_link_wireguard_change(NMPlatform *                              self
                                                                buf_change_flags,
                                                                sizeof(buf_change_flags)));
                 }
-                nm_utils_strbuf_append_str(&b, &len, " } ");
+                nm_strbuf_append_str(&b, &len, " } ");
             }
-            nm_utils_strbuf_append_str(&b, &len, "}");
+            nm_strbuf_append_str(&b, &len, "}");
         }
 
         _LOG3D("link: change wireguard ifindex %d, %s, (%s), %u peers%s",
@@ -2531,6 +2526,8 @@ slave_category(NMPlatform *self, int slave)
     switch (nm_platform_link_get_type(self, master)) {
     case NM_LINK_TYPE_BRIDGE:
         return "brport";
+    case NM_LINK_TYPE_BOND:
+        return "bonding_slave";
     default:
         return NULL;
     }
@@ -2620,30 +2617,30 @@ nm_platform_link_vlan_change(NMPlatform *            self,
         len  = sizeof(buf);
 
         if (flags_mask)
-            nm_utils_strbuf_append(&b,
-                                   &len,
-                                   " flags 0x%x/0x%x",
-                                   (unsigned) flags_set,
-                                   (unsigned) flags_mask);
+            nm_strbuf_append(&b,
+                             &len,
+                             " flags 0x%x/0x%x",
+                             (unsigned) flags_set,
+                             (unsigned) flags_mask);
 
         if (ingress_reset_all || n_ingress_map) {
-            nm_utils_strbuf_append_str(&b, &len, " ingress-qos-map");
+            nm_strbuf_append_str(&b, &len, " ingress-qos-map");
             nm_platform_vlan_qos_mapping_to_string("", ingress_map, n_ingress_map, b, len);
             i = strlen(b);
             b += i;
             len -= i;
             if (ingress_reset_all)
-                nm_utils_strbuf_append_str(&b, &len, " (reset-all)");
+                nm_strbuf_append_str(&b, &len, " (reset-all)");
         }
 
         if (egress_reset_all || n_egress_map) {
-            nm_utils_strbuf_append_str(&b, &len, " egress-qos-map");
+            nm_strbuf_append_str(&b, &len, " egress-qos-map");
             nm_platform_vlan_qos_mapping_to_string("", egress_map, n_egress_map, b, len);
             i = strlen(b);
             b += i;
             len -= i;
             if (egress_reset_all)
-                nm_utils_strbuf_append_str(&b, &len, " (reset-all)");
+                nm_strbuf_append_str(&b, &len, " (reset-all)");
         }
 
         _LOG3D("link: change vlan %s", buf);
@@ -3668,7 +3665,7 @@ _addr_array_clean_expired(int          addr_family,
         }
 #endif
 
-        if (!NM_IS_IPv4(addr_family) && NM_FLAGS_HAS(a->n_ifa_flags, IFA_F_TEMPORARY)) {
+        if (!NM_IS_IPv4(addr_family) && NM_FLAGS_HAS(a->n_ifa_flags, IFA_F_SECONDARY)) {
             /* temporary addresses are never added explicitly by NetworkManager but
              * kernel adds them via mngtempaddr flag.
              *
@@ -3913,7 +3910,6 @@ nm_platform_ip_address_sync(NMPlatform *self,
     gs_unref_hashtable GHashTable *known_addresses_idx = NULL;
     GPtrArray *                    plat_addresses;
     GHashTable *                   known_subnets = NULL;
-    guint32                        ifa_flags;
     guint                          i_plat;
     guint                          i_know;
     guint                          i;
@@ -4140,10 +4136,6 @@ next_plat:;
     if (IS_IPv4)
         ip4_addr_subnets_destroy_index(known_subnets, known_addresses);
 
-    ifa_flags = nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS)
-                    ? IFA_F_NOPREFIXROUTE
-                    : 0;
-
     /* Add missing addresses. New addresses are added by kernel with top
      * priority.
      */
@@ -4178,7 +4170,7 @@ next_plat:;
                     nm_platform_ip4_broadcast_address_from_addr(&known_address->a4),
                     lifetime,
                     preferred,
-                    ifa_flags,
+                    IFA_F_NOPREFIXROUTE,
                     known_address->a4.label)) {
                 /* ignore error, for unclear reasons. */
             }
@@ -4190,7 +4182,7 @@ next_plat:;
                                              known_address->a6.peer_address,
                                              lifetime,
                                              preferred,
-                                             ifa_flags | known_address->a6.n_ifa_flags))
+                                             IFA_F_NOPREFIXROUTE | known_address->a6.n_ifa_flags))
                 return FALSE;
         }
     }
@@ -4278,7 +4270,7 @@ nm_platform_ip_address_get_prune_list(NMPlatform *self,
 
         if (!IS_IPv4) {
             if (exclude_ipv6_temporary_addrs
-                && NM_FLAGS_HAS(NMP_OBJECT_CAST_IP_ADDRESS(obj)->n_ifa_flags, IFA_F_TEMPORARY))
+                && NM_FLAGS_HAS(NMP_OBJECT_CAST_IP_ADDRESS(obj)->n_ifa_flags, IFA_F_SECONDARY))
                 continue;
         }
 
@@ -5311,92 +5303,13 @@ nm_platform_qdisc_add(NMPlatform *self, NMPNlmFlags flags, const NMPlatformQdisc
     return klass->qdisc_add(self, flags, qdisc);
 }
 
-/**
- * nm_platform_qdisc_sync:
- * @self: the #NMPlatform instance
- * @ifindex: the ifindex where to configure the qdiscs.
- * @known_qdiscs: the list of qdiscs (#NMPObject).
- *
- * The function promises not to take any reference to the qdisc
- * instances from @known_qdiscs, nor to keep them around after
- * the function returns. This is important, because it allows the
- * caller to pass NMPlatformQdisc instances which "kind" string
- * have a limited lifetime.
- *
- * Returns: %TRUE on success.
- */
-gboolean
-nm_platform_qdisc_sync(NMPlatform *self, int ifindex, GPtrArray *known_qdiscs)
+int
+nm_platform_qdisc_delete(NMPlatform *self, int ifindex, guint32 parent, gboolean log_error)
 {
-    gs_unref_ptrarray GPtrArray *plat_qdiscs = NULL;
-    NMPLookup                    lookup;
-    guint                        i;
-    gboolean                     success            = TRUE;
-    gs_unref_hashtable GHashTable *known_qdiscs_idx = NULL;
-
-    nm_assert(NM_IS_PLATFORM(self));
-    nm_assert(ifindex > 0);
-
-    known_qdiscs_idx =
-        g_hash_table_new((GHashFunc) nmp_object_id_hash, (GEqualFunc) nmp_object_id_equal);
-    if (known_qdiscs) {
-        for (i = 0; i < known_qdiscs->len; i++) {
-            const NMPObject *q = g_ptr_array_index(known_qdiscs, i);
-
-            if (!g_hash_table_insert(known_qdiscs_idx, (gpointer) q, (gpointer) q)) {
-                _LOGW("duplicate qdisc %s", nm_platform_qdisc_to_string(&q->qdisc, NULL, 0));
-                return FALSE;
-            }
-        }
-    }
-
-    plat_qdiscs =
-        nm_platform_lookup_clone(self,
-                                 nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_QDISC, ifindex),
-                                 NULL,
-                                 NULL);
-    if (plat_qdiscs) {
-        for (i = 0; i < plat_qdiscs->len; i++) {
-            const NMPObject *p = g_ptr_array_index(plat_qdiscs, i);
-            const NMPObject *k;
-
-            /* look up known qdisc with same parent */
-            k = g_hash_table_lookup(known_qdiscs_idx, p);
-
-            if (k) {
-                const NMPlatformQdisc *qdisc_k = NMP_OBJECT_CAST_QDISC(k);
-                const NMPlatformQdisc *qdisc_p = NMP_OBJECT_CAST_QDISC(p);
-
-                /* check other fields */
-                if (nm_platform_qdisc_cmp_full(qdisc_k, qdisc_p, FALSE) != 0
-                    || (qdisc_k->handle != qdisc_p->handle && qdisc_k != 0)) {
-                    k = NULL;
-                }
-            }
-
-            if (k) {
-                g_hash_table_remove(known_qdiscs_idx, k);
-            } else {
-                /* can't delete qdisc with zero handle */
-                if (TC_H_MAJ(p->qdisc.handle) != 0) {
-                    success &= nm_platform_object_delete(self, p);
-                }
-            }
-        }
-    }
-
-    if (known_qdiscs) {
-        for (i = 0; i < known_qdiscs->len; i++) {
-            const NMPObject *q = g_ptr_array_index(known_qdiscs, i);
-
-            if (g_hash_table_contains(known_qdiscs_idx, q)) {
-                success &=
-                    (nm_platform_qdisc_add(self, NMP_NLM_FLAG_ADD, NMP_OBJECT_CAST_QDISC(q)) >= 0);
-            }
-        }
-    }
+    _CHECK_SELF(self, klass, -NME_BUG);
 
-    return success;
+    _LOG3D("deleting a qdisc: parent 0x%08x", parent);
+    return klass->qdisc_delete(self, ifindex, parent, log_error);
 }
 
 /*****************************************************************************/
@@ -5414,55 +5327,58 @@ nm_platform_tfilter_add(NMPlatform *self, NMPNlmFlags flags, const NMPlatformTfi
     return klass->tfilter_add(self, flags, tfilter);
 }
 
+int
+nm_platform_tfilter_delete(NMPlatform *self, int ifindex, guint32 parent, gboolean log_error)
+{
+    _CHECK_SELF(self, klass, -NME_BUG);
+
+    _LOG3D("deleting a tfilter: parent 0x%08x", parent);
+    return klass->tfilter_delete(self, ifindex, parent, log_error);
+}
+
 /**
- * nm_platform_qdisc_sync:
+ * nm_platform_tc_sync:
  * @self: the #NMPlatform instance
- * @ifindex: the ifindex where to configure the qdiscs.
+ * @ifindex: the ifindex where to configure qdiscs and filters.
+ * @known_qdiscs: the list of qdiscs (#NMPObject).
  * @known_tfilters: the list of tfilters (#NMPObject).
  *
- * The function promises not to take any reference to the tfilter
- * instances from @known_tfilters, nor to keep them around after
- * the function returns. This is important, because it allows the
- * caller to pass NMPlatformTfilter instances which "kind" string
- * have a limited lifetime.
+ * The function promises not to take any reference to the
+ * instances from @known_qdiscs and @known_tfilters, nor to
+ * keep them around after the function returns. This is important,
+ * because it allows the caller to pass NMPlatformQdisc and
+ * NMPlatformTfilter instances which "kind" string have a limited
+ * lifetime.
  *
  * Returns: %TRUE on success.
  */
 gboolean
-nm_platform_tfilter_sync(NMPlatform *self, int ifindex, GPtrArray *known_tfilters)
+nm_platform_tc_sync(NMPlatform *self,
+                    int         ifindex,
+                    GPtrArray * known_qdiscs,
+                    GPtrArray * known_tfilters)
 {
-    gs_unref_ptrarray GPtrArray *plat_tfilters = NULL;
-    NMPLookup                    lookup;
-    guint                        i;
-    gboolean                     success              = TRUE;
-    gs_unref_hashtable GHashTable *known_tfilters_idx = NULL;
+    guint    i;
+    gboolean success = TRUE;
 
     nm_assert(NM_IS_PLATFORM(self));
     nm_assert(ifindex > 0);
 
-    known_tfilters_idx =
-        g_hash_table_new((GHashFunc) nmp_object_id_hash, (GEqualFunc) nmp_object_id_equal);
-
-    if (known_tfilters) {
-        for (i = 0; i < known_tfilters->len; i++) {
-            const NMPObject *q = g_ptr_array_index(known_tfilters, i);
-
-            g_hash_table_insert(known_tfilters_idx, (gpointer) q, (gpointer) q);
-        }
-    }
+    nm_platform_qdisc_delete(self, ifindex, TC_H_ROOT, FALSE);
+    nm_platform_qdisc_delete(self, ifindex, TC_H_INGRESS, FALSE);
 
-    plat_tfilters =
-        nm_platform_lookup_clone(self,
-                                 nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_TFILTER, ifindex),
-                                 NULL,
-                                 NULL);
+    /* At this point we can only have a root default qdisc
+     * (which can't be deleted). Ensure it doesn't have any
+     * filters attached.
+     */
+    nm_platform_tfilter_delete(self, ifindex, TC_H_ROOT, FALSE);
 
-    if (plat_tfilters) {
-        for (i = 0; i < plat_tfilters->len; i++) {
-            const NMPObject *q = g_ptr_array_index(plat_tfilters, i);
+    if (known_qdiscs) {
+        for (i = 0; i < known_qdiscs->len; i++) {
+            const NMPObject *q = g_ptr_array_index(known_qdiscs, i);
 
-            if (!g_hash_table_lookup(known_tfilters_idx, q))
-                success &= nm_platform_object_delete(self, q);
+            success &=
+                (nm_platform_qdisc_add(self, NMP_NLM_FLAG_ADD, NMP_OBJECT_CAST_QDISC(q)) >= 0);
         }
     }
 
@@ -5493,7 +5409,7 @@ nm_platform_vlan_qos_mapping_to_string(const char *            name,
     nm_utils_to_string_buffer_init(&buf, &len);
 
     if (!n_map) {
-        nm_utils_strbuf_append_str(&buf, &len, "");
+        nm_strbuf_append_str(&buf, &len, "");
         return buf;
     }
 
@@ -5503,14 +5419,14 @@ nm_platform_vlan_qos_mapping_to_string(const char *            name,
     b = buf;
 
     if (name) {
-        nm_utils_strbuf_append_str(&b, &len, name);
-        nm_utils_strbuf_append_str(&b, &len, " {");
+        nm_strbuf_append_str(&b, &len, name);
+        nm_strbuf_append_str(&b, &len, " {");
     } else
-        nm_utils_strbuf_append_c(&b, &len, '{');
+        nm_strbuf_append_c(&b, &len, '{');
 
     for (i = 0; i < n_map; i++)
-        nm_utils_strbuf_append(&b, &len, " %u:%u", map[i].from, map[i].to);
-    nm_utils_strbuf_append_str(&b, &len, " }");
+        nm_strbuf_append(&b, &len, " %u:%u", map[i].from, map[i].to);
+    nm_strbuf_append_str(&b, &len, " }");
     return buf;
 }
 
@@ -5566,6 +5482,7 @@ nm_platform_link_to_string(const NMPlatformLink *link, char *buf, gsize len)
     gsize       l;
     char        str_addrmode[30];
     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];
     const char *str_link_type;
@@ -5576,13 +5493,13 @@ nm_platform_link_to_string(const NMPlatformLink *link, char *buf, gsize len)
     s = str_highlighted_flags;
     l = sizeof(str_highlighted_flags);
     if (NM_FLAGS_HAS(link->n_ifi_flags, IFF_NOARP))
-        nm_utils_strbuf_append_str(&s, &l, "NOARP,");
+        nm_strbuf_append_str(&s, &l, "NOARP,");
     if (NM_FLAGS_HAS(link->n_ifi_flags, IFF_UP))
-        nm_utils_strbuf_append_str(&s, &l, "UP");
+        nm_strbuf_append_str(&s, &l, "UP");
     else
-        nm_utils_strbuf_append_str(&s, &l, "DOWN");
+        nm_strbuf_append_str(&s, &l, "DOWN");
     if (link->connected)
-        nm_utils_strbuf_append_str(&s, &l, ",LOWER_UP");
+        nm_strbuf_append_str(&s, &l, ",LOWER_UP");
     nm_assert(s > str_highlighted_flags && l > 0);
 
     if (link->n_ifi_flags) {
@@ -5604,6 +5521,7 @@ nm_platform_link_to_string(const NMPlatformLink *link, char *buf, gsize len)
         parent[0] = 0;
 
     _nmp_link_address_to_string(&link->l_address, str_address);
+    _nmp_link_address_to_string(&link->l_perm_address, str_perm_address);
     _nmp_link_address_to_string(&link->l_broadcast, str_broadcast);
 
     str_link_type = nm_link_type_to_string(link->type);
@@ -5623,6 +5541,7 @@ nm_platform_link_to_string(const NMPlatformLink *link, char *buf, gsize len)
         "%s"      /* is-in-udev */
         "%s%s"    /* addr-gen-mode */
         "%s%s"    /* l_address */
+        "%s%s"    /* l_perm_address */
         "%s%s"    /* l_broadcast */
         "%s%s"    /* inet6_token */
         "%s%s"    /* driver */
@@ -5648,11 +5567,13 @@ nm_platform_link_to_string(const NMPlatformLink *link, char *buf, gsize len)
                                       : "",
         str_address[0] ? " addr " : "",
         str_address[0] ? str_address : "",
+        str_perm_address[0] ? " permaddr " : "",
+        str_perm_address[0] ? str_perm_address : "",
         str_broadcast[0] ? " brd " : "",
         str_broadcast[0] ? str_broadcast : "",
         link->inet6_token.id ? " inet6token " : "",
         link->inet6_token.id
-            ? nm_utils_inet6_interface_identifier_to_token(link->inet6_token, str_inet6_token)
+            ? nm_utils_inet6_interface_identifier_to_token(&link->inet6_token, str_inet6_token)
             : "",
         link->driver ? " driver " : "",
         link->driver ?: "",
@@ -6067,9 +5988,9 @@ nm_platform_lnk_vlan_to_string(const NMPlatformLnkVlan *lnk, char *buf, gsize le
 
     b = buf;
 
-    nm_utils_strbuf_append(&b, &len, "vlan %u", lnk->id);
+    nm_strbuf_append(&b, &len, "vlan %u", lnk->id);
     if (lnk->flags)
-        nm_utils_strbuf_append(&b, &len, " flags 0x%x", lnk->flags);
+        nm_strbuf_append(&b, &len, " flags 0x%x", lnk->flags);
     return buf;
 }
 
@@ -6083,7 +6004,7 @@ nm_platform_lnk_vrf_to_string(const NMPlatformLnkVrf *lnk, char *buf, gsize len)
 
     b = buf;
 
-    nm_utils_strbuf_append(&b, &len, "table %u", lnk->table);
+    nm_strbuf_append(&b, &len, "table %u", lnk->table);
     return buf;
 }
 
@@ -6208,36 +6129,36 @@ nm_platform_wireguard_peer_to_string(const NMPWireGuardPeer *peer, char *buf, gs
     } else
         s_endpoint[0] = '\0';
 
-    nm_utils_strbuf_append(
-        &buf,
-        &len,
-        "public-key %s"
-        "%s"                                                   /* preshared-key */
-        "%s"                                                   /* endpoint */
-        " rx %" G_GUINT64_FORMAT " tx %" G_GUINT64_FORMAT "%s" /* persistent-keepalive */
-        "%s",                                                  /* allowed-ips */
-        public_key_b64,
-        nm_utils_memeqzero_secret(peer->preshared_key, sizeof(peer->preshared_key))
-            ? ""
-            : " preshared-key (hidden)",
-        s_endpoint,
-        peer->rx_bytes,
-        peer->tx_bytes,
-        peer->persistent_keepalive_interval > 0
-            ? nm_sprintf_buf(s_keepalive,
-                             " keepalive %u",
-                             (guint) peer->persistent_keepalive_interval)
-            : "",
-        peer->allowed_ips_len > 0 ? " allowed-ips" : "");
+    nm_strbuf_append(&buf,
+                     &len,
+                     "public-key %s"
+                     "%s" /* preshared-key */
+                     "%s" /* endpoint */
+                     " rx %" G_GUINT64_FORMAT " tx %" G_GUINT64_FORMAT
+                     "%s"  /* persistent-keepalive */
+                     "%s", /* allowed-ips */
+                     public_key_b64,
+                     nm_utils_memeqzero_secret(peer->preshared_key, sizeof(peer->preshared_key))
+                         ? ""
+                         : " preshared-key (hidden)",
+                     s_endpoint,
+                     peer->rx_bytes,
+                     peer->tx_bytes,
+                     peer->persistent_keepalive_interval > 0
+                         ? nm_sprintf_buf(s_keepalive,
+                                          " keepalive %u",
+                                          (guint) peer->persistent_keepalive_interval)
+                         : "",
+                     peer->allowed_ips_len > 0 ? " allowed-ips" : "");
 
     for (i = 0; i < peer->allowed_ips_len; i++) {
         const NMPWireGuardAllowedIP *allowed_ip = &peer->allowed_ips[i];
 
-        nm_utils_strbuf_append(&buf,
-                               &len,
-                               " %s/%u",
-                               nm_utils_inet_ntop(allowed_ip->family, &allowed_ip->addr, s_addr),
-                               allowed_ip->mask);
+        nm_strbuf_append(&buf,
+                         &len,
+                         " %s/%u",
+                         nm_utils_inet_ntop(allowed_ip->family, &allowed_ip->addr, s_addr),
+                         allowed_ip->mask);
     }
 
     return buf0;
@@ -6353,7 +6274,8 @@ nm_platform_ip4_address_to_string(const NMPlatformIP4Address *address, char *buf
         "%s" /* label */
         " src %s"
         "%s" /* external */
-        "%s" /* ip4acd_not_ready */
+        "%s" /* a_acd_not_ready */
+        "%s" /* a_assume_config_once */
         "",
         s_address,
         address->plen,
@@ -6372,7 +6294,8 @@ nm_platform_ip4_address_to_string(const NMPlatformIP4Address *address, char *buf
         str_label,
         nmp_utils_ip_config_source_to_string(address->addr_source, s_source, sizeof(s_source)),
         address->external ? " ext" : "",
-        address->ip4acd_not_ready ? " ip4acd-not-ready" : "");
+        address->a_acd_not_ready ? " ip4acd-not-ready" : "",
+        address->a_assume_config_once ? " assume-config-once" : "");
     g_free(str_peer);
     return buf;
 }
@@ -6406,17 +6329,22 @@ NM_UTILS_ENUM2STR_DEFINE(nm_platform_link_inet6_addrgenmode2str,
                          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_TENTATIVE, "tentative"), );
+                          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,
@@ -6486,7 +6414,10 @@ nm_platform_ip6_address_to_string(const NMPlatformIP6Address *address, char *buf
     g_snprintf(
         buf,
         len,
-        "%s/%d lft %s pref %s%s%s%s%s src %s%s",
+        "%s/%d lft %s pref %s%s%s%s%s src %s"
+        "%s" /* external */
+        "%s" /* a_assume_config_once */
+        "",
         s_address,
         address->plen,
         str_lft_p,
@@ -6496,7 +6427,8 @@ nm_platform_ip6_address_to_string(const NMPlatformIP6Address *address, char *buf
         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->external ? " ext" : "");
+        address->external ? " external" : "",
+        address->a_assume_config_once ? " assume-config-once" : "");
     g_free(str_peer);
     return buf;
 }
@@ -6529,7 +6461,7 @@ _rtm_flags_to_string_full(char *buf, gsize buf_size, unsigned rtm_flags)
     if (!rtm_flags)
         return "";
 
-    nm_utils_strbuf_append_str(&buf, &buf_size, " rtm_flags ");
+    nm_strbuf_append_str(&buf, &buf_size, " rtm_flags ");
     _rtm_flags_to_string(rtm_flags, buf, buf_size);
     nm_assert(strlen(buf) < buf_size);
     return buf0;
@@ -6588,6 +6520,7 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz
         "%s"                                   /* initrwnd */
         "%s"                                   /* mtu */
         "%s"                                   /* is_external */
+        "%s"                                   /* r_assume_config_once */
         "",
         nm_net_aux_rtnl_rtntype_n2a_maybe_buf(nm_platform_route_type_uncoerce(route->type_coerced),
                                               str_type),
@@ -6644,7 +6577,8 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz
                                                        route->lock_mtu ? "lock " : "",
                                                        route->mtu)
                                       : "",
-        route->is_external ? " (E)" : "");
+        route->is_external ? " is-external" : "",
+        route->r_assume_config_once ? " assume-config-once" : "");
     return buf;
 }
 
@@ -6715,6 +6649,7 @@ nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsiz
         "%s"                                   /* mtu */
         "%s"                                   /* pref */
         "%s"                                   /* is_external */
+        "%s"                                   /* r_assume_config_once */
         "",
         nm_net_aux_rtnl_rtntype_n2a_maybe_buf(nm_platform_route_type_uncoerce(route->type_coerced),
                                               str_type),
@@ -6775,7 +6710,8 @@ nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsiz
             " pref %s",
             nm_icmpv6_router_pref_to_string(route->rt_pref, str_pref2, sizeof(str_pref2)))
                        : "",
-        route->is_external ? " (E)" : "");
+        route->is_external ? " is-external" : "",
+        route->r_assume_config_once ? " assume-config-once" : "");
 
     return buf;
 }
@@ -6801,18 +6737,18 @@ _routing_rule_addr_to_string(char **         buf,
 
     if (plen == 0 && is_zero) {
         if (is_src)
-            nm_utils_strbuf_append_str(buf, len, " from all");
+            nm_strbuf_append_str(buf, len, " from all");
         else
-            nm_utils_strbuf_append_str(buf, len, "");
+            nm_strbuf_append_str(buf, len, "");
         return;
     }
 
-    nm_utils_strbuf_append_str(buf, len, is_src ? " from " : " to ");
+    nm_strbuf_append_str(buf, len, is_src ? " from " : " to ");
 
-    nm_utils_strbuf_append_str(buf, len, nm_utils_inet_ntop(addr_family, addr, s_addr));
+    nm_strbuf_append_str(buf, len, nm_utils_inet_ntop(addr_family, addr, s_addr));
 
     if (plen != (addr_size * 8))
-        nm_utils_strbuf_append(buf, len, "/%u", plen);
+        nm_strbuf_append(buf, len, "/%u", plen);
 }
 
 static void
@@ -6822,11 +6758,11 @@ _routing_rule_port_range_to_string(char **                   buf,
                                    const char *              name)
 {
     if (port_range->start == 0 && port_range->end == 0)
-        nm_utils_strbuf_append_str(buf, len, "");
+        nm_strbuf_append_str(buf, len, "");
     else {
-        nm_utils_strbuf_append(buf, len, " %s %u", name, port_range->start);
+        nm_strbuf_append(buf, len, " %s %u", name, port_range->start);
         if (port_range->start != port_range->end)
-            nm_utils_strbuf_append(buf, len, "-%u", port_range->end);
+            nm_strbuf_append(buf, len, "-%u", port_range->end);
     }
 }
 
@@ -6853,14 +6789,14 @@ nm_platform_routing_rule_to_string(const NMPlatformRoutingRule *routing_rule, ch
     rr_flags = routing_rule->flags;
 
     rr_flags = NM_FLAGS_UNSET(rr_flags, FIB_RULE_INVERT);
-    nm_utils_strbuf_append(&buf,
-                           &len,
-                           "[%c] " /* addr-family */
-                           "%u:"   /* priority */
-                           "%s",   /* not/FIB_RULE_INVERT */
-                           nm_utils_addr_family_to_char(routing_rule->addr_family),
-                           routing_rule->priority,
-                           (NM_FLAGS_HAS(routing_rule->flags, FIB_RULE_INVERT) ? " not" : ""));
+    nm_strbuf_append(&buf,
+                     &len,
+                     "[%c] " /* addr-family */
+                     "%u:"   /* priority */
+                     "%s",   /* not/FIB_RULE_INVERT */
+                     nm_utils_addr_family_to_char(routing_rule->addr_family),
+                     routing_rule->priority,
+                     (NM_FLAGS_HAS(routing_rule->flags, FIB_RULE_INVERT) ? " not" : ""));
 
     _routing_rule_addr_to_string(&buf,
                                  &len,
@@ -6877,53 +6813,53 @@ nm_platform_routing_rule_to_string(const NMPlatformRoutingRule *routing_rule, ch
                                  FALSE);
 
     if (routing_rule->tos)
-        nm_utils_strbuf_append(&buf, &len, " tos 0x%02x", routing_rule->tos);
+        nm_strbuf_append(&buf, &len, " tos 0x%02x", routing_rule->tos);
 
     if (routing_rule->fwmark != 0 || routing_rule->fwmask != 0) {
-        nm_utils_strbuf_append(&buf, &len, " fwmark %#x", (unsigned) routing_rule->fwmark);
+        nm_strbuf_append(&buf, &len, " fwmark %#x", (unsigned) routing_rule->fwmark);
         if (routing_rule->fwmark != 0xFFFFFFFFu)
-            nm_utils_strbuf_append(&buf, &len, "/%#x", (unsigned) routing_rule->fwmask);
+            nm_strbuf_append(&buf, &len, "/%#x", (unsigned) routing_rule->fwmask);
     }
 
     if (routing_rule->iifname[0]) {
-        nm_utils_strbuf_append(&buf, &len, " iif %s", routing_rule->iifname);
+        nm_strbuf_append(&buf, &len, " iif %s", routing_rule->iifname);
         rr_flags = NM_FLAGS_UNSET(rr_flags, FIB_RULE_IIF_DETACHED);
         if (NM_FLAGS_HAS(routing_rule->flags, FIB_RULE_IIF_DETACHED))
-            nm_utils_strbuf_append_str(&buf, &len, " [detached]");
+            nm_strbuf_append_str(&buf, &len, " [detached]");
     }
 
     if (routing_rule->oifname[0]) {
-        nm_utils_strbuf_append(&buf, &len, " oif %s", routing_rule->oifname);
+        nm_strbuf_append(&buf, &len, " oif %s", routing_rule->oifname);
         rr_flags = NM_FLAGS_UNSET(rr_flags, FIB_RULE_OIF_DETACHED);
         if (NM_FLAGS_HAS(routing_rule->flags, FIB_RULE_OIF_DETACHED))
-            nm_utils_strbuf_append_str(&buf, &len, " [detached]");
+            nm_strbuf_append_str(&buf, &len, " [detached]");
     }
 
     if (routing_rule->l3mdev != 0) {
         if (routing_rule->l3mdev == 1)
-            nm_utils_strbuf_append_str(&buf, &len, " lookup [l3mdev-table]");
+            nm_strbuf_append_str(&buf, &len, " lookup [l3mdev-table]");
         else {
-            nm_utils_strbuf_append(&buf,
-                                   &len,
-                                   " lookup [l3mdev-table/%u]",
-                                   (unsigned) routing_rule->l3mdev);
+            nm_strbuf_append(&buf,
+                             &len,
+                             " lookup [l3mdev-table/%u]",
+                             (unsigned) routing_rule->l3mdev);
         }
     }
 
     if (routing_rule->uid_range_has || routing_rule->uid_range.start
         || routing_rule->uid_range.end) {
-        nm_utils_strbuf_append(&buf,
-                               &len,
-                               " uidrange %u-%u%s",
-                               routing_rule->uid_range.start,
-                               routing_rule->uid_range.end,
-                               routing_rule->uid_range_has ? "" : "(?)");
+        nm_strbuf_append(&buf,
+                         &len,
+                         " uidrange %u-%u%s",
+                         routing_rule->uid_range.start,
+                         routing_rule->uid_range.end,
+                         routing_rule->uid_range_has ? "" : "(?)");
     }
 
     if (routing_rule->ip_proto != 0) {
         /* we don't call getprotobynumber(), just print the numeric value.
          * This differs from what ip-rule prints. */
-        nm_utils_strbuf_append(&buf, &len, " ipproto %u", routing_rule->ip_proto);
+        nm_strbuf_append(&buf, &len, " ipproto %u", routing_rule->ip_proto);
     }
 
     _routing_rule_port_range_to_string(&buf, &len, &routing_rule->sport_range, "sport");
@@ -6931,25 +6867,25 @@ nm_platform_routing_rule_to_string(const NMPlatformRoutingRule *routing_rule, ch
     _routing_rule_port_range_to_string(&buf, &len, &routing_rule->dport_range, "dport");
 
     if (routing_rule->tun_id != 0) {
-        nm_utils_strbuf_append(&buf, &len, " tun_id %" G_GUINT64_FORMAT, routing_rule->tun_id);
+        nm_strbuf_append(&buf, &len, " tun_id %" G_GUINT64_FORMAT, routing_rule->tun_id);
     }
 
     if (routing_rule->table != 0) {
-        nm_utils_strbuf_append(&buf, &len, " lookup %u", routing_rule->table);
+        nm_strbuf_append(&buf, &len, " lookup %u", routing_rule->table);
     }
 
     if (routing_rule->suppress_prefixlen_inverse != 0) {
-        nm_utils_strbuf_append(&buf,
-                               &len,
-                               " suppress_prefixlen %d",
-                               (int) (~routing_rule->suppress_prefixlen_inverse));
+        nm_strbuf_append(&buf,
+                         &len,
+                         " suppress_prefixlen %d",
+                         (int) (~routing_rule->suppress_prefixlen_inverse));
     }
 
     if (routing_rule->suppress_ifgroup_inverse != 0) {
-        nm_utils_strbuf_append(&buf,
-                               &len,
-                               " suppress_ifgroup %d",
-                               (int) (~routing_rule->suppress_ifgroup_inverse));
+        nm_strbuf_append(&buf,
+                         &len,
+                         " suppress_ifgroup %d",
+                         (int) (~routing_rule->suppress_ifgroup_inverse));
     }
 
     if (routing_rule->flow) {
@@ -6958,7 +6894,7 @@ nm_platform_routing_rule_to_string(const NMPlatformRoutingRule *routing_rule, ch
          *
          * We don't follow the style how ip-rule prints flow/realms. It's confusing. Just
          * print the value hex. */
-        nm_utils_strbuf_append(&buf, &len, " realms 0x%08x", routing_rule->flow);
+        nm_strbuf_append(&buf, &len, " realms 0x%08x", routing_rule->flow);
     }
 
     if (routing_rule->action == RTN_NAT) {
@@ -6966,36 +6902,36 @@ nm_platform_routing_rule_to_string(const NMPlatformRoutingRule *routing_rule, ch
 
         /* NAT is deprecated for many years. We don't support RTA_GATEWAY/FRA_UNUSED2
          * for the gateway, and so do recent kernels ignore that parameter. */
-        nm_utils_strbuf_append_str(&buf, &len, " masquerade");
+        nm_strbuf_append_str(&buf, &len, " masquerade");
     } else if (routing_rule->action == FR_ACT_GOTO) {
         if (routing_rule->goto_target != 0)
-            nm_utils_strbuf_append(&buf, &len, " goto %u", routing_rule->goto_target);
+            nm_strbuf_append(&buf, &len, " goto %u", routing_rule->goto_target);
         else
-            nm_utils_strbuf_append_str(&buf, &len, " goto none");
+            nm_strbuf_append_str(&buf, &len, " goto none");
         rr_flags = NM_FLAGS_UNSET(rr_flags, FIB_RULE_UNRESOLVED);
         if (NM_FLAGS_HAS(routing_rule->flags, FIB_RULE_UNRESOLVED))
-            nm_utils_strbuf_append_str(&buf, &len, " unresolved");
+            nm_strbuf_append_str(&buf, &len, " unresolved");
     } else if (routing_rule->action != FR_ACT_TO_TBL) {
         char ss_buf[60];
 
-        nm_utils_strbuf_append(&buf,
-                               &len,
-                               " %s",
-                               nm_net_aux_rtnl_rtntype_n2a(routing_rule->action)
-                                   ?: nm_sprintf_buf(ss_buf, "action-%u", routing_rule->action));
+        nm_strbuf_append(&buf,
+                         &len,
+                         " %s",
+                         nm_net_aux_rtnl_rtntype_n2a(routing_rule->action)
+                             ?: nm_sprintf_buf(ss_buf, "action-%u", routing_rule->action));
     }
 
     if (routing_rule->protocol != RTPROT_UNSPEC)
-        nm_utils_strbuf_append(&buf, &len, " protocol %u", routing_rule->protocol);
+        nm_strbuf_append(&buf, &len, " protocol %u", routing_rule->protocol);
 
     if (routing_rule->goto_target != 0 && routing_rule->action != FR_ACT_GOTO) {
         /* a trailing target is set for an unexpected action. Print it. */
-        nm_utils_strbuf_append(&buf, &len, " goto-target %u", routing_rule->goto_target);
+        nm_strbuf_append(&buf, &len, " goto-target %u", routing_rule->goto_target);
     }
 
     if (rr_flags != 0) {
         /* we have some flags we didn't print about yet. */
-        nm_utils_strbuf_append(&buf, &len, " remaining-flags %x", rr_flags);
+        nm_strbuf_append(&buf, &len, " remaining-flags %x", rr_flags);
     }
 
     return buf0;
@@ -7012,53 +6948,53 @@ nm_platform_qdisc_to_string(const NMPlatformQdisc *qdisc, char *buf, gsize len)
 
     buf0 = buf;
 
-    nm_utils_strbuf_append(&buf,
-                           &len,
-                           "%s%s family %u handle %x parent %x info %x",
-                           qdisc->kind,
-                           _to_string_dev(NULL, qdisc->ifindex, str_dev, sizeof(str_dev)),
-                           qdisc->addr_family,
-                           qdisc->handle,
-                           qdisc->parent,
-                           qdisc->info);
+    nm_strbuf_append(&buf,
+                     &len,
+                     "%s%s family %u handle %x parent %x info %x",
+                     qdisc->kind,
+                     _to_string_dev(NULL, qdisc->ifindex, str_dev, sizeof(str_dev)),
+                     qdisc->addr_family,
+                     qdisc->handle,
+                     qdisc->parent,
+                     qdisc->info);
 
     if (nm_streq0(qdisc->kind, "fq_codel")) {
         if (qdisc->fq_codel.limit)
-            nm_utils_strbuf_append(&buf, &len, " limit %u", qdisc->fq_codel.limit);
+            nm_strbuf_append(&buf, &len, " limit %u", qdisc->fq_codel.limit);
         if (qdisc->fq_codel.flows)
-            nm_utils_strbuf_append(&buf, &len, " flows %u", qdisc->fq_codel.flows);
+            nm_strbuf_append(&buf, &len, " flows %u", qdisc->fq_codel.flows);
         if (qdisc->fq_codel.target)
-            nm_utils_strbuf_append(&buf, &len, " target %u", qdisc->fq_codel.target);
+            nm_strbuf_append(&buf, &len, " target %u", qdisc->fq_codel.target);
         if (qdisc->fq_codel.interval)
-            nm_utils_strbuf_append(&buf, &len, " interval %u", qdisc->fq_codel.interval);
+            nm_strbuf_append(&buf, &len, " interval %u", qdisc->fq_codel.interval);
         if (qdisc->fq_codel.quantum)
-            nm_utils_strbuf_append(&buf, &len, " quantum %u", qdisc->fq_codel.quantum);
+            nm_strbuf_append(&buf, &len, " quantum %u", qdisc->fq_codel.quantum);
         if (qdisc->fq_codel.ce_threshold != NM_PLATFORM_FQ_CODEL_CE_THRESHOLD_DISABLED)
-            nm_utils_strbuf_append(&buf, &len, " ce_threshold %u", qdisc->fq_codel.ce_threshold);
+            nm_strbuf_append(&buf, &len, " ce_threshold %u", qdisc->fq_codel.ce_threshold);
         if (qdisc->fq_codel.memory_limit != NM_PLATFORM_FQ_CODEL_MEMORY_LIMIT_UNSET)
-            nm_utils_strbuf_append(&buf, &len, " memory_limit %u", qdisc->fq_codel.memory_limit);
+            nm_strbuf_append(&buf, &len, " memory_limit %u", qdisc->fq_codel.memory_limit);
         if (qdisc->fq_codel.ecn)
-            nm_utils_strbuf_append(&buf, &len, " ecn");
+            nm_strbuf_append(&buf, &len, " ecn");
     } else if (nm_streq0(qdisc->kind, "sfq")) {
         if (qdisc->sfq.quantum)
-            nm_utils_strbuf_append(&buf, &len, " quantum %u", qdisc->sfq.quantum);
+            nm_strbuf_append(&buf, &len, " quantum %u", qdisc->sfq.quantum);
         if (qdisc->sfq.perturb_period)
-            nm_utils_strbuf_append(&buf, &len, " perturb %d", qdisc->sfq.perturb_period);
+            nm_strbuf_append(&buf, &len, " perturb %d", qdisc->sfq.perturb_period);
         if (qdisc->sfq.limit)
-            nm_utils_strbuf_append(&buf, &len, " limit %u", (guint) qdisc->sfq.limit);
+            nm_strbuf_append(&buf, &len, " limit %u", (guint) qdisc->sfq.limit);
         if (qdisc->sfq.divisor)
-            nm_utils_strbuf_append(&buf, &len, " divisor %u", qdisc->sfq.divisor);
+            nm_strbuf_append(&buf, &len, " divisor %u", qdisc->sfq.divisor);
         if (qdisc->sfq.flows)
-            nm_utils_strbuf_append(&buf, &len, " flows %u", qdisc->sfq.flows);
+            nm_strbuf_append(&buf, &len, " flows %u", qdisc->sfq.flows);
         if (qdisc->sfq.depth)
-            nm_utils_strbuf_append(&buf, &len, " depth %u", qdisc->sfq.depth);
+            nm_strbuf_append(&buf, &len, " depth %u", qdisc->sfq.depth);
     } else if (nm_streq0(qdisc->kind, "tbf")) {
-        nm_utils_strbuf_append(&buf, &len, " rate %" G_GUINT64_FORMAT, qdisc->tbf.rate);
-        nm_utils_strbuf_append(&buf, &len, " burst %u", qdisc->tbf.burst);
+        nm_strbuf_append(&buf, &len, " rate %" G_GUINT64_FORMAT, qdisc->tbf.rate);
+        nm_strbuf_append(&buf, &len, " burst %u", qdisc->tbf.burst);
         if (qdisc->tbf.limit)
-            nm_utils_strbuf_append(&buf, &len, " limit %u", qdisc->tbf.limit);
+            nm_strbuf_append(&buf, &len, " limit %u", qdisc->tbf.limit);
         if (qdisc->tbf.latency)
-            nm_utils_strbuf_append(&buf, &len, " latency %uns", qdisc->tbf.latency);
+            nm_strbuf_append(&buf, &len, " latency %uns", qdisc->tbf.latency);
     }
 
     return buf0;
@@ -7153,11 +7089,11 @@ nm_platform_tfilter_to_string(const NMPlatformTfilter *tfilter, char *buf, gsize
         p = act_buf;
         l = sizeof(act_buf);
 
-        nm_utils_strbuf_append(&p, &l, " \"%s\"", tfilter->action.kind);
+        nm_strbuf_append(&p, &l, " \"%s\"", tfilter->action.kind);
         if (nm_streq(tfilter->action.kind, NM_PLATFORM_ACTION_KIND_SIMPLE)) {
             gs_free char *t = NULL;
 
-            nm_utils_strbuf_append(
+            nm_strbuf_append(
                 &p,
                 &l,
                 " (\"%s\")",
@@ -7166,14 +7102,14 @@ nm_platform_tfilter_to_string(const NMPlatformTfilter *tfilter, char *buf, gsize
                                                  | NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII,
                                              &t));
         } else if (nm_streq(tfilter->action.kind, NM_PLATFORM_ACTION_KIND_MIRRED)) {
-            nm_utils_strbuf_append(&p,
-                                   &l,
-                                   "%s%s%s%s dev %d",
-                                   tfilter->action.mirred.ingress ? " ingress" : "",
-                                   tfilter->action.mirred.egress ? " egress" : "",
-                                   tfilter->action.mirred.mirror ? " mirror" : "",
-                                   tfilter->action.mirred.redirect ? " redirect" : "",
-                                   tfilter->action.mirred.ifindex);
+            nm_strbuf_append(&p,
+                             &l,
+                             "%s%s%s%s dev %d",
+                             tfilter->action.mirred.ingress ? " ingress" : "",
+                             tfilter->action.mirred.egress ? " egress" : "",
+                             tfilter->action.mirred.mirror ? " mirror" : "",
+                             tfilter->action.mirred.redirect ? " redirect" : "",
+                             tfilter->action.mirred.ifindex);
         }
     } else
         act_buf[0] = '\0';
@@ -7345,6 +7281,9 @@ nm_platform_link_hash_update(const NMPlatformLink *obj, NMHashState *h)
                        obj->l_address.data,
                        NM_MIN(obj->l_address.len, sizeof(obj->l_address.data)));
     nm_hash_update_mem(h,
+                       obj->l_perm_address.data,
+                       NM_MIN(obj->l_perm_address.len, sizeof(obj->l_perm_address.data)));
+    nm_hash_update_mem(h,
                        obj->l_broadcast.data,
                        NM_MIN(obj->l_broadcast.len, sizeof(obj->l_broadcast.data)));
 }
@@ -7364,12 +7303,15 @@ nm_platform_link_cmp(const NMPlatformLink *a, const NMPlatformLink *b)
     NM_CMP_FIELD_BOOL(a, b, initialized);
     NM_CMP_FIELD(a, b, arptype);
     NM_CMP_FIELD(a, b, l_address.len);
+    NM_CMP_FIELD(a, b, l_perm_address.len);
     NM_CMP_FIELD(a, b, l_broadcast.len);
     NM_CMP_FIELD(a, b, inet6_addr_gen_mode_inv);
     NM_CMP_FIELD_STR_INTERNED(a, b, kind);
     NM_CMP_FIELD_STR_INTERNED(a, b, driver);
     if (a->l_address.len)
         NM_CMP_FIELD_MEMCMP_LEN(a, b, l_address.data, a->l_address.len);
+    if (a->l_perm_address.len)
+        NM_CMP_FIELD_MEMCMP_LEN(a, b, l_perm_address.data, a->l_perm_address.len);
     if (a->l_broadcast.len)
         NM_CMP_FIELD_MEMCMP_LEN(a, b, l_broadcast.data, a->l_broadcast.len);
     NM_CMP_FIELD_MEMCMP(a, b, inet6_token);
@@ -7853,20 +7795,20 @@ nm_platform_ip6_address_pretty_sort_cmp(const NMPlatformIP6Address *a1,
     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_TEMPORARY);
-    ipv6_privacy2 = NM_FLAGS_ANY(a2->n_ifa_flags, IFA_F_MANAGETEMPADDR | IFA_F_TEMPORARY);
+    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_TEMPORARY)
+            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_TEMPORARY)
+            if (a2->n_ifa_flags & IFA_F_SECONDARY)
                 public2 = prefer_temp;
             else
                 public2 = !prefer_temp;
@@ -7905,7 +7847,8 @@ nm_platform_ip4_address_hash_update(const NMPlatformIP4Address *obj, NMHashState
                         NM_HASH_COMBINE_BOOLS(guint8,
                                               obj->external,
                                               obj->use_ip4_broadcast_address,
-                                              obj->ip4acd_not_ready));
+                                              obj->a_acd_not_ready,
+                                              obj->a_assume_config_once));
     nm_hash_update_strarr(h, obj->label);
 }
 
@@ -7927,7 +7870,8 @@ nm_platform_ip4_address_cmp(const NMPlatformIP4Address *a, const NMPlatformIP4Ad
     NM_CMP_FIELD(a, b, n_ifa_flags);
     NM_CMP_FIELD_STR(a, b, label);
     NM_CMP_FIELD_UNSAFE(a, b, external);
-    NM_CMP_FIELD_UNSAFE(a, b, ip4acd_not_ready);
+    NM_CMP_FIELD_UNSAFE(a, b, a_acd_not_ready);
+    NM_CMP_FIELD_UNSAFE(a, b, a_assume_config_once);
     return 0;
 }
 
@@ -7944,7 +7888,7 @@ nm_platform_ip6_address_hash_update(const NMPlatformIP6Address *obj, NMHashState
                         obj->plen,
                         obj->address,
                         obj->peer_address,
-                        NM_HASH_COMBINE_BOOLS(guint8, obj->external));
+                        NM_HASH_COMBINE_BOOLS(guint8, obj->external, obj->a_assume_config_once));
 }
 
 int
@@ -7965,6 +7909,7 @@ nm_platform_ip6_address_cmp(const NMPlatformIP6Address *a, const NMPlatformIP6Ad
     NM_CMP_FIELD(a, b, preferred);
     NM_CMP_FIELD(a, b, n_ifa_flags);
     NM_CMP_FIELD_UNSAFE(a, b, external);
+    NM_CMP_FIELD_UNSAFE(a, b, a_assume_config_once);
     return 0;
 }
 
@@ -8065,7 +8010,7 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
                             obj->initrwnd,
                             obj->mtu,
                             obj->r_rtm_flags,
-                            NM_HASH_COMBINE_BOOLS(guint8,
+                            NM_HASH_COMBINE_BOOLS(guint16,
                                                   obj->metric_any,
                                                   obj->table_any,
                                                   obj->lock_window,
@@ -8073,7 +8018,8 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
                                                   obj->lock_initcwnd,
                                                   obj->lock_initrwnd,
                                                   obj->lock_mtu,
-                                                  obj->is_external));
+                                                  obj->is_external,
+                                                  obj->r_assume_config_once));
         break;
     }
 }
@@ -8163,8 +8109,10 @@ nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
         NM_CMP_FIELD(a, b, initcwnd);
         NM_CMP_FIELD(a, b, initrwnd);
         NM_CMP_FIELD(a, b, mtu);
-        if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL)
+        if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL) {
             NM_CMP_FIELD_UNSAFE(a, b, is_external);
+            NM_CMP_FIELD_UNSAFE(a, b, r_assume_config_once);
+        }
         break;
     }
     return 0;
@@ -8249,7 +8197,7 @@ nm_platform_ip6_route_hash_update(const NMPlatformIP6Route *obj,
                             obj->rt_source,
                             obj->mss,
                             obj->r_rtm_flags,
-                            NM_HASH_COMBINE_BOOLS(guint8,
+                            NM_HASH_COMBINE_BOOLS(guint16,
                                                   obj->metric_any,
                                                   obj->table_any,
                                                   obj->lock_window,
@@ -8257,7 +8205,8 @@ nm_platform_ip6_route_hash_update(const NMPlatformIP6Route *obj,
                                                   obj->lock_initcwnd,
                                                   obj->lock_initrwnd,
                                                   obj->lock_mtu,
-                                                  obj->is_external),
+                                                  obj->is_external,
+                                                  obj->r_assume_config_once),
                             obj->window,
                             obj->cwnd,
                             obj->initcwnd,
@@ -8340,8 +8289,10 @@ nm_platform_ip6_route_cmp(const NMPlatformIP6Route *a,
             NM_CMP_DIRECT(_route_pref_normalize(a->rt_pref), _route_pref_normalize(b->rt_pref));
         else
             NM_CMP_FIELD(a, b, rt_pref);
-        if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL)
+        if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL) {
             NM_CMP_FIELD_UNSAFE(a, b, is_external);
+            NM_CMP_FIELD_UNSAFE(a, b, r_assume_config_once);
+        }
         break;
     }
     return 0;
@@ -8627,6 +8578,57 @@ nm_platform_ip4_address_addr_to_hash(NMPlatform *self, int ifindex)
 
 /*****************************************************************************/
 
+NMPlatformIP4Route *
+nm_platform_ip4_address_generate_device_route(const NMPlatformIP4Address *addr,
+                                              int                         ifindex,
+                                              guint32                     route_table,
+                                              guint32                     route_metric,
+                                              NMPlatformIP4Route *        dst)
+{
+    in_addr_t network_4;
+
+    /* When you add an IPv4 address (without "noprefixroute" flag), then kernel will
+     * automatically add a device route for the IPv4 subnet. This function generates
+     * such a route for the given address. */
+
+    nm_assert(addr);
+    nm_assert(addr->plen <= 32);
+    if (addr->plen == 0)
+        return NULL;
+
+    if (addr->plen == 0)
+        return NULL;
+
+    network_4 = nm_utils_ip4_address_clear_host_address(addr->peer_address, addr->plen);
+
+    if (nm_utils_ip4_address_is_zeronet(network_4)) {
+        /* Kernel doesn't add device-routes for destinations that
+         * start with 0.x.y.z. Skip them. */
+        return NULL;
+    }
+
+    if (addr->plen == 32 && addr->address == addr->peer_address) {
+        /* Kernel doesn't add device-routes for /32 addresses unless
+         * they have a peer. */
+        return NULL;
+    }
+
+    *dst = (NMPlatformIP4Route){
+        .ifindex       = ifindex,
+        .rt_source     = NM_IP_CONFIG_SOURCE_KERNEL,
+        .network       = network_4,
+        .plen          = addr->plen,
+        .pref_src      = addr->address,
+        .table_coerced = nm_platform_route_table_coerce(route_table),
+        .metric        = route_metric,
+        .scope_inv     = nm_platform_route_scope_inv(NM_RT_SCOPE_LINK),
+    };
+
+    nm_platform_ip_route_normalize(AF_INET, (NMPlatformIPRoute *) dst);
+
+    return dst;
+}
+
 const char *
 nm_platform_signal_change_type_to_string(NMPlatformSignalChangeType change_type)
 {
@@ -8918,6 +8920,10 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps
         /* construct-only */
         priv->log_with_ptr = g_value_get_boolean(value);
         break;
+    case PROP_CACHE_TC:
+        /* construct-only */
+        priv->cache_tc = g_value_get_boolean(value);
+        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
         break;
@@ -9003,6 +9009,15 @@ nm_platform_class_init(NMPlatformClass *platform_class)
                              TRUE,
                              G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
 
+    g_object_class_install_property(
+        object_class,
+        PROP_CACHE_TC,
+        g_param_spec_boolean(NM_PLATFORM_CACHE_TC,
+                             "",
+                             "",
+                             FALSE,
+                             G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
 #define SIGNAL(signal, signal_id, method)                                                \
     G_STMT_START                                                                         \
     {                                                                                    \
diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h
index 9d40cbbe..7101b48c 100644
--- a/src/libnm-platform/nm-platform.h
+++ b/src/libnm-platform/nm-platform.h
@@ -25,6 +25,7 @@
 #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"
 
 /*****************************************************************************/
 
@@ -42,10 +43,10 @@ typedef gboolean (*NMPObjectPredicateFunc)(const NMPObject *obj, gpointer user_d
 
 /* workaround for older libnl version, that does not define these flags. */
 #ifndef IFA_F_MANAGETEMPADDR
-    #define IFA_F_MANAGETEMPADDR 0x100
+#define IFA_F_MANAGETEMPADDR 0x100
 #endif
 #ifndef IFA_F_NOPREFIXROUTE
-    #define IFA_F_NOPREFIXROUTE 0x200
+#define IFA_F_NOPREFIXROUTE 0x200
 #endif
 
 #define NM_RT_SCOPE_LINK 253 /* RT_SCOPE_LINK */
@@ -231,6 +232,9 @@ struct _NMPlatformLink {
     /* IFLA_ADDRESS */
     NMPLinkAddress l_address;
 
+    /* IFLA_PERM_ADDRESS */
+    NMPLinkAddress l_perm_address;
+
     /* IFLA_BROADCAST */
     NMPLinkAddress l_broadcast;
 
@@ -314,18 +318,18 @@ typedef enum {
      * IFA_FLAGS attribute. */         \
     guint32 n_ifa_flags;                                                                     \
                                                                                              \
-    guint8 plen;                                                                             \
-                                                                                             \
     /* FIXME(l3cfg): the external marker won't be necessary anymore, because we only
      * merge addresses we care about, and ignore (don't remove) external addresses. */         \
     bool external : 1;                                                                       \
                                                                                              \
     bool use_ip4_broadcast_address : 1;                                                      \
                                                                                              \
-    /* 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 ip4acd_not_ready : 1;                                                               \
+    /* Whether the address is should be configured once during assume. This is a meta flag
+     * that is not honored by NMPlatform (netlink code). Instead, it can be used by the upper
+     * layers which use NMPlatformIPAddress to track addresses that should be configured. */   \
+    bool a_assume_config_once : 1;                                                           \
+                                                                                             \
+    guint8 plen;                                                                             \
     ;
 
 /**
@@ -348,6 +352,11 @@ typedef struct {
 struct _NMPlatformIP4Address {
     __NMPlatformIPAddress_COMMON;
 
+    /* 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;
+
     /* The local address IFA_LOCAL. */
     in_addr_t address;
 
@@ -470,6 +479,11 @@ typedef union {
      * and is not reflected on netlink. */   \
     bool is_external : 1;                                                                 \
                                                                                           \
+    /* Whether the route is should be configured once during assume. This is a meta flag
+     * that is not honored by NMPlatform (netlink code). Instead, it can be used by the upper
+     * layers which use NMPlatformIPRoute to track routes that should be configured. */  \
+    bool r_assume_config_once : 1;                                                        \
+                                                                                          \
     /* rtnh_flags
      *
      * Routes with rtm_flags RTM_F_CLONED are hidden by platform and
@@ -518,8 +532,7 @@ typedef union {
      * it means RTN_UNSPEC otherwise the type value is preserved.
      * */                                                                          \
     guint8 type_coerced;                                                                  \
-                                                                                          \
-    /*end*/
+    ;
 
 typedef struct {
     __NMPlatformIPRoute_COMMON;
@@ -613,6 +626,10 @@ typedef union {
 
 #undef __NMPlatformIPRoute_COMMON
 
+#define NM_PLATFORM_IP4_ROUTE_INIT(...) (&((const NMPlatformIP4Route){__VA_ARGS__}))
+
+#define NM_PLATFORM_IP6_ROUTE_INIT(...) (&((const NMPlatformIP6Route){__VA_ARGS__}))
+
 typedef struct {
     /* struct fib_rule_uid_range */
     guint32 start;
@@ -1008,13 +1025,11 @@ typedef void (*NMPlatformAsyncCallback)(GError *error, gpointer user_data);
 /*****************************************************************************/
 
 typedef enum {
-    NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS,
-    NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL,
-    NM_PLATFORM_KERNEL_SUPPORT_TYPE_RTA_PREF,
     NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_L3MDEV,
     NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_UID_RANGE,
     NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_PROTOCOL,
     NM_PLATFORM_KERNEL_SUPPORT_TYPE_IFLA_BR_VLAN_STATS_ENABLED,
+    NM_PLATFORM_KERNEL_SUPPORT_TYPE_IFLA_PERM_ADDRESS,
 
     /* this also includes FRA_SPORT_RANGE and FRA_DPORT_RANGE which
      * were added at the same time. */
@@ -1105,13 +1120,12 @@ typedef struct {
                              unsigned    flags_mask,
                              unsigned    flags_set);
 
-    int (*link_set_user_ipv6ll_enabled)(NMPlatform *self, int ifindex, gboolean enabled);
-    gboolean (*link_set_token)(NMPlatform *self, int ifindex, NMUtilsIPv6IfaceId iid);
+    int (*link_set_inet6_addr_gen_mode)(NMPlatform *self, int ifindex, guint8 enabled);
+    gboolean (*link_set_token)(NMPlatform *self, int ifindex, const NMUtilsIPv6IfaceId *iid);
 
-    gboolean (*link_get_permanent_address)(NMPlatform *self,
-                                           int         ifindex,
-                                           guint8 *    buf,
-                                           size_t *    length);
+    gboolean (*link_get_permanent_address_ethtool)(NMPlatform *    self,
+                                                   int             ifindex,
+                                                   NMPLinkAddress *out_address);
     int (*link_set_address)(NMPlatform *self, int ifindex, gconstpointer address, size_t length);
     int (*link_set_mtu)(NMPlatform *self, int ifindex, guint32 mtu);
     gboolean (*link_set_name)(NMPlatform *self, int ifindex, const char *name);
@@ -1253,8 +1267,10 @@ typedef struct {
                             const NMPlatformRoutingRule *routing_rule);
 
     int (*qdisc_add)(NMPlatform *self, NMPNlmFlags flags, const NMPlatformQdisc *qdisc);
+    int (*qdisc_delete)(NMPlatform *self, int ifindex, guint32 parent, gboolean log_error);
 
     int (*tfilter_add)(NMPlatform *self, NMPNlmFlags flags, const NMPlatformTfilter *tfilter);
+    int (*tfilter_delete)(NMPlatform *self, int ifindex, guint32 parent, gboolean log_error);
 } NMPlatformClass;
 
 /* NMPlatform signals
@@ -1391,6 +1407,14 @@ _nm_platform_uint8_inv(guint8 scope)
     return (guint8) ~scope;
 }
 
+static inline int
+_nm_platform_link_get_inet6_addr_gen_mode(const NMPlatformLink *pllink)
+{
+    if (!pllink)
+        return -ENODEV;
+    return _nm_platform_uint8_inv(pllink->inet6_addr_gen_mode_inv);
+}
+
 /**
  * nm_platform_route_type_coerce:
  * @table: the route type, in its original value.
@@ -1426,6 +1450,7 @@ nm_platform_route_type_uncoerce(guint8 type_coerced)
 
 gboolean nm_platform_get_use_udev(NMPlatform *self);
 gboolean nm_platform_get_log_with_ptr(NMPlatform *self);
+gboolean nm_platform_get_cache_tc(NMPlatform *self);
 
 NMPNetns *nm_platform_netns_get(NMPlatform *self);
 gboolean  nm_platform_netns_push(NMPlatform *self, NMPNetns **netns);
@@ -1808,7 +1833,7 @@ gboolean    nm_platform_link_is_up(NMPlatform *self, int ifindex);
 gboolean    nm_platform_link_is_connected(NMPlatform *self, int ifindex);
 gboolean    nm_platform_link_uses_arp(NMPlatform *self, int ifindex);
 guint32     nm_platform_link_get_mtu(NMPlatform *self, int ifindex);
-gboolean    nm_platform_link_get_user_ipv6ll_enabled(NMPlatform *self, int ifindex);
+int         nm_platform_link_get_inet6_addr_gen_mode(NMPlatform *self, int ifindex);
 
 gconstpointer nm_platform_link_get_address(NMPlatform *self, int ifindex, size_t *length);
 
@@ -1858,11 +1883,16 @@ const char *nm_platform_link_get_path(NMPlatform *self, int ifindex);
 
 struct udev_device *nm_platform_link_get_udev_device(NMPlatform *self, int ifindex);
 
-int      nm_platform_link_set_user_ipv6ll_enabled(NMPlatform *self, int ifindex, gboolean enabled);
-gboolean nm_platform_link_set_ipv6_token(NMPlatform *self, int ifindex, NMUtilsIPv6IfaceId iid);
-
+int nm_platform_link_set_inet6_addr_gen_mode(NMPlatform *self, int ifindex, guint8 mode);
 gboolean
-nm_platform_link_get_permanent_address(NMPlatform *self, int ifindex, guint8 *buf, size_t *length);
+nm_platform_link_set_ipv6_token(NMPlatform *self, int ifindex, const NMUtilsIPv6IfaceId *iid);
+
+gboolean nm_platform_link_get_permanent_address_ethtool(NMPlatform *    self,
+                                                        int             ifindex,
+                                                        NMPLinkAddress *out_address);
+gboolean nm_platform_link_get_permanent_address(NMPlatform *          self,
+                                                const NMPlatformLink *plink,
+                                                NMPLinkAddress *      out_address);
 int nm_platform_link_set_address(NMPlatform *self, int ifindex, const void *address, size_t length);
 int nm_platform_link_set_mtu(NMPlatform *self, int ifindex, guint32 mtu);
 gboolean nm_platform_link_set_name(NMPlatform *self, int ifindex, const char *name);
@@ -2113,7 +2143,7 @@ nm_platform_ip6_address_sync(NMPlatform *self,
 
 gboolean nm_platform_ip_address_flush(NMPlatform *self, int addr_family, int ifindex);
 
-static inline gconstpointer
+static inline gpointer
 nm_platform_ip_address_get_peer_address(int addr_family, const NMPlatformIPAddress *addr)
 {
     nm_assert_addr_family(addr_family);
@@ -2193,11 +2223,14 @@ int nm_platform_routing_rule_add(NMPlatform *                 self,
                                  NMPNlmFlags                  flags,
                                  const NMPlatformRoutingRule *routing_rule);
 
-int      nm_platform_qdisc_add(NMPlatform *self, NMPNlmFlags flags, const NMPlatformQdisc *qdisc);
-gboolean nm_platform_qdisc_sync(NMPlatform *self, int ifindex, GPtrArray *known_qdiscs);
-
+int nm_platform_qdisc_add(NMPlatform *self, NMPNlmFlags flags, const NMPlatformQdisc *qdisc);
+int nm_platform_qdisc_delete(NMPlatform *self, int ifindex, guint32 parent, gboolean log_error);
 int nm_platform_tfilter_add(NMPlatform *self, NMPNlmFlags flags, const NMPlatformTfilter *tfilter);
-gboolean nm_platform_tfilter_sync(NMPlatform *self, int ifindex, GPtrArray *known_tfilters);
+int nm_platform_tfilter_delete(NMPlatform *self, int ifindex, guint32 parent, gboolean log_error);
+gboolean nm_platform_tc_sync(NMPlatform *self,
+                             int         ifindex,
+                             GPtrArray * known_qdiscs,
+                             GPtrArray * known_tfilters);
 
 const char *nm_platform_link_to_string(const NMPlatformLink *link, char *buf, gsize len);
 const char *nm_platform_lnk_bridge_to_string(const NMPlatformLnkBridge *lnk, char *buf, gsize len);
@@ -2389,6 +2422,14 @@ 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,
+                                                                  NMPlatformIP4Route *dst);
+
+/*****************************************************************************/
+
 gboolean nm_platform_ip_address_match(int                        addr_family,
                                       const NMPlatformIPAddress *addr,
                                       NMPlatformMatchFlags       match_flag);
diff --git a/src/libnm-platform/nmp-netns.c b/src/libnm-platform/nmp-netns.c
index 2b28a4cd..d06c3e11 100644
--- a/src/libnm-platform/nmp-netns.c
+++ b/src/libnm-platform/nmp-netns.c
@@ -43,22 +43,20 @@ __ns_types_to_str(int ns_types, int ns_types_already_set, char *buf, gsize len)
     const char *b = buf;
     char        bb[200];
 
-    nm_utils_strbuf_append_c(&buf, &len, '[');
+    nm_strbuf_append_c(&buf, &len, '[');
     if (ns_types & ~ns_types_already_set) {
-        nm_utils_strbuf_append_str(
-            &buf,
-            &len,
-            _clone_ns_to_str(ns_types & ~ns_types_already_set, bb, sizeof(bb)));
+        nm_strbuf_append_str(&buf,
+                             &len,
+                             _clone_ns_to_str(ns_types & ~ns_types_already_set, bb, sizeof(bb)));
     }
     if (ns_types & ns_types_already_set) {
         if (ns_types & ~ns_types_already_set)
-            nm_utils_strbuf_append_c(&buf, &len, '/');
-        nm_utils_strbuf_append_str(
-            &buf,
-            &len,
-            _clone_ns_to_str(ns_types & ns_types_already_set, bb, sizeof(bb)));
+            nm_strbuf_append_c(&buf, &len, '/');
+        nm_strbuf_append_str(&buf,
+                             &len,
+                             _clone_ns_to_str(ns_types & ns_types_already_set, bb, sizeof(bb)));
     }
-    nm_utils_strbuf_append_c(&buf, &len, ']');
+    nm_strbuf_append_c(&buf, &len, ']');
     return b;
 }
 #define _ns_types_to_str(ns_types, ns_types_already_set, buf) \
diff --git a/src/libnm-platform/nmp-object.c b/src/libnm-platform/nmp-object.c
index a7a46ef0..559fd8d6 100644
--- a/src/libnm-platform/nmp-object.c
+++ b/src/libnm-platform/nmp-object.c
@@ -949,30 +949,30 @@ _vt_cmd_obj_to_string_link(const NMPObject *     obj,
     case NMP_OBJECT_TO_STRING_ID:
         return klass->cmd_plobj_to_string_id(&obj->object, buf, buf_size);
     case NMP_OBJECT_TO_STRING_ALL:
-        nm_utils_strbuf_append(&b,
-                               &buf_size,
-                               "[%s,%p,%u,%calive,%cvisible,%cin-nl,%p; ",
-                               klass->obj_type_name,
-                               obj,
-                               obj->parent._ref_count,
-                               nmp_object_is_alive(obj) ? '+' : '-',
-                               nmp_object_is_visible(obj) ? '+' : '-',
-                               obj->_link.netlink.is_in_netlink ? '+' : '-',
-                               obj->_link.udev.device);
+        nm_strbuf_append(&b,
+                         &buf_size,
+                         "[%s,%p,%u,%calive,%cvisible,%cin-nl,%p; ",
+                         klass->obj_type_name,
+                         obj,
+                         obj->parent._ref_count,
+                         nmp_object_is_alive(obj) ? '+' : '-',
+                         nmp_object_is_visible(obj) ? '+' : '-',
+                         obj->_link.netlink.is_in_netlink ? '+' : '-',
+                         obj->_link.udev.device);
         NMP_OBJECT_GET_CLASS(obj)->cmd_plobj_to_string(&obj->object, b, buf_size);
-        nm_utils_strbuf_seek_end(&b, &buf_size);
+        nm_strbuf_seek_end(&b, &buf_size);
         if (obj->_link.netlink.lnk) {
-            nm_utils_strbuf_append_str(&b, &buf_size, "; ");
+            nm_strbuf_append_str(&b, &buf_size, "; ");
             nmp_object_to_string(obj->_link.netlink.lnk, NMP_OBJECT_TO_STRING_ALL, b, buf_size);
-            nm_utils_strbuf_seek_end(&b, &buf_size);
+            nm_strbuf_seek_end(&b, &buf_size);
         }
-        nm_utils_strbuf_append_c(&b, &buf_size, ']');
+        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);
         if (obj->_link.netlink.lnk) {
-            nm_utils_strbuf_seek_end(&b, &buf_size);
-            nm_utils_strbuf_append_str(&b, &buf_size, "; ");
+            nm_strbuf_seek_end(&b, &buf_size);
+            nm_strbuf_append_str(&b, &buf_size, "; ");
             nmp_object_to_string(obj->_link.netlink.lnk, NMP_OBJECT_TO_STRING_PUBLIC, b, buf_size);
         }
         return buf;
@@ -1065,29 +1065,28 @@ _vt_cmd_obj_to_string_lnk_wireguard(const NMPObject *     obj,
     case NMP_OBJECT_TO_STRING_ALL:
         b = buf;
 
-        nm_utils_strbuf_append(
-            &b,
-            &buf_size,
-            "[%s,%p,%u,%calive,%cvisible; %s"
-            "%s",
-            klass->obj_type_name,
-            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)),
-            obj->_lnk_wireguard.peers_len > 0 ? " peers {" : "");
+        nm_strbuf_append(&b,
+                         &buf_size,
+                         "[%s,%p,%u,%calive,%cvisible; %s"
+                         "%s",
+                         klass->obj_type_name,
+                         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)),
+                         obj->_lnk_wireguard.peers_len > 0 ? " peers {" : "");
 
         for (i = 0; i < obj->_lnk_wireguard.peers_len; i++) {
             const NMPWireGuardPeer *peer = &obj->_lnk_wireguard.peers[i];
 
-            nm_utils_strbuf_append_str(&b, &buf_size, " { ");
+            nm_strbuf_append_str(&b, &buf_size, " { ");
             nm_platform_wireguard_peer_to_string(peer, b, buf_size);
-            nm_utils_strbuf_seek_end(&b, &buf_size);
-            nm_utils_strbuf_append_str(&b, &buf_size, " }");
+            nm_strbuf_seek_end(&b, &buf_size);
+            nm_strbuf_append_str(&b, &buf_size, " }");
         }
         if (obj->_lnk_wireguard.peers_len)
-            nm_utils_strbuf_append_str(&b, &buf_size, " }");
+            nm_strbuf_append_str(&b, &buf_size, " }");
 
         return buf;
     case NMP_OBJECT_TO_STRING_PUBLIC:
@@ -1199,9 +1198,11 @@ _vt_cmd_obj_hash_update_lnk_wireguard(const NMPObject *obj, NMHashState *h)
 }
 
 int
-nmp_object_cmp(const NMPObject *obj1, const NMPObject *obj2)
+nmp_object_cmp_full(const NMPObject *obj1, const NMPObject *obj2, NMPObjectCmpFlags flags)
 {
-    const NMPClass *klass1, *klass2;
+    const NMPClass *klass1;
+    const NMPClass *klass2;
+    NMPObject       obj_stackcopy;
 
     NM_CMP_SELF(obj1, obj2);
 
@@ -1216,6 +1217,22 @@ nmp_object_cmp(const NMPObject *obj1, const NMPObject *obj2)
         return klass1->obj_type < klass2->obj_type ? -1 : 1;
     }
 
+    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))) {
+            /* 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);
+            obj_stackcopy.obj_with_ifindex.ifindex = obj1->obj_with_ifindex.ifindex;
+            obj2                                   = &obj_stackcopy;
+        }
+    }
+
     if (klass1->cmd_obj_cmp)
         return klass1->cmd_obj_cmp(obj1, obj2);
     return klass1->cmd_plobj_cmp(&obj1->object, &obj2->object);
@@ -2079,6 +2096,9 @@ nmp_lookup_init_obj_type(NMPLookup *lookup, NMPObjectType obj_type)
     nm_assert(lookup);
 
     switch (obj_type) {
+    default:
+        nm_assert_not_reached();
+        /* fall-through */
     case NMP_OBJECT_TYPE_LINK:
     case NMP_OBJECT_TYPE_IP4_ADDRESS:
     case NMP_OBJECT_TYPE_IP6_ADDRESS:
@@ -2090,9 +2110,6 @@ nmp_lookup_init_obj_type(NMPLookup *lookup, NMPObjectType obj_type)
         _nmp_object_stackinit_from_type(&lookup->selector_obj, obj_type);
         lookup->cache_id_type = NMP_CACHE_ID_TYPE_OBJECT_TYPE;
         return _L(lookup);
-    default:
-        nm_assert_not_reached();
-        return NULL;
     }
 }
 
@@ -2107,7 +2124,7 @@ nmp_lookup_init_link_by_ifname(NMPLookup *lookup, const char *ifname)
 
     o = _nmp_object_stackinit_from_type(&lookup->selector_obj, NMP_OBJECT_TYPE_LINK);
     if (g_strlcpy(o->link.name, ifname, sizeof(o->link.name)) >= sizeof(o->link.name))
-        g_return_val_if_reached(NULL);
+        nm_assert_not_reached();
     lookup->cache_id_type = NMP_CACHE_ID_TYPE_LINK_BY_IFNAME;
     return _L(lookup);
 }
diff --git a/src/libnm-platform/nmp-object.h b/src/libnm-platform/nmp-object.h
index 021829db..bf140d78 100644
--- a/src/libnm-platform/nmp-object.h
+++ b/src/libnm-platform/nmp-object.h
@@ -120,10 +120,10 @@ typedef enum { /*< skip >*/
  * the index NMP_CACHE_ID_TYPE_OBJECT_BY_IFINDEX with
  * matching v4/v6 and ifindex -- or maybe not at all if it isn't visible.
  * */
-typedef enum { /*< skip >*/
-               NMP_CACHE_ID_TYPE_NONE,
+typedef enum {
+    NMP_CACHE_ID_TYPE_NONE,
 
-               /* all the objects of a certain type.
+    /* all the objects of a certain type.
      *
      * This index is special. It is the only one that contains *all* object.
      * Other indexes may consider some object as non "partitionable", hence
@@ -136,20 +136,20 @@ typedef enum { /*< skip >*/
      * expose all links, even invisible ones. For addresses/routes, this
      * distinction doesn't exist, as all addresses/routes that are alive
      * are visible as well. */
-               NMP_CACHE_ID_TYPE_OBJECT_TYPE,
+    NMP_CACHE_ID_TYPE_OBJECT_TYPE,
 
-               /* index for the link objects by ifname. */
-               NMP_CACHE_ID_TYPE_LINK_BY_IFNAME,
+    /* index for the link objects by ifname. */
+    NMP_CACHE_ID_TYPE_LINK_BY_IFNAME,
 
-               /* indices for the visible default-routes, ignoring ifindex.
+    /* indices for the visible default-routes, ignoring ifindex.
      * This index only contains two partitions: all visible default-routes,
      * separate for IPv4 and IPv6. */
-               NMP_CACHE_ID_TYPE_DEFAULT_ROUTES,
+    NMP_CACHE_ID_TYPE_DEFAULT_ROUTES,
 
-               /* all the objects that have an ifindex (by object-type) for an ifindex. */
-               NMP_CACHE_ID_TYPE_OBJECT_BY_IFINDEX,
+    /* all the objects that have an ifindex (by object-type) for an ifindex. */
+    NMP_CACHE_ID_TYPE_OBJECT_BY_IFINDEX,
 
-               /* Consider all the destination fields of a route, that is, the ID without the ifindex
+    /* Consider all the destination fields of a route, that is, the ID without the ifindex
      * and gateway (meaning: network/plen,metric).
      * The reason for this is that `ip route change` can replace an existing route
      * and modify its ifindex/gateway. Effectively, that means it deletes an existing
@@ -157,15 +157,15 @@ typedef enum { /*< skip >*/
      * sends one RTM_NEWADDR notification without notifying about the deletion. We detect
      * that by having this index to contain overlapping routes which require special
      * cache-resync. */
-               NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID,
+    NMP_CACHE_ID_TYPE_ROUTES_BY_WEAK_ID,
 
-               /* a filter for objects that track an explicit address family.
+    /* a filter for objects that track an explicit address family.
      *
      * Note that currently on NMPObjectRoutingRule is indexed by this filter. */
-               NMP_CACHE_ID_TYPE_OBJECT_BY_ADDR_FAMILY,
+    NMP_CACHE_ID_TYPE_OBJECT_BY_ADDR_FAMILY,
 
-               __NMP_CACHE_ID_TYPE_MAX,
-               NMP_CACHE_ID_TYPE_MAX = __NMP_CACHE_ID_TYPE_MAX - 1,
+    __NMP_CACHE_ID_TYPE_MAX,
+    NMP_CACHE_ID_TYPE_MAX = __NMP_CACHE_ID_TYPE_MAX - 1,
 } NMPCacheIdType;
 
 typedef struct {
@@ -625,6 +625,20 @@ nmp_object_unref(const NMPObject *obj)
         _changed;                       \
     })
 
+#define nm_clear_nmp_object_up_cast(ptr)                 \
+    ({                                                   \
+        typeof(ptr)   _ptr = (ptr);                      \
+        typeof(*_ptr) _pptr;                             \
+        gboolean      _changed = FALSE;                  \
+                                                         \
+        if (_ptr && (_pptr = *_ptr)) {                   \
+            *_ptr = NULL;                                \
+            nmp_object_unref(NMP_OBJECT_UP_CAST(_pptr)); \
+            _changed = TRUE;                             \
+        }                                                \
+        _changed;                                        \
+    })
+
 static inline gboolean
 nmp_object_ref_set(const NMPObject **pp, const NMPObject *obj)
 {
@@ -643,6 +657,25 @@ nmp_object_ref_set(const NMPObject **pp, const NMPObject *obj)
     return _changed;
 }
 
+static inline gboolean
+nmp_object_ref_set_up_cast(gpointer pp, gconstpointer obj)
+{
+    gboolean         _changed = FALSE;
+    const NMPObject *p;
+    gconstpointer *  pp2 = pp;
+
+    nm_assert(!pp2 || !*pp2 || NMP_OBJECT_IS_VALID(NMP_OBJECT_UP_CAST(*pp2)));
+    nm_assert(!obj || NMP_OBJECT_IS_VALID(NMP_OBJECT_UP_CAST(obj)));
+
+    if (pp2 && ((p = *pp2) != obj)) {
+        nmp_object_ref(NMP_OBJECT_UP_CAST(obj));
+        *pp2 = obj;
+        nmp_object_unref(NMP_OBJECT_UP_CAST(p));
+        _changed = TRUE;
+    }
+    return _changed;
+}
+
 NMPObject *nmp_object_new(NMPObjectType obj_type, gconstpointer plobj);
 NMPObject *nmp_object_new_link(int ifindex);
 
@@ -671,7 +704,22 @@ const char *nmp_object_to_string(const NMPObject *     obj,
                                  char *                buf,
                                  gsize                 buf_size);
 void        nmp_object_hash_update(const NMPObject *obj, NMHashState *h);
-int         nmp_object_cmp(const NMPObject *obj1, const NMPObject *obj2);
+
+typedef enum {
+    NMP_OBJECT_CMP_FLAGS_NONE = 0,
+
+    /* Warning: this flag is currently only implemented for certain object types
+     * (address and routes). */
+    NMP_OBJECT_CMP_FLAGS_IGNORE_IFINDEX = (1llu << 0),
+} NMPObjectCmpFlags;
+
+int nmp_object_cmp_full(const NMPObject *obj1, const NMPObject *obj2, NMPObjectCmpFlags flags);
+
+static inline int
+nmp_object_cmp(const NMPObject *obj1, const NMPObject *obj2)
+{
+    return nmp_object_cmp_full(obj1, obj2, NMP_OBJECT_CMP_FLAGS_NONE);
+}
 
 static inline gboolean
 nmp_object_equal(const NMPObject *obj1, const NMPObject *obj2)
@@ -1049,6 +1097,21 @@ nm_platform_lookup_object_by_addr_family(NMPlatform *  platform,
 
 /*****************************************************************************/
 
+static inline gboolean
+nmp_object_get_assume_config_once(const NMPObject *obj)
+{
+    switch (NMP_OBJECT_GET_TYPE(obj)) {
+    case NMP_OBJECT_TYPE_IP4_ADDRESS:
+    case NMP_OBJECT_TYPE_IP6_ADDRESS:
+        return NMP_OBJECT_CAST_IP_ADDRESS(obj)->a_assume_config_once;
+    case NMP_OBJECT_TYPE_IP4_ROUTE:
+    case NMP_OBJECT_TYPE_IP6_ROUTE:
+        return NMP_OBJECT_CAST_IP_ROUTE(obj)->r_assume_config_once;
+    default:
+        return nm_assert_unreachable_val(FALSE);
+    }
+}
+
 static inline const char *
 nmp_object_link_get_ifname(const NMPObject *obj)
 {
@@ -1058,7 +1121,7 @@ nmp_object_link_get_ifname(const NMPObject *obj)
 }
 
 static inline gboolean
-nmp_object_ip_route_is_best_defaut_route(const NMPObject *obj)
+nmp_object_ip_route_is_best_default_route(const NMPObject *obj)
 {
     const NMPlatformIPRoute *r = NMP_OBJECT_CAST_IP_ROUTE(obj);
 
diff --git a/src/libnm-platform/wifi/nm-wifi-utils-wext.c b/src/libnm-platform/wifi/nm-wifi-utils-wext.c
index 13b47c2c..6fb37c49 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils-wext.c
+++ b/src/libnm-platform/wifi/nm-wifi-utils-wext.c
@@ -14,7 +14,7 @@
 
 /* Hacks necessary to #include wireless.h; yay for WEXT */
 #ifndef __user
-    #define __user
+#define __user
 #endif
 #include <sys/types.h>
 #include <linux/types.h>
diff --git a/src/libnm-platform/wifi/nm-wifi-utils.c b/src/libnm-platform/wifi/nm-wifi-utils.c
index 3c952b6f..0b5f8d71 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils.c
+++ b/src/libnm-platform/wifi/nm-wifi-utils.c
@@ -15,7 +15,7 @@
 #include "nm-wifi-utils-private.h"
 #include "nm-wifi-utils-nl80211.h"
 #if HAVE_WEXT
-    #include "nm-wifi-utils-wext.h"
+#include "nm-wifi-utils-wext.h"
 #endif
 #include "libnm-platform/nm-platform-utils.h"