about summary refs log tree commit diff
path: root/src/libnm-platform
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2024-01-25 09:46:18 +0100
committerMichael Biebl <biebl@debian.org>2024-01-25 09:46:18 +0100
commit70e18d99b8e3e77bb37e218d7ac582130156f8ef (patch)
treed40c587e6d3f0e094ff558e415f1bb9803643214 /src/libnm-platform
parentd4d8b2b91f7ba000d97a8b2aab48c85000c11314 (diff)
New upstream version 1.45.90 upstream/1.45.90
Diffstat (limited to 'src/libnm-platform')
-rw-r--r--src/libnm-platform/nm-linux-platform.c148
-rw-r--r--src/libnm-platform/nm-platform-utils.c128
-rw-r--r--src/libnm-platform/nm-platform-utils.h8
-rw-r--r--src/libnm-platform/nm-platform.c376
-rw-r--r--src/libnm-platform/nm-platform.h110
-rw-r--r--src/libnm-platform/nmp-base.h16
-rw-r--r--src/libnm-platform/nmp-object.c12
-rw-r--r--src/libnm-platform/nmp-object.h8
-rw-r--r--src/libnm-platform/nmp-plobj.c4
-rw-r--r--src/libnm-platform/nmp-plobj.h2
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils-nl80211.c113
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils-private.h5
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils-wext.c16
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils.c22
-rw-r--r--src/libnm-platform/wifi/nm-wifi-utils.h6
15 files changed, 672 insertions, 302 deletions
diff --git a/src/libnm-platform/nm-linux-platform.c b/src/libnm-platform/nm-linux-platform.c
index 99eab9c7..22cd5784 100644
--- a/src/libnm-platform/nm-linux-platform.c
+++ b/src/libnm-platform/nm-linux-platform.c
@@ -163,7 +163,7 @@ typedef enum _nm_packed {
 G_STATIC_ASSERT(RTA_MAX == (__RTA_MAX - 1));
 #define RTA_PREF 20
 #undef RTA_MAX
-#define RTA_MAX (MAX((__RTA_MAX - 1), RTA_PREF))
+#define RTA_MAX (NM_MAX_CONST((__RTA_MAX - 1), RTA_PREF))
 
 #ifndef MACVLAN_FLAG_NOPROMISC
 #define MACVLAN_FLAG_NOPROMISC 1
@@ -229,6 +229,18 @@ G_STATIC_ASSERT(RTA_MAX == (__RTA_MAX - 1));
 
 /*****************************************************************************/
 
+#define IFLA_HSR_UNSPEC           0
+#define IFLA_HSR_PORT1            1
+#define IFLA_HSR_PORT2            2
+#define IFLA_HSR_MULTICAST_SPEC   3
+#define IFLA_HSR_SUPERVISION_ADDR 4
+#define IFLA_HSR_SEQ_NR           5
+#define IFLA_HSR_VERSION          6
+#define IFLA_HSR_PROTOCOL         7
+#define __IFLA_HSR_MAX            8
+
+/*****************************************************************************/
+
 #define IFLA_VTI_UNSPEC 0
 #define IFLA_VTI_LINK   1
 #define IFLA_VTI_IKEY   2
@@ -847,6 +859,7 @@ static const LinkDesc link_descs[] = {
 
     [NM_LINK_TYPE_BRIDGE] = {"bridge", "bridge", "bridge"},
     [NM_LINK_TYPE_BOND]   = {"bond", "bond", "bond"},
+    [NM_LINK_TYPE_HSR]    = {"hsr", "hsr", "hsr"},
     [NM_LINK_TYPE_TEAM]   = {"team", "team", NULL},
 };
 
@@ -869,6 +882,7 @@ _link_type_from_rtnl_type(const char *name)
         NM_LINK_TYPE_DUMMY,       /* "dummy"       */
         NM_LINK_TYPE_GRE,         /* "gre"         */
         NM_LINK_TYPE_GRETAP,      /* "gretap"      */
+        NM_LINK_TYPE_HSR,         /* "hsr"         */
         NM_LINK_TYPE_IFB,         /* "ifb"         */
         NM_LINK_TYPE_IP6GRE,      /* "ip6gre"      */
         NM_LINK_TYPE_IP6GRETAP,   /* "ip6gretap"   */
@@ -943,6 +957,7 @@ _link_type_from_devtype(const char *name)
         NM_LINK_TYPE_BNEP,      /* "bluetooth" */
         NM_LINK_TYPE_BOND,      /* "bond"      */
         NM_LINK_TYPE_BRIDGE,    /* "bridge"    */
+        NM_LINK_TYPE_HSR,       /* "hsr"       */
         NM_LINK_TYPE_PPP,       /* "ppp"       */
         NM_LINK_TYPE_VLAN,      /* "vlan"      */
         NM_LINK_TYPE_VRF,       /* "vrf"       */
@@ -1096,7 +1111,7 @@ _addrtime_extend_lifetime(guint32 lifetime, guint32 seconds)
         return lifetime;
 
     v = (guint64) lifetime + (guint64) seconds;
-    return MIN(v, NM_PLATFORM_LIFETIME_PERMANENT - 1);
+    return NM_MIN(v, NM_PLATFORM_LIFETIME_PERMANENT - 1);
 }
 
 /* The rtnl_addr object contains relative lifetimes @valid and @preferred
@@ -1805,6 +1820,51 @@ _parse_lnk_gre(const char *kind, struct nlattr *info_data)
 
 /*****************************************************************************/
 
+static NMPObject *
+_parse_lnk_hsr(const char *kind, struct nlattr *info_data)
+{
+    static const struct nla_policy policy[] = {
+        [IFLA_HSR_PORT1]            = {.type = NLA_U32},
+        [IFLA_HSR_PORT2]            = {.type = NLA_U32},
+        [IFLA_HSR_MULTICAST_SPEC]   = {.type = NLA_U8},
+        [IFLA_HSR_SUPERVISION_ADDR] = {.minlen = sizeof(NMEtherAddr)},
+        [IFLA_HSR_PROTOCOL]         = {.type = NLA_U8},
+    };
+    NMPlatformLnkHsr *props;
+    struct nlattr    *tb[G_N_ELEMENTS(policy)];
+    NMPObject        *obj;
+    guint32           v_u32;
+
+    if (!info_data || !kind)
+        return NULL;
+
+    if (nla_parse_nested_arr(tb, info_data, policy) < 0)
+        return NULL;
+
+    obj   = nmp_object_new(NMP_OBJECT_TYPE_LNK_HSR, NULL);
+    props = &obj->lnk_hsr;
+    if (tb[IFLA_HSR_PORT1]) {
+        v_u32 = nla_get_u32(tb[IFLA_HSR_PORT1]);
+        if (v_u32 <= (unsigned) G_MAXINT)
+            props->port1 = v_u32;
+    }
+    if (tb[IFLA_HSR_PORT2]) {
+        v_u32 = nla_get_u32(tb[IFLA_HSR_PORT2]);
+        if (v_u32 <= (unsigned) G_MAXINT)
+            props->port2 = v_u32;
+    }
+    if (tb[IFLA_HSR_MULTICAST_SPEC])
+        props->multicast_spec = nla_get_u8(tb[IFLA_HSR_MULTICAST_SPEC]);
+    if (tb[IFLA_HSR_SUPERVISION_ADDR])
+        nla_memcpy(&props->supervision_address, tb[IFLA_HSR_SUPERVISION_ADDR], sizeof(NMEtherAddr));
+    if (tb[IFLA_HSR_PROTOCOL])
+        props->prp = nla_get_u8(tb[IFLA_HSR_PROTOCOL]);
+
+    return obj;
+}
+
+/*****************************************************************************/
+
 /* IFLA_IPOIB_* were introduced in the 3.7 kernel, but the kernel headers
  * we're building against might not have those properties even though the
  * running kernel might.
@@ -3408,6 +3468,8 @@ _new_from_nl_link(NMPlatform            *platform,
 
             if (nm_streq(s, "bond"))
                 obj->link.port_kind = NM_PORT_KIND_BOND;
+            else if (nm_streq(s, "bridge"))
+                obj->link.port_kind = NM_PORT_KIND_BRIDGE;
         }
 
         if (li[IFLA_INFO_SLAVE_DATA]) {
@@ -3415,7 +3477,13 @@ _new_from_nl_link(NMPlatform            *platform,
                 [IFLA_BOND_SLAVE_QUEUE_ID] = {.type = NLA_U16},
                 [IFLA_BOND_SLAVE_PRIO]     = {.type = NLA_S32},
             };
-            struct nlattr *bp[G_N_ELEMENTS(policy_bond_port)];
+            struct nlattr                 *bp[G_N_ELEMENTS(policy_bond_port)];
+            static const struct nla_policy policy_bridge_port[] = {
+                [IFLA_BRPORT_COST]     = {.type = NLA_U32},
+                [IFLA_BRPORT_PRIORITY] = {.type = NLA_U16},
+                [IFLA_BRPORT_MODE]     = {.type = NLA_U8},
+            };
+            struct nlattr *brp[G_N_ELEMENTS(policy_bridge_port)];
 
             switch (obj->link.port_kind) {
             case NM_PORT_KIND_BOND:
@@ -3441,6 +3509,19 @@ _new_from_nl_link(NMPlatform            *platform,
                     }
                 }
                 break;
+            case NM_PORT_KIND_BRIDGE:
+                if (nla_parse_nested_arr(brp, li[IFLA_INFO_SLAVE_DATA], policy_bridge_port) < 0)
+                    return NULL;
+
+                if (brp[IFLA_BRPORT_COST])
+                    obj->link.port_data.bridge.path_cost = nla_get_u32(brp[IFLA_BRPORT_COST]);
+
+                if (brp[IFLA_BRPORT_PRIORITY])
+                    obj->link.port_data.bridge.priority = nla_get_u16(brp[IFLA_BRPORT_PRIORITY]);
+
+                if (brp[IFLA_BRPORT_MODE])
+                    obj->link.port_data.bridge.hairpin = nla_get_u8(brp[IFLA_BRPORT_MODE]);
+                break;
             case NM_PORT_KIND_NONE:
                 break;
             }
@@ -3547,6 +3628,9 @@ _new_from_nl_link(NMPlatform            *platform,
     case NM_LINK_TYPE_GRETAP:
         lnk_data = _parse_lnk_gre(nl_info_kind, nl_info_data);
         break;
+    case NM_LINK_TYPE_HSR:
+        lnk_data = _parse_lnk_hsr(nl_info_kind, nl_info_data);
+        break;
     case NM_LINK_TYPE_INFINIBAND:
         lnk_data = _parse_lnk_infiniband(nl_info_kind, nl_info_data);
         break;
@@ -3922,7 +4006,7 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter
                  * hops in this list). */
                 nm_assert(v4_n_nexthops > 0u);
                 if (v4_n_nexthops - 1u >= v4_nh_extra_alloc) {
-                    v4_nh_extra_alloc = NM_MAX(4, v4_nh_extra_alloc * 2u);
+                    v4_nh_extra_alloc = NM_MAX(4u, v4_nh_extra_alloc * 2u);
                     if (!v4_nh_extra_nexthops_heap) {
                         v4_nh_extra_nexthops_heap =
                             g_new(NMPlatformIP4RtNextHop, v4_nh_extra_alloc);
@@ -4977,6 +5061,24 @@ _nl_msg_new_link_set_linkinfo(struct nl_msg *msg, NMLinkType link_type, gconstpo
         NLA_PUT_U16(msg, IFLA_GRE_OFLAGS, htons(props->output_flags));
         break;
     }
+    case NM_LINK_TYPE_HSR:
+    {
+        const NMPlatformLnkHsr *props = extra_data;
+
+        nm_assert(props);
+
+        if (!(data = nla_nest_start(msg, IFLA_INFO_DATA)))
+            goto nla_put_failure;
+
+        NLA_PUT_U32(msg, IFLA_HSR_PORT1, props->port1);
+        NLA_PUT_U32(msg, IFLA_HSR_PORT2, props->port2);
+
+        if (props->multicast_spec)
+            NLA_PUT_U8(msg, IFLA_HSR_MULTICAST_SPEC, props->multicast_spec);
+
+        NLA_PUT_U8(msg, IFLA_HSR_PROTOCOL, props->prp);
+        break;
+    }
     case NM_LINK_TYPE_SIT:
     {
         const NMPlatformLnkSit *props = extra_data;
@@ -8533,6 +8635,26 @@ link_change(NMPlatform                   *platform,
         nla_nest_end(nlmsg, nl_port_data);
         nla_nest_end(nlmsg, nl_info);
         break;
+    case NM_PORT_KIND_BRIDGE:
+
+        nm_assert(port_data);
+
+        if (!(nl_info = nla_nest_start(nlmsg, IFLA_LINKINFO)))
+            goto nla_put_failure;
+
+        nm_assert(nm_streq0("bridge", nm_link_type_to_rtnl_type_string(NM_LINK_TYPE_BRIDGE)));
+        NLA_PUT_STRING(nlmsg, IFLA_INFO_SLAVE_KIND, "bridge");
+
+        if (!(nl_port_data = nla_nest_start(nlmsg, IFLA_INFO_SLAVE_DATA)))
+            goto nla_put_failure;
+
+        NLA_PUT_U32(nlmsg, IFLA_BRPORT_COST, port_data->bridge.path_cost);
+        NLA_PUT_U16(nlmsg, IFLA_BRPORT_PRIORITY, port_data->bridge.priority);
+        NLA_PUT_U8(nlmsg, IFLA_BRPORT_MODE, port_data->bridge.hairpin);
+
+        nla_nest_end(nlmsg, nl_port_data);
+        nla_nest_end(nlmsg, nl_info);
+        break;
     case NM_PORT_KIND_NONE:
         break;
     }
@@ -9551,22 +9673,6 @@ wifi_set_wake_on_wlan(NMPlatform *platform, int ifindex, _NMSettingWirelessWakeO
     return nm_wifi_utils_set_wake_on_wlan(wifi_data, wowl);
 }
 
-static gboolean
-wifi_get_csme_conn_info(NMPlatform *platform, int ifindex, NMPlatformCsmeConnInfo *out_conn_info)
-{
-    WIFI_GET_WIFI_DATA_NETNS(wifi_data, platform, ifindex, FALSE);
-
-    return nm_wifi_utils_get_csme_conn_info(wifi_data, out_conn_info);
-}
-
-static gboolean
-wifi_get_device_from_csme(NMPlatform *platform, int ifindex)
-{
-    WIFI_GET_WIFI_DATA_NETNS(wifi_data, platform, ifindex, FALSE);
-
-    return nm_wifi_utils_get_device_from_csme(wifi_data);
-}
-
 /*****************************************************************************/
 
 static gboolean
@@ -11446,8 +11552,6 @@ nm_linux_platform_class_init(NMLinuxPlatformClass *klass)
     platform_class->wifi_indicate_addressing_running = wifi_indicate_addressing_running;
     platform_class->wifi_get_wake_on_wlan            = wifi_get_wake_on_wlan;
     platform_class->wifi_set_wake_on_wlan            = wifi_set_wake_on_wlan;
-    platform_class->wifi_get_csme_conn_info          = wifi_get_csme_conn_info;
-    platform_class->wifi_get_device_from_csme        = wifi_get_device_from_csme;
 
     platform_class->mesh_get_channel = mesh_get_channel;
     platform_class->mesh_set_channel = mesh_set_channel;
diff --git a/src/libnm-platform/nm-platform-utils.c b/src/libnm-platform/nm-platform-utils.c
index 08d82fe8..6074c342 100644
--- a/src/libnm-platform/nm-platform-utils.c
+++ b/src/libnm-platform/nm-platform-utils.c
@@ -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_strv_find_first(inf->kernel_names, k, name) < 0);
+            g_assert(!nm_strv_contains(inf->kernel_names, k, name));
 
             /* 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. */
@@ -1068,6 +1068,69 @@ nmp_utils_ethtool_set_ring(int ifindex, const NMEthtoolRingState *ring)
 }
 
 gboolean
+nmp_utils_ethtool_get_channels(int ifindex, NMEthtoolChannelsState *channels)
+{
+    struct ethtool_channels eth_data;
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+    g_return_val_if_fail(channels, FALSE);
+
+    eth_data.cmd = ETHTOOL_GCHANNELS;
+
+    if (_ethtool_call_once(ifindex, &eth_data, sizeof(eth_data)) < 0) {
+        nm_log_trace(LOGD_PLATFORM,
+                     "ethtool[%d]: %s: failure getting channels settings",
+                     ifindex,
+                     "get-channels");
+        return FALSE;
+    }
+
+    *channels = (NMEthtoolChannelsState){
+        .rx       = eth_data.rx_count,
+        .tx       = eth_data.tx_count,
+        .other    = eth_data.other_count,
+        .combined = eth_data.combined_count,
+    };
+
+    nm_log_trace(LOGD_PLATFORM,
+                 "ethtool[%d]: %s: retrieved kernel channels settings",
+                 ifindex,
+                 "get-channels");
+    return TRUE;
+}
+
+gboolean
+nmp_utils_ethtool_set_channels(int ifindex, const NMEthtoolChannelsState *channels)
+{
+    struct ethtool_channels eth_data;
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+    g_return_val_if_fail(channels, FALSE);
+
+    eth_data = (struct ethtool_channels){
+        .cmd            = ETHTOOL_SCHANNELS,
+        .rx_count       = channels->rx,
+        .tx_count       = channels->tx,
+        .other_count    = channels->other,
+        .combined_count = channels->combined,
+    };
+
+    if (_ethtool_call_once(ifindex, &eth_data, sizeof(eth_data)) < 0) {
+        nm_log_trace(LOGD_PLATFORM,
+                     "ethtool[%d]: %s: failure setting channels settings",
+                     ifindex,
+                     "set-channels");
+        return FALSE;
+    }
+
+    nm_log_trace(LOGD_PLATFORM,
+                 "ethtool[%d]: %s: set kernel channels settings",
+                 ifindex,
+                 "set-channels");
+    return TRUE;
+}
+
+gboolean
 nmp_utils_ethtool_get_pause(int ifindex, NMEthtoolPauseState *pause)
 {
     struct ethtool_pauseparam          eth_data;
@@ -1099,6 +1162,35 @@ nmp_utils_ethtool_get_pause(int ifindex, NMEthtoolPauseState *pause)
 }
 
 gboolean
+nmp_utils_ethtool_get_eee(int ifindex, NMEthtoolEEEState *eee)
+{
+    struct ethtool_eee                 eth_data;
+    nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT(ifindex);
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+    g_return_val_if_fail(eee, FALSE);
+
+    eth_data.cmd = ETHTOOL_GEEE;
+    if (_ethtool_call_handle(&shandle, &eth_data, sizeof(struct ethtool_eee)) != 0) {
+        nm_log_trace(LOGD_PLATFORM,
+                     "ethtool[%d]: %s: failure getting eee settings",
+                     ifindex,
+                     "get-eee");
+        return FALSE;
+    }
+
+    *eee = (NMEthtoolEEEState){
+        .enabled = eth_data.eee_enabled == 1,
+    };
+
+    nm_log_trace(LOGD_PLATFORM,
+                 "ethtool[%d]: %s: retrieved kernel eee settings",
+                 ifindex,
+                 "get-eee");
+    return TRUE;
+}
+
+gboolean
 nmp_utils_ethtool_set_pause(int ifindex, const NMEthtoolPauseState *pause)
 {
     struct ethtool_pauseparam          eth_data;
@@ -1125,6 +1217,36 @@ nmp_utils_ethtool_set_pause(int ifindex, const NMEthtoolPauseState *pause)
     return TRUE;
 }
 
+gboolean
+nmp_utils_ethtool_set_eee(int ifindex, const NMEthtoolEEEState *eee)
+{
+    struct ethtool_eee                 eth_data;
+    nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT(ifindex);
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+    g_return_val_if_fail(eee, FALSE);
+
+    eth_data.cmd = ETHTOOL_GEEE;
+    if (_ethtool_call_handle(&shandle, &eth_data, sizeof(struct ethtool_eee)) != 0) {
+        nm_log_trace(LOGD_PLATFORM,
+                     "ethtool[%d]: %s: failure getting eee settings",
+                     ifindex,
+                     "get-eee");
+        return FALSE;
+    }
+
+    eth_data.cmd = ETHTOOL_SEEE, eth_data.eee_enabled = eee->enabled ? 1 : 0;
+
+    if (_ethtool_call_handle(&shandle, &eth_data, sizeof(struct ethtool_eee)) != 0) {
+        nm_log_trace(LOGD_PLATFORM,
+                     "ethtool[%d]: %s: failure setting eee settings",
+                     ifindex,
+                     "set-eee");
+        return FALSE;
+    }
+    nm_log_trace(LOGD_PLATFORM, "ethtool[%d]: %s: set kernel eee settings", ifindex, "set-eee");
+    return TRUE;
+}
 /*****************************************************************************/
 
 gboolean
@@ -2055,7 +2177,7 @@ nmp_utils_new_vlan_name(const char *parent_iface, guint32 vlan_id)
     ifname = g_new(char, IFNAMSIZ);
 
     parent_len = strlen(parent_iface);
-    parent_len = MIN(parent_len, IFNAMSIZ - 1 - id_len);
+    parent_len = NM_MIN(parent_len, IFNAMSIZ - 1 - id_len);
     memcpy(ifname, parent_iface, parent_len);
     g_snprintf(&ifname[parent_len], IFNAMSIZ - parent_len, ".%u", vlan_id);
 
@@ -2134,7 +2256,7 @@ nmp_utils_lifetime_get(guint32  timestamp,
 
     t_preferred = nmp_utils_lifetime_rebase_relative_time_on_now(timestamp, preferred, now);
 
-    NM_SET_OUT(out_preferred, MIN(t_preferred, t_lifetime));
+    NM_SET_OUT(out_preferred, NM_MIN(t_preferred, t_lifetime));
 
     /* Assert that non-permanent addresses have a (positive) @timestamp. nmp_utils_lifetime_rebase_relative_time_on_now()
      * treats addresses with timestamp 0 as *now*. Addresses passed to _address_get_lifetime() always
diff --git a/src/libnm-platform/nm-platform-utils.h b/src/libnm-platform/nm-platform-utils.h
index 14b09a79..18fc6155 100644
--- a/src/libnm-platform/nm-platform-utils.h
+++ b/src/libnm-platform/nm-platform-utils.h
@@ -54,10 +54,18 @@ gboolean nmp_utils_ethtool_get_ring(int ifindex, NMEthtoolRingState *ring);
 
 gboolean nmp_utils_ethtool_set_ring(int ifindex, const NMEthtoolRingState *ring);
 
+gboolean nmp_utils_ethtool_get_channels(int ifindex, NMEthtoolChannelsState *channels);
+
+gboolean nmp_utils_ethtool_set_channels(int ifindex, const NMEthtoolChannelsState *channels);
+
 gboolean nmp_utils_ethtool_get_pause(int ifindex, NMEthtoolPauseState *pause);
 
 gboolean nmp_utils_ethtool_set_pause(int ifindex, const NMEthtoolPauseState *pause);
 
+gboolean nmp_utils_ethtool_get_eee(int ifindex, NMEthtoolEEEState *eee);
+
+gboolean nmp_utils_ethtool_set_eee(int ifindex, const NMEthtoolEEEState *eee);
+
 /*****************************************************************************/
 
 gboolean nmp_utils_mii_supports_carrier_detect(int ifindex);
diff --git a/src/libnm-platform/nm-platform.c b/src/libnm-platform/nm-platform.c
index 041354cf..531c98e8 100644
--- a/src/libnm-platform/nm-platform.c
+++ b/src/libnm-platform/nm-platform.c
@@ -88,6 +88,14 @@ _nmp_link_port_data_to_string(NMPortKind                    port_kind,
                                               port_data->bond.prio)
                              : "");
         goto out;
+    case NM_PORT_KIND_BRIDGE:
+        nm_strbuf_append(&sbuf,
+                         &sbuf_len,
+                         "port bridge path_cost %u priority %u hairpin %s",
+                         port_data->bridge.path_cost,
+                         port_data->bridge.priority,
+                         port_data->bridge.hairpin ? "true" : "false");
+        goto out;
     }
 
     nm_strbuf_append(&sbuf, &sbuf_len, "invalid-port-type %d", (int) port_kind);
@@ -719,7 +727,7 @@ nm_platform_sysctl_ip_neigh_set_ipv6_reachable_time(NMPlatform *self,
 
     /* RFC 4861 says the value can't be greater than one hour.
      * Also use a reasonable lower threshold. */
-    clamped = NM_CLAMP(value_ms, 100, 3600000);
+    clamped = NM_CLAMP(value_ms, 100u, 3600000u);
     nm_sprintf_buf(path, "/proc/sys/net/ipv6/neigh/%s/base_reachable_time_ms", iface);
     nm_sprintf_buf(str, "%u", clamped);
     if (!nm_platform_sysctl_set(self, NMP_SYSCTL_PATHID_ABSOLUTE(path), str))
@@ -746,7 +754,7 @@ nm_platform_sysctl_ip_neigh_set_ipv6_retrans_time(NMPlatform *self,
         return TRUE;
 
     nm_sprintf_buf(path, "/proc/sys/net/ipv6/neigh/%s/retrans_time_ms", iface);
-    nm_sprintf_buf(str, "%u", NM_CLAMP(value_ms, 10, 3600000));
+    nm_sprintf_buf(str, "%u", NM_CLAMP(value_ms, 10u, 3600000u));
 
     return nm_platform_sysctl_set(self, NMP_SYSCTL_PATHID_ABSOLUTE(path), str);
 }
@@ -983,7 +991,7 @@ nm_platform_sysctl_ip_conf_get_rp_filter_ipv4(NMPlatform *self,
 /*****************************************************************************/
 
 static int
-_link_get_all_presort(gconstpointer p_a, gconstpointer p_b, gpointer sort_by_name)
+_link_get_all_presort(gconstpointer p_a, gconstpointer p_b)
 {
     const NMPlatformLink *a = NMP_OBJECT_CAST_LINK(*((const NMPObject **) p_a));
     const NMPlatformLink *b = NMP_OBJECT_CAST_LINK(*((const NMPObject **) p_b));
@@ -994,28 +1002,30 @@ _link_get_all_presort(gconstpointer p_a, gconstpointer p_b, gpointer sort_by_nam
     if (b->ifindex == NM_LOOPBACK_IFINDEX)
         return 1;
 
-    if (GPOINTER_TO_INT(sort_by_name)) {
-        /* Initialized links first */
-        if (a->initialized > b->initialized)
-            return -1;
-        if (a->initialized < b->initialized)
-            return 1;
+    /* Initialized links first */
+    if (a->initialized > b->initialized)
+        return -1;
+    if (a->initialized < b->initialized)
+        return 1;
 
-        return strcmp(a->name, b->name);
-    } else
-        return a->ifindex - b->ifindex;
+    NM_CMP_DIRECT_STRCMP(a->name, b->name);
+    /* Fallback to ifindex */
+    NM_CMP_DIRECT(a->ifindex, b->ifindex);
+    /* Fallback to pointer comparison */
+    NM_CMP_DIRECT_PTR(a, b);
+
+    return 0;
 }
 
 /**
  * nm_platform_link_get_all:
  * @self: platform instance
- * @sort_by_name: whether to sort by name or ifindex.
  *
  * Retrieve a snapshot of configuration for all links at once. The result is
  * owned by the caller and should be freed with g_ptr_array_unref().
  */
 GPtrArray *
-nm_platform_link_get_all(NMPlatform *self, gboolean sort_by_name)
+nm_platform_link_get_all(NMPlatform *self)
 {
     gs_unref_ptrarray GPtrArray   *links = NULL;
     GPtrArray                     *result;
@@ -1041,9 +1051,9 @@ nm_platform_link_get_all(NMPlatform *self, gboolean sort_by_name)
     if (links->len == 0)
         return NULL;
 
-    /* first sort the links by their ifindex or name. Below we will sort
+    /* first sort the links by their name. Below we will sort
      * further by moving children/slaves to the end. */
-    g_ptr_array_sort_with_data(links, _link_get_all_presort, GINT_TO_POINTER(sort_by_name));
+    g_ptr_array_sort(links, _link_get_all_presort);
 
     unseen = g_hash_table_new(nm_direct_hash, NULL);
     for (i = 0; i < links->len; i++) {
@@ -1383,6 +1393,12 @@ nm_platform_link_add(NMPlatform            *self,
                                                  buf_p,
                                                  buf_len);
                    break;
+               case NM_LINK_TYPE_HSR:
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_platform_lnk_hsr_to_string((const NMPlatformLnkHsr *) extra_data,
+                                                 buf_p,
+                                                 buf_len);
+                   break;
                case NM_LINK_TYPE_IP6TNL:
                case NM_LINK_TYPE_IP6GRE:
                case NM_LINK_TYPE_IP6GRETAP:
@@ -1628,7 +1644,7 @@ nm_platform_link_get_udev_property(NMPlatform  *self,
                                    const char  *name,
                                    const char **out_value)
 {
-    struct udev_device *udevice = NULL;
+    struct udev_device *udevice;
     const char         *uproperty;
 
     udevice = nm_platform_link_get_udev_device(self, ifindex);
@@ -1647,22 +1663,34 @@ nm_platform_link_get_udev_property(NMPlatform  *self,
  * nm_platform_link_get_unmanaged:
  * @self: platform instance
  * @ifindex: interface index
- * @unmanaged: management status (in case %TRUE is returned)
  *
- * Returns: %TRUE if platform overrides NM default-unmanaged status,
- * %FALSE otherwise (with @unmanaged unmodified).
+ * Returns: %NM_OPTION_BOOL_DEFAULT if the udev property NM_UNMANAGED
+ * is not set. Otherwise, return NM_UNMANAGED as boolean.
  */
-gboolean
-nm_platform_link_get_unmanaged(NMPlatform *self, int ifindex, gboolean *unmanaged)
+NMOptionBool
+nm_platform_link_get_unmanaged(NMPlatform *self, int ifindex)
 {
-    const char *value;
+    struct udev_device *udevice;
+    const char         *val;
 
-    if (nm_platform_link_get_udev_property(self, ifindex, "NM_UNMANAGED", &value)) {
-        NM_SET_OUT(unmanaged, _nm_utils_ascii_str_to_bool(value, FALSE));
-        return TRUE;
+    udevice = nm_platform_link_get_udev_device(self, ifindex);
+    if (!udevice)
+        return NM_OPTION_BOOL_DEFAULT;
+
+    val = udev_device_get_property_value(udevice, "NM_UNMANAGED");
+    if (val)
+        return _nm_utils_ascii_str_to_bool(val, FALSE);
+
+    val = udev_device_get_property_value(udevice, "ID_NET_MANAGED_BY");
+    if (val) {
+        if (!nm_streq(val, "org.freedesktop.NetworkManager")) {
+            /* There is another manager. UNMANAGED. */
+            return TRUE;
+        }
+        return FALSE;
     }
 
-    return FALSE;
+    return NM_OPTION_BOOL_DEFAULT;
 }
 
 /**
@@ -2173,9 +2201,12 @@ nm_platform_link_change(NMPlatform               *self,
                         int                       ifindex,
                         NMPlatformLinkProps      *props,
                         NMPlatformLinkBondPort   *bond_port,
+                        NMPlatformLinkBridgePort *bridge_port,
                         NMPlatformLinkChangeFlags flags)
 {
-    char sbuf_prio[100];
+    NMPortKind             port_kind = NM_PORT_KIND_NONE;
+    NMPlatformLinkPortData port_data;
+    char                   sbuf_prio[100];
 
     _CHECK_SELF(self, klass, FALSE);
 
@@ -2187,6 +2218,7 @@ nm_platform_link_change(NMPlatform               *self,
                                 | NM_PLATFORM_LINK_CHANGE_GSO_MAX_SEGMENTS
                                 | NM_PLATFORM_LINK_CHANGE_GRO_MAX_SIZE)
               || props);
+    nm_assert((!!bond_port + !!bridge_port) <= 1);
 
     if (_LOGD_ENABLED()) {
         nm_auto_free_gstring GString *str = g_string_new("");
@@ -2210,6 +2242,12 @@ nm_platform_link_change(NMPlatform               *self,
                                                         !bond_port->prio_has ? "?" : "",
                                                         bond_port->prio)
                                        : "");
+        } else if (bridge_port) {
+            g_string_append_printf(str,
+                                   "bridge-port path_cost %u priority %u hairpin %s",
+                                   bridge_port->path_cost,
+                                   bridge_port->priority,
+                                   bridge_port->hairpin ? "true" : "false");
         }
 
         if (str->len > 0 && str->str[str->len - 1] == ' ')
@@ -2218,12 +2256,15 @@ nm_platform_link_change(NMPlatform               *self,
         _LOG3D("link: change: %s", str->str);
     }
 
-    return klass->link_change(self,
-                              ifindex,
-                              props,
-                              bond_port ? NM_PORT_KIND_BOND : NM_PORT_KIND_NONE,
-                              (const NMPlatformLinkPortData *) bond_port,
-                              flags);
+    if (bond_port) {
+        port_data.bond = *bond_port;
+        port_kind      = NM_PORT_KIND_BOND;
+    } else if (bridge_port) {
+        port_data.bridge = *bridge_port;
+        port_kind        = NM_PORT_KIND_BRIDGE;
+    }
+
+    return klass->link_change(self, ifindex, props, port_kind, &port_data, flags);
 }
 
 /**
@@ -2482,6 +2523,12 @@ nm_platform_link_get_lnk_gretap(NMPlatform *self, int ifindex, const NMPlatformL
     return _link_get_lnk(self, ifindex, NM_LINK_TYPE_GRETAP, out_link);
 }
 
+const NMPlatformLnkHsr *
+nm_platform_link_get_lnk_hsr(NMPlatform *self, int ifindex, const NMPlatformLink **out_link)
+{
+    return _link_get_lnk(self, ifindex, NM_LINK_TYPE_HSR, out_link);
+}
+
 const NMPlatformLnkInfiniband *
 nm_platform_link_get_lnk_infiniband(NMPlatform *self, int ifindex, const NMPlatformLink **out_link)
 {
@@ -3353,28 +3400,6 @@ nm_platform_wifi_set_wake_on_wlan(NMPlatform *self, int ifindex, _NMSettingWirel
     return klass->wifi_set_wake_on_wlan(self, ifindex, wowl);
 }
 
-gboolean
-nm_platform_wifi_get_csme_conn_info(NMPlatform             *self,
-                                    int                     ifindex,
-                                    NMPlatformCsmeConnInfo *out_conn_info)
-{
-    _CHECK_SELF(self, klass, FALSE);
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-
-    return klass->wifi_get_csme_conn_info(self, ifindex, out_conn_info);
-}
-
-gboolean
-nm_platform_wifi_get_device_from_csme(NMPlatform *self, int ifindex)
-{
-    _CHECK_SELF(self, klass, FALSE);
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-
-    return klass->wifi_get_device_from_csme(self, ifindex);
-}
-
 guint32
 nm_platform_mesh_get_channel(NMPlatform *self, int ifindex)
 {
@@ -3582,6 +3607,31 @@ nm_platform_ethtool_set_ring(NMPlatform *self, int ifindex, const NMEthtoolRingS
 }
 
 gboolean
+nm_platform_ethtool_get_link_channels(NMPlatform             *self,
+                                      int                     ifindex,
+                                      NMEthtoolChannelsState *channels)
+{
+    _CHECK_SELF_NETNS(self, klass, netns, FALSE);
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+    g_return_val_if_fail(channels, FALSE);
+
+    return nmp_utils_ethtool_get_channels(ifindex, channels);
+}
+
+gboolean
+nm_platform_ethtool_set_channels(NMPlatform                   *self,
+                                 int                           ifindex,
+                                 const NMEthtoolChannelsState *channels)
+{
+    _CHECK_SELF_NETNS(self, klass, netns, FALSE);
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+
+    return nmp_utils_ethtool_set_channels(ifindex, channels);
+}
+
+gboolean
 nm_platform_ethtool_get_link_pause(NMPlatform *self, int ifindex, NMEthtoolPauseState *pause)
 {
     _CHECK_SELF_NETNS(self, klass, netns, FALSE);
@@ -3593,6 +3643,17 @@ nm_platform_ethtool_get_link_pause(NMPlatform *self, int ifindex, NMEthtoolPause
 }
 
 gboolean
+nm_platform_ethtool_get_link_eee(NMPlatform *self, int ifindex, NMEthtoolEEEState *eee)
+{
+    _CHECK_SELF_NETNS(self, klass, netns, FALSE);
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+    g_return_val_if_fail(eee, FALSE);
+
+    return nmp_utils_ethtool_get_eee(ifindex, eee);
+}
+
+gboolean
 nm_platform_ethtool_set_pause(NMPlatform *self, int ifindex, const NMEthtoolPauseState *pause)
 {
     _CHECK_SELF_NETNS(self, klass, netns, FALSE);
@@ -3602,6 +3663,15 @@ nm_platform_ethtool_set_pause(NMPlatform *self, int ifindex, const NMEthtoolPaus
     return nmp_utils_ethtool_set_pause(ifindex, pause);
 }
 
+gboolean
+nm_platform_ethtool_set_eee(NMPlatform *self, int ifindex, const NMEthtoolEEEState *eee)
+{
+    _CHECK_SELF_NETNS(self, klass, netns, FALSE);
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+
+    return nmp_utils_ethtool_set_eee(ifindex, eee);
+}
 /*****************************************************************************/
 
 const NMDedupMultiHeadEntry *
@@ -5218,6 +5288,33 @@ _route_pref_normalize(guint8 pref)
                : NM_ICMPV6_ROUTER_PREF_MEDIUM;
 }
 
+static guint16
+_ip4_route_weight_normalize(guint n_nexthops, guint16 weight, gboolean normalize_ecmp_weight)
+{
+    if (n_nexthops > 1u) {
+        /* This is a multihop-route. The weight is relevant.
+         *
+         * We only normalize a zero to one (because in kernel such weights
+         * don't exist. */
+        return NM_MAX(weight, 1u);
+    }
+    if (n_nexthops == 0) {
+        /* This route has no next-hop (e.g. blackhole type). The weight is
+         * always irrelevant. Normalize to zero. */
+        return 0;
+    }
+
+    /* We have a IPv4 single-hop route. In kernel, the weight does not exist.
+     * It's always zero.
+     *
+     * For upper layers, we find it useful to track such routes with a positive
+     * weight. They are candidates to be merged into a multi-hop ECMP route.
+     *
+     * Depending on what the caller requests, we normalize it (or leave it
+     * unchanged). */
+    return normalize_ecmp_weight ? 0u : weight;
+}
+
 /**
  * nm_platform_ip_route_normalize:
  * @addr_family: AF_INET or AF_INET6
@@ -5230,6 +5327,15 @@ _route_pref_normalize(guint8 pref)
  * Note that this function is related to NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY
  * in that if two routes compare semantically equal, after normalizing they also shall
  * compare equal with NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL.
+ *
+ * Note that a positive "weight" of IPv4 single hop routes is not meaningful in
+ * kernel. While we track such routes at upper layers, they don't exist in
+ * kernel (well, they exist, with their weight set to zero, which makes them a
+ * different route according to NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID. It will
+ * be normalized to zero too, making basically it a different route.
+ *
+ * Also, "metric_any" is normalized to FALSE. This also makes it a different route
+ * according to NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID.
  */
 void
 nm_platform_ip_route_normalize(int addr_family, NMPlatformIPRoute *route)
@@ -5243,6 +5349,33 @@ nm_platform_ip_route_normalize(int addr_family, NMPlatformIPRoute *route)
 
     route->rt_source = nmp_utils_ip_config_source_round_trip_rtprot(route->rt_source);
 
+    /* For the most part, nm_platform_ip_route_normalize() tries to normalize some fields
+     * as it happens when they go through kernel.
+     *
+     * In most cases, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID comparison performs the same
+     * relaxed comparison. For example, normalize() will normalize "scope_inv", and also
+     * the NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID comparison will do that on-the-fly. Optimally,
+     * looking into a hash table gives you the same result, whether you normalize the
+     * needle first (or whether the entries in the hash table are normalized).
+     *
+     * Unfortunately, that's not always the case. Examples:
+     *
+     * - "metric": we have a "metric_any" field. This is used by higher layers
+     *     to indicate that the metric is dynamically chosen (e.g. by the default
+     *     metric of the default route). As such, as far as NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID
+     *     is concerned, the "metric_any" and "metric" values are treated as distinguishing
+     *     properties. But when we add a route in kernel, "metric_any" no longer exist.
+     *     It becomes a fixed metric. Normalize will fix the metric.
+     * - "weight": for IPv4 single-hop routes, the weight does not exist in kernel. We however
+     *     use the field to track ECMP information in higher layers. Consequently,
+     *     NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID treats the weight as-is, while normalization
+     *     (and adding it to kernel) will mangle it.
+     *
+     * You thus must be careful when you track NMPlatformIP4Route that make use of such
+     * higher-level features, which cannot be represented in kernel or the NMPlatform
+     * cache.
+     */
+
     switch (addr_family) {
     case AF_INET:
         r4                = (NMPlatformIP4Route *) route;
@@ -5250,6 +5383,8 @@ nm_platform_ip_route_normalize(int addr_family, NMPlatformIPRoute *route)
         route->metric_any = FALSE;
         r4->network       = nm_ip4_addr_clear_host_address(r4->network, r4->plen);
         r4->scope_inv     = _ip_route_scope_inv_get_normalized(r4);
+        r4->n_nexthops    = nm_platform_ip4_route_get_n_nexthops(r4);
+        r4->weight        = _ip4_route_weight_normalize(r4->n_nexthops, r4->weight, TRUE);
         break;
     case AF_INET6:
         r6                = (NMPlatformIP6Route *) route;
@@ -6307,6 +6442,27 @@ nm_platform_lnk_gre_to_string(const NMPlatformLnkGre *lnk, char *buf, gsize len)
 }
 
 const char *
+nm_platform_lnk_hsr_to_string(const NMPlatformLnkHsr *lnk, char *buf, gsize len)
+{
+    if (!nm_utils_to_string_buffer_init_null(lnk, &buf, &len))
+        return buf;
+
+    g_snprintf(buf,
+               len,
+               "hsr "
+               "port1 %d "
+               "port2 %d "
+               "supervision_address " NM_ETHER_ADDR_FORMAT_STR " multicast_spec %u "
+               "prp %s",
+               lnk->port1,
+               lnk->port2,
+               NM_ETHER_ADDR_FORMAT_VAL(&lnk->supervision_address),
+               lnk->multicast_spec,
+               lnk->prp ? "on" : "off");
+    return buf;
+}
+
+const char *
 nm_platform_lnk_infiniband_to_string(const NMPlatformLnkInfiniband *lnk, char *buf, gsize len)
 {
     char str_p_key[64];
@@ -6983,7 +7139,7 @@ nm_platform_ip4_route_to_string_full(const NMPlatformIP4Route     *route,
         route->plen,
         n_nexthops <= 1 && s_gateway[0] ? " via " : "",
         n_nexthops <= 1 ? s_gateway : "",
-        NM_PRINT_FMT_QUOTED2(n_nexthops <= 1 && route->weight != 0,
+        NM_PRINT_FMT_QUOTED2(n_nexthops <= 1 && route->weight != 0u,
                              " weight ",
                              nm_sprintf_buf(weight_str, "%u", route->weight),
                              ""),
@@ -7901,6 +8057,9 @@ nm_platform_link_hash_update(const NMPlatformLink *obj, NMHashState *h)
     case NM_PORT_KIND_BOND:
         nm_platform_link_bond_port_hash_update(&obj->port_data.bond, h);
         break;
+    case NM_PORT_KIND_BRIDGE:
+        nm_platform_link_bridge_port_hash_update(&obj->port_data.bridge, h);
+        break;
     }
 }
 
@@ -7910,6 +8069,12 @@ nm_platform_link_bond_port_hash_update(const NMPlatformLinkBondPort *obj, NMHash
     nm_hash_update_vals(h, obj->prio, obj->queue_id, NM_HASH_COMBINE_BOOLS(guint8, obj->prio_has));
 }
 
+void
+nm_platform_link_bridge_port_hash_update(const NMPlatformLinkBridgePort *obj, NMHashState *h)
+{
+    nm_hash_update_vals(h, obj->path_cost, obj->priority, obj->hairpin);
+}
+
 int
 nm_platform_link_cmp(const NMPlatformLink *a, const NMPlatformLink *b)
 {
@@ -7948,6 +8113,9 @@ nm_platform_link_cmp(const NMPlatformLink *a, const NMPlatformLink *b)
     case NM_PORT_KIND_BOND:
         NM_CMP_RETURN(nm_platform_link_bond_port_cmp(&a->port_data.bond, &b->port_data.bond));
         break;
+    case NM_PORT_KIND_BRIDGE:
+        NM_CMP_RETURN(nm_platform_link_bridge_port_cmp(&a->port_data.bridge, &b->port_data.bridge));
+        break;
     }
     NM_CMP_FIELD(a, b, rx_packets);
     NM_CMP_FIELD(a, b, rx_bytes);
@@ -8045,6 +8213,18 @@ nm_platform_link_bond_port_cmp(const NMPlatformLinkBondPort *a, const NMPlatform
 }
 
 int
+nm_platform_link_bridge_port_cmp(const NMPlatformLinkBridgePort *a,
+                                 const NMPlatformLinkBridgePort *b)
+{
+    NM_CMP_SELF(a, b);
+    NM_CMP_FIELD(a, b, path_cost);
+    NM_CMP_FIELD(a, b, priority);
+    NM_CMP_FIELD(a, b, hairpin);
+
+    return 0;
+}
+
+int
 nm_platform_lnk_bond_cmp(const NMPlatformLnkBond *a, const NMPlatformLnkBond *b)
 {
     NM_CMP_SELF(a, b);
@@ -8164,6 +8344,29 @@ nm_platform_lnk_gre_cmp(const NMPlatformLnkGre *a, const NMPlatformLnkGre *b)
 }
 
 void
+nm_platform_lnk_hsr_hash_update(const NMPlatformLnkHsr *obj, NMHashState *h)
+{
+    nm_hash_update_vals(h,
+                        obj->port1,
+                        obj->port2,
+                        obj->supervision_address,
+                        obj->multicast_spec,
+                        NM_HASH_COMBINE_BOOLS(guint8, obj->prp));
+}
+
+int
+nm_platform_lnk_hsr_cmp(const NMPlatformLnkHsr *a, const NMPlatformLnkHsr *b)
+{
+    NM_CMP_SELF(a, b);
+    NM_CMP_FIELD(a, b, port1);
+    NM_CMP_FIELD(a, b, port2);
+    NM_CMP_FIELD_MEMCMP(a, b, supervision_address);
+    NM_CMP_FIELD(a, b, multicast_spec);
+    NM_CMP_FIELD_BOOL(a, b, prp);
+    return 0;
+}
+
+void
 nm_platform_lnk_infiniband_hash_update(const NMPlatformLnkInfiniband *obj, NMHashState *h)
 {
     nm_hash_update_val(h, obj->p_key);
@@ -8516,12 +8719,11 @@ nm_platform_ip4_rt_nexthop_hash_update(const NMPlatformIP4RtNextHop *obj,
                                        gboolean                      for_id,
                                        NMHashState                  *h)
 {
-    guint8 w;
+    guint16 w;
 
     nm_assert(obj);
 
     w = for_id ? NM_MAX(obj->weight, 1u) : obj->weight;
-
     nm_hash_update_vals(h, obj->ifindex, obj->gateway, w);
 }
 
@@ -8530,6 +8732,8 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
                                   NMPlatformIPRouteCmpType  cmp_type,
                                   NMHashState              *h)
 {
+    guint n_nexthops;
+
     switch (cmp_type) {
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID:
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID:
@@ -8567,15 +8771,17 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
                                                       obj->lock_mtu,
                                                       obj->lock_mss));
             if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) {
+                n_nexthops = nm_platform_ip4_route_get_n_nexthops(obj);
                 nm_hash_update_vals(h,
                                     obj->ifindex,
-                                    nm_platform_ip4_route_get_n_nexthops(obj),
+                                    n_nexthops,
                                     obj->gateway,
-                                    (guint8) MAX(obj->weight, 1u));
+                                    _ip4_route_weight_normalize(n_nexthops, obj->weight, FALSE));
             }
         }
         break;
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY:
+        n_nexthops = nm_platform_ip4_route_get_n_nexthops(obj);
         nm_hash_update_vals(
             h,
             obj->type_coerced,
@@ -8584,9 +8790,9 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
             nm_ip4_addr_clear_host_address(obj->network, obj->plen),
             obj->plen,
             obj->metric,
-            nm_platform_ip4_route_get_n_nexthops(obj),
+            n_nexthops,
             obj->gateway,
-            (guint8) MAX(obj->weight, 1u),
+            _ip4_route_weight_normalize(n_nexthops, obj->weight, FALSE),
             nmp_utils_ip_config_source_round_trip_rtprot(obj->rt_source),
             _ip_route_scope_inv_get_normalized(obj),
             obj->tos,
@@ -8652,14 +8858,9 @@ nm_platform_ip4_rt_nexthop_cmp(const NMPlatformIP4RtNextHop *a,
                                const NMPlatformIP4RtNextHop *b,
                                gboolean                      for_id)
 {
-    guint8 w_a;
-    guint8 w_b;
+    guint16 w_a;
+    guint16 w_b;
 
-    /* Note that weight zero is not valid (in kernel). We thus treat
-     * weight zero usually the same as 1.
-     *
-     * Not here for cmp/hash_update functions. These functions check for the exact
-     * bit-pattern, and not the it means at other places. */
     NM_CMP_SELF(a, b);
     NM_CMP_FIELD(a, b, ifindex);
     NM_CMP_FIELD(a, b, gateway);
@@ -8676,6 +8877,8 @@ nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
                           const NMPlatformIP4Route *b,
                           NMPlatformIPRouteCmpType  cmp_type)
 {
+    guint n_nexthops;
+
     NM_CMP_SELF(a, b);
     switch (cmp_type) {
     case NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID:
@@ -8684,7 +8887,7 @@ nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
         NM_CMP_FIELD_UNSAFE(a, b, table_any);
         NM_CMP_DIRECT(nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(a)),
                       nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(b)));
-        NM_CMP_DIRECT_IP4_ADDR_SAME_PREFIX(a->network, b->network, MIN(a->plen, b->plen));
+        NM_CMP_DIRECT_IP4_ADDR_SAME_PREFIX(a->network, b->network, NM_MIN(a->plen, b->plen));
         NM_CMP_FIELD(a, b, plen);
         NM_CMP_FIELD_UNSAFE(a, b, metric_any);
         NM_CMP_FIELD(a, b, metric);
@@ -8722,9 +8925,10 @@ nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
             if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) {
                 NM_CMP_FIELD(a, b, ifindex);
                 NM_CMP_FIELD(a, b, gateway);
-                NM_CMP_DIRECT(NM_MAX(a->weight, 1u), NM_MAX(b->weight, 1u));
-                NM_CMP_DIRECT(nm_platform_ip4_route_get_n_nexthops(a),
-                              nm_platform_ip4_route_get_n_nexthops(b));
+                n_nexthops = nm_platform_ip4_route_get_n_nexthops(a);
+                NM_CMP_DIRECT(n_nexthops, nm_platform_ip4_route_get_n_nexthops(b));
+                NM_CMP_DIRECT(_ip4_route_weight_normalize(n_nexthops, a->weight, FALSE),
+                              _ip4_route_weight_normalize(n_nexthops, b->weight, FALSE));
             }
         }
         break;
@@ -8739,22 +8943,22 @@ nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
             NM_CMP_FIELD(a, b, table_coerced);
         NM_CMP_FIELD(a, b, ifindex);
         if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
-            NM_CMP_DIRECT_IP4_ADDR_SAME_PREFIX(a->network, b->network, MIN(a->plen, b->plen));
+            NM_CMP_DIRECT_IP4_ADDR_SAME_PREFIX(a->network, b->network, NM_MIN(a->plen, b->plen));
         else
             NM_CMP_FIELD(a, b, network);
         NM_CMP_FIELD(a, b, plen);
         NM_CMP_FIELD_UNSAFE(a, b, metric_any);
         NM_CMP_FIELD(a, b, metric);
+        NM_CMP_FIELD(a, b, gateway);
         if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) {
-            NM_CMP_DIRECT(nm_platform_ip4_route_get_n_nexthops(a),
-                          nm_platform_ip4_route_get_n_nexthops(b));
-        } else
+            n_nexthops = nm_platform_ip4_route_get_n_nexthops(a);
+            NM_CMP_DIRECT(n_nexthops, nm_platform_ip4_route_get_n_nexthops(b));
+            NM_CMP_DIRECT(_ip4_route_weight_normalize(n_nexthops, a->weight, FALSE),
+                          _ip4_route_weight_normalize(n_nexthops, b->weight, FALSE));
+        } else {
             NM_CMP_FIELD(a, b, n_nexthops);
-        NM_CMP_FIELD(a, b, gateway);
-        if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
-            NM_CMP_DIRECT(NM_MAX(a->weight, 1u), NM_MAX(b->weight, 1u));
-        else
             NM_CMP_FIELD(a, b, weight);
+        }
         if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) {
             NM_CMP_DIRECT(nmp_utils_ip_config_source_round_trip_rtprot(a->rt_source),
                           nmp_utils_ip_config_source_round_trip_rtprot(b->rt_source));
@@ -8911,11 +9115,11 @@ nm_platform_ip6_route_cmp(const NMPlatformIP6Route *a,
         NM_CMP_FIELD_UNSAFE(a, b, table_any);
         NM_CMP_DIRECT(nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(a)),
                       nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(b)));
-        NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->network, &b->network, MIN(a->plen, b->plen));
+        NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->network, &b->network, NM_MIN(a->plen, b->plen));
         NM_CMP_FIELD(a, b, plen);
         NM_CMP_FIELD_UNSAFE(a, b, metric_any);
         NM_CMP_FIELD(a, b, metric);
-        NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->src, &b->src, MIN(a->src_plen, b->src_plen));
+        NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->src, &b->src, NM_MIN(a->src_plen, b->src_plen));
         NM_CMP_FIELD(a, b, src_plen);
         if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) {
             NM_CMP_FIELD(a, b, ifindex);
@@ -8934,7 +9138,7 @@ nm_platform_ip6_route_cmp(const NMPlatformIP6Route *a,
             NM_CMP_FIELD(a, b, table_coerced);
         NM_CMP_FIELD(a, b, ifindex);
         if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY)
-            NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->network, &b->network, MIN(a->plen, b->plen));
+            NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->network, &b->network, NM_MIN(a->plen, b->plen));
         else
             NM_CMP_FIELD_IN6ADDR(a, b, network);
         NM_CMP_FIELD(a, b, plen);
@@ -8943,7 +9147,7 @@ nm_platform_ip6_route_cmp(const NMPlatformIP6Route *a,
         NM_CMP_FIELD_IN6ADDR(a, b, gateway);
         NM_CMP_FIELD_IN6ADDR(a, b, pref_src);
         if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) {
-            NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->src, &b->src, MIN(a->src_plen, b->src_plen));
+            NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->src, &b->src, NM_MIN(a->src_plen, b->src_plen));
             NM_CMP_FIELD(a, b, src_plen);
             NM_CMP_DIRECT(nmp_utils_ip_config_source_round_trip_rtprot(a->rt_source),
                           nmp_utils_ip_config_source_round_trip_rtprot(b->rt_source));
diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h
index aeea5c42..996f84b8 100644
--- a/src/libnm-platform/nm-platform.h
+++ b/src/libnm-platform/nm-platform.h
@@ -175,8 +175,15 @@ typedef struct {
     bool    prio_has : 1;
 } NMPlatformLinkBondPort;
 
+typedef struct {
+    guint32 path_cost;
+    guint16 priority;
+    bool    hairpin;
+} NMPlatformLinkBridgePort;
+
 typedef union {
-    NMPlatformLinkBondPort bond;
+    NMPlatformLinkBondPort   bond;
+    NMPlatformLinkBridgePort bridge;
 } NMPlatformLinkPortData;
 
 struct _NMPlatformLink {
@@ -437,18 +444,27 @@ struct _NMPlatformIP4Route {
      * pref_src must match, unless set to 0.0.0.0 to match any. */
     in_addr_t pref_src;
 
-    /* This is the weight of for the first next-hop, in case of n_nexthops > 1.
+    /* This is the weight of for the first next-hop.
      *
-     * If n_nexthops is zero, this value is undefined (should be zero).
-     * If n_nexthops is 1, this also doesn't matter, but it's usually set to
-     * zero.
-     * If n_nexthops is greater or equal to one, this is the weight of
-     * the first hop.
+     * For multi-hop routes (n_nexthops > 1) this is the weight of the first
+     * hop. Note that the valid range is from 1-256. Zero is treated the same
+     * as 1 (for NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID comparison).
      *
-     * Note that upper layers (nm_utils_ip_route_attribute_to_platform()) use this flag to indicate
-     * whether this is a multihop route. Single-hop, non-ECMP routes will have a weight of zero.
+     * For routes without next-hop (e.g. blackhole type), the weight is
+     * meaningless. It should be set to zero. NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID
+     * will treat it as zero.
      *
-     * The valid range for weight in kernel is 1-256. */
+     * For single-hop routes, in kernel they don't have a weight. That means,
+     * all routes in the platform cache have a weight of zero. For tracking
+     * purposes, we find it useful that upper layers have single-hop routes
+     * with a positive weight. Such routes can never exist in kernel. Trying
+     * to add such a route will somewhat work, because
+     * nm_platform_ip_route_normalize() normalizes the weight to zero
+     * (effectively adding another route, according to
+     * NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID). A lookup in the platform cache with
+     * such a route will not yield a result. It does not exist there. If you
+     * want to find such a route, normalize it first.
+     */
     guint16 weight;
 
     /* rtm_tos (iproute2: tos)
@@ -697,13 +713,11 @@ typedef struct {
 typedef struct {
     int       ifindex;
     in_addr_t gateway;
-    /* The valid range for weight is 1-256. Single hop routes in kernel
-     * don't have a weight, we assign them weight zero (to indicate the
-     * weight is missing).
+
+    /* The weight of the next hop. The valid range for weight is 1-256.
      *
-     * Upper layers (nm_utils_ip_route_attribute_to_platform()) care about
-     * the distinction of unset weight (no-ECMP). They express no-ECMP as
-     * zero.
+     * Zero is allowed too, but treated as 1 (by
+     * NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID comparison).
      */
     guint16 weight;
 
@@ -829,6 +843,14 @@ typedef struct {
 } _nm_alignas(NMPlatformObject) NMPlatformLnkGre;
 
 typedef struct {
+    int         port1;
+    int         port2;
+    NMEtherAddr supervision_address;
+    guint8      multicast_spec;
+    bool        prp : 1;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkHsr;
+
+typedef struct {
     int         p_key;
     const char *mode;
 } _nm_alignas(NMPlatformObject) NMPlatformLnkInfiniband;
@@ -1009,14 +1031,6 @@ typedef struct {
 
 /*****************************************************************************/
 
-typedef struct _NMPlatformCsmeConnInfo {
-    guint8      ssid[32];
-    guint32     channel;
-    NMEtherAddr addr;
-    guint8      sta_cipher;
-    guint8      auth_mode;
-} NMPlatformCsmeConnInfo;
-
 typedef enum {
     NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_L3MDEV,
     NM_PLATFORM_KERNEL_SUPPORT_TYPE_FRA_UID_RANGE,
@@ -1242,10 +1256,6 @@ typedef struct {
     gboolean (*wifi_set_wake_on_wlan)(NMPlatform                  *self,
                                       int                          ifindex,
                                       _NMSettingWirelessWakeOnWLan wowl);
-    gboolean (*wifi_get_csme_conn_info)(NMPlatform             *self,
-                                        int                     ifindex,
-                                        NMPlatformCsmeConnInfo *out_conn_info);
-    gboolean (*wifi_get_device_from_csme)(NMPlatform *self, int ifindex);
 
     guint32 (*mesh_get_channel)(NMPlatform *self, int ifindex);
     gboolean (*mesh_set_channel)(NMPlatform *self, int ifindex, guint32 channel);
@@ -1641,7 +1651,7 @@ const NMPlatformLink *nm_platform_link_get_by_address(NMPlatform   *self,
                                                       gconstpointer address,
                                                       size_t        length);
 
-GPtrArray *nm_platform_link_get_all(NMPlatform *self, gboolean sort_by_name);
+GPtrArray *nm_platform_link_get_all(NMPlatform *self);
 
 int nm_platform_link_add(NMPlatform            *self,
                          NMLinkType             type,
@@ -1748,6 +1758,17 @@ nm_platform_link_gre_add(NMPlatform             *self,
 }
 
 static inline int
+nm_platform_link_hsr_add(NMPlatform             *self,
+                         const char             *name,
+                         const NMPlatformLnkHsr *props,
+                         const NMPlatformLink  **out_link)
+{
+    g_return_val_if_fail(props, -NME_BUG);
+
+    return nm_platform_link_add(self, NM_LINK_TYPE_HSR, name, 0, NULL, 0, 0, props, out_link);
+}
+
+static inline int
 nm_platform_link_sit_add(NMPlatform             *self,
                          const char             *name,
                          const NMPlatformLnkSit *props,
@@ -1946,9 +1967,9 @@ int nm_platform_link_get_master(NMPlatform *self, int slave);
 
 gboolean nm_platform_link_can_assume(NMPlatform *self, int ifindex);
 
-gboolean    nm_platform_link_get_unmanaged(NMPlatform *self, int ifindex, gboolean *unmanaged);
-gboolean    nm_platform_link_supports_slaves(NMPlatform *self, int ifindex);
-const char *nm_platform_link_get_type_name(NMPlatform *self, int ifindex);
+NMOptionBool nm_platform_link_get_unmanaged(NMPlatform *self, int ifindex);
+gboolean     nm_platform_link_supports_slaves(NMPlatform *self, int ifindex);
+const char  *nm_platform_link_get_type_name(NMPlatform *self, int ifindex);
 
 gboolean nm_platform_link_refresh(NMPlatform *self, int ifindex);
 void     nm_platform_process_events(NMPlatform *self);
@@ -1983,6 +2004,7 @@ gboolean nm_platform_link_change(NMPlatform               *self,
                                  int                       ifindex,
                                  NMPlatformLinkProps      *props,
                                  NMPlatformLinkBondPort   *bond_port,
+                                 NMPlatformLinkBridgePort *bridge_port,
                                  NMPlatformLinkChangeFlags flags);
 
 gboolean    nm_platform_link_get_udev_property(NMPlatform  *self,
@@ -2065,6 +2087,8 @@ const NMPlatformLnkGre *
 nm_platform_link_get_lnk_gre(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
 const NMPlatformLnkGre *
 nm_platform_link_get_lnk_gretap(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
+const NMPlatformLnkHsr *
+nm_platform_link_get_lnk_hsr(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
 const NMPlatformLnkIp6Tnl *
 nm_platform_link_get_lnk_ip6tnl(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
 const NMPlatformLnkIp6Tnl *
@@ -2146,10 +2170,6 @@ void nm_platform_wifi_indicate_addressing_running(NMPlatform *self, int ifindex,
 _NMSettingWirelessWakeOnWLan nm_platform_wifi_get_wake_on_wlan(NMPlatform *self, int ifindex);
 gboolean
 nm_platform_wifi_set_wake_on_wlan(NMPlatform *self, int ifindex, _NMSettingWirelessWakeOnWLan wowl);
-gboolean nm_platform_wifi_get_csme_conn_info(NMPlatform             *self,
-                                             int                     ifindex,
-                                             NMPlatformCsmeConnInfo *out_conn_info);
-gboolean nm_platform_wifi_get_device_from_csme(NMPlatform *self, int ifindex);
 
 guint32  nm_platform_mesh_get_channel(NMPlatform *self, int ifindex);
 gboolean nm_platform_mesh_set_channel(NMPlatform *self, int ifindex, guint32 channel);
@@ -2391,6 +2411,7 @@ const char *nm_platform_link_to_string(const NMPlatformLink *link, char *buf, gs
 const char *nm_platform_lnk_bond_to_string(const NMPlatformLnkBond *lnk, char *buf, gsize len);
 const char *nm_platform_lnk_bridge_to_string(const NMPlatformLnkBridge *lnk, char *buf, gsize len);
 const char *nm_platform_lnk_gre_to_string(const NMPlatformLnkGre *lnk, char *buf, gsize len);
+const char *nm_platform_lnk_hsr_to_string(const NMPlatformLnkHsr *lnk, char *buf, gsize len);
 const char *
 nm_platform_lnk_infiniband_to_string(const NMPlatformLnkInfiniband *lnk, char *buf, gsize len);
 const char *nm_platform_lnk_ip6tnl_to_string(const NMPlatformLnkIp6Tnl *lnk, char *buf, gsize len);
@@ -2444,6 +2465,7 @@ int nm_platform_link_cmp(const NMPlatformLink *a, const NMPlatformLink *b);
 int nm_platform_lnk_bond_cmp(const NMPlatformLnkBond *a, const NMPlatformLnkBond *b);
 int nm_platform_lnk_bridge_cmp(const NMPlatformLnkBridge *a, const NMPlatformLnkBridge *b);
 int nm_platform_lnk_gre_cmp(const NMPlatformLnkGre *a, const NMPlatformLnkGre *b);
+int nm_platform_lnk_hsr_cmp(const NMPlatformLnkHsr *a, const NMPlatformLnkHsr *b);
 int nm_platform_lnk_infiniband_cmp(const NMPlatformLnkInfiniband *a,
                                    const NMPlatformLnkInfiniband *b);
 int nm_platform_lnk_ip6tnl_cmp(const NMPlatformLnkIp6Tnl *a, const NMPlatformLnkIp6Tnl *b);
@@ -2484,8 +2506,11 @@ int nm_platform_mptcp_addr_cmp(const NMPlatformMptcpAddr *a, const NMPlatformMpt
 
 void nm_platform_link_hash_update(const NMPlatformLink *obj, NMHashState *h);
 void nm_platform_link_bond_port_hash_update(const NMPlatformLinkBondPort *obj, NMHashState *h);
+void nm_platform_link_bridge_port_hash_update(const NMPlatformLinkBridgePort *obj, NMHashState *h);
 int  nm_platform_link_bond_port_cmp(const NMPlatformLinkBondPort *a,
                                     const NMPlatformLinkBondPort *b);
+int  nm_platform_link_bridge_port_cmp(const NMPlatformLinkBridgePort *a,
+                                      const NMPlatformLinkBridgePort *b);
 void nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
                                        NMPlatformIPRouteCmpType  cmp_type,
                                        NMHashState              *h);
@@ -2512,6 +2537,7 @@ void nm_platform_routing_rule_hash_update(const NMPlatformRoutingRule *obj,
 void nm_platform_lnk_bond_hash_update(const NMPlatformLnkBond *obj, NMHashState *h);
 void nm_platform_lnk_bridge_hash_update(const NMPlatformLnkBridge *obj, NMHashState *h);
 void nm_platform_lnk_gre_hash_update(const NMPlatformLnkGre *obj, NMHashState *h);
+void nm_platform_lnk_hsr_hash_update(const NMPlatformLnkHsr *obj, NMHashState *h);
 void nm_platform_lnk_infiniband_hash_update(const NMPlatformLnkInfiniband *obj, NMHashState *h);
 void nm_platform_lnk_ip6tnl_hash_update(const NMPlatformLnkIp6Tnl *obj, NMHashState *h);
 void nm_platform_lnk_ipip_hash_update(const NMPlatformLnkIpIp *obj, NMHashState *h);
@@ -2572,12 +2598,24 @@ gboolean nm_platform_ethtool_get_link_ring(NMPlatform *self, int ifindex, NMEtht
 gboolean
 nm_platform_ethtool_set_ring(NMPlatform *self, int ifindex, const NMEthtoolRingState *ring);
 
+gboolean nm_platform_ethtool_get_link_channels(NMPlatform             *self,
+                                               int                     ifindex,
+                                               NMEthtoolChannelsState *channels);
+
+gboolean nm_platform_ethtool_set_channels(NMPlatform                   *self,
+                                          int                           ifindex,
+                                          const NMEthtoolChannelsState *channels);
+
 gboolean
 nm_platform_ethtool_get_link_pause(NMPlatform *self, int ifindex, NMEthtoolPauseState *pause);
 
+gboolean nm_platform_ethtool_get_link_eee(NMPlatform *self, int ifindex, NMEthtoolEEEState *eee);
+
 gboolean
 nm_platform_ethtool_set_pause(NMPlatform *self, int ifindex, const NMEthtoolPauseState *pause);
 
+gboolean nm_platform_ethtool_set_eee(NMPlatform *self, int ifindex, const NMEthtoolEEEState *eee);
+
 void nm_platform_ip4_dev_route_blacklist_set(NMPlatform *self,
                                              int         ifindex,
                                              GPtrArray  *ip4_dev_route_blacklist);
diff --git a/src/libnm-platform/nmp-base.h b/src/libnm-platform/nmp-base.h
index ffe59363..70b5d1bc 100644
--- a/src/libnm-platform/nmp-base.h
+++ b/src/libnm-platform/nmp-base.h
@@ -93,16 +93,12 @@ typedef struct {
     const NMEthtoolFeatureState states_list[];
 } NMEthtoolFeatureStates;
 
-/*****************************************************************************/
-
 typedef struct {
     guint32
         s[_NM_ETHTOOL_ID_COALESCE_NUM /* indexed by (NMEthtoolID - _NM_ETHTOOL_ID_COALESCE_FIRST) */
     ];
 } NMEthtoolCoalesceState;
 
-/*****************************************************************************/
-
 typedef struct {
     guint32 rx_pending;
     guint32 rx_mini_pending;
@@ -116,6 +112,17 @@ typedef struct {
     bool tx : 1;
 } NMEthtoolPauseState;
 
+typedef struct {
+    guint32 rx;
+    guint32 tx;
+    guint32 other;
+    guint32 combined;
+} NMEthtoolChannelsState;
+
+typedef struct {
+    bool enabled : 1;
+} NMEthtoolEEEState;
+
 /*****************************************************************************/
 
 typedef struct _NMPNetns                 NMPNetns;
@@ -152,6 +159,7 @@ typedef enum _nm_packed {
     NMP_OBJECT_TYPE_LNK_BRIDGE,
     NMP_OBJECT_TYPE_LNK_GRE,
     NMP_OBJECT_TYPE_LNK_GRETAP,
+    NMP_OBJECT_TYPE_LNK_HSR,
     NMP_OBJECT_TYPE_LNK_INFINIBAND,
     NMP_OBJECT_TYPE_LNK_IP6TNL,
     NMP_OBJECT_TYPE_LNK_IP6GRE,
diff --git a/src/libnm-platform/nmp-object.c b/src/libnm-platform/nmp-object.c
index 7e3fb1a3..4090da71 100644
--- a/src/libnm-platform/nmp-object.c
+++ b/src/libnm-platform/nmp-object.c
@@ -3485,6 +3485,18 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
             .cmd_plobj_hash_update = (CmdPlobjHashUpdateFunc) nm_platform_lnk_gre_hash_update,
             .cmd_plobj_cmp         = (CmdPlobjCmpFunc) nm_platform_lnk_gre_cmp,
         },
+    [NMP_OBJECT_TYPE_LNK_HSR - 1] =
+        {
+            .parent                = DEDUP_MULTI_OBJ_CLASS_INIT(),
+            .obj_type              = NMP_OBJECT_TYPE_LNK_HSR,
+            .sizeof_data           = sizeof(NMPObjectLnkHsr),
+            .sizeof_public         = sizeof(NMPlatformLnkHsr),
+            .obj_type_name         = "hsr",
+            .lnk_link_type         = NM_LINK_TYPE_HSR,
+            .cmd_plobj_to_string   = (CmdPlobjToStringFunc) nm_platform_lnk_hsr_to_string,
+            .cmd_plobj_hash_update = (CmdPlobjHashUpdateFunc) nm_platform_lnk_hsr_hash_update,
+            .cmd_plobj_cmp         = (CmdPlobjCmpFunc) nm_platform_lnk_hsr_cmp,
+        },
     [NMP_OBJECT_TYPE_LNK_INFINIBAND - 1] =
         {
             .parent              = DEDUP_MULTI_OBJ_CLASS_INIT(),
diff --git a/src/libnm-platform/nmp-object.h b/src/libnm-platform/nmp-object.h
index 408f0318..19cace3d 100644
--- a/src/libnm-platform/nmp-object.h
+++ b/src/libnm-platform/nmp-object.h
@@ -254,6 +254,10 @@ typedef struct {
 } NMPObjectLnkGre;
 
 typedef struct {
+    NMPlatformLnkHsr _public;
+} NMPObjectLnkHsr;
+
+typedef struct {
     NMPlatformLnkInfiniband _public;
 } NMPObjectLnkInfiniband;
 
@@ -377,6 +381,9 @@ struct _NMPObject {
         NMPlatformLnkGre lnk_gre;
         NMPObjectLnkGre  _lnk_gre;
 
+        NMPlatformLnkHsr lnk_hsr;
+        NMPObjectLnkHsr  _lnk_hsr;
+
         NMPlatformLnkInfiniband lnk_infiniband;
         NMPObjectLnkInfiniband  _lnk_infiniband;
 
@@ -530,6 +537,7 @@ _NMP_OBJECT_TYPE_IS_OBJ_WITH_IFINDEX(NMPObjectType obj_type)
     case NMP_OBJECT_TYPE_LNK_BOND:
     case NMP_OBJECT_TYPE_LNK_GRE:
     case NMP_OBJECT_TYPE_LNK_GRETAP:
+    case NMP_OBJECT_TYPE_LNK_HSR:
     case NMP_OBJECT_TYPE_LNK_INFINIBAND:
     case NMP_OBJECT_TYPE_LNK_IP6TNL:
     case NMP_OBJECT_TYPE_LNK_IP6GRE:
diff --git a/src/libnm-platform/nmp-plobj.c b/src/libnm-platform/nmp-plobj.c
index d3643ae1..70664634 100644
--- a/src/libnm-platform/nmp-plobj.c
+++ b/src/libnm-platform/nmp-plobj.c
@@ -424,7 +424,7 @@ nm_platform_ip4_address_to_string(const NMPlatformIP4Address *address, char *buf
         (address->lifetime == address->preferred)
             ? str_lft_p
             : (_lifetime_to_string(address->timestamp,
-                                   address->lifetime ? MIN(address->preferred, address->lifetime)
+                                   address->lifetime ? NM_MIN(address->preferred, address->lifetime)
                                                      : NM_PLATFORM_LIFETIME_PERMANENT,
                                    now,
                                    str_pref,
@@ -522,7 +522,7 @@ nm_platform_ip6_address_to_string(const NMPlatformIP6Address *address, char *buf
         (address->lifetime == address->preferred)
             ? str_lft_p
             : (_lifetime_to_string(address->timestamp,
-                                   address->lifetime ? MIN(address->preferred, address->lifetime)
+                                   address->lifetime ? NM_MIN(address->preferred, address->lifetime)
                                                      : NM_PLATFORM_LIFETIME_PERMANENT,
                                    now,
                                    str_pref,
diff --git a/src/libnm-platform/nmp-plobj.h b/src/libnm-platform/nmp-plobj.h
index 35c81509..ad18573f 100644
--- a/src/libnm-platform/nmp-plobj.h
+++ b/src/libnm-platform/nmp-plobj.h
@@ -17,7 +17,7 @@
  * also "packed" is specified. That's what we want.
  * https://gcc.gnu.org/onlinedocs/gcc/Common-Type-Attributes.html#Common-Type-Attributes.
  */
-#define _NMPlatformObject_Align (MAX(_nm_alignof(void *), _nm_alignof(gint64)))
+#define _NMPlatformObject_Align (NM_MAX_CONST(_nm_alignof(void *), _nm_alignof(gint64)))
 
 struct _NMPlatformObject {
     /* the object type has no fields of its own, it is only used to having
diff --git a/src/libnm-platform/wifi/nm-wifi-utils-nl80211.c b/src/libnm-platform/wifi/nm-wifi-utils-nl80211.c
index beddaf22..6109849a 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils-nl80211.c
+++ b/src/libnm-platform/wifi/nm-wifi-utils-nl80211.c
@@ -696,10 +696,12 @@ nl80211_wiphy_info_handler(const struct nl_msg *msg, void *arg)
 
             info->caps |= _NM_WIFI_DEVICE_CAP_FREQ_VALID;
 
-            if (f->freq > 2400 && f->freq < 2500)
+            if (f->freq >= 2401 && f->freq <= 2495)
                 info->caps |= _NM_WIFI_DEVICE_CAP_FREQ_2GHZ;
-            if (f->freq > 4900 && f->freq < 6000)
+            if (f->freq >= 5150 && f->freq <= 5895)
                 info->caps |= _NM_WIFI_DEVICE_CAP_FREQ_5GHZ;
+            if (f->freq >= 5925 && f->freq <= 7125)
+                info->caps |= _NM_WIFI_DEVICE_CAP_FREQ_6GHZ;
 
             info->num_freqs++;
         }
@@ -825,111 +827,6 @@ nla_put_failure:
     g_return_val_if_reached(FALSE);
 }
 
-struct nl80211_csme_conn_info {
-    NMWifiUtilsNl80211     *self;
-    NMPlatformCsmeConnInfo *conn_info;
-};
-
-static int
-nl80211_csme_conn_event_handler(const struct nl_msg *msg, void *arg)
-{
-    struct nl80211_csme_conn_info *info          = arg;
-    NMPlatformCsmeConnInfo        *out_conn_info = info->conn_info;
-    NMWifiUtilsNl80211            *self          = info->self;
-    struct genlmsghdr             *gnlh          = (void *) nlmsg_data(nlmsg_hdr(msg));
-    struct nlattr                 *tb[NL80211_ATTR_MAX + 1];
-    struct nlattr                 *data;
-    struct nlattr                 *attrs[NUM_IWL_MVM_VENDOR_ATTR];
-    int                            err;
-
-    static const struct nla_policy iwl_vendor_policy[NUM_IWL_MVM_VENDOR_ATTR] = {
-        [IWL_MVM_VENDOR_ATTR_AUTH_MODE]   = {.type = NLA_U32},
-        [IWL_MVM_VENDOR_ATTR_SSID]        = {.type = NLA_UNSPEC, .maxlen = NM_IW_ESSID_MAX_SIZE},
-        [IWL_MVM_VENDOR_ATTR_STA_CIPHER]  = {.type = NLA_U32},
-        [IWL_MVM_VENDOR_ATTR_CHANNEL_NUM] = {.type = NLA_U8},
-        [IWL_MVM_VENDOR_ATTR_ADDR] = {.type = NLA_UNSPEC, .minlen = ETH_ALEN, .maxlen = ETH_ALEN},
-    };
-
-    nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0), genlmsg_attrlen(gnlh, 0), NULL);
-    data = tb[NL80211_ATTR_VENDOR_DATA];
-
-    *out_conn_info = (NMPlatformCsmeConnInfo){};
-
-    err = nla_parse_nested(attrs, MAX_IWL_MVM_VENDOR_ATTR, data, iwl_vendor_policy);
-    if (err) {
-        _LOGD("IWL_MVM_VENDOR_CMD_GET_CSME_CONN_INFO Failed to parse CSME connection info: %s",
-              nm_strerror(err));
-        return -EINVAL;
-    }
-
-    if (attrs[IWL_MVM_VENDOR_ATTR_AUTH_MODE])
-        out_conn_info->auth_mode = nla_get_u8(attrs[IWL_MVM_VENDOR_ATTR_AUTH_MODE]);
-
-    if (attrs[IWL_MVM_VENDOR_ATTR_SSID])
-        memcpy(out_conn_info->ssid,
-               nla_data(attrs[IWL_MVM_VENDOR_ATTR_SSID]),
-               nla_len(attrs[IWL_MVM_VENDOR_ATTR_SSID]));
-
-    if (attrs[IWL_MVM_VENDOR_ATTR_STA_CIPHER])
-        out_conn_info->sta_cipher = nla_get_u8(attrs[IWL_MVM_VENDOR_ATTR_STA_CIPHER]);
-
-    if (attrs[IWL_MVM_VENDOR_ATTR_CHANNEL_NUM])
-        out_conn_info->channel = nla_get_u8(attrs[IWL_MVM_VENDOR_ATTR_CHANNEL_NUM]);
-
-    if (attrs[IWL_MVM_VENDOR_ATTR_ADDR])
-        memcpy(&out_conn_info->addr,
-               nla_data(attrs[IWL_MVM_VENDOR_ATTR_ADDR]),
-               sizeof(out_conn_info->addr));
-
-    return NL_SKIP;
-}
-
-static gboolean
-wifi_nl80211_intel_vnd_get_csme_conn_info(NMWifiUtils *data, NMPlatformCsmeConnInfo *out_conn_info)
-{
-    NMWifiUtilsNl80211           *self = (NMWifiUtilsNl80211 *) data;
-    nm_auto_nlmsg struct nl_msg  *msg  = NULL;
-    int                           err;
-    struct nl80211_csme_conn_info conn_info = {
-        .self      = self,
-        .conn_info = out_conn_info,
-    };
-
-    msg = nl80211_alloc_msg(self, NL80211_CMD_VENDOR, 0);
-    NLA_PUT_U32(msg, NL80211_ATTR_VENDOR_ID, INTEL_OUI);
-    NLA_PUT_U32(msg, NL80211_ATTR_VENDOR_SUBCMD, IWL_MVM_VENDOR_CMD_GET_CSME_CONN_INFO);
-
-    err = nl80211_send_and_recv(self, msg, nl80211_csme_conn_event_handler, &conn_info);
-    if (err < 0)
-        _LOGD("IWL_MVM_VENDOR_CMD_GET_CSME_CONN_INFO request failed: %s", nm_strerror(err));
-
-    return err >= 0;
-
-nla_put_failure:
-    g_return_val_if_reached(FALSE);
-}
-
-static gboolean
-wifi_nl80211_intel_vnd_get_device_from_csme(NMWifiUtils *data)
-{
-    NMWifiUtilsNl80211          *self = (NMWifiUtilsNl80211 *) data;
-    nm_auto_nlmsg struct nl_msg *msg  = NULL;
-    int                          err;
-
-    msg = nl80211_alloc_msg(self, NL80211_CMD_VENDOR, 0);
-    NLA_PUT_U32(msg, NL80211_ATTR_VENDOR_ID, INTEL_OUI);
-    NLA_PUT_U32(msg, NL80211_ATTR_VENDOR_SUBCMD, IWL_MVM_VENDOR_CMD_HOST_GET_OWNERSHIP);
-
-    err = nl80211_send_and_recv(self, msg, NULL, NULL);
-    if (err < 0)
-        _LOGD("IWL_MVM_VENDOR_CMD_HOST_GET_OWNERSHIP request failed: %s", nm_strerror(err));
-
-    return err >= 0;
-
-nla_put_failure:
-    g_return_val_if_reached(FALSE);
-}
-
 static void
 nm_wifi_utils_nl80211_init(NMWifiUtilsNl80211 *self)
 {}
@@ -954,8 +851,6 @@ nm_wifi_utils_nl80211_class_init(NMWifiUtilsNl80211Class *klass)
     wifi_utils_class->get_mesh_channel            = wifi_nl80211_get_mesh_channel;
     wifi_utils_class->set_mesh_channel            = wifi_nl80211_set_mesh_channel;
     wifi_utils_class->set_mesh_ssid               = wifi_nl80211_set_mesh_ssid;
-    wifi_utils_class->get_csme_conn_info          = wifi_nl80211_intel_vnd_get_csme_conn_info;
-    wifi_utils_class->get_device_from_csme        = wifi_nl80211_intel_vnd_get_device_from_csme;
 }
 
 NMWifiUtils *
diff --git a/src/libnm-platform/wifi/nm-wifi-utils-private.h b/src/libnm-platform/wifi/nm-wifi-utils-private.h
index 8fa593c0..abec38e2 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils-private.h
+++ b/src/libnm-platform/wifi/nm-wifi-utils-private.h
@@ -7,7 +7,6 @@
 #define __WIFI_UTILS_PRIVATE_H__
 
 #include "nm-wifi-utils.h"
-#include "libnm-platform/nm-platform.h"
 
 typedef struct {
     GObjectClass parent;
@@ -55,10 +54,6 @@ typedef struct {
     gboolean (*set_mesh_ssid)(NMWifiUtils *data, const guint8 *ssid, gsize len);
 
     gboolean (*indicate_addressing_running)(NMWifiUtils *data, gboolean running);
-
-    gboolean (*get_csme_conn_info)(NMWifiUtils *data, NMPlatformCsmeConnInfo *out_conn_info);
-
-    gboolean (*get_device_from_csme)(NMWifiUtils *data);
 } NMWifiUtilsClass;
 
 struct NMWifiUtils {
diff --git a/src/libnm-platform/wifi/nm-wifi-utils-wext.c b/src/libnm-platform/wifi/nm-wifi-utils-wext.c
index 0cc8b6a6..16503430 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils-wext.c
+++ b/src/libnm-platform/wifi/nm-wifi-utils-wext.c
@@ -548,7 +548,7 @@ wifi_wext_set_mesh_ssid(NMWifiUtils *data, const guint8 *ssid, gsize len)
         return FALSE;
 
     memset(buf, 0, sizeof(buf));
-    memcpy(buf, ssid, MIN(sizeof(buf) - 1, len));
+    memcpy(buf, ssid, NM_MIN(sizeof(buf) - 1, len));
 
     wrq.u.essid.pointer = (caddr_t) buf;
     wrq.u.essid.length  = len;
@@ -724,8 +724,8 @@ nm_wifi_utils_wext_new(int ifindex, gboolean check_scan)
     guint32                         response_len = 0;
     struct iw_range_with_scan_capa *scan_capa_range;
     int                             i;
-    gboolean                        freq_valid = FALSE, has_5ghz = FALSE, has_2ghz = FALSE;
-    char                            ifname[IFNAMSIZ];
+    gboolean freq_valid = FALSE, has_6ghz = FALSE, has_5ghz = FALSE, has_2ghz = FALSE;
+    char     ifname[IFNAMSIZ];
 
     if (!nmp_utils_if_indextoname(ifindex, ifname)) {
         _LOGW(LOGD_PLATFORM | LOGD_WIFI, "can't determine interface name for ifindex %d", ifindex);
@@ -758,14 +758,16 @@ nm_wifi_utils_wext_new(int ifindex, gboolean check_scan)
     wext->max_qual.noise   = range.max_qual.noise;
     wext->max_qual.updated = range.max_qual.updated;
 
-    wext->num_freqs = MIN(range.num_frequency, IW_MAX_FREQUENCIES);
+    wext->num_freqs = NM_MIN(range.num_frequency, (guint) IW_MAX_FREQUENCIES);
     for (i = 0; i < wext->num_freqs; i++) {
         wext->freqs[i] = iw_freq_to_uint32(&range.freq[i]);
         freq_valid     = TRUE;
-        if (wext->freqs[i] > 2400 && wext->freqs[i] < 2500)
+        if (wext->freqs[i] >= 2401 && wext->freqs[i] <= 2495)
             has_2ghz = TRUE;
-        else if (wext->freqs[i] > 4900 && wext->freqs[i] < 6000)
+        else if (wext->freqs[i] >= 5150 && wext->freqs[i] <= 5895)
             has_5ghz = TRUE;
+        else if (wext->freqs[i] >= 5925 && wext->freqs[i] <= 7125)
+            has_6ghz = TRUE;
     }
 
     /* Check for scanning capability; cards that can't scan are not supported */
@@ -798,6 +800,8 @@ nm_wifi_utils_wext_new(int ifindex, gboolean check_scan)
         wext->parent.caps |= _NM_WIFI_DEVICE_CAP_FREQ_2GHZ;
     if (has_5ghz)
         wext->parent.caps |= _NM_WIFI_DEVICE_CAP_FREQ_5GHZ;
+    if (has_6ghz)
+        wext->parent.caps |= _NM_WIFI_DEVICE_CAP_FREQ_6GHZ;
 
     _LOGI(LOGD_PLATFORM | LOGD_WIFI, "(%s): using WEXT for Wi-Fi device control", ifname);
 
diff --git a/src/libnm-platform/wifi/nm-wifi-utils.c b/src/libnm-platform/wifi/nm-wifi-utils.c
index 6f87e8c0..596ff644 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils.c
+++ b/src/libnm-platform/wifi/nm-wifi-utils.c
@@ -157,28 +157,6 @@ nm_wifi_utils_is_wifi(int dirfd, const char *ifname)
     return FALSE;
 }
 
-gboolean
-nm_wifi_utils_get_csme_conn_info(NMWifiUtils *data, NMPlatformCsmeConnInfo *out_conn_info)
-{
-    NMWifiUtilsClass *klass;
-
-    g_return_val_if_fail(data != NULL, FALSE);
-
-    klass = NM_WIFI_UTILS_GET_CLASS(data);
-    return klass->get_csme_conn_info ? klass->get_csme_conn_info(data, out_conn_info) : FALSE;
-}
-
-gboolean
-nm_wifi_utils_get_device_from_csme(NMWifiUtils *data)
-{
-    NMWifiUtilsClass *klass;
-
-    g_return_val_if_fail(data != NULL, FALSE);
-
-    klass = NM_WIFI_UTILS_GET_CLASS(data);
-    return klass->get_device_from_csme ? klass->get_device_from_csme(data) : FALSE;
-}
-
 /* OLPC Mesh-only functions */
 
 guint32
diff --git a/src/libnm-platform/wifi/nm-wifi-utils.h b/src/libnm-platform/wifi/nm-wifi-utils.h
index 84d724a5..0532817e 100644
--- a/src/libnm-platform/wifi/nm-wifi-utils.h
+++ b/src/libnm-platform/wifi/nm-wifi-utils.h
@@ -62,12 +62,6 @@ _NMSettingWirelessWakeOnWLan nm_wifi_utils_get_wake_on_wlan(NMWifiUtils *data);
 
 gboolean nm_wifi_utils_set_wake_on_wlan(NMWifiUtils *data, _NMSettingWirelessWakeOnWLan wowl);
 
-struct _NMPlatformCsmeConnInfo;
-gboolean nm_wifi_utils_get_csme_conn_info(NMWifiUtils                    *data,
-                                          struct _NMPlatformCsmeConnInfo *out_conn_info);
-
-gboolean nm_wifi_utils_get_device_from_csme(NMWifiUtils *data);
-
 /* OLPC Mesh-only functions */
 guint32 nm_wifi_utils_get_mesh_channel(NMWifiUtils *data);