summary refs log tree commit diff
path: root/src/libnm-platform
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2025-08-01 19:15:13 +0200
committerMichael Biebl <biebl@debian.org>2025-08-01 19:15:13 +0200
commit7e9091a5960f67a84787c03153324915220e4816 (patch)
treedc743de962532932ebc3b4ed3a36ddd093d97d68 /src/libnm-platform
parent582eb4472a0f3375042afb9026cbeb50e058a27d (diff)
New upstream version 1.54.0 upstream/1.54.0
Diffstat (limited to 'src/libnm-platform')
-rw-r--r--src/libnm-platform/devlink/nm-devlink.c3
-rw-r--r--src/libnm-platform/meson.build2
-rw-r--r--src/libnm-platform/nm-linux-platform.c146
-rw-r--r--src/libnm-platform/nm-linux-platform.h2
-rw-r--r--src/libnm-platform/nm-platform-utils.c1765
-rw-r--r--src/libnm-platform/nm-platform-utils.h57
-rw-r--r--src/libnm-platform/nm-platform.c65
-rw-r--r--src/libnm-platform/nm-platform.h34
-rw-r--r--src/libnm-platform/nmp-ethtool-ioctl.c1603
-rw-r--r--src/libnm-platform/nmp-ethtool-ioctl.h55
-rw-r--r--src/libnm-platform/nmp-ethtool.c559
-rw-r--r--src/libnm-platform/nmp-ethtool.h35
-rw-r--r--src/libnm-platform/nmp-object.c7
13 files changed, 2433 insertions, 1900 deletions
diff --git a/src/libnm-platform/devlink/nm-devlink.c b/src/libnm-platform/devlink/nm-devlink.c
index f06697cf..97749cda 100644
--- a/src/libnm-platform/devlink/nm-devlink.c
+++ b/src/libnm-platform/devlink/nm-devlink.c
@@ -14,6 +14,7 @@
 #include "libnm-platform/nm-netlink.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nm-platform-utils.h"
+#include "libnm-platform/nmp-ethtool-ioctl.h"
 
 #define _NMLOG_PREFIX_NAME "devlink"
 #define _NMLOG_DOMAIN      LOGD_PLATFORM | LOGD_DEVICE
@@ -118,7 +119,7 @@ nm_devlink_get_dev_identifier(NMDevlink *self, char **out_bus, char **out_addr,
         return FALSE;
     }
 
-    if (!nmp_utils_ethtool_get_driver_info(self->ifindex, &ethtool_driver_info)) {
+    if (!nmp_ethtool_ioctl_get_driver_info(self->ifindex, &ethtool_driver_info)) {
         g_set_error(error,
                     NM_UTILS_ERROR,
                     NM_UTILS_ERROR_UNKNOWN,
diff --git a/src/libnm-platform/meson.build b/src/libnm-platform/meson.build
index 7b6ad042..35015b02 100644
--- a/src/libnm-platform/meson.build
+++ b/src/libnm-platform/meson.build
@@ -12,6 +12,8 @@ libnm_platform = static_library(
     'nmp-netns.c',
     'nmp-object.c',
     'nmp-plobj.c',
+    'nmp-ethtool.c',
+    'nmp-ethtool-ioctl.c',
     'devlink/nm-devlink.c',
     'wifi/nm-wifi-utils-nl80211.c',
     'wifi/nm-wifi-utils.c',
diff --git a/src/libnm-platform/nm-linux-platform.c b/src/libnm-platform/nm-linux-platform.c
index 24109568..d45893fe 100644
--- a/src/libnm-platform/nm-linux-platform.c
+++ b/src/libnm-platform/nm-linux-platform.c
@@ -41,6 +41,8 @@
 #include "libnm-platform/nm-netlink.h"
 #include "libnm-platform/nm-platform-utils.h"
 #include "libnm-platform/nmp-netns.h"
+#include "libnm-platform/nmp-ethtool.h"
+#include "libnm-platform/nmp-ethtool-ioctl.h"
 #include "libnm-platform/devlink/nm-devlink.h"
 #include "libnm-platform/wifi/nm-wifi-utils-wext.h"
 #include "libnm-platform/wifi/nm-wifi-utils.h"
@@ -1320,7 +1322,7 @@ _linktype_get_type(NMPlatform       *platform,
         NMPUtilsEthtoolDriverInfo driver_info;
 
         /* Fallback OVS detection for kernel <= 3.16 */
-        if (nmp_utils_ethtool_get_driver_info(ifindex, &driver_info)) {
+        if (nmp_ethtool_ioctl_get_driver_info(ifindex, &driver_info)) {
             if (nm_streq(driver_info.driver, "openvswitch"))
                 return NM_LINK_TYPE_OPENVSWITCH;
 
@@ -4037,14 +4039,15 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter
     NMPlatformIP4RtNextHop         *v4_nh_extra_nexthops      = v4_nh_extra_nexthops_stack;
     guint                           v4_nh_extra_alloc = G_N_ELEMENTS(v4_nh_extra_nexthops_stack);
     guint32                         mss;
-    guint32                         window   = 0;
-    guint32                         cwnd     = 0;
-    guint32                         initcwnd = 0;
-    guint32                         initrwnd = 0;
-    guint32                         mtu      = 0;
-    guint32                         rto_min  = 0;
-    guint32                         lock     = 0;
-    gboolean                        quickack = FALSE;
+    guint32                         window      = 0;
+    guint32                         cwnd        = 0;
+    guint32                         initcwnd    = 0;
+    guint32                         initrwnd    = 0;
+    guint32                         mtu         = 0;
+    guint32                         rto_min     = 0;
+    guint32                         lock        = 0;
+    gboolean                        quickack    = FALSE;
+    gboolean                        rto_min_set = FALSE;
 
     nm_assert((parse_nlmsg_iter->iter_more && parse_nlmsg_iter->ip6_route.next_multihop > 0)
               || (!parse_nlmsg_iter->iter_more && parse_nlmsg_iter->ip6_route.next_multihop == 0));
@@ -4290,8 +4293,10 @@ rta_multipath_done:
             initrwnd = nla_get_u32(mtb[RTAX_INITRWND]);
         if (mtb[RTAX_MTU])
             mtu = nla_get_u32(mtb[RTAX_MTU]);
-        if (mtb[RTAX_RTO_MIN])
-            rto_min = nla_get_u32(mtb[RTAX_RTO_MIN]);
+        if (mtb[RTAX_RTO_MIN] && NM_FLAGS_HAS(lock, 1U << RTAX_RTO_MIN)) {
+            rto_min     = nla_get_u32(mtb[RTAX_RTO_MIN]);
+            rto_min_set = TRUE;
+        }
         if (mtb[RTAX_QUICKACK])
             quickack = !!nla_get_u32(mtb[RTAX_QUICKACK]);
     }
@@ -4362,6 +4367,7 @@ rta_multipath_done:
     obj->ip_route.initcwnd      = initcwnd;
     obj->ip_route.initrwnd      = initrwnd;
     obj->ip_route.rto_min       = rto_min;
+    obj->ip_route.rto_min_set   = rto_min_set;
     obj->ip_route.quickack      = quickack;
     obj->ip_route.mtu           = mtu;
     obj->ip_route.lock_window   = NM_FLAGS_HAS(lock, 1 << RTAX_WINDOW);
@@ -5634,12 +5640,19 @@ nla_put_failure:
 static guint32
 ip_route_get_lock_flag(const NMPlatformIPRoute *route)
 {
-    return (((guint32) route->lock_window) << RTAX_WINDOW)
-           | (((guint32) route->lock_cwnd) << RTAX_CWND)
-           | (((guint32) route->lock_initcwnd) << RTAX_INITCWND)
-           | (((guint32) route->lock_initrwnd) << RTAX_INITRWND)
-           | (((guint32) route->lock_mtu) << RTAX_MTU)
-           | (((guint32) route->lock_mss) << RTAX_ADVMSS);
+    guint32 ret;
+
+    ret = (((guint32) route->lock_window) << RTAX_WINDOW)
+          | (((guint32) route->lock_cwnd) << RTAX_CWND)
+          | (((guint32) route->lock_initcwnd) << RTAX_INITCWND)
+          | (((guint32) route->lock_initrwnd) << RTAX_INITRWND)
+          | (((guint32) route->lock_mtu) << RTAX_MTU)
+          | (((guint32) route->lock_mss) << RTAX_ADVMSS);
+
+    /* the rto-min value is ignored by kernel unless the lock flag is set */
+    ret |= (((guint32) route->rto_min_set) << RTAX_RTO_MIN);
+
+    return ret;
 }
 
 static gboolean
@@ -5803,7 +5816,7 @@ _nl_msg_new_route(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPObject *ob
             NLA_PUT_U32(msg, RTAX_INITRWND, obj->ip_route.initrwnd);
         if (obj->ip_route.mtu)
             NLA_PUT_U32(msg, RTAX_MTU, obj->ip_route.mtu);
-        if (obj->ip_route.rto_min)
+        if (obj->ip_route.rto_min_set)
             NLA_PUT_U32(msg, RTAX_RTO_MIN, obj->ip_route.rto_min);
         if (obj->ip_route.quickack)
             NLA_PUT_U32(msg, RTAX_QUICKACK, obj->ip_route.quickack);
@@ -6130,7 +6143,7 @@ nla_put_failure:
         const int         _dirfd  = (dirfd);                                                      \
         const char *const _path   = (path);                                                       \
                                                                                                   \
-        nm_assert(_path &&_path[0]);                                                              \
+        nm_assert(_path && _path[0]);                                                             \
         g_assert(!strstr(_path, "/../"));                                                         \
         if (_dirfd < 0) {                                                                         \
             nm_assert(!_pathid);                                                                  \
@@ -6138,7 +6151,7 @@ nla_put_failure:
             nm_assert(NM_STR_HAS_PREFIX(_path, "/proc/sys/") || NM_STR_HAS_PREFIX(_path, "/sys/") \
                       || NM_STR_HAS_PREFIX(_path, "/proc/net"));                                  \
         } else {                                                                                  \
-            nm_assert(_pathid &&_pathid[0] && _pathid[0] != '/');                                 \
+            nm_assert(_pathid && _pathid[0] && _pathid[0] != '/');                                \
             nm_assert(_path[0] != '/');                                                           \
         }                                                                                         \
     }                                                                                             \
@@ -7562,7 +7575,7 @@ cache_on_change(NMPlatform      *platform,
                              NM_LINK_TYPE_IP6GRETAP,
                              NM_LINK_TYPE_INFINIBAND,
                              NM_LINK_TYPE_MACVLAN,
-                             NM_LINK_TYPE_MACVLAN,
+                             NM_LINK_TYPE_MACVTAP,
                              NM_LINK_TYPE_SIT,
                              NM_LINK_TYPE_TUN,
                              NM_LINK_TYPE_VLAN,
@@ -8955,8 +8968,8 @@ link_supports_carrier_detect(NMPlatform *platform, int ifindex)
      * us whether the device actually supports carrier detection in the first
      * place. We assume any device that does implements one of these two APIs.
      */
-    return nmp_utils_ethtool_supports_carrier_detect(ifindex)
-           || nmp_utils_mii_supports_carrier_detect(ifindex);
+    return nmp_ethtool_ioctl_supports_carrier_detect(ifindex)
+           || nmp_mii_ioctl_supports_carrier_detect(ifindex);
 }
 
 static gboolean
@@ -8974,7 +8987,7 @@ link_supports_vlans(NMPlatform *platform, int ifindex)
     if (!nm_platform_netns_push(platform, &netns))
         return FALSE;
 
-    return nmp_utils_ethtool_supports_vlans(ifindex);
+    return nmp_ethtool_ioctl_supports_vlans(ifindex);
 }
 
 static gboolean
@@ -9052,7 +9065,7 @@ link_get_permanent_address_ethtool(NMPlatform *platform, int ifindex, NMPLinkAdd
     if (!nm_platform_netns_push(platform, &netns))
         return FALSE;
 
-    if (!nmp_utils_ethtool_get_permanent_address(ifindex, buffer, &len))
+    if (!nmp_ethtool_ioctl_get_permanent_address(ifindex, buffer, &len))
         return FALSE;
     nm_assert(len <= _NM_UTILS_HWADDR_LEN_MAX);
     memcpy(out_address->data, buffer, len);
@@ -9195,7 +9208,7 @@ sriov_async_finish_err(SriovAsyncState *async_state, GError *error)
     g_object_unref(async_state->platform);
     g_object_unref(async_state->cancellable);
     g_free(async_state);
-    g_free(error);
+    nm_g_error_free(error);
 }
 
 static void
@@ -10380,7 +10393,7 @@ link_get_wake_on_lan(NMPlatform *platform, int ifindex)
         return FALSE;
 
     if (type == NM_LINK_TYPE_ETHERNET)
-        return nmp_utils_ethtool_get_wake_on_lan(ifindex);
+        return nmp_ethtool_ioctl_get_wake_on_lan(ifindex);
     else if (type == NM_LINK_TYPE_WIFI) {
         WIFI_GET_WIFI_DATA(wifi_data, platform, ifindex, FALSE);
 
@@ -10405,7 +10418,7 @@ link_get_driver_info(NMPlatform *platform,
     if (!nm_platform_netns_push(platform, &netns))
         return FALSE;
 
-    if (!nmp_utils_ethtool_get_driver_info(ifindex, &driver_info))
+    if (!nmp_ethtool_ioctl_get_driver_info(ifindex, &driver_info))
         return FALSE;
     NM_SET_OUT(out_driver_name, g_strdup(driver_info.driver));
     NM_SET_OUT(out_driver_version, g_strdup(driver_info.version));
@@ -10465,7 +10478,7 @@ parse_fdb_cb(const struct nl_msg *msg, void *arg)
 }
 
 NMEtherAddr **
-nm_linux_platform_get_link_fdb_table(NMPlatform *platform, int *ifindexes, guint ifindexes_len)
+nm_linux_platform_get_bridge_fdb(NMPlatform *platform, int *ifindexes, guint ifindexes_len)
 {
     int                            nle;
     struct nl_sock                *sk        = NULL;
@@ -11820,6 +11833,74 @@ mptcp_addrs_dump(NMPlatform *platform)
 
 /*****************************************************************************/
 
+static gboolean
+ethtool_get_pause(NMPlatform *platform, int ifindex, NMEthtoolPauseState *pause)
+{
+    NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+
+    return nmp_ethtool_get_pause(priv->sk_genl_sync,
+                                 genl_get_family_id(platform, NMP_GENL_FAMILY_TYPE_ETHTOOL),
+                                 ifindex,
+                                 pause);
+}
+
+static gboolean
+ethtool_set_pause(NMPlatform *platform, int ifindex, const NMEthtoolPauseState *pause)
+{
+    NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+
+    return nmp_ethtool_set_pause(priv->sk_genl_sync,
+                                 genl_get_family_id(platform, NMP_GENL_FAMILY_TYPE_ETHTOOL),
+                                 ifindex,
+                                 pause);
+}
+
+static gboolean
+ethtool_get_eee(NMPlatform *platform, int ifindex, NMEthtoolEEEState *eee)
+{
+    NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+
+    return nmp_ethtool_get_eee(priv->sk_genl_sync,
+                               genl_get_family_id(platform, NMP_GENL_FAMILY_TYPE_ETHTOOL),
+                               ifindex,
+                               eee);
+}
+
+static gboolean
+ethtool_set_eee(NMPlatform *platform, int ifindex, const NMEthtoolEEEState *eee)
+{
+    NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+
+    return nmp_ethtool_set_eee(priv->sk_genl_sync,
+                               genl_get_family_id(platform, NMP_GENL_FAMILY_TYPE_ETHTOOL),
+                               ifindex,
+                               eee);
+}
+
+static gboolean
+ethtool_get_ring(NMPlatform *platform, int ifindex, NMEthtoolRingState *ring)
+{
+    NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+
+    return nmp_ethtool_get_ring(priv->sk_genl_sync,
+                                genl_get_family_id(platform, NMP_GENL_FAMILY_TYPE_ETHTOOL),
+                                ifindex,
+                                ring);
+}
+
+static gboolean
+ethtool_set_ring(NMPlatform *platform, int ifindex, const NMEthtoolRingState *ring)
+{
+    NMLinuxPlatformPrivate *priv = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+
+    return nmp_ethtool_set_ring(priv->sk_genl_sync,
+                                genl_get_family_id(platform, NMP_GENL_FAMILY_TYPE_ETHTOOL),
+                                ifindex,
+                                ring);
+}
+
+/*****************************************************************************/
+
 static void
 cache_update_link_udev(NMPlatform *platform, int ifindex, struct udev_device *udevice)
 {
@@ -12317,4 +12398,11 @@ nm_linux_platform_class_init(NMLinuxPlatformClass *klass)
     platform_class->genl_get_family_id = genl_get_family_id;
     platform_class->mptcp_addr_update  = mptcp_addr_update;
     platform_class->mptcp_addrs_dump   = mptcp_addrs_dump;
+
+    platform_class->ethtool_set_pause = ethtool_set_pause;
+    platform_class->ethtool_get_pause = ethtool_get_pause;
+    platform_class->ethtool_set_eee   = ethtool_set_eee;
+    platform_class->ethtool_get_eee   = ethtool_get_eee;
+    platform_class->ethtool_set_ring  = ethtool_set_ring;
+    platform_class->ethtool_get_ring  = ethtool_get_ring;
 }
diff --git a/src/libnm-platform/nm-linux-platform.h b/src/libnm-platform/nm-linux-platform.h
index 3f591b7f..c7f1b17a 100644
--- a/src/libnm-platform/nm-linux-platform.h
+++ b/src/libnm-platform/nm-linux-platform.h
@@ -26,7 +26,7 @@ GType nm_linux_platform_get_type(void);
 struct _NMDedupMultiIndex;
 
 NMEtherAddr **
-nm_linux_platform_get_link_fdb_table(NMPlatform *platform, int *ifindexes, guint ifindexes_len);
+nm_linux_platform_get_bridge_fdb(NMPlatform *platform, int *ifindexes, guint ifindexes_len);
 
 NMPlatform *nm_linux_platform_new(struct _NMDedupMultiIndex *multi_idx,
                                   gboolean                   log_with_ptr,
diff --git a/src/libnm-platform/nm-platform-utils.c b/src/libnm-platform/nm-platform-utils.c
index 15aac11c..d35ff6fe 100644
--- a/src/libnm-platform/nm-platform-utils.c
+++ b/src/libnm-platform/nm-platform-utils.c
@@ -12,14 +12,12 @@
 #include <unistd.h>
 #include <sys/ioctl.h>
 #include <linux/sockios.h>
-#include <linux/mii.h>
 #include <linux/if.h>
 #include <linux/version.h>
 #include <linux/rtnetlink.h>
 #include <fcntl.h>
 #include <libudev.h>
 
-#include "libnm-base/nm-ethtool-base.h"
 #include "libnm-log-core/nm-logging.h"
 #include "libnm-glib-aux/nm-time-utils.h"
 
@@ -60,1453 +58,6 @@ NM_UTILS_LOOKUP_STR_DEFINE(nm_platform_link_duplex_type_to_string,
                            NM_UTILS_LOOKUP_STR_ITEM(NM_PLATFORM_LINK_DUPLEX_FULL, "full"),
                            NM_UTILS_LOOKUP_STR_ITEM(NM_PLATFORM_LINK_DUPLEX_HALF, "half"), );
 
-/*****************************************************************************/
-
-typedef struct {
-    int       fd;
-    const int ifindex;
-    char      ifname[IFNAMSIZ];
-} SocketHandle;
-
-#define SOCKET_HANDLE_INIT(_ifindex) \
-    {                                \
-        .fd      = -1,               \
-        .ifindex = (_ifindex),       \
-    }
-
-static void
-_nm_auto_socket_handle(SocketHandle *shandle)
-{
-    if (shandle->fd >= 0)
-        nm_close(shandle->fd);
-}
-
-#define nm_auto_socket_handle nm_auto(_nm_auto_socket_handle)
-
-/*****************************************************************************/
-
-typedef enum {
-    IOCTL_CALL_DATA_TYPE_NONE,
-    IOCTL_CALL_DATA_TYPE_IFRDATA,
-    IOCTL_CALL_DATA_TYPE_IFRU,
-} IoctlCallDataType;
-
-static int
-_ioctl_call(const char       *log_ioctl_type,
-            const char       *log_subtype,
-            unsigned long int ioctl_request,
-            int               ifindex,
-            int              *inout_fd,
-            char             *inout_ifname,
-            IoctlCallDataType edata_type,
-            gpointer          edata,
-            gsize             edata_size,
-            struct ifreq     *out_ifreq)
-{
-    nm_auto_close int fd_close = -1;
-    int               fd;
-    int               r;
-    gpointer          edata_backup      = NULL;
-    gs_free gpointer  edata_backup_free = NULL;
-    guint             try_count;
-    char              known_ifnames[2][IFNAMSIZ];
-    const char       *failure_reason = NULL;
-    struct ifreq      ifr;
-
-    nm_assert(ifindex > 0);
-    nm_assert(NM_IN_SET(edata_type,
-                        IOCTL_CALL_DATA_TYPE_NONE,
-                        IOCTL_CALL_DATA_TYPE_IFRDATA,
-                        IOCTL_CALL_DATA_TYPE_IFRU));
-    nm_assert(edata_type != IOCTL_CALL_DATA_TYPE_NONE || edata_size == 0);
-    nm_assert(edata_type != IOCTL_CALL_DATA_TYPE_IFRDATA || edata_size > 0);
-    nm_assert(edata_type != IOCTL_CALL_DATA_TYPE_IFRU
-              || (edata_size > 0 && edata_size <= sizeof(ifr.ifr_ifru)));
-    nm_assert(edata_size == 0 || edata);
-
-    /* open a file descriptor (or use the one provided). */
-    if (inout_fd && *inout_fd >= 0)
-        fd = *inout_fd;
-    else {
-        fd = socket(PF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
-        if (fd < 0) {
-            r              = -NM_ERRNO_NATIVE(errno);
-            failure_reason = "failed creating socket or ioctl";
-            goto out;
-        }
-        if (inout_fd)
-            *inout_fd = fd;
-        else
-            fd_close = fd;
-    }
-
-    /* resolve the ifindex to name (or use the one provided). */
-    if (inout_ifname && inout_ifname[0])
-        nm_utils_ifname_cpy(known_ifnames[0], inout_ifname);
-    else {
-        if (!nmp_utils_if_indextoname(ifindex, known_ifnames[0])) {
-            failure_reason = "cannot resolve ifindex";
-            r              = -ENODEV;
-            goto out;
-        }
-        if (inout_ifname)
-            nm_utils_ifname_cpy(inout_ifname, known_ifnames[0]);
-    }
-
-    /* we might need to retry the request. Backup edata so that we can
-     * restore it on retry. */
-    if (edata_size > 0)
-        edata_backup = nm_memdup_maybe_a(500, edata, edata_size, &edata_backup_free);
-
-    try_count = 0;
-
-again:
-{
-    const char *ifname = known_ifnames[try_count % 2];
-
-    nm_assert(ifindex > 0);
-    nm_assert(ifname && nm_utils_ifname_valid_kernel(ifname, NULL));
-    nm_assert(fd >= 0);
-
-    memset(&ifr, 0, sizeof(ifr));
-    nm_utils_ifname_cpy(ifr.ifr_name, ifname);
-    if (edata_type == IOCTL_CALL_DATA_TYPE_IFRDATA)
-        ifr.ifr_data = edata;
-    else if (edata_type == IOCTL_CALL_DATA_TYPE_IFRU)
-        memcpy(&ifr.ifr_ifru, edata, NM_MIN(edata_size, sizeof(ifr.ifr_ifru)));
-
-    if (ioctl(fd, ioctl_request, &ifr) < 0) {
-        r = -NM_ERRNO_NATIVE(errno);
-        nm_log_trace(LOGD_PLATFORM,
-                     "%s[%d]: %s, %s: failed: %s",
-                     log_ioctl_type,
-                     ifindex,
-                     log_subtype,
-                     ifname,
-                     nm_strerror_native(-r));
-    } else {
-        r = 0;
-        nm_log_trace(LOGD_PLATFORM,
-                     "%s[%d]: %s, %s: success",
-                     log_ioctl_type,
-                     ifindex,
-                     log_subtype,
-                     ifname);
-    }
-}
-
-    try_count++;
-
-    /* resolve the name again to see whether the ifindex still has the same name. */
-    if (!nmp_utils_if_indextoname(ifindex, known_ifnames[try_count % 2])) {
-        /* we could not find the ifindex again. Probably the device just got
-         * removed.
-         *
-         * In both cases we return the error code we got from ioctl above.
-         * Either it failed because the device was gone already or it still
-         * managed to complete the call. In both cases, the error code is good. */
-        failure_reason =
-            "cannot resolve ifindex after ioctl call. Probably the device was just removed";
-        goto out;
-    }
-
-    /* check whether the ifname changed in the meantime. If yes, would render the result
-     * invalid. Note that this cannot detect every race regarding renames, for example:
-     *
-     *  - if_indextoname(#10) gives eth0
-     *  - rename(#10) => eth0_tmp
-     *  - rename(#11) => eth0
-     *  - ioctl(eth0) (wrongly fetching #11, formerly eth1)
-     *  - rename(#11) => eth_something
-     *  - rename(#10) => eth0
-     *  - if_indextoname(#10) gives eth0
-     */
-    if (!nm_streq(known_ifnames[0], known_ifnames[1])) {
-        gboolean retry;
-
-        /* we detected a possible(!) rename.
-         *
-         * For getters it's straight forward to just retry the call.
-         *
-         * For setters we also always retry. If our previous call operated on the right device,
-         * calling it again should have no bad effect (just setting the same thing more than once).
-         *
-         * The only potential bad thing is if there was a race involving swapping names, and we just
-         * set the ioctl option on the wrong device. But then the bad thing already happenned and
-         * we cannot detect it (nor do anything about it). At least, we can retry and set the
-         * option on the right interface. */
-        retry = (try_count < 5);
-
-        nm_log_trace(LOGD_PLATFORM,
-                     "%s[%d]: %s: rename detected from \"%s\" to \"%s\". %s",
-                     log_ioctl_type,
-                     ifindex,
-                     log_subtype,
-                     known_ifnames[(try_count - 1) % 2],
-                     known_ifnames[try_count % 2],
-                     retry ? "Retry" : "No retry");
-        if (inout_ifname)
-            nm_utils_ifname_cpy(inout_ifname, known_ifnames[try_count % 2]);
-        if (retry) {
-            if (edata_size > 0)
-                memcpy(edata, edata_backup, edata_size);
-            goto again;
-        }
-    }
-
-out:
-    if (failure_reason) {
-        nm_log_trace(LOGD_PLATFORM,
-                     "%s[%d]: %s: %s: %s",
-                     log_ioctl_type,
-                     ifindex,
-                     log_subtype,
-                     failure_reason,
-                     r < 0 ? nm_strerror_native(-r) : "assume success");
-    }
-    if (r >= 0)
-        NM_SET_OUT(out_ifreq, ifr);
-    return r;
-}
-
-/******************************************************************************
- * ethtool
- *****************************************************************************/
-
-static NM_UTILS_ENUM2STR_DEFINE(_ethtool_cmd_to_string,
-                                guint32,
-                                NM_UTILS_ENUM2STR(ETHTOOL_GCOALESCE, "ETHTOOL_GCOALESCE"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_GDRVINFO, "ETHTOOL_GDRVINFO"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_GFEATURES, "ETHTOOL_GFEATURES"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_GLINK, "ETHTOOL_GLINK"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_GLINKSETTINGS, "ETHTOOL_GLINKSETTINGS"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_GPERMADDR, "ETHTOOL_GPERMADDR"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_GRINGPARAM, "ETHTOOL_GRINGPARAM"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_GPAUSEPARAM, "ETHTOOL_GPAUSEPARAM"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_GSET, "ETHTOOL_GSET"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_GSSET_INFO, "ETHTOOL_GSSET_INFO"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_GSTATS, "ETHTOOL_GSTATS"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_GSTRINGS, "ETHTOOL_GSTRINGS"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_GWOL, "ETHTOOL_GWOL"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_SCOALESCE, "ETHTOOL_SCOALESCE"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_SFEATURES, "ETHTOOL_SFEATURES"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_SLINKSETTINGS, "ETHTOOL_SLINKSETTINGS"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_SRINGPARAM, "ETHTOOL_SRINGPARAM"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_SPAUSEPARAM, "ETHTOOL_SPAUSEPARAM"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_SSET, "ETHTOOL_SSET"),
-                                NM_UTILS_ENUM2STR(ETHTOOL_SWOL, "ETHTOOL_SWOL"), );
-
-static const char *
-_ethtool_edata_to_string(gpointer edata, gsize edata_size, char *sbuf, gsize sbuf_len)
-{
-    nm_assert(edata);
-    nm_assert(edata_size >= sizeof(guint32));
-    nm_assert((((intptr_t) edata) % _nm_alignof(guint32)) == 0);
-
-    return _ethtool_cmd_to_string(*((guint32 *) edata), sbuf, sbuf_len);
-}
-
-/*****************************************************************************/
-
-#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
-#endif
-
-static int
-_ethtool_call_handle(SocketHandle *shandle, gpointer edata, gsize edata_size)
-{
-    char sbuf[50];
-
-    return _ioctl_call("ethtool",
-                       _ethtool_edata_to_string(edata, edata_size, sbuf, sizeof(sbuf)),
-                       SIOCETHTOOL,
-                       shandle->ifindex,
-                       &shandle->fd,
-                       shandle->ifname,
-                       IOCTL_CALL_DATA_TYPE_IFRDATA,
-                       edata,
-                       edata_size,
-                       NULL);
-}
-
-static int
-_ethtool_call_once(int ifindex, gpointer edata, gsize edata_size)
-{
-    char sbuf[50];
-
-    return _ioctl_call("ethtool",
-                       _ethtool_edata_to_string(edata, edata_size, sbuf, sizeof(sbuf)),
-                       SIOCETHTOOL,
-                       ifindex,
-                       NULL,
-                       NULL,
-                       IOCTL_CALL_DATA_TYPE_IFRDATA,
-                       edata,
-                       edata_size,
-                       NULL);
-}
-
-/*****************************************************************************/
-
-static struct ethtool_gstrings *
-ethtool_get_stringset(SocketHandle *shandle, int stringset_id)
-{
-    struct {
-        struct ethtool_sset_info info;
-        guint32                  sentinel;
-    } sset_info = {
-        .info.cmd       = ETHTOOL_GSSET_INFO,
-        .info.reserved  = 0,
-        .info.sset_mask = (1ULL << stringset_id),
-    };
-    const guint32                   *pdata;
-    gs_free struct ethtool_gstrings *gstrings = NULL;
-    gsize                            gstrings_len;
-    guint32                          i, len;
-
-    if (_ethtool_call_handle(shandle, &sset_info, sizeof(sset_info)) < 0)
-        return NULL;
-    if (!sset_info.info.sset_mask)
-        return NULL;
-
-    pdata = (guint32 *) sset_info.info.data;
-
-    len = *pdata;
-
-    gstrings_len         = sizeof(*gstrings) + (len * ETH_GSTRING_LEN);
-    gstrings             = g_malloc0(gstrings_len);
-    gstrings->cmd        = ETHTOOL_GSTRINGS;
-    gstrings->string_set = stringset_id;
-    gstrings->len        = len;
-    if (gstrings->len > 0) {
-        if (_ethtool_call_handle(shandle, gstrings, gstrings_len) < 0)
-            return NULL;
-        for (i = 0; i < gstrings->len; i++) {
-            /* ensure NUL terminated */
-            gstrings->data[i * ETH_GSTRING_LEN + (ETH_GSTRING_LEN - 1)] = '\0';
-        }
-    }
-
-    return g_steal_pointer(&gstrings);
-}
-
-static int
-ethtool_gstrings_find(const struct ethtool_gstrings *gstrings, const char *needle)
-{
-    guint32 i;
-
-    /* ethtool_get_stringset() always ensures NUL terminated strings at ETH_GSTRING_LEN.
-     * that means, we cannot possibly request longer names. */
-    nm_assert(needle && strlen(needle) < ETH_GSTRING_LEN);
-
-    for (i = 0; i < gstrings->len; i++) {
-        if (nm_streq((char *) &gstrings->data[i * ETH_GSTRING_LEN], needle))
-            return i;
-    }
-    return -1;
-}
-
-static int
-ethtool_get_stringset_index(SocketHandle *shandle, int stringset_id, const char *needle)
-{
-    gs_free struct ethtool_gstrings *gstrings = NULL;
-
-    /* ethtool_get_stringset() always ensures NUL terminated strings at ETH_GSTRING_LEN.
-     * that means, we cannot possibly request longer names. */
-    nm_assert(needle && strlen(needle) < ETH_GSTRING_LEN);
-
-    gstrings = ethtool_get_stringset(shandle, stringset_id);
-    if (gstrings)
-        return ethtool_gstrings_find(gstrings, needle);
-    return -1;
-}
-
-/*****************************************************************************/
-
-static const NMEthtoolFeatureInfo _ethtool_feature_infos[_NM_ETHTOOL_ID_FEATURE_NUM] = {
-#define ETHT_FEAT(eid, ...)                                      \
-    {                                                            \
-        .ethtool_id     = eid,                                   \
-        .n_kernel_names = NM_NARG(__VA_ARGS__),                  \
-        .kernel_names   = ((const char *const[]) {__VA_ARGS__}), \
-    }
-
-    /* the order does only matter for one thing: if it happens that more than one NMEthtoolID
-     * reference the same kernel-name, then the one that is mentioned *later* will win in
-     * case these NMEthtoolIDs are set. That mostly only makes sense for ethtool-ids which
-     * refer to multiple features ("feature-tso"), while also having more specific ids
-     * ("feature-tx-tcp-segmentation"). */
-
-    /* names from ethtool utility, which are aliases for multiple features. */
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_SG, "tx-scatter-gather", "tx-scatter-gather-fraglist"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TSO,
-              "tx-tcp-segmentation",
-              "tx-tcp-ecn-segmentation",
-              "tx-tcp-mangleid-segmentation",
-              "tx-tcp6-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX,
-              "tx-checksum-ipv4",
-              "tx-checksum-ip-generic",
-              "tx-checksum-ipv6",
-              "tx-checksum-fcoe-crc",
-              "tx-checksum-sctp"),
-
-    /* names from ethtool utility, which are aliases for one feature. */
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_GRO, "rx-gro"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_GSO, "tx-generic-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_LRO, "rx-lro"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_NTUPLE, "rx-ntuple-filter"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX, "rx-checksum"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RXHASH, "rx-hashing"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RXVLAN, "rx-vlan-hw-parse"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TXVLAN, "tx-vlan-hw-insert"),
-
-    /* names of features, as known by kernel. */
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_ESP_HW_OFFLOAD, "esp-hw-offload"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_ESP_TX_CSUM_HW_OFFLOAD, "esp-tx-csum-hw-offload"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_FCOE_MTU, "fcoe-mtu"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_HIGHDMA, "highdma"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_HW_TC_OFFLOAD, "hw-tc-offload"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_L2_FWD_OFFLOAD, "l2-fwd-offload"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_LOOPBACK, "loopback"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_MACSEC_HW_OFFLOAD, "macsec-hw-offload"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_ALL, "rx-all"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_FCS, "rx-fcs"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_GRO_HW, "rx-gro-hw"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_GRO_LIST, "rx-gro-list"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_UDP_GRO_FORWARDING, "rx-udp-gro-forwarding"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_UDP_TUNNEL_PORT_OFFLOAD, "rx-udp_tunnel-port-offload"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_VLAN_FILTER, "rx-vlan-filter"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_VLAN_STAG_FILTER, "rx-vlan-stag-filter"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_VLAN_STAG_HW_PARSE, "rx-vlan-stag-hw-parse"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TLS_HW_RECORD, "tls-hw-record"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TLS_HW_RX_OFFLOAD, "tls-hw-rx-offload"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TLS_HW_TX_OFFLOAD, "tls-hw-tx-offload"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_FCOE_CRC, "tx-checksum-fcoe-crc"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_IPV4, "tx-checksum-ipv4"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_IPV6, "tx-checksum-ipv6"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_IP_GENERIC, "tx-checksum-ip-generic"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_SCTP, "tx-checksum-sctp"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_ESP_SEGMENTATION, "tx-esp-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_FCOE_SEGMENTATION, "tx-fcoe-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_GRE_CSUM_SEGMENTATION, "tx-gre-csum-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_GRE_SEGMENTATION, "tx-gre-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_GSO_LIST, "tx-gso-list"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_GSO_PARTIAL, "tx-gso-partial"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_GSO_ROBUST, "tx-gso-robust"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_IPXIP4_SEGMENTATION, "tx-ipxip4-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_IPXIP6_SEGMENTATION, "tx-ipxip6-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_NOCACHE_COPY, "tx-nocache-copy"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_SCATTER_GATHER, "tx-scatter-gather"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_SCATTER_GATHER_FRAGLIST, "tx-scatter-gather-fraglist"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_SCTP_SEGMENTATION, "tx-sctp-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_TCP6_SEGMENTATION, "tx-tcp6-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_TCP_ECN_SEGMENTATION, "tx-tcp-ecn-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_TCP_MANGLEID_SEGMENTATION, "tx-tcp-mangleid-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_TCP_SEGMENTATION, "tx-tcp-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_TUNNEL_REMCSUM_SEGMENTATION,
-              "tx-tunnel-remcsum-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_UDP_SEGMENTATION, "tx-udp-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_UDP_TNL_CSUM_SEGMENTATION, "tx-udp_tnl-csum-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_UDP_TNL_SEGMENTATION, "tx-udp_tnl-segmentation"),
-    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_VLAN_STAG_HW_INSERT, "tx-vlan-stag-hw-insert"),
-};
-
-/* the number of kernel features that we handle. It essentially is the sum of all
- * kernel_names. So, all ethtool-ids that reference exactly one kernel-name
- * (_NM_ETHTOOL_ID_FEATURE_NUM) + some extra, for ethtool-ids that are aliases
- * for multiple kernel-names. */
-#define N_ETHTOOL_KERNEL_FEATURES (((guint) _NM_ETHTOOL_ID_FEATURE_NUM) + 8u)
-
-static void
-_ASSERT_ethtool_feature_infos(void)
-{
-#if NM_MORE_ASSERTS > 10
-    guint i, k, n;
-    bool  found[_NM_ETHTOOL_ID_FEATURE_NUM] = {};
-
-    G_STATIC_ASSERT_EXPR(G_N_ELEMENTS(_ethtool_feature_infos) == _NM_ETHTOOL_ID_FEATURE_NUM);
-
-    n = 0;
-    for (i = 0; i < G_N_ELEMENTS(_ethtool_feature_infos); i++) {
-        NMEthtoolFeatureState       kstate;
-        const NMEthtoolFeatureInfo *inf = &_ethtool_feature_infos[i];
-
-        g_assert(inf->ethtool_id >= _NM_ETHTOOL_ID_FEATURE_FIRST);
-        g_assert(inf->ethtool_id <= _NM_ETHTOOL_ID_FEATURE_LAST);
-        g_assert(inf->n_kernel_names > 0);
-
-        for (k = 0; k < i; k++)
-            g_assert(inf->ethtool_id != _ethtool_feature_infos[k].ethtool_id);
-
-        g_assert(!found[_NM_ETHTOOL_ID_FEATURE_AS_IDX(inf->ethtool_id)]);
-        found[_NM_ETHTOOL_ID_FEATURE_AS_IDX(inf->ethtool_id)] = TRUE;
-
-        kstate.idx_kernel_name = inf->n_kernel_names - 1;
-        g_assert((guint) kstate.idx_kernel_name == (guint) (inf->n_kernel_names - 1));
-
-        n += inf->n_kernel_names;
-        for (k = 0; k < inf->n_kernel_names; k++) {
-            const char *name = inf->kernel_names[k];
-
-            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. */
-            g_assert(!nm_streq(name, "netns-local"));
-            g_assert(!nm_streq(name, "tx-lockless"));
-            g_assert(!nm_streq(name, "vlan-challenged"));
-        }
-    }
-
-    for (i = 0; i < _NM_ETHTOOL_ID_FEATURE_NUM; i++)
-        g_assert(found[i]);
-
-    g_assert(n == N_ETHTOOL_KERNEL_FEATURES);
-#endif
-}
-
-static NMEthtoolFeatureStates *
-ethtool_get_features(SocketHandle *shandle)
-{
-    gs_free NMEthtoolFeatureStates  *states      = NULL;
-    gs_free struct ethtool_gstrings *ss_features = NULL;
-
-    _ASSERT_ethtool_feature_infos();
-
-    ss_features = ethtool_get_stringset(shandle, ETH_SS_FEATURES);
-    if (!ss_features)
-        return NULL;
-
-    if (ss_features->len > 0) {
-        gs_free struct ethtool_gfeatures   *gfeatures_free = NULL;
-        struct ethtool_gfeatures           *gfeatures;
-        gsize                               gfeatures_len;
-        guint                               idx;
-        const NMEthtoolFeatureState        *states_list0   = NULL;
-        const NMEthtoolFeatureState *const *states_plist0  = NULL;
-        guint                               states_plist_n = 0;
-
-        gfeatures_len = sizeof(struct ethtool_gfeatures)
-                        + (NM_DIV_ROUND_UP(ss_features->len, 32u) * sizeof(gfeatures->features[0]));
-        gfeatures       = nm_malloc0_maybe_a(300, gfeatures_len, &gfeatures_free);
-        gfeatures->cmd  = ETHTOOL_GFEATURES;
-        gfeatures->size = NM_DIV_ROUND_UP(ss_features->len, 32u);
-        if (_ethtool_call_handle(shandle, gfeatures, gfeatures_len) < 0)
-            return NULL;
-
-        for (idx = 0; idx < G_N_ELEMENTS(_ethtool_feature_infos); idx++) {
-            const NMEthtoolFeatureInfo *info = &_ethtool_feature_infos[idx];
-            guint                       idx_kernel_name;
-
-            for (idx_kernel_name = 0; idx_kernel_name < info->n_kernel_names; idx_kernel_name++) {
-                NMEthtoolFeatureState *kstate;
-                const char            *kernel_name = info->kernel_names[idx_kernel_name];
-                int                    i_feature;
-                guint                  i_block;
-                guint32                i_flag;
-
-                i_feature = ethtool_gstrings_find(ss_features, kernel_name);
-                if (i_feature < 0)
-                    continue;
-
-                i_block = ((guint) i_feature) / 32u;
-                i_flag  = (guint32) (1u << (((guint) i_feature) % 32u));
-
-                if (!states) {
-                    states = g_malloc0(
-                        sizeof(NMEthtoolFeatureStates)
-                        + (N_ETHTOOL_KERNEL_FEATURES * sizeof(NMEthtoolFeatureState))
-                        + ((N_ETHTOOL_KERNEL_FEATURES + G_N_ELEMENTS(_ethtool_feature_infos))
-                           * sizeof(NMEthtoolFeatureState *)));
-                    states_list0          = &states->states_list[0];
-                    states_plist0         = (gpointer) &states_list0[N_ETHTOOL_KERNEL_FEATURES];
-                    states->n_ss_features = ss_features->len;
-                }
-
-                nm_assert(states->n_states < N_ETHTOOL_KERNEL_FEATURES);
-                kstate = (NMEthtoolFeatureState *) &states_list0[states->n_states];
-                states->n_states++;
-
-                kstate->info            = info;
-                kstate->idx_ss_features = i_feature;
-                kstate->idx_kernel_name = idx_kernel_name;
-                kstate->available       = !!(gfeatures->features[i_block].available & i_flag);
-                kstate->requested       = !!(gfeatures->features[i_block].requested & i_flag);
-                kstate->active          = !!(gfeatures->features[i_block].active & i_flag);
-                kstate->never_changed   = !!(gfeatures->features[i_block].never_changed & i_flag);
-
-                nm_assert(states_plist_n
-                          < N_ETHTOOL_KERNEL_FEATURES + G_N_ELEMENTS(_ethtool_feature_infos));
-
-                if (!states->states_indexed[_NM_ETHTOOL_ID_FEATURE_AS_IDX(info->ethtool_id)])
-                    states->states_indexed[_NM_ETHTOOL_ID_FEATURE_AS_IDX(info->ethtool_id)] =
-                        &states_plist0[states_plist_n];
-                ((const NMEthtoolFeatureState **) states_plist0)[states_plist_n] = kstate;
-                states_plist_n++;
-            }
-
-            if (states && states->states_indexed[_NM_ETHTOOL_ID_FEATURE_AS_IDX(info->ethtool_id)]) {
-                nm_assert(states_plist_n
-                          < N_ETHTOOL_KERNEL_FEATURES + G_N_ELEMENTS(_ethtool_feature_infos));
-                nm_assert(!states_plist0[states_plist_n]);
-                states_plist_n++;
-            }
-        }
-    }
-
-    return g_steal_pointer(&states);
-}
-
-NMEthtoolFeatureStates *
-nmp_utils_ethtool_get_features(int ifindex)
-{
-    nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT(ifindex);
-    NMEthtoolFeatureStates            *features;
-
-    g_return_val_if_fail(ifindex > 0, 0);
-
-    features = ethtool_get_features(&shandle);
-
-    if (!features) {
-        nm_log_trace(LOGD_PLATFORM,
-                     "ethtool[%d]: %s: failure getting features",
-                     ifindex,
-                     "get-features");
-        return NULL;
-    }
-
-    nm_log_trace(LOGD_PLATFORM,
-                 "ethtool[%d]: %s: retrieved kernel features",
-                 ifindex,
-                 "get-features");
-    return features;
-}
-
-static const char *
-_ethtool_feature_state_to_string(char                        *buf,
-                                 gsize                        buf_size,
-                                 const NMEthtoolFeatureState *s,
-                                 const char                  *prefix)
-{
-    int l;
-
-    l = g_snprintf(buf,
-                   buf_size,
-                   "%s %s%s",
-                   prefix ?: "",
-                   ONOFF(s->active),
-                   (!s->available || s->never_changed)
-                       ? ", [fixed]"
-                       : ((s->requested != s->active)
-                              ? (s->requested ? ", [requested on]" : ", [requested off]")
-                              : ""));
-    nm_assert(l < buf_size);
-    return buf;
-}
-
-gboolean
-nmp_utils_ethtool_set_features(
-    int                           ifindex,
-    const NMEthtoolFeatureStates *features,
-    const NMOptionBool *requested /* indexed by NMEthtoolID - _NM_ETHTOOL_ID_FEATURE_FIRST */,
-    gboolean            do_set /* or reset */)
-{
-    nm_auto_socket_handle SocketHandle shandle        = SOCKET_HANDLE_INIT(ifindex);
-    gs_free struct ethtool_sfeatures  *sfeatures_free = NULL;
-    struct ethtool_sfeatures          *sfeatures;
-    gsize                              sfeatures_len;
-    int                                r;
-    guint                              i, j;
-    struct {
-        const NMEthtoolFeatureState *f_state;
-        NMOptionBool                 requested;
-    } set_states[N_ETHTOOL_KERNEL_FEATURES];
-    guint    set_states_n = 0;
-    gboolean success      = TRUE;
-
-    g_return_val_if_fail(ifindex > 0, 0);
-    g_return_val_if_fail(features, 0);
-    g_return_val_if_fail(requested, 0);
-
-    nm_assert(features->n_states <= N_ETHTOOL_KERNEL_FEATURES);
-
-    for (i = 0; i < _NM_ETHTOOL_ID_FEATURE_NUM; i++) {
-        const NMEthtoolFeatureState *const *states_indexed;
-
-        if (requested[i] == NM_OPTION_BOOL_DEFAULT)
-            continue;
-
-        if (!(states_indexed = features->states_indexed[i])) {
-            if (do_set) {
-                nm_log_trace(LOGD_PLATFORM,
-                             "ethtool[%d]: %s: set feature %s: skip (not found)",
-                             ifindex,
-                             "set-features",
-                             nm_ethtool_data[i + _NM_ETHTOOL_ID_FEATURE_FIRST]->optname);
-                success = FALSE;
-            }
-            continue;
-        }
-
-        for (j = 0; states_indexed[j]; j++) {
-            const NMEthtoolFeatureState *s = states_indexed[j];
-            char                         sbuf[255];
-
-            if (set_states_n >= G_N_ELEMENTS(set_states))
-                g_return_val_if_reached(FALSE);
-
-            if (s->never_changed) {
-                nm_log_trace(LOGD_PLATFORM,
-                             "ethtool[%d]: %s: %s feature %s (%s): %s, %s (skip feature marked as "
-                             "never changed)",
-                             ifindex,
-                             "set-features",
-                             do_set ? "set" : "reset",
-                             nm_ethtool_data[i + _NM_ETHTOOL_ID_FEATURE_FIRST]->optname,
-                             s->info->kernel_names[s->idx_kernel_name],
-                             ONOFF(do_set ? requested[i] == NM_OPTION_BOOL_TRUE : s->active),
-                             _ethtool_feature_state_to_string(sbuf,
-                                                              sizeof(sbuf),
-                                                              s,
-                                                              do_set ? " currently:" : " before:"));
-                continue;
-            }
-
-            nm_log_trace(LOGD_PLATFORM,
-                         "ethtool[%d]: %s: %s feature %s (%s): %s, %s",
-                         ifindex,
-                         "set-features",
-                         do_set ? "set" : "reset",
-                         nm_ethtool_data[i + _NM_ETHTOOL_ID_FEATURE_FIRST]->optname,
-                         s->info->kernel_names[s->idx_kernel_name],
-                         ONOFF(do_set ? requested[i] == NM_OPTION_BOOL_TRUE : s->active),
-                         _ethtool_feature_state_to_string(sbuf,
-                                                          sizeof(sbuf),
-                                                          s,
-                                                          do_set ? " currently:" : " before:"));
-
-            if (do_set && (!s->available || s->never_changed)
-                && (s->active != (requested[i] == NM_OPTION_BOOL_TRUE))) {
-                /* we request to change a flag which kernel reported as fixed.
-                 * While the ethtool operation will silently succeed, mark the request
-                 * as failure. */
-                success = FALSE;
-            }
-
-            set_states[set_states_n].f_state   = s;
-            set_states[set_states_n].requested = requested[i];
-            set_states_n++;
-        }
-    }
-
-    if (set_states_n == 0) {
-        nm_log_trace(LOGD_PLATFORM,
-                     "ethtool[%d]: %s: no feature requested",
-                     ifindex,
-                     "set-features");
-        return TRUE;
-    }
-
-    sfeatures_len =
-        sizeof(struct ethtool_sfeatures)
-        + (NM_DIV_ROUND_UP(features->n_ss_features, 32U) * sizeof(sfeatures->features[0]));
-    sfeatures       = nm_malloc0_maybe_a(300, sfeatures_len, &sfeatures_free);
-    sfeatures->cmd  = ETHTOOL_SFEATURES;
-    sfeatures->size = NM_DIV_ROUND_UP(features->n_ss_features, 32U);
-
-    for (i = 0; i < set_states_n; i++) {
-        const NMEthtoolFeatureState *s = set_states[i].f_state;
-        guint                        i_block;
-        guint32                      i_flag;
-        gboolean                     is_requested;
-
-        i_block = s->idx_ss_features / 32u;
-        i_flag  = (guint32) (1u << (s->idx_ss_features % 32u));
-
-        sfeatures->features[i_block].valid |= i_flag;
-
-        if (do_set)
-            is_requested = (set_states[i].requested == NM_OPTION_BOOL_TRUE);
-        else
-            is_requested = s->active;
-
-        if (is_requested)
-            sfeatures->features[i_block].requested |= i_flag;
-        else
-            sfeatures->features[i_block].requested &= ~i_flag;
-    }
-
-    r = _ethtool_call_handle(&shandle, sfeatures, sfeatures_len);
-    if (r < 0) {
-        success = FALSE;
-        nm_log_trace(LOGD_PLATFORM,
-                     "ethtool[%d]: %s: failure setting features (%s)",
-                     ifindex,
-                     "set-features",
-                     nm_strerror_native(-r));
-        return FALSE;
-    }
-
-    nm_log_trace(LOGD_PLATFORM,
-                 "ethtool[%d]: %s: %s",
-                 ifindex,
-                 "set-features",
-                 success ? "successfully setting features"
-                         : "at least some of the features were not successfully set");
-    return success;
-}
-
-gboolean
-nmp_utils_ethtool_get_coalesce(int ifindex, NMEthtoolCoalesceState *coalesce)
-{
-    struct ethtool_coalesce eth_data;
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-    g_return_val_if_fail(coalesce, FALSE);
-
-    eth_data.cmd = ETHTOOL_GCOALESCE;
-
-    if (_ethtool_call_once(ifindex, &eth_data, sizeof(eth_data)) < 0) {
-        nm_log_trace(LOGD_PLATFORM,
-                     "ethtool[%d]: %s: failure getting coalesce settings",
-                     ifindex,
-                     "get-coalesce");
-        return FALSE;
-    }
-
-    *coalesce = (NMEthtoolCoalesceState) {
-        .s = {
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS)] =
-                eth_data.rx_coalesce_usecs,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES)] =
-                eth_data.rx_max_coalesced_frames,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS_IRQ)] =
-                eth_data.rx_coalesce_usecs_irq,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES_IRQ)] =
-                eth_data.rx_max_coalesced_frames_irq,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS)] =
-                eth_data.tx_coalesce_usecs,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES)] =
-                eth_data.tx_max_coalesced_frames,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS_IRQ)] =
-                eth_data.tx_coalesce_usecs_irq,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES_IRQ)] =
-                eth_data.tx_max_coalesced_frames_irq,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_STATS_BLOCK_USECS)] =
-                eth_data.stats_block_coalesce_usecs,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_ADAPTIVE_RX)] =
-                eth_data.use_adaptive_rx_coalesce,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_ADAPTIVE_TX)] =
-                eth_data.use_adaptive_tx_coalesce,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_PKT_RATE_LOW)] =
-                eth_data.pkt_rate_low,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS_LOW)] =
-                eth_data.rx_coalesce_usecs_low,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES_LOW)] =
-                eth_data.rx_max_coalesced_frames_low,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS_LOW)] =
-                eth_data.tx_coalesce_usecs_low,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES_LOW)] =
-                eth_data.tx_max_coalesced_frames_low,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_PKT_RATE_HIGH)] =
-                eth_data.pkt_rate_high,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS_HIGH)] =
-                eth_data.rx_coalesce_usecs_high,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES_HIGH)] =
-                eth_data.rx_max_coalesced_frames_high,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS_HIGH)] =
-                eth_data.tx_coalesce_usecs_high,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES_HIGH)] =
-                eth_data.tx_max_coalesced_frames_high,
-            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_SAMPLE_INTERVAL)] =
-                eth_data.rate_sample_interval,
-        }};
-    return TRUE;
-
-    nm_log_trace(LOGD_PLATFORM,
-                 "ethtool[%d]: %s: retrieved kernel coalesce settings",
-                 ifindex,
-                 "get-coalesce");
-    return TRUE;
-}
-
-gboolean
-nmp_utils_ethtool_set_coalesce(int ifindex, const NMEthtoolCoalesceState *coalesce)
-{
-    struct ethtool_coalesce eth_data;
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-    g_return_val_if_fail(coalesce, FALSE);
-
-    eth_data = (struct ethtool_coalesce) {
-        .cmd = ETHTOOL_SCOALESCE,
-        .rx_coalesce_usecs =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS)],
-        .rx_max_coalesced_frames =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES)],
-        .rx_coalesce_usecs_irq =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS_IRQ)],
-        .rx_max_coalesced_frames_irq =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES_IRQ)],
-        .tx_coalesce_usecs =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS)],
-        .tx_max_coalesced_frames =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES)],
-        .tx_coalesce_usecs_irq =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS_IRQ)],
-        .tx_max_coalesced_frames_irq =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES_IRQ)],
-        .stats_block_coalesce_usecs =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_STATS_BLOCK_USECS)],
-        .use_adaptive_rx_coalesce =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_ADAPTIVE_RX)],
-        .use_adaptive_tx_coalesce =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_ADAPTIVE_TX)],
-        .pkt_rate_low =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_PKT_RATE_LOW)],
-        .rx_coalesce_usecs_low =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS_LOW)],
-        .rx_max_coalesced_frames_low =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES_LOW)],
-        .tx_coalesce_usecs_low =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS_LOW)],
-        .tx_max_coalesced_frames_low =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES_LOW)],
-        .pkt_rate_high =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_PKT_RATE_HIGH)],
-        .rx_coalesce_usecs_high =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS_HIGH)],
-        .rx_max_coalesced_frames_high =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES_HIGH)],
-        .tx_coalesce_usecs_high =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS_HIGH)],
-        .tx_max_coalesced_frames_high =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES_HIGH)],
-        .rate_sample_interval =
-            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_SAMPLE_INTERVAL)],
-    };
-
-    if (_ethtool_call_once(ifindex, &eth_data, sizeof(eth_data)) < 0) {
-        nm_log_trace(LOGD_PLATFORM,
-                     "ethtool[%d]: %s: failure setting coalesce settings",
-                     ifindex,
-                     "set-coalesce");
-        return FALSE;
-    }
-
-    nm_log_trace(LOGD_PLATFORM,
-                 "ethtool[%d]: %s: set kernel coalesce settings",
-                 ifindex,
-                 "set-coalesce");
-    return TRUE;
-}
-
-gboolean
-nmp_utils_ethtool_get_ring(int ifindex, NMEthtoolRingState *ring)
-{
-    struct ethtool_ringparam eth_data;
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-    g_return_val_if_fail(ring, FALSE);
-
-    eth_data.cmd = ETHTOOL_GRINGPARAM;
-
-    if (_ethtool_call_once(ifindex, &eth_data, sizeof(eth_data)) < 0) {
-        nm_log_trace(LOGD_PLATFORM,
-                     "ethtool[%d]: %s: failure getting ring settings",
-                     ifindex,
-                     "get-ring");
-        return FALSE;
-    }
-
-    *ring = (NMEthtoolRingState) {
-        .rx_pending       = eth_data.rx_pending,
-        .rx_jumbo_pending = eth_data.rx_jumbo_pending,
-        .rx_mini_pending  = eth_data.rx_mini_pending,
-        .tx_pending       = eth_data.tx_pending,
-    };
-
-    nm_log_trace(LOGD_PLATFORM,
-                 "ethtool[%d]: %s: retrieved kernel ring settings",
-                 ifindex,
-                 "get-ring");
-    return TRUE;
-}
-
-gboolean
-nmp_utils_ethtool_set_ring(int ifindex, const NMEthtoolRingState *ring)
-{
-    struct ethtool_ringparam eth_data;
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-    g_return_val_if_fail(ring, FALSE);
-
-    eth_data = (struct ethtool_ringparam) {
-        .cmd              = ETHTOOL_SRINGPARAM,
-        .rx_pending       = ring->rx_pending,
-        .rx_jumbo_pending = ring->rx_jumbo_pending,
-        .rx_mini_pending  = ring->rx_mini_pending,
-        .tx_pending       = ring->tx_pending,
-    };
-
-    if (_ethtool_call_once(ifindex, &eth_data, sizeof(eth_data)) < 0) {
-        nm_log_trace(LOGD_PLATFORM,
-                     "ethtool[%d]: %s: failure setting ring settings",
-                     ifindex,
-                     "set-ring");
-        return FALSE;
-    }
-
-    nm_log_trace(LOGD_PLATFORM, "ethtool[%d]: %s: set kernel ring settings", ifindex, "set-ring");
-    return TRUE;
-}
-
-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;
-    nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT(ifindex);
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-    g_return_val_if_fail(pause, FALSE);
-
-    eth_data.cmd = ETHTOOL_GPAUSEPARAM;
-    if (_ethtool_call_handle(&shandle, &eth_data, sizeof(struct ethtool_pauseparam)) != 0) {
-        nm_log_trace(LOGD_PLATFORM,
-                     "ethtool[%d]: %s: failure getting pause settings",
-                     ifindex,
-                     "get-pause");
-        return FALSE;
-    }
-
-    *pause = (NMEthtoolPauseState) {
-        .autoneg = eth_data.autoneg == 1,
-        .rx      = eth_data.rx_pause == 1,
-        .tx      = eth_data.tx_pause == 1,
-    };
-
-    nm_log_trace(LOGD_PLATFORM,
-                 "ethtool[%d]: %s: retrieved kernel pause settings",
-                 ifindex,
-                 "get-pause");
-    return TRUE;
-}
-
-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;
-    nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT(ifindex);
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-    g_return_val_if_fail(pause, FALSE);
-
-    eth_data = (struct ethtool_pauseparam) {
-        .cmd      = ETHTOOL_SPAUSEPARAM,
-        .autoneg  = pause->autoneg ? 1 : 0,
-        .rx_pause = pause->rx ? 1 : 0,
-        .tx_pause = pause->tx ? 1 : 0,
-    };
-
-    if (_ethtool_call_handle(&shandle, &eth_data, sizeof(struct ethtool_pauseparam)) != 0) {
-        nm_log_trace(LOGD_PLATFORM,
-                     "ethtool[%d]: %s: failure setting pause settings",
-                     ifindex,
-                     "set-pause");
-        return FALSE;
-    }
-    nm_log_trace(LOGD_PLATFORM, "ethtool[%d]: %s: set kernel puase settings", ifindex, "set-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
-nmp_utils_ethtool_get_driver_info(int ifindex, NMPUtilsEthtoolDriverInfo *data)
-{
-    struct ethtool_drvinfo *drvinfo;
-
-    G_STATIC_ASSERT_EXPR(sizeof(*data) == sizeof(*drvinfo));
-    G_STATIC_ASSERT_EXPR(offsetof(NMPUtilsEthtoolDriverInfo, driver)
-                         == offsetof(struct ethtool_drvinfo, driver));
-    G_STATIC_ASSERT_EXPR(offsetof(NMPUtilsEthtoolDriverInfo, version)
-                         == offsetof(struct ethtool_drvinfo, version));
-    G_STATIC_ASSERT_EXPR(offsetof(NMPUtilsEthtoolDriverInfo, fw_version)
-                         == offsetof(struct ethtool_drvinfo, fw_version));
-    G_STATIC_ASSERT_EXPR(sizeof(data->driver) == sizeof(drvinfo->driver));
-    G_STATIC_ASSERT_EXPR(sizeof(data->version) == sizeof(drvinfo->version));
-    G_STATIC_ASSERT_EXPR(sizeof(data->fw_version) == sizeof(drvinfo->fw_version));
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-    g_return_val_if_fail(data, FALSE);
-
-    drvinfo  = (struct ethtool_drvinfo *) data;
-    *drvinfo = (struct ethtool_drvinfo) {
-        .cmd = ETHTOOL_GDRVINFO,
-    };
-    return _ethtool_call_once(ifindex, drvinfo, sizeof(*drvinfo)) >= 0;
-}
-
-gboolean
-nmp_utils_ethtool_get_permanent_address(int ifindex, guint8 *buf, size_t *length)
-{
-    struct {
-        struct ethtool_perm_addr e;
-        guint8                   _extra_data[_NM_UTILS_HWADDR_LEN_MAX + 1];
-    } edata = {
-        .e.cmd  = ETHTOOL_GPERMADDR,
-        .e.size = _NM_UTILS_HWADDR_LEN_MAX,
-    };
-    const guint8 *pdata;
-
-    guint i;
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-
-    if (_ethtool_call_once(ifindex, &edata, sizeof(edata)) < 0)
-        return FALSE;
-
-    if (edata.e.size > _NM_UTILS_HWADDR_LEN_MAX)
-        return FALSE;
-    if (edata.e.size < 1)
-        return FALSE;
-
-    pdata = (const guint8 *) edata.e.data;
-
-    if (NM_IN_SET(pdata[0], 0, 0xFF)) {
-        /* Some drivers might return a permanent address of all zeros.
-         * Reject that (rh#1264024)
-         *
-         * Some drivers return a permanent address of all ones. Reject that too */
-        for (i = 1; i < edata.e.size; i++) {
-            if (pdata[0] != pdata[i])
-                goto not_all_0or1;
-        }
-        return FALSE;
-    }
-
-not_all_0or1:
-    memcpy(buf, pdata, edata.e.size);
-    *length = edata.e.size;
-    return TRUE;
-}
-
-gboolean
-nmp_utils_ethtool_supports_carrier_detect(int ifindex)
-{
-    struct ethtool_cmd edata = {.cmd = ETHTOOL_GLINK};
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-
-    /* We ignore the result. If the ETHTOOL_GLINK call succeeded, then we
-     * assume the device supports carrier-detect, otherwise we assume it
-     * doesn't.
-     */
-    return _ethtool_call_once(ifindex, &edata, sizeof(edata)) >= 0;
-}
-
-gboolean
-nmp_utils_ethtool_supports_vlans(int ifindex)
-{
-    nm_auto_socket_handle SocketHandle shandle       = SOCKET_HANDLE_INIT(ifindex);
-    gs_free struct ethtool_gfeatures  *features_free = NULL;
-    struct ethtool_gfeatures          *features;
-    gsize                              features_len;
-    int                                idx, block, bit, size;
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-
-    idx = ethtool_get_stringset_index(&shandle, ETH_SS_FEATURES, "vlan-challenged");
-    if (idx < 0) {
-        nm_log_dbg(LOGD_PLATFORM,
-                   "ethtool[%d]: vlan-challenged ethtool feature does not exist?",
-                   ifindex);
-        return FALSE;
-    }
-
-    block = idx / 32;
-    bit   = idx % 32;
-    size  = block + 1;
-
-    features_len   = sizeof(*features) + (size * sizeof(struct ethtool_get_features_block));
-    features       = nm_malloc0_maybe_a(300, features_len, &features_free);
-    features->cmd  = ETHTOOL_GFEATURES;
-    features->size = size;
-
-    if (_ethtool_call_handle(&shandle, features, features_len) < 0)
-        return FALSE;
-
-    return !(features->features[block].active & (1 << bit));
-}
-
-int
-nmp_utils_ethtool_get_peer_ifindex(int ifindex)
-{
-    nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT(ifindex);
-    gsize                              stats_len;
-    gs_free struct ethtool_stats      *stats_free = NULL;
-    struct ethtool_stats              *stats;
-    int                                peer_ifindex_stat;
-
-    g_return_val_if_fail(ifindex > 0, 0);
-
-    peer_ifindex_stat = ethtool_get_stringset_index(&shandle, ETH_SS_STATS, "peer_ifindex");
-    if (peer_ifindex_stat < 0) {
-        nm_log_dbg(LOGD_PLATFORM, "ethtool[%d]: peer_ifindex stat does not exist?", ifindex);
-        return FALSE;
-    }
-
-    stats_len      = sizeof(*stats) + (peer_ifindex_stat + 1) * sizeof(guint64);
-    stats          = nm_malloc0_maybe_a(300, stats_len, &stats_free);
-    stats->cmd     = ETHTOOL_GSTATS;
-    stats->n_stats = peer_ifindex_stat + 1;
-    if (_ethtool_call_handle(&shandle, stats, stats_len) < 0)
-        return 0;
-
-    return stats->data[peer_ifindex_stat];
-}
-
-gboolean
-nmp_utils_ethtool_get_wake_on_lan(int ifindex)
-{
-    struct ethtool_wolinfo wol = {
-        .cmd = ETHTOOL_GWOL,
-    };
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-
-    if (_ethtool_call_once(ifindex, &wol, sizeof(wol)) < 0)
-        return FALSE;
-
-    return wol.wolopts != 0;
-}
-
-gboolean
-nmp_utils_ethtool_get_link_settings(int                       ifindex,
-                                    gboolean                 *out_autoneg,
-                                    guint32                  *out_speed,
-                                    NMPlatformLinkDuplexType *out_duplex)
-{
-    struct ethtool_cmd edata = {
-        .cmd = ETHTOOL_GSET,
-    };
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-
-    if (_ethtool_call_once(ifindex, &edata, sizeof(edata)) < 0)
-        return FALSE;
-
-    NM_SET_OUT(out_autoneg, (edata.autoneg == AUTONEG_ENABLE));
-
-    if (out_speed) {
-        guint32 speed;
-
-        speed = ethtool_cmd_speed(&edata);
-        if (speed == G_MAXUINT16 || speed == G_MAXUINT32)
-            speed = 0;
-
-        *out_speed = speed;
-    }
-
-    if (out_duplex) {
-        switch (edata.duplex) {
-        case DUPLEX_HALF:
-            *out_duplex = NM_PLATFORM_LINK_DUPLEX_HALF;
-            break;
-        case DUPLEX_FULL:
-            *out_duplex = NM_PLATFORM_LINK_DUPLEX_FULL;
-            break;
-        default: /* DUPLEX_UNKNOWN */
-            *out_duplex = NM_PLATFORM_LINK_DUPLEX_UNKNOWN;
-            break;
-        }
-    }
-
-    return TRUE;
-}
-
-#define ADVERTISED_INVALID 0
-
-static guint32
-get_baset_mode(guint32 speed, NMPlatformLinkDuplexType duplex)
-{
-    if (duplex == NM_PLATFORM_LINK_DUPLEX_UNKNOWN)
-        return ADVERTISED_INVALID;
-
-    if (duplex == NM_PLATFORM_LINK_DUPLEX_HALF) {
-        switch (speed) {
-        case 10:
-            return ADVERTISED_10baseT_Half;
-        case 100:
-            return ADVERTISED_100baseT_Half;
-        case 1000:
-            return ADVERTISED_1000baseT_Half;
-        default:
-            return ADVERTISED_INVALID;
-        }
-    } else {
-        switch (speed) {
-        case 10:
-            return ADVERTISED_10baseT_Full;
-        case 100:
-            return ADVERTISED_100baseT_Full;
-        case 1000:
-            return ADVERTISED_1000baseT_Full;
-        case 10000:
-            return ADVERTISED_10000baseT_Full;
-        default:
-            return ADVERTISED_INVALID;
-        }
-    }
-}
-
-static gboolean
-platform_link_duplex_type_to_native(NMPlatformLinkDuplexType duplex_type, guint8 *out_native)
-{
-    switch (duplex_type) {
-    case NM_PLATFORM_LINK_DUPLEX_HALF:
-        *out_native = DUPLEX_HALF;
-        return TRUE;
-    case NM_PLATFORM_LINK_DUPLEX_FULL:
-        *out_native = DUPLEX_FULL;
-        return TRUE;
-    case NM_PLATFORM_LINK_DUPLEX_UNKNOWN:
-        return FALSE;
-    default:
-        g_return_val_if_reached(FALSE);
-    }
-}
-
 const guint8 _nmp_link_mode_all_advertised_modes_bits[] = {
     ETHTOOL_LINK_MODE_10baseT_Half_BIT,
     ETHTOOL_LINK_MODE_10baseT_Full_BIT,
@@ -1597,322 +148,6 @@ const guint32 _nmp_link_mode_all_advertised_modes[] = {
     0x0ffffbffu,
 };
 
-static NMOptionBool
-set_link_settings_new(SocketHandle            *shandle,
-                      gboolean                 autoneg,
-                      guint32                  speed,
-                      NMPlatformLinkDuplexType duplex)
-{
-    struct ethtool_link_settings          edata0;
-    gs_free struct ethtool_link_settings *edata = NULL;
-    gsize                                 edata_size;
-    guint                                 nwords;
-    guint                                 i;
-
-    edata0 = (struct ethtool_link_settings) {
-        .cmd                    = ETHTOOL_GLINKSETTINGS,
-        .link_mode_masks_nwords = 0,
-    };
-
-    /* perform the handshake to find the size of masks */
-    if (_ethtool_call_handle(shandle, &edata0, sizeof(edata0)) < 0
-        || edata0.link_mode_masks_nwords >= 0) {
-        /* new API not supported */
-        return NM_OPTION_BOOL_DEFAULT;
-    }
-
-    nwords                        = -edata0.link_mode_masks_nwords;
-    edata_size                    = sizeof(*edata) + sizeof(guint32) * nwords * 3;
-    edata                         = g_malloc0(edata_size);
-    edata->cmd                    = ETHTOOL_GLINKSETTINGS;
-    edata->link_mode_masks_nwords = nwords;
-
-    /* retrieve first current settings */
-    if (_ethtool_call_handle(shandle, edata, edata_size) < 0)
-        return FALSE;
-
-    /* then change the needed ones */
-    edata->cmd = ETHTOOL_SLINKSETTINGS;
-
-    {
-        const guint32 *v_map_supported      = &edata->link_mode_masks[0];
-        guint32       *v_map_advertising    = &edata->link_mode_masks[nwords];
-        guint32       *v_map_lp_advertising = &edata->link_mode_masks[2 * nwords];
-
-        memcpy(v_map_advertising, v_map_supported, sizeof(guint32) * nwords);
-        (void) v_map_lp_advertising;
-
-        if (speed != 0) {
-            guint32 mode;
-
-            mode = get_baset_mode(speed, duplex);
-
-            if (mode == ADVERTISED_INVALID) {
-                if (!autoneg)
-                    goto set_autoneg;
-                nm_log_trace(LOGD_PLATFORM,
-                             "ethtool[%d]: %uBASE-T %s duplex mode cannot be advertised",
-                             shandle->ifindex,
-                             speed,
-                             nm_platform_link_duplex_type_to_string(duplex));
-                return FALSE;
-            }
-
-            if (!(v_map_supported[0] & mode)) {
-                if (!autoneg)
-                    goto set_autoneg;
-                nm_log_trace(LOGD_PLATFORM,
-                             "ethtool[%d]: device does not support %uBASE-T %s duplex mode",
-                             shandle->ifindex,
-                             speed,
-                             nm_platform_link_duplex_type_to_string(duplex));
-                return FALSE;
-            }
-
-            for (i = 0; i < (guint) G_N_ELEMENTS(_nmp_link_mode_all_advertised_modes); i++)
-                v_map_advertising[i] &= ~_nmp_link_mode_all_advertised_modes[i];
-            v_map_advertising[0] |= mode;
-        }
-    }
-
-set_autoneg:
-    if (autoneg)
-        edata->autoneg = AUTONEG_ENABLE;
-    else {
-        edata->autoneg = AUTONEG_DISABLE;
-
-        if (speed)
-            edata->speed = speed;
-
-        platform_link_duplex_type_to_native(duplex, &edata->duplex);
-    }
-
-    return _ethtool_call_handle(shandle, edata, edata_size) >= 0;
-}
-
-gboolean
-nmp_utils_ethtool_set_link_settings(int                      ifindex,
-                                    gboolean                 autoneg,
-                                    guint32                  speed,
-                                    NMPlatformLinkDuplexType duplex)
-{
-    nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT(ifindex);
-    struct ethtool_cmd                 edata   = {
-                          .cmd = ETHTOOL_GSET,
-    };
-    NMOptionBool ret;
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-    g_return_val_if_fail((speed && duplex != NM_PLATFORM_LINK_DUPLEX_UNKNOWN)
-                             || (!speed && duplex == NM_PLATFORM_LINK_DUPLEX_UNKNOWN),
-                         FALSE);
-
-    nm_log_trace(LOGD_PLATFORM,
-                 "ethtool[%d]: set link: autoneg=%d, speed=%d, duplex=%s",
-                 ifindex,
-                 autoneg,
-                 speed,
-                 nm_platform_link_duplex_type_to_string(duplex));
-
-    ret = set_link_settings_new(&shandle, autoneg, speed, duplex);
-    if (ret != NM_OPTION_BOOL_DEFAULT)
-        return ret;
-
-    /* new ETHTOOL_GLINKSETTINGS API not supported, fall back to GSET */
-
-    /* retrieve first current settings */
-    if (_ethtool_call_handle(&shandle, &edata, sizeof(edata)) < 0)
-        return FALSE;
-
-    /* then change the needed ones */
-    edata.cmd = ETHTOOL_SSET;
-
-    edata.advertising = edata.supported;
-    if (speed != 0) {
-        guint32 mode;
-
-        mode = get_baset_mode(speed, duplex);
-
-        if (mode == ADVERTISED_INVALID) {
-            if (!autoneg)
-                goto set_autoneg;
-            nm_log_trace(LOGD_PLATFORM,
-                         "ethtool[%d]: %uBASE-T %s duplex mode cannot be advertised",
-                         ifindex,
-                         speed,
-                         nm_platform_link_duplex_type_to_string(duplex));
-            return FALSE;
-        }
-        if (!(edata.supported & mode)) {
-            if (!autoneg)
-                goto set_autoneg;
-            nm_log_trace(LOGD_PLATFORM,
-                         "ethtool[%d]: device does not support %uBASE-T %s duplex mode",
-                         ifindex,
-                         speed,
-                         nm_platform_link_duplex_type_to_string(duplex));
-            return FALSE;
-        }
-        edata.advertising &= ~_nmp_link_mode_all_advertised_modes[0];
-        edata.advertising |= mode;
-    }
-
-set_autoneg:
-    if (autoneg)
-        edata.autoneg = AUTONEG_ENABLE;
-    else {
-        edata.autoneg = AUTONEG_DISABLE;
-
-        if (speed)
-            ethtool_cmd_speed_set(&edata, speed);
-
-        platform_link_duplex_type_to_native(duplex, &edata.duplex);
-    }
-
-    return _ethtool_call_handle(&shandle, &edata, sizeof(edata)) >= 0;
-}
-
-gboolean
-nmp_utils_ethtool_set_wake_on_lan(int                      ifindex,
-                                  _NMSettingWiredWakeOnLan wol,
-                                  const char              *wol_password)
-{
-    struct ethtool_wolinfo wol_info = {
-        .cmd     = ETHTOOL_SWOL,
-        .wolopts = 0,
-    };
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-
-    if (wol == _NM_SETTING_WIRED_WAKE_ON_LAN_IGNORE)
-        return TRUE;
-
-    nm_log_dbg(LOGD_PLATFORM,
-               "ethtool[%d]: setting Wake-on-LAN options 0x%x, password '%s'",
-               ifindex,
-               (unsigned) wol,
-               wol_password);
-
-    if (NM_FLAGS_HAS(wol, _NM_SETTING_WIRED_WAKE_ON_LAN_PHY))
-        wol_info.wolopts |= WAKE_PHY;
-    if (NM_FLAGS_HAS(wol, _NM_SETTING_WIRED_WAKE_ON_LAN_UNICAST))
-        wol_info.wolopts |= WAKE_UCAST;
-    if (NM_FLAGS_HAS(wol, _NM_SETTING_WIRED_WAKE_ON_LAN_MULTICAST))
-        wol_info.wolopts |= WAKE_MCAST;
-    if (NM_FLAGS_HAS(wol, _NM_SETTING_WIRED_WAKE_ON_LAN_BROADCAST))
-        wol_info.wolopts |= WAKE_BCAST;
-    if (NM_FLAGS_HAS(wol, _NM_SETTING_WIRED_WAKE_ON_LAN_ARP))
-        wol_info.wolopts |= WAKE_ARP;
-    if (NM_FLAGS_HAS(wol, _NM_SETTING_WIRED_WAKE_ON_LAN_MAGIC))
-        wol_info.wolopts |= WAKE_MAGIC;
-
-    if (wol_password) {
-        if (!_nm_utils_hwaddr_aton_exact(wol_password, wol_info.sopass, ETH_ALEN)) {
-            nm_log_dbg(LOGD_PLATFORM,
-                       "ethtool[%d]: couldn't parse Wake-on-LAN password '%s'",
-                       ifindex,
-                       wol_password);
-            return FALSE;
-        }
-        wol_info.wolopts |= WAKE_MAGICSECURE;
-    }
-
-    return _ethtool_call_once(ifindex, &wol_info, sizeof(wol_info)) >= 0;
-}
-
-gboolean
-nmp_utils_ethtool_get_fec_mode(int ifindex, uint32_t *fec_mode)
-{
-    int                     r;
-    struct ethtool_fecparam fec_param = {
-        .cmd = ETHTOOL_GFECPARAM,
-        .fec = 0,
-    };
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-
-    if (_ethtool_call_once(ifindex, &fec_param, sizeof(fec_param)) >= 0) {
-        nm_log_dbg(LOGD_PLATFORM, "ethtool[%d]: get FEC options 0x%x", ifindex, fec_param.fec);
-        *fec_mode = fec_param.fec;
-        return TRUE;
-    } else {
-        r = -NM_ERRNO_NATIVE(errno);
-        nm_log_dbg(LOGD_PLATFORM,
-                   "ethtool[%d]: ETHTOOL_GFECPARAM failure get fec mode: (%s)",
-                   ifindex,
-                   nm_strerror_native(-r));
-        return FALSE;
-    }
-}
-
-gboolean
-nmp_utils_ethtool_set_fec_mode(int ifindex, uint32_t fec_mode)
-{
-    struct ethtool_fecparam fec_param = {
-        .cmd = ETHTOOL_SFECPARAM,
-        .fec = fec_mode,
-    };
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-
-    nm_log_dbg(LOGD_PLATFORM, "ethtool[%d]: setting FEC options 0x%x", ifindex, fec_mode);
-
-    return _ethtool_call_once(ifindex, &fec_param, sizeof(fec_param)) >= 0;
-}
-
-/******************************************************************************
- * mii
- *****************************************************************************/
-
-gboolean
-nmp_utils_mii_supports_carrier_detect(int ifindex)
-{
-    nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT(ifindex);
-    int                                r;
-    struct ifreq                       ifr;
-    struct mii_ioctl_data             *mii;
-
-    g_return_val_if_fail(ifindex > 0, FALSE);
-
-    r = _ioctl_call("mii",
-                    "SIOCGMIIPHY",
-                    SIOCGMIIPHY,
-                    shandle.ifindex,
-                    &shandle.fd,
-                    shandle.ifname,
-                    IOCTL_CALL_DATA_TYPE_NONE,
-                    NULL,
-                    0,
-                    &ifr);
-    if (r < 0)
-        return FALSE;
-
-    /* If we can read the BMSR register, we assume that the card supports MII link detection */
-    mii          = (struct mii_ioctl_data *) &ifr.ifr_ifru;
-    mii->reg_num = MII_BMSR;
-
-    r = _ioctl_call("mii",
-                    "SIOCGMIIREG",
-                    SIOCGMIIREG,
-                    shandle.ifindex,
-                    &shandle.fd,
-                    shandle.ifname,
-                    IOCTL_CALL_DATA_TYPE_IFRU,
-                    mii,
-                    sizeof(*mii),
-                    &ifr);
-    if (r < 0)
-        return FALSE;
-
-    mii = (struct mii_ioctl_data *) &ifr.ifr_ifru;
-    nm_log_trace(LOGD_PLATFORM,
-                 "mii[%d,%s]: carrier-detect yes: SIOCGMIIREG result 0x%X",
-                 ifindex,
-                 shandle.ifname,
-                 mii->val_out);
-    return TRUE;
-}
-
 /******************************************************************************
  * udev
  *****************************************************************************/
diff --git a/src/libnm-platform/nm-platform-utils.h b/src/libnm-platform/nm-platform-utils.h
index f47bcd49..4477d7ed 100644
--- a/src/libnm-platform/nm-platform-utils.h
+++ b/src/libnm-platform/nm-platform-utils.h
@@ -8,72 +8,17 @@
 
 #include "libnm-base/nm-base.h"
 #include "libnm-platform/nmp-base.h"
+#include "libnm-platform/nm-platform.h"
 
 /*****************************************************************************/
 
-const char *nmp_utils_ethtool_get_driver(int ifindex);
-gboolean    nmp_utils_ethtool_supports_carrier_detect(int ifindex);
-gboolean    nmp_utils_ethtool_supports_vlans(int ifindex);
-int         nmp_utils_ethtool_get_peer_ifindex(int ifindex);
-gboolean    nmp_utils_ethtool_get_wake_on_lan(int ifindex);
-gboolean    nmp_utils_ethtool_set_wake_on_lan(int                      ifindex,
-                                              _NMSettingWiredWakeOnLan wol,
-                                              const char              *wol_password);
-
 const char *nm_platform_link_duplex_type_to_string(NMPlatformLinkDuplexType duplex);
 
 extern const guint8  _nmp_link_mode_all_advertised_modes_bits[79];
 extern const guint32 _nmp_link_mode_all_advertised_modes[3];
 
-gboolean nmp_utils_ethtool_get_link_settings(int                       ifindex,
-                                             gboolean                 *out_autoneg,
-                                             guint32                  *out_speed,
-                                             NMPlatformLinkDuplexType *out_duplex);
-gboolean nmp_utils_ethtool_set_link_settings(int                      ifindex,
-                                             gboolean                 autoneg,
-                                             guint32                  speed,
-                                             NMPlatformLinkDuplexType duplex);
-
-gboolean nmp_utils_ethtool_get_permanent_address(int ifindex, guint8 *buf, size_t *length);
-
-gboolean nmp_utils_ethtool_get_driver_info(int ifindex, NMPUtilsEthtoolDriverInfo *data);
-
-NMEthtoolFeatureStates *nmp_utils_ethtool_get_features(int ifindex);
-
-gboolean nmp_utils_ethtool_set_features(
-    int                           ifindex,
-    const NMEthtoolFeatureStates *features,
-    const NMOptionBool *requested /* indexed by NMEthtoolID - _NM_ETHTOOL_ID_FEATURE_FIRST */,
-    gboolean            do_set /* or reset */);
-
-gboolean nmp_utils_ethtool_get_coalesce(int ifindex, NMEthtoolCoalesceState *coalesce);
-
-gboolean nmp_utils_ethtool_set_coalesce(int ifindex, const NMEthtoolCoalesceState *coalesce);
-
-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_ethtool_get_fec_mode(int ifindex, uint32_t *fec_mode);
-
-gboolean nmp_utils_ethtool_set_fec_mode(int ifindex, uint32_t fec_mode);
-
 /*****************************************************************************/
 
-gboolean nmp_utils_mii_supports_carrier_detect(int ifindex);
-
 struct udev_device;
 
 const char *nmp_utils_udev_get_driver(struct udev_device *udevice);
diff --git a/src/libnm-platform/nm-platform.c b/src/libnm-platform/nm-platform.c
index a8d34b71..fd966d2c 100644
--- a/src/libnm-platform/nm-platform.c
+++ b/src/libnm-platform/nm-platform.c
@@ -31,6 +31,7 @@
 #include "libnm-log-core/nm-logging.h"
 #include "libnm-platform/nm-platform-utils.h"
 #include "libnm-platform/nmp-netns.h"
+#include "libnm-platform/nmp-ethtool-ioctl.h"
 #include "libnm-udev-aux/nm-udev-utils.h"
 #include "nm-platform-private.h"
 #include "nmp-object.h"
@@ -3230,7 +3231,7 @@ nm_platform_link_veth_get_properties(NMPlatform *self, int ifindex, int *out_pee
 
         if (!nm_platform_netns_push(self, &netns))
             return FALSE;
-        peer_ifindex = nmp_utils_ethtool_get_peer_ifindex(plink->ifindex);
+        peer_ifindex = nmp_ethtool_ioctl_get_peer_ifindex(plink->ifindex);
         if (peer_ifindex <= 0)
             return FALSE;
 
@@ -3561,7 +3562,7 @@ nm_platform_ethtool_set_wake_on_lan(NMPlatform              *self,
 
     g_return_val_if_fail(ifindex > 0, FALSE);
 
-    return nmp_utils_ethtool_set_wake_on_lan(ifindex, wol, wol_password);
+    return nmp_ethtool_ioctl_set_wake_on_lan(ifindex, wol, wol_password);
 }
 
 gboolean
@@ -3575,7 +3576,7 @@ nm_platform_ethtool_set_link_settings(NMPlatform              *self,
 
     g_return_val_if_fail(ifindex > 0, FALSE);
 
-    return nmp_utils_ethtool_set_link_settings(ifindex, autoneg, speed, duplex);
+    return nmp_ethtool_ioctl_set_link_settings(ifindex, autoneg, speed, duplex);
 }
 
 gboolean
@@ -3589,19 +3590,19 @@ nm_platform_ethtool_get_link_settings(NMPlatform               *self,
 
     g_return_val_if_fail(ifindex > 0, FALSE);
 
-    return nmp_utils_ethtool_get_link_settings(ifindex, out_autoneg, out_speed, out_duplex);
+    return nmp_ethtool_ioctl_get_link_settings(ifindex, out_autoneg, out_speed, out_duplex);
 }
 
 /*****************************************************************************/
 
 NMEthtoolFeatureStates *
-nm_platform_ethtool_get_link_features(NMPlatform *self, int ifindex)
+nm_platform_ethtool_get_features(NMPlatform *self, int ifindex)
 {
     _CHECK_SELF_NETNS(self, klass, netns, NULL);
 
     g_return_val_if_fail(ifindex > 0, NULL);
 
-    return nmp_utils_ethtool_get_features(ifindex);
+    return nmp_ethtool_ioctl_get_features(ifindex);
 }
 
 gboolean
@@ -3616,7 +3617,7 @@ nm_platform_ethtool_set_features(
 
     g_return_val_if_fail(ifindex > 0, FALSE);
 
-    return nmp_utils_ethtool_set_features(ifindex, features, requested, do_set);
+    return nmp_ethtool_ioctl_set_features(ifindex, features, requested, do_set);
 }
 
 gboolean
@@ -3626,7 +3627,7 @@ nm_platform_ethtool_get_fec_mode(NMPlatform *self, int ifindex, uint32_t *fec_mo
 
     g_return_val_if_fail(ifindex > 0, FALSE);
 
-    return nmp_utils_ethtool_get_fec_mode(ifindex, fec_mode);
+    return nmp_ethtool_ioctl_get_fec_mode(ifindex, fec_mode);
 }
 
 gboolean
@@ -3636,20 +3637,18 @@ nm_platform_ethtool_set_fec_mode(NMPlatform *self, int ifindex, uint32_t fec_mod
 
     g_return_val_if_fail(ifindex > 0, FALSE);
 
-    return nmp_utils_ethtool_set_fec_mode(ifindex, fec_mode);
+    return nmp_ethtool_ioctl_set_fec_mode(ifindex, fec_mode);
 }
 
 gboolean
-nm_platform_ethtool_get_link_coalesce(NMPlatform             *self,
-                                      int                     ifindex,
-                                      NMEthtoolCoalesceState *coalesce)
+nm_platform_ethtool_get_coalesce(NMPlatform *self, int ifindex, NMEthtoolCoalesceState *coalesce)
 {
     _CHECK_SELF_NETNS(self, klass, netns, FALSE);
 
     g_return_val_if_fail(ifindex > 0, FALSE);
     g_return_val_if_fail(coalesce, FALSE);
 
-    return nmp_utils_ethtool_get_coalesce(ifindex, coalesce);
+    return nmp_ethtool_ioctl_get_coalesce(ifindex, coalesce);
 }
 
 gboolean
@@ -3661,18 +3660,18 @@ nm_platform_ethtool_set_coalesce(NMPlatform                   *self,
 
     g_return_val_if_fail(ifindex > 0, FALSE);
 
-    return nmp_utils_ethtool_set_coalesce(ifindex, coalesce);
+    return nmp_ethtool_ioctl_set_coalesce(ifindex, coalesce);
 }
 
 gboolean
-nm_platform_ethtool_get_link_ring(NMPlatform *self, int ifindex, NMEthtoolRingState *ring)
+nm_platform_ethtool_get_ring(NMPlatform *self, int ifindex, NMEthtoolRingState *ring)
 {
     _CHECK_SELF_NETNS(self, klass, netns, FALSE);
 
     g_return_val_if_fail(ifindex > 0, FALSE);
     g_return_val_if_fail(ring, FALSE);
 
-    return nmp_utils_ethtool_get_ring(ifindex, ring);
+    return klass->ethtool_get_ring(self, ifindex, ring);
 }
 
 gboolean
@@ -3681,21 +3680,20 @@ nm_platform_ethtool_set_ring(NMPlatform *self, int ifindex, const NMEthtoolRingS
     _CHECK_SELF_NETNS(self, klass, netns, FALSE);
 
     g_return_val_if_fail(ifindex > 0, FALSE);
+    g_return_val_if_fail(ring, FALSE);
 
-    return nmp_utils_ethtool_set_ring(ifindex, ring);
+    return klass->ethtool_set_ring(self, ifindex, ring);
 }
 
 gboolean
-nm_platform_ethtool_get_link_channels(NMPlatform             *self,
-                                      int                     ifindex,
-                                      NMEthtoolChannelsState *channels)
+nm_platform_ethtool_get_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);
+    return nmp_ethtool_ioctl_get_channels(ifindex, channels);
 }
 
 gboolean
@@ -3707,39 +3705,40 @@ nm_platform_ethtool_set_channels(NMPlatform                   *self,
 
     g_return_val_if_fail(ifindex > 0, FALSE);
 
-    return nmp_utils_ethtool_set_channels(ifindex, channels);
+    return nmp_ethtool_ioctl_set_channels(ifindex, channels);
 }
 
 gboolean
-nm_platform_ethtool_get_link_pause(NMPlatform *self, int ifindex, NMEthtoolPauseState *pause)
+nm_platform_ethtool_get_pause(NMPlatform *self, int ifindex, NMEthtoolPauseState *pause)
 {
     _CHECK_SELF_NETNS(self, klass, netns, FALSE);
 
     g_return_val_if_fail(ifindex > 0, FALSE);
     g_return_val_if_fail(pause, FALSE);
 
-    return nmp_utils_ethtool_get_pause(ifindex, pause);
+    return klass->ethtool_get_pause(self, ifindex, pause);
 }
 
 gboolean
-nm_platform_ethtool_get_link_eee(NMPlatform *self, int ifindex, NMEthtoolEEEState *eee)
+nm_platform_ethtool_set_pause(NMPlatform *self, int ifindex, const NMEthtoolPauseState *pause)
 {
     _CHECK_SELF_NETNS(self, klass, netns, FALSE);
 
     g_return_val_if_fail(ifindex > 0, FALSE);
-    g_return_val_if_fail(eee, FALSE);
+    g_return_val_if_fail(pause, FALSE);
 
-    return nmp_utils_ethtool_get_eee(ifindex, eee);
+    return klass->ethtool_set_pause(self, ifindex, pause);
 }
 
 gboolean
-nm_platform_ethtool_set_pause(NMPlatform *self, int ifindex, const NMEthtoolPauseState *pause)
+nm_platform_ethtool_get_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_set_pause(ifindex, pause);
+    return klass->ethtool_get_eee(self, ifindex, eee);
 }
 
 gboolean
@@ -3748,8 +3747,9 @@ nm_platform_ethtool_set_eee(NMPlatform *self, int ifindex, const NMEthtoolEEESta
     _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_set_eee(ifindex, eee);
+    return klass->ethtool_set_eee(self, ifindex, eee);
 }
 /*****************************************************************************/
 
@@ -7315,8 +7315,9 @@ nm_platform_ip4_route_to_string_full(const NMPlatformIP4Route     *route,
                              route->lock_initrwnd ? "lock " : "",
                              route->initrwnd)
             : "",
-        route->rto_min ? nm_sprintf_buf(str_rto_min, " rto_min %" G_GUINT32_FORMAT, route->rto_min)
-                       : "",
+        route->rto_min_set
+            ? nm_sprintf_buf(str_rto_min, " rto_min %" G_GUINT32_FORMAT, route->rto_min)
+            : "",
         route->quickack ? " quickack 1" : "",
         route->mtu || route->lock_mtu ? nm_sprintf_buf(str_mtu,
                                                        " mtu %s%" G_GUINT32_FORMAT,
diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h
index 1c8a26b3..e4ccb9b1 100644
--- a/src/libnm-platform/nm-platform.h
+++ b/src/libnm-platform/nm-platform.h
@@ -89,7 +89,7 @@ typedef enum {
      * long as NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID differs. */
     NM_PLATFORM_IP_ROUTE_CMP_TYPE_WEAK_ID,
 
-    /* compare two routes as kernel would allow to add them with
+    /* compare two routes as kernel would allow one to add them with
      * `ip route append`. In other words, kernel does not allow you to
      * add two routes (at the same time) which compare equal according
      * to NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID.
@@ -314,6 +314,7 @@ guint _nm_platform_signal_id_get(NMPlatformSignalIdType signal_type);
     guint32 initrwnd;                                                                     \
                                                                                           \
     /* RTA_METRICS.RTAX_RTO_MIN (iproute2: rto_min) */                                    \
+    /* Valid only when 'rto_min_set' is true. */                                          \
     guint32 rto_min;                                                                      \
                                                                                           \
     /* RTA_METRICS.RTAX_MTU (iproute2: mtu) */                                            \
@@ -367,6 +368,10 @@ guint _nm_platform_signal_id_get(NMPlatformSignalIdType signal_type);
     /* RTA_METRICS.RTAX_QUICKACK (iproute2: quickack) */                                  \
     bool quickack : 1;                                                                    \
                                                                                           \
+    /* RTA_METRICS.RTAX_RTO_MIN (iproute2: rto_min) */                                    \
+    /* If true, the 'rto_min' value is valid. */                                          \
+    bool rto_min_set : 1;                                                                 \
+                                                                                          \
     /* if TRUE, the "metric" field is interpreted as an offset that is added to a default
      * metric. For example, form a DHCP lease we don't know the actually used metric, because
      * that is determined by upper layers (the configuration). However, we have a default
@@ -508,7 +513,7 @@ struct _NMPlatformIP6Route {
      * Kernel clears the host part of src/src_plen.
      *
      * src/src_plen is part of the ID of a route just like network/plen. That is,
-     * Not only `ip route append`, but also `ip route add` allows to add routes that only
+     * Not only `ip route append`, but also `ip route add` allows one to add routes that only
      * differ in their src/src_plen.
      */
     struct in6_addr src;
@@ -1341,6 +1346,12 @@ typedef struct {
 
     GPtrArray *(*mptcp_addrs_dump)(NMPlatform *self);
 
+    gboolean (*ethtool_get_pause)(NMPlatform *self, int ifindex, NMEthtoolPauseState *pause);
+    gboolean (*ethtool_set_pause)(NMPlatform *self, int ifindex, const NMEthtoolPauseState *pause);
+    gboolean (*ethtool_get_eee)(NMPlatform *self, int ifindex, NMEthtoolEEEState *eee);
+    gboolean (*ethtool_set_eee)(NMPlatform *self, int ifindex, const NMEthtoolEEEState *eee);
+    gboolean (*ethtool_get_ring)(NMPlatform *self, int ifindex, NMEthtoolRingState *ring);
+    gboolean (*ethtool_set_ring)(NMPlatform *self, int ifindex, const NMEthtoolRingState *ring);
 } NMPlatformClass;
 
 /* NMPlatform signals
@@ -2626,7 +2637,7 @@ gboolean nm_platform_ethtool_get_link_settings(NMPlatform               *self,
                                                guint32                  *out_speed,
                                                NMPlatformLinkDuplexType *out_duplex);
 
-NMEthtoolFeatureStates *nm_platform_ethtool_get_link_features(NMPlatform *self, int ifindex);
+NMEthtoolFeatureStates *nm_platform_ethtool_get_features(NMPlatform *self, int ifindex);
 gboolean                nm_platform_ethtool_set_features(
                    NMPlatform                   *self,
                    int                           ifindex,
@@ -2634,22 +2645,20 @@ gboolean                nm_platform_ethtool_set_features(
                    const NMOptionBool *requested /* indexed by NMEthtoolID - _NM_ETHTOOL_ID_FEATURE_FIRST */,
                    gboolean            do_set /* or reset */);
 
-gboolean nm_platform_ethtool_get_link_coalesce(NMPlatform             *self,
-                                               int                     ifindex,
-                                               NMEthtoolCoalesceState *coalesce);
+gboolean
+nm_platform_ethtool_get_coalesce(NMPlatform *self, int ifindex, NMEthtoolCoalesceState *coalesce);
 
 gboolean nm_platform_ethtool_set_coalesce(NMPlatform                   *self,
                                           int                           ifindex,
                                           const NMEthtoolCoalesceState *coalesce);
 
-gboolean nm_platform_ethtool_get_link_ring(NMPlatform *self, int ifindex, NMEthtoolRingState *ring);
+gboolean nm_platform_ethtool_get_ring(NMPlatform *self, int ifindex, NMEthtoolRingState *ring);
 
 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_get_channels(NMPlatform *self, int ifindex, NMEthtoolChannelsState *channels);
 
 gboolean nm_platform_ethtool_set_channels(NMPlatform                   *self,
                                           int                           ifindex,
@@ -2659,10 +2668,9 @@ gboolean nm_platform_ethtool_get_fec_mode(NMPlatform *self, int ifindex, uint32_
 
 gboolean nm_platform_ethtool_set_fec_mode(NMPlatform *self, int ifindex, uint32_t fec_mode);
 
-gboolean
-nm_platform_ethtool_get_link_pause(NMPlatform *self, int ifindex, NMEthtoolPauseState *pause);
+gboolean nm_platform_ethtool_get_pause(NMPlatform *self, int ifindex, NMEthtoolPauseState *pause);
 
-gboolean nm_platform_ethtool_get_link_eee(NMPlatform *self, int ifindex, NMEthtoolEEEState *eee);
+gboolean nm_platform_ethtool_get_eee(NMPlatform *self, int ifindex, NMEthtoolEEEState *eee);
 
 gboolean
 nm_platform_ethtool_set_pause(NMPlatform *self, int ifindex, const NMEthtoolPauseState *pause);
diff --git a/src/libnm-platform/nmp-ethtool-ioctl.c b/src/libnm-platform/nmp-ethtool-ioctl.c
new file mode 100644
index 00000000..830b02f6
--- /dev/null
+++ b/src/libnm-platform/nmp-ethtool-ioctl.c
@@ -0,0 +1,1603 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nmp-ethtool-ioctl.h"
+
+#include "libnm-platform/nm-platform.h"
+#include "libnm-platform/nm-platform-utils.h"
+#include "libnm-log-core/nm-logging.h"
+#include "libnm-base/nm-ethtool-base.h"
+
+#include <linux/ethtool.h>
+#include <linux/if.h>
+#include <linux/mii.h>
+#include <linux/sockios.h>
+#include <linux/version.h>
+#include <sys/ioctl.h>
+
+#define ONOFF(bool_val) ((bool_val) ? "on" : "off")
+
+/*****************************************************************************/
+
+typedef struct {
+    int       fd;
+    const int ifindex;
+    char      ifname[IFNAMSIZ];
+} SocketHandle;
+
+#define SOCKET_HANDLE_INIT(_ifindex) \
+    {                                \
+        .fd      = -1,               \
+        .ifindex = (_ifindex),       \
+    }
+
+static void
+_nm_auto_socket_handle(SocketHandle *shandle)
+{
+    if (shandle->fd >= 0)
+        nm_close(shandle->fd);
+}
+
+#define nm_auto_socket_handle nm_auto(_nm_auto_socket_handle)
+
+/*****************************************************************************/
+
+typedef enum {
+    IOCTL_CALL_DATA_TYPE_NONE,
+    IOCTL_CALL_DATA_TYPE_IFRDATA,
+    IOCTL_CALL_DATA_TYPE_IFRU,
+} IoctlCallDataType;
+
+static int
+_ioctl_call(const char       *log_ioctl_type,
+            const char       *log_subtype,
+            unsigned long int ioctl_request,
+            int               ifindex,
+            int              *inout_fd,
+            char             *inout_ifname,
+            IoctlCallDataType edata_type,
+            gpointer          edata,
+            gsize             edata_size,
+            struct ifreq     *out_ifreq)
+{
+    nm_auto_close int fd_close = -1;
+    int               fd;
+    int               r;
+    gpointer          edata_backup      = NULL;
+    gs_free gpointer  edata_backup_free = NULL;
+    guint             try_count;
+    char              known_ifnames[2][IFNAMSIZ];
+    const char       *failure_reason = NULL;
+    struct ifreq      ifr;
+
+    nm_assert(ifindex > 0);
+    nm_assert(NM_IN_SET(edata_type,
+                        IOCTL_CALL_DATA_TYPE_NONE,
+                        IOCTL_CALL_DATA_TYPE_IFRDATA,
+                        IOCTL_CALL_DATA_TYPE_IFRU));
+    nm_assert(edata_type != IOCTL_CALL_DATA_TYPE_NONE || edata_size == 0);
+    nm_assert(edata_type != IOCTL_CALL_DATA_TYPE_IFRDATA || edata_size > 0);
+    nm_assert(edata_type != IOCTL_CALL_DATA_TYPE_IFRU
+              || (edata_size > 0 && edata_size <= sizeof(ifr.ifr_ifru)));
+    nm_assert(edata_size == 0 || edata);
+
+    /* open a file descriptor (or use the one provided). */
+    if (inout_fd && *inout_fd >= 0)
+        fd = *inout_fd;
+    else {
+        fd = socket(PF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+        if (fd < 0) {
+            r              = -NM_ERRNO_NATIVE(errno);
+            failure_reason = "failed creating socket or ioctl";
+            goto out;
+        }
+        if (inout_fd)
+            *inout_fd = fd;
+        else
+            fd_close = fd;
+    }
+
+    /* resolve the ifindex to name (or use the one provided). */
+    if (inout_ifname && inout_ifname[0])
+        nm_utils_ifname_cpy(known_ifnames[0], inout_ifname);
+    else {
+        if (!nmp_utils_if_indextoname(ifindex, known_ifnames[0])) {
+            failure_reason = "cannot resolve ifindex";
+            r              = -ENODEV;
+            goto out;
+        }
+        if (inout_ifname)
+            nm_utils_ifname_cpy(inout_ifname, known_ifnames[0]);
+    }
+
+    /* we might need to retry the request. Backup edata so that we can
+     * restore it on retry. */
+    if (edata_size > 0)
+        edata_backup = nm_memdup_maybe_a(500, edata, edata_size, &edata_backup_free);
+
+    try_count = 0;
+
+again:
+{
+    const char *ifname = known_ifnames[try_count % 2];
+
+    nm_assert(ifindex > 0);
+    nm_assert(ifname && nm_utils_ifname_valid_kernel(ifname, NULL));
+    nm_assert(fd >= 0);
+
+    memset(&ifr, 0, sizeof(ifr));
+    nm_utils_ifname_cpy(ifr.ifr_name, ifname);
+    if (edata_type == IOCTL_CALL_DATA_TYPE_IFRDATA)
+        ifr.ifr_data = edata;
+    else if (edata_type == IOCTL_CALL_DATA_TYPE_IFRU)
+        memcpy(&ifr.ifr_ifru, edata, NM_MIN(edata_size, sizeof(ifr.ifr_ifru)));
+
+    if (ioctl(fd, ioctl_request, &ifr) < 0) {
+        r = -NM_ERRNO_NATIVE(errno);
+        nm_log_trace(LOGD_PLATFORM,
+                     "%s[%d]: %s, %s: failed: %s",
+                     log_ioctl_type,
+                     ifindex,
+                     log_subtype,
+                     ifname,
+                     nm_strerror_native(-r));
+    } else {
+        r = 0;
+        nm_log_trace(LOGD_PLATFORM,
+                     "%s[%d]: %s, %s: success",
+                     log_ioctl_type,
+                     ifindex,
+                     log_subtype,
+                     ifname);
+    }
+}
+
+    try_count++;
+
+    /* resolve the name again to see whether the ifindex still has the same name. */
+    if (!nmp_utils_if_indextoname(ifindex, known_ifnames[try_count % 2])) {
+        /* we could not find the ifindex again. Probably the device just got
+         * removed.
+         *
+         * In both cases we return the error code we got from ioctl above.
+         * Either it failed because the device was gone already or it still
+         * managed to complete the call. In both cases, the error code is good. */
+        failure_reason =
+            "cannot resolve ifindex after ioctl call. Probably the device was just removed";
+        goto out;
+    }
+
+    /* check whether the ifname changed in the meantime. If yes, would render the result
+     * invalid. Note that this cannot detect every race regarding renames, for example:
+     *
+     *  - if_indextoname(#10) gives eth0
+     *  - rename(#10) => eth0_tmp
+     *  - rename(#11) => eth0
+     *  - ioctl(eth0) (wrongly fetching #11, formerly eth1)
+     *  - rename(#11) => eth_something
+     *  - rename(#10) => eth0
+     *  - if_indextoname(#10) gives eth0
+     */
+    if (!nm_streq(known_ifnames[0], known_ifnames[1])) {
+        gboolean retry;
+
+        /* we detected a possible(!) rename.
+         *
+         * For getters it's straight forward to just retry the call.
+         *
+         * For setters we also always retry. If our previous call operated on the right device,
+         * calling it again should have no bad effect (just setting the same thing more than once).
+         *
+         * The only potential bad thing is if there was a race involving swapping names, and we just
+         * set the ioctl option on the wrong device. But then the bad thing already happenned and
+         * we cannot detect it (nor do anything about it). At least, we can retry and set the
+         * option on the right interface. */
+        retry = (try_count < 5);
+
+        nm_log_trace(LOGD_PLATFORM,
+                     "%s[%d]: %s: rename detected from \"%s\" to \"%s\". %s",
+                     log_ioctl_type,
+                     ifindex,
+                     log_subtype,
+                     known_ifnames[(try_count - 1) % 2],
+                     known_ifnames[try_count % 2],
+                     retry ? "Retry" : "No retry");
+        if (inout_ifname)
+            nm_utils_ifname_cpy(inout_ifname, known_ifnames[try_count % 2]);
+        if (retry) {
+            if (edata_size > 0)
+                memcpy(edata, edata_backup, edata_size);
+            goto again;
+        }
+    }
+
+out:
+    if (failure_reason) {
+        nm_log_trace(LOGD_PLATFORM,
+                     "%s[%d]: %s: %s: %s",
+                     log_ioctl_type,
+                     ifindex,
+                     log_subtype,
+                     failure_reason,
+                     r < 0 ? nm_strerror_native(-r) : "assume success");
+    }
+    if (r >= 0)
+        NM_SET_OUT(out_ifreq, ifr);
+    return r;
+}
+
+/******************************************************************************
+ * ethtool
+ *****************************************************************************/
+
+static NM_UTILS_ENUM2STR_DEFINE(_ethtool_cmd_to_string,
+                                guint32,
+                                NM_UTILS_ENUM2STR(ETHTOOL_GCOALESCE, "ETHTOOL_GCOALESCE"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_GDRVINFO, "ETHTOOL_GDRVINFO"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_GFEATURES, "ETHTOOL_GFEATURES"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_GLINK, "ETHTOOL_GLINK"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_GLINKSETTINGS, "ETHTOOL_GLINKSETTINGS"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_GPERMADDR, "ETHTOOL_GPERMADDR"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_GRINGPARAM, "ETHTOOL_GRINGPARAM"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_GPAUSEPARAM, "ETHTOOL_GPAUSEPARAM"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_GSET, "ETHTOOL_GSET"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_GSSET_INFO, "ETHTOOL_GSSET_INFO"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_GSTATS, "ETHTOOL_GSTATS"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_GSTRINGS, "ETHTOOL_GSTRINGS"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_GWOL, "ETHTOOL_GWOL"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_SCOALESCE, "ETHTOOL_SCOALESCE"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_SFEATURES, "ETHTOOL_SFEATURES"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_SLINKSETTINGS, "ETHTOOL_SLINKSETTINGS"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_SRINGPARAM, "ETHTOOL_SRINGPARAM"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_SPAUSEPARAM, "ETHTOOL_SPAUSEPARAM"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_SSET, "ETHTOOL_SSET"),
+                                NM_UTILS_ENUM2STR(ETHTOOL_SWOL, "ETHTOOL_SWOL"), );
+
+static const char *
+_ethtool_edata_to_string(gpointer edata, gsize edata_size, char *sbuf, gsize sbuf_len)
+{
+    nm_assert(edata);
+    nm_assert(edata_size >= sizeof(guint32));
+    nm_assert((((intptr_t) edata) % _nm_alignof(guint32)) == 0);
+
+    return _ethtool_cmd_to_string(*((guint32 *) edata), sbuf, sbuf_len);
+}
+
+/*****************************************************************************/
+
+#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
+#endif
+
+static int
+_ethtool_call_handle(SocketHandle *shandle, gpointer edata, gsize edata_size)
+{
+    char sbuf[50];
+
+    return _ioctl_call("ethtool",
+                       _ethtool_edata_to_string(edata, edata_size, sbuf, sizeof(sbuf)),
+                       SIOCETHTOOL,
+                       shandle->ifindex,
+                       &shandle->fd,
+                       shandle->ifname,
+                       IOCTL_CALL_DATA_TYPE_IFRDATA,
+                       edata,
+                       edata_size,
+                       NULL);
+}
+
+static int
+_ethtool_call_once(int ifindex, gpointer edata, gsize edata_size)
+{
+    char sbuf[50];
+
+    return _ioctl_call("ethtool",
+                       _ethtool_edata_to_string(edata, edata_size, sbuf, sizeof(sbuf)),
+                       SIOCETHTOOL,
+                       ifindex,
+                       NULL,
+                       NULL,
+                       IOCTL_CALL_DATA_TYPE_IFRDATA,
+                       edata,
+                       edata_size,
+                       NULL);
+}
+
+/*****************************************************************************/
+
+static struct ethtool_gstrings *
+ethtool_get_stringset(SocketHandle *shandle, int stringset_id)
+{
+    struct {
+        struct ethtool_sset_info info;
+        guint32                  sentinel;
+    } sset_info = {
+        .info.cmd       = ETHTOOL_GSSET_INFO,
+        .info.reserved  = 0,
+        .info.sset_mask = (1ULL << stringset_id),
+    };
+    const guint32                   *pdata;
+    gs_free struct ethtool_gstrings *gstrings = NULL;
+    gsize                            gstrings_len;
+    guint32                          i, len;
+
+    if (_ethtool_call_handle(shandle, &sset_info, sizeof(sset_info)) < 0)
+        return NULL;
+    if (!sset_info.info.sset_mask)
+        return NULL;
+
+    pdata = (guint32 *) sset_info.info.data;
+
+    len = *pdata;
+
+    gstrings_len         = sizeof(*gstrings) + (len * ETH_GSTRING_LEN);
+    gstrings             = g_malloc0(gstrings_len);
+    gstrings->cmd        = ETHTOOL_GSTRINGS;
+    gstrings->string_set = stringset_id;
+    gstrings->len        = len;
+    if (gstrings->len > 0) {
+        if (_ethtool_call_handle(shandle, gstrings, gstrings_len) < 0)
+            return NULL;
+        for (i = 0; i < gstrings->len; i++) {
+            /* ensure NUL terminated */
+            gstrings->data[i * ETH_GSTRING_LEN + (ETH_GSTRING_LEN - 1)] = '\0';
+        }
+    }
+
+    return g_steal_pointer(&gstrings);
+}
+
+static int
+ethtool_gstrings_find(const struct ethtool_gstrings *gstrings, const char *needle)
+{
+    guint32 i;
+
+    /* ethtool_get_stringset() always ensures NUL terminated strings at ETH_GSTRING_LEN.
+     * that means, we cannot possibly request longer names. */
+    nm_assert(needle && strlen(needle) < ETH_GSTRING_LEN);
+
+    for (i = 0; i < gstrings->len; i++) {
+        if (nm_streq((char *) &gstrings->data[i * ETH_GSTRING_LEN], needle))
+            return i;
+    }
+    return -1;
+}
+
+static int
+ethtool_get_stringset_index(SocketHandle *shandle, int stringset_id, const char *needle)
+{
+    gs_free struct ethtool_gstrings *gstrings = NULL;
+
+    /* ethtool_get_stringset() always ensures NUL terminated strings at ETH_GSTRING_LEN.
+     * that means, we cannot possibly request longer names. */
+    nm_assert(needle && strlen(needle) < ETH_GSTRING_LEN);
+
+    gstrings = ethtool_get_stringset(shandle, stringset_id);
+    if (gstrings)
+        return ethtool_gstrings_find(gstrings, needle);
+    return -1;
+}
+
+/*****************************************************************************/
+
+static const NMEthtoolFeatureInfo _ethtool_feature_infos[_NM_ETHTOOL_ID_FEATURE_NUM] = {
+#define ETHT_FEAT(eid, ...)                                      \
+    {                                                            \
+        .ethtool_id     = eid,                                   \
+        .n_kernel_names = NM_NARG(__VA_ARGS__),                  \
+        .kernel_names   = ((const char *const[]) {__VA_ARGS__}), \
+    }
+
+    /* the order does only matter for one thing: if it happens that more than one NMEthtoolID
+     * reference the same kernel-name, then the one that is mentioned *later* will win in
+     * case these NMEthtoolIDs are set. That mostly only makes sense for ethtool-ids which
+     * refer to multiple features ("feature-tso"), while also having more specific ids
+     * ("feature-tx-tcp-segmentation"). */
+
+    /* names from ethtool utility, which are aliases for multiple features. */
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_SG, "tx-scatter-gather", "tx-scatter-gather-fraglist"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TSO,
+              "tx-tcp-segmentation",
+              "tx-tcp-ecn-segmentation",
+              "tx-tcp-mangleid-segmentation",
+              "tx-tcp6-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX,
+              "tx-checksum-ipv4",
+              "tx-checksum-ip-generic",
+              "tx-checksum-ipv6",
+              "tx-checksum-fcoe-crc",
+              "tx-checksum-sctp"),
+
+    /* names from ethtool utility, which are aliases for one feature. */
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_GRO, "rx-gro"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_GSO, "tx-generic-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_LRO, "rx-lro"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_NTUPLE, "rx-ntuple-filter"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX, "rx-checksum"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RXHASH, "rx-hashing"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RXVLAN, "rx-vlan-hw-parse"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TXVLAN, "tx-vlan-hw-insert"),
+
+    /* names of features, as known by kernel. */
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_ESP_HW_OFFLOAD, "esp-hw-offload"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_ESP_TX_CSUM_HW_OFFLOAD, "esp-tx-csum-hw-offload"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_FCOE_MTU, "fcoe-mtu"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_HIGHDMA, "highdma"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_HW_TC_OFFLOAD, "hw-tc-offload"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_L2_FWD_OFFLOAD, "l2-fwd-offload"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_LOOPBACK, "loopback"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_MACSEC_HW_OFFLOAD, "macsec-hw-offload"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_ALL, "rx-all"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_FCS, "rx-fcs"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_GRO_HW, "rx-gro-hw"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_GRO_LIST, "rx-gro-list"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_UDP_GRO_FORWARDING, "rx-udp-gro-forwarding"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_UDP_TUNNEL_PORT_OFFLOAD, "rx-udp_tunnel-port-offload"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_VLAN_FILTER, "rx-vlan-filter"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_VLAN_STAG_FILTER, "rx-vlan-stag-filter"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_RX_VLAN_STAG_HW_PARSE, "rx-vlan-stag-hw-parse"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TLS_HW_RECORD, "tls-hw-record"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TLS_HW_RX_OFFLOAD, "tls-hw-rx-offload"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TLS_HW_TX_OFFLOAD, "tls-hw-tx-offload"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_FCOE_CRC, "tx-checksum-fcoe-crc"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_IPV4, "tx-checksum-ipv4"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_IPV6, "tx-checksum-ipv6"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_IP_GENERIC, "tx-checksum-ip-generic"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_CHECKSUM_SCTP, "tx-checksum-sctp"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_ESP_SEGMENTATION, "tx-esp-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_FCOE_SEGMENTATION, "tx-fcoe-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_GRE_CSUM_SEGMENTATION, "tx-gre-csum-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_GRE_SEGMENTATION, "tx-gre-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_GSO_LIST, "tx-gso-list"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_GSO_PARTIAL, "tx-gso-partial"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_GSO_ROBUST, "tx-gso-robust"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_IPXIP4_SEGMENTATION, "tx-ipxip4-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_IPXIP6_SEGMENTATION, "tx-ipxip6-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_NOCACHE_COPY, "tx-nocache-copy"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_SCATTER_GATHER, "tx-scatter-gather"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_SCATTER_GATHER_FRAGLIST, "tx-scatter-gather-fraglist"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_SCTP_SEGMENTATION, "tx-sctp-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_TCP6_SEGMENTATION, "tx-tcp6-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_TCP_ECN_SEGMENTATION, "tx-tcp-ecn-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_TCP_MANGLEID_SEGMENTATION, "tx-tcp-mangleid-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_TCP_SEGMENTATION, "tx-tcp-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_TUNNEL_REMCSUM_SEGMENTATION,
+              "tx-tunnel-remcsum-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_UDP_SEGMENTATION, "tx-udp-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_UDP_TNL_CSUM_SEGMENTATION, "tx-udp_tnl-csum-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_UDP_TNL_SEGMENTATION, "tx-udp_tnl-segmentation"),
+    ETHT_FEAT(NM_ETHTOOL_ID_FEATURE_TX_VLAN_STAG_HW_INSERT, "tx-vlan-stag-hw-insert"),
+};
+
+/* the number of kernel features that we handle. It essentially is the sum of all
+ * kernel_names. So, all ethtool-ids that reference exactly one kernel-name
+ * (_NM_ETHTOOL_ID_FEATURE_NUM) + some extra, for ethtool-ids that are aliases
+ * for multiple kernel-names. */
+#define N_ETHTOOL_KERNEL_FEATURES (((guint) _NM_ETHTOOL_ID_FEATURE_NUM) + 8u)
+
+static void
+_ASSERT_ethtool_feature_infos(void)
+{
+#if NM_MORE_ASSERTS > 10
+    guint i, k, n;
+    bool  found[_NM_ETHTOOL_ID_FEATURE_NUM] = {};
+
+    G_STATIC_ASSERT_EXPR(G_N_ELEMENTS(_ethtool_feature_infos) == _NM_ETHTOOL_ID_FEATURE_NUM);
+
+    n = 0;
+    for (i = 0; i < G_N_ELEMENTS(_ethtool_feature_infos); i++) {
+        NMEthtoolFeatureState       kstate;
+        const NMEthtoolFeatureInfo *inf = &_ethtool_feature_infos[i];
+
+        g_assert(inf->ethtool_id >= _NM_ETHTOOL_ID_FEATURE_FIRST);
+        g_assert(inf->ethtool_id <= _NM_ETHTOOL_ID_FEATURE_LAST);
+        g_assert(inf->n_kernel_names > 0);
+
+        for (k = 0; k < i; k++)
+            g_assert(inf->ethtool_id != _ethtool_feature_infos[k].ethtool_id);
+
+        g_assert(!found[_NM_ETHTOOL_ID_FEATURE_AS_IDX(inf->ethtool_id)]);
+        found[_NM_ETHTOOL_ID_FEATURE_AS_IDX(inf->ethtool_id)] = TRUE;
+
+        kstate.idx_kernel_name = inf->n_kernel_names - 1;
+        g_assert((guint) kstate.idx_kernel_name == (guint) (inf->n_kernel_names - 1));
+
+        n += inf->n_kernel_names;
+        for (k = 0; k < inf->n_kernel_names; k++) {
+            const char *name = inf->kernel_names[k];
+
+            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. */
+            g_assert(!nm_streq(name, "netns-local"));
+            g_assert(!nm_streq(name, "tx-lockless"));
+            g_assert(!nm_streq(name, "vlan-challenged"));
+        }
+    }
+
+    for (i = 0; i < _NM_ETHTOOL_ID_FEATURE_NUM; i++)
+        g_assert(found[i]);
+
+    g_assert(n == N_ETHTOOL_KERNEL_FEATURES);
+#endif
+}
+
+static NMEthtoolFeatureStates *
+ethtool_get_features(SocketHandle *shandle)
+{
+    gs_free NMEthtoolFeatureStates  *states      = NULL;
+    gs_free struct ethtool_gstrings *ss_features = NULL;
+
+    _ASSERT_ethtool_feature_infos();
+
+    ss_features = ethtool_get_stringset(shandle, ETH_SS_FEATURES);
+    if (!ss_features)
+        return NULL;
+
+    if (ss_features->len > 0) {
+        gs_free struct ethtool_gfeatures   *gfeatures_free = NULL;
+        struct ethtool_gfeatures           *gfeatures;
+        gsize                               gfeatures_len;
+        guint                               idx;
+        const NMEthtoolFeatureState        *states_list0   = NULL;
+        const NMEthtoolFeatureState *const *states_plist0  = NULL;
+        guint                               states_plist_n = 0;
+
+        gfeatures_len = sizeof(struct ethtool_gfeatures)
+                        + (NM_DIV_ROUND_UP(ss_features->len, 32u) * sizeof(gfeatures->features[0]));
+        gfeatures       = nm_malloc0_maybe_a(300, gfeatures_len, &gfeatures_free);
+        gfeatures->cmd  = ETHTOOL_GFEATURES;
+        gfeatures->size = NM_DIV_ROUND_UP(ss_features->len, 32u);
+        if (_ethtool_call_handle(shandle, gfeatures, gfeatures_len) < 0)
+            return NULL;
+
+        for (idx = 0; idx < G_N_ELEMENTS(_ethtool_feature_infos); idx++) {
+            const NMEthtoolFeatureInfo *info = &_ethtool_feature_infos[idx];
+            guint                       idx_kernel_name;
+
+            for (idx_kernel_name = 0; idx_kernel_name < info->n_kernel_names; idx_kernel_name++) {
+                NMEthtoolFeatureState *kstate;
+                const char            *kernel_name = info->kernel_names[idx_kernel_name];
+                int                    i_feature;
+                guint                  i_block;
+                guint32                i_flag;
+
+                i_feature = ethtool_gstrings_find(ss_features, kernel_name);
+                if (i_feature < 0)
+                    continue;
+
+                i_block = ((guint) i_feature) / 32u;
+                i_flag  = (guint32) (1u << (((guint) i_feature) % 32u));
+
+                if (!states) {
+                    states = g_malloc0(
+                        sizeof(NMEthtoolFeatureStates)
+                        + (N_ETHTOOL_KERNEL_FEATURES * sizeof(NMEthtoolFeatureState))
+                        + ((N_ETHTOOL_KERNEL_FEATURES + G_N_ELEMENTS(_ethtool_feature_infos))
+                           * sizeof(NMEthtoolFeatureState *)));
+                    states_list0          = &states->states_list[0];
+                    states_plist0         = (gpointer) &states_list0[N_ETHTOOL_KERNEL_FEATURES];
+                    states->n_ss_features = ss_features->len;
+                }
+
+                nm_assert(states->n_states < N_ETHTOOL_KERNEL_FEATURES);
+                kstate = (NMEthtoolFeatureState *) &states_list0[states->n_states];
+                states->n_states++;
+
+                kstate->info            = info;
+                kstate->idx_ss_features = i_feature;
+                kstate->idx_kernel_name = idx_kernel_name;
+                kstate->available       = !!(gfeatures->features[i_block].available & i_flag);
+                kstate->requested       = !!(gfeatures->features[i_block].requested & i_flag);
+                kstate->active          = !!(gfeatures->features[i_block].active & i_flag);
+                kstate->never_changed   = !!(gfeatures->features[i_block].never_changed & i_flag);
+
+                nm_assert(states_plist_n
+                          < N_ETHTOOL_KERNEL_FEATURES + G_N_ELEMENTS(_ethtool_feature_infos));
+
+                if (!states->states_indexed[_NM_ETHTOOL_ID_FEATURE_AS_IDX(info->ethtool_id)])
+                    states->states_indexed[_NM_ETHTOOL_ID_FEATURE_AS_IDX(info->ethtool_id)] =
+                        &states_plist0[states_plist_n];
+                ((const NMEthtoolFeatureState **) states_plist0)[states_plist_n] = kstate;
+                states_plist_n++;
+            }
+
+            if (states && states->states_indexed[_NM_ETHTOOL_ID_FEATURE_AS_IDX(info->ethtool_id)]) {
+                nm_assert(states_plist_n
+                          < N_ETHTOOL_KERNEL_FEATURES + G_N_ELEMENTS(_ethtool_feature_infos));
+                nm_assert(!states_plist0[states_plist_n]);
+                states_plist_n++;
+            }
+        }
+    }
+
+    return g_steal_pointer(&states);
+}
+
+NMEthtoolFeatureStates *
+nmp_ethtool_ioctl_get_features(int ifindex)
+{
+    nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT(ifindex);
+    NMEthtoolFeatureStates            *features;
+
+    g_return_val_if_fail(ifindex > 0, 0);
+
+    features = ethtool_get_features(&shandle);
+
+    if (!features) {
+        nm_log_trace(LOGD_PLATFORM,
+                     "ethtool[%d]: %s: failure getting features",
+                     ifindex,
+                     "get-features");
+        return NULL;
+    }
+
+    nm_log_trace(LOGD_PLATFORM,
+                 "ethtool[%d]: %s: retrieved kernel features",
+                 ifindex,
+                 "get-features");
+    return features;
+}
+
+static const char *
+_ethtool_feature_state_to_string(char                        *buf,
+                                 gsize                        buf_size,
+                                 const NMEthtoolFeatureState *s,
+                                 const char                  *prefix)
+{
+    int l;
+
+    l = g_snprintf(buf,
+                   buf_size,
+                   "%s %s%s",
+                   prefix ?: "",
+                   ONOFF(s->active),
+                   (!s->available || s->never_changed)
+                       ? ", [fixed]"
+                       : ((s->requested != s->active)
+                              ? (s->requested ? ", [requested on]" : ", [requested off]")
+                              : ""));
+    nm_assert(l < buf_size);
+    return buf;
+}
+
+gboolean
+nmp_ethtool_ioctl_set_features(
+    int                           ifindex,
+    const NMEthtoolFeatureStates *features,
+    const NMOptionBool *requested /* indexed by NMEthtoolID - _NM_ETHTOOL_ID_FEATURE_FIRST */,
+    gboolean            do_set /* or reset */)
+{
+    nm_auto_socket_handle SocketHandle shandle        = SOCKET_HANDLE_INIT(ifindex);
+    gs_free struct ethtool_sfeatures  *sfeatures_free = NULL;
+    struct ethtool_sfeatures          *sfeatures;
+    gsize                              sfeatures_len;
+    int                                r;
+    guint                              i, j;
+    struct {
+        const NMEthtoolFeatureState *f_state;
+        NMOptionBool                 requested;
+    } set_states[N_ETHTOOL_KERNEL_FEATURES];
+    guint    set_states_n = 0;
+    gboolean success      = TRUE;
+
+    g_return_val_if_fail(ifindex > 0, 0);
+    g_return_val_if_fail(features, 0);
+    g_return_val_if_fail(requested, 0);
+
+    nm_assert(features->n_states <= N_ETHTOOL_KERNEL_FEATURES);
+
+    for (i = 0; i < _NM_ETHTOOL_ID_FEATURE_NUM; i++) {
+        const NMEthtoolFeatureState *const *states_indexed;
+
+        if (requested[i] == NM_OPTION_BOOL_DEFAULT)
+            continue;
+
+        if (!(states_indexed = features->states_indexed[i])) {
+            if (do_set) {
+                nm_log_trace(LOGD_PLATFORM,
+                             "ethtool[%d]: %s: set feature %s: skip (not found)",
+                             ifindex,
+                             "set-features",
+                             nm_ethtool_data[i + _NM_ETHTOOL_ID_FEATURE_FIRST]->optname);
+                success = FALSE;
+            }
+            continue;
+        }
+
+        for (j = 0; states_indexed[j]; j++) {
+            const NMEthtoolFeatureState *s = states_indexed[j];
+            char                         sbuf[255];
+
+            if (set_states_n >= G_N_ELEMENTS(set_states))
+                g_return_val_if_reached(FALSE);
+
+            if (s->never_changed) {
+                nm_log_trace(LOGD_PLATFORM,
+                             "ethtool[%d]: %s: %s feature %s (%s): %s, %s (skip feature marked as "
+                             "never changed)",
+                             ifindex,
+                             "set-features",
+                             do_set ? "set" : "reset",
+                             nm_ethtool_data[i + _NM_ETHTOOL_ID_FEATURE_FIRST]->optname,
+                             s->info->kernel_names[s->idx_kernel_name],
+                             ONOFF(do_set ? requested[i] == NM_OPTION_BOOL_TRUE : s->active),
+                             _ethtool_feature_state_to_string(sbuf,
+                                                              sizeof(sbuf),
+                                                              s,
+                                                              do_set ? " currently:" : " before:"));
+                continue;
+            }
+
+            nm_log_trace(LOGD_PLATFORM,
+                         "ethtool[%d]: %s: %s feature %s (%s): %s, %s",
+                         ifindex,
+                         "set-features",
+                         do_set ? "set" : "reset",
+                         nm_ethtool_data[i + _NM_ETHTOOL_ID_FEATURE_FIRST]->optname,
+                         s->info->kernel_names[s->idx_kernel_name],
+                         ONOFF(do_set ? requested[i] == NM_OPTION_BOOL_TRUE : s->active),
+                         _ethtool_feature_state_to_string(sbuf,
+                                                          sizeof(sbuf),
+                                                          s,
+                                                          do_set ? " currently:" : " before:"));
+
+            if (do_set && (!s->available || s->never_changed)
+                && (s->active != (requested[i] == NM_OPTION_BOOL_TRUE))) {
+                /* we request to change a flag which kernel reported as fixed.
+                 * While the ethtool operation will silently succeed, mark the request
+                 * as failure. */
+                success = FALSE;
+            }
+
+            set_states[set_states_n].f_state   = s;
+            set_states[set_states_n].requested = requested[i];
+            set_states_n++;
+        }
+    }
+
+    if (set_states_n == 0) {
+        nm_log_trace(LOGD_PLATFORM,
+                     "ethtool[%d]: %s: no feature requested",
+                     ifindex,
+                     "set-features");
+        return TRUE;
+    }
+
+    sfeatures_len =
+        sizeof(struct ethtool_sfeatures)
+        + (NM_DIV_ROUND_UP(features->n_ss_features, 32U) * sizeof(sfeatures->features[0]));
+    sfeatures       = nm_malloc0_maybe_a(300, sfeatures_len, &sfeatures_free);
+    sfeatures->cmd  = ETHTOOL_SFEATURES;
+    sfeatures->size = NM_DIV_ROUND_UP(features->n_ss_features, 32U);
+
+    for (i = 0; i < set_states_n; i++) {
+        const NMEthtoolFeatureState *s = set_states[i].f_state;
+        guint                        i_block;
+        guint32                      i_flag;
+        gboolean                     is_requested;
+
+        i_block = s->idx_ss_features / 32u;
+        i_flag  = (guint32) (1u << (s->idx_ss_features % 32u));
+
+        sfeatures->features[i_block].valid |= i_flag;
+
+        if (do_set)
+            is_requested = (set_states[i].requested == NM_OPTION_BOOL_TRUE);
+        else
+            is_requested = s->active;
+
+        if (is_requested)
+            sfeatures->features[i_block].requested |= i_flag;
+        else
+            sfeatures->features[i_block].requested &= ~i_flag;
+    }
+
+    r = _ethtool_call_handle(&shandle, sfeatures, sfeatures_len);
+    if (r < 0) {
+        success = FALSE;
+        nm_log_trace(LOGD_PLATFORM,
+                     "ethtool[%d]: %s: failure setting features (%s)",
+                     ifindex,
+                     "set-features",
+                     nm_strerror_native(-r));
+        return FALSE;
+    }
+
+    nm_log_trace(LOGD_PLATFORM,
+                 "ethtool[%d]: %s: %s",
+                 ifindex,
+                 "set-features",
+                 success ? "successfully setting features"
+                         : "at least some of the features were not successfully set");
+    return success;
+}
+
+gboolean
+nmp_ethtool_ioctl_get_coalesce(int ifindex, NMEthtoolCoalesceState *coalesce)
+{
+    struct ethtool_coalesce eth_data;
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+    g_return_val_if_fail(coalesce, FALSE);
+
+    eth_data.cmd = ETHTOOL_GCOALESCE;
+
+    if (_ethtool_call_once(ifindex, &eth_data, sizeof(eth_data)) < 0) {
+        nm_log_trace(LOGD_PLATFORM,
+                     "ethtool[%d]: %s: failure getting coalesce settings",
+                     ifindex,
+                     "get-coalesce");
+        return FALSE;
+    }
+
+    *coalesce = (NMEthtoolCoalesceState) {
+        .s = {
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS)] =
+                eth_data.rx_coalesce_usecs,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES)] =
+                eth_data.rx_max_coalesced_frames,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS_IRQ)] =
+                eth_data.rx_coalesce_usecs_irq,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES_IRQ)] =
+                eth_data.rx_max_coalesced_frames_irq,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS)] =
+                eth_data.tx_coalesce_usecs,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES)] =
+                eth_data.tx_max_coalesced_frames,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS_IRQ)] =
+                eth_data.tx_coalesce_usecs_irq,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES_IRQ)] =
+                eth_data.tx_max_coalesced_frames_irq,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_STATS_BLOCK_USECS)] =
+                eth_data.stats_block_coalesce_usecs,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_ADAPTIVE_RX)] =
+                eth_data.use_adaptive_rx_coalesce,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_ADAPTIVE_TX)] =
+                eth_data.use_adaptive_tx_coalesce,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_PKT_RATE_LOW)] =
+                eth_data.pkt_rate_low,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS_LOW)] =
+                eth_data.rx_coalesce_usecs_low,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES_LOW)] =
+                eth_data.rx_max_coalesced_frames_low,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS_LOW)] =
+                eth_data.tx_coalesce_usecs_low,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES_LOW)] =
+                eth_data.tx_max_coalesced_frames_low,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_PKT_RATE_HIGH)] =
+                eth_data.pkt_rate_high,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS_HIGH)] =
+                eth_data.rx_coalesce_usecs_high,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES_HIGH)] =
+                eth_data.rx_max_coalesced_frames_high,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS_HIGH)] =
+                eth_data.tx_coalesce_usecs_high,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES_HIGH)] =
+                eth_data.tx_max_coalesced_frames_high,
+            [_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_SAMPLE_INTERVAL)] =
+                eth_data.rate_sample_interval,
+        }};
+    return TRUE;
+
+    nm_log_trace(LOGD_PLATFORM,
+                 "ethtool[%d]: %s: retrieved kernel coalesce settings",
+                 ifindex,
+                 "get-coalesce");
+    return TRUE;
+}
+
+gboolean
+nmp_ethtool_ioctl_set_coalesce(int ifindex, const NMEthtoolCoalesceState *coalesce)
+{
+    struct ethtool_coalesce eth_data;
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+    g_return_val_if_fail(coalesce, FALSE);
+
+    eth_data = (struct ethtool_coalesce) {
+        .cmd = ETHTOOL_SCOALESCE,
+        .rx_coalesce_usecs =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS)],
+        .rx_max_coalesced_frames =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES)],
+        .rx_coalesce_usecs_irq =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS_IRQ)],
+        .rx_max_coalesced_frames_irq =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES_IRQ)],
+        .tx_coalesce_usecs =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS)],
+        .tx_max_coalesced_frames =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES)],
+        .tx_coalesce_usecs_irq =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS_IRQ)],
+        .tx_max_coalesced_frames_irq =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES_IRQ)],
+        .stats_block_coalesce_usecs =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_STATS_BLOCK_USECS)],
+        .use_adaptive_rx_coalesce =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_ADAPTIVE_RX)],
+        .use_adaptive_tx_coalesce =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_ADAPTIVE_TX)],
+        .pkt_rate_low =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_PKT_RATE_LOW)],
+        .rx_coalesce_usecs_low =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS_LOW)],
+        .rx_max_coalesced_frames_low =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES_LOW)],
+        .tx_coalesce_usecs_low =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS_LOW)],
+        .tx_max_coalesced_frames_low =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES_LOW)],
+        .pkt_rate_high =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_PKT_RATE_HIGH)],
+        .rx_coalesce_usecs_high =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_USECS_HIGH)],
+        .rx_max_coalesced_frames_high =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_RX_FRAMES_HIGH)],
+        .tx_coalesce_usecs_high =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_USECS_HIGH)],
+        .tx_max_coalesced_frames_high =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_TX_FRAMES_HIGH)],
+        .rate_sample_interval =
+            coalesce->s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(NM_ETHTOOL_ID_COALESCE_SAMPLE_INTERVAL)],
+    };
+
+    if (_ethtool_call_once(ifindex, &eth_data, sizeof(eth_data)) < 0) {
+        nm_log_trace(LOGD_PLATFORM,
+                     "ethtool[%d]: %s: failure setting coalesce settings",
+                     ifindex,
+                     "set-coalesce");
+        return FALSE;
+    }
+
+    nm_log_trace(LOGD_PLATFORM,
+                 "ethtool[%d]: %s: set kernel coalesce settings",
+                 ifindex,
+                 "set-coalesce");
+    return TRUE;
+}
+
+gboolean
+nmp_ethtool_ioctl_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_ethtool_ioctl_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_ethtool_ioctl_get_fec_mode(int ifindex, uint32_t *fec_mode)
+{
+    int                     r;
+    struct ethtool_fecparam fec_param = {
+        .cmd = ETHTOOL_GFECPARAM,
+        .fec = 0,
+    };
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+
+    if (_ethtool_call_once(ifindex, &fec_param, sizeof(fec_param)) >= 0) {
+        nm_log_dbg(LOGD_PLATFORM, "ethtool[%d]: get FEC options 0x%x", ifindex, fec_param.fec);
+        *fec_mode = fec_param.fec;
+        return TRUE;
+    } else {
+        r = -NM_ERRNO_NATIVE(errno);
+        nm_log_dbg(LOGD_PLATFORM,
+                   "ethtool[%d]: ETHTOOL_GFECPARAM failure get fec mode: (%s)",
+                   ifindex,
+                   nm_strerror_native(-r));
+        return FALSE;
+    }
+}
+
+gboolean
+nmp_ethtool_ioctl_set_fec_mode(int ifindex, uint32_t fec_mode)
+{
+    struct ethtool_fecparam fec_param = {
+        .cmd = ETHTOOL_SFECPARAM,
+        .fec = fec_mode,
+    };
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+
+    nm_log_dbg(LOGD_PLATFORM, "ethtool[%d]: setting FEC options 0x%x", ifindex, fec_mode);
+
+    return _ethtool_call_once(ifindex, &fec_param, sizeof(fec_param)) >= 0;
+}
+
+/*****************************************************************************/
+
+gboolean
+nmp_ethtool_ioctl_get_driver_info(int ifindex, NMPUtilsEthtoolDriverInfo *data)
+{
+    struct ethtool_drvinfo *drvinfo;
+
+    G_STATIC_ASSERT_EXPR(sizeof(*data) == sizeof(*drvinfo));
+    G_STATIC_ASSERT_EXPR(offsetof(NMPUtilsEthtoolDriverInfo, driver)
+                         == offsetof(struct ethtool_drvinfo, driver));
+    G_STATIC_ASSERT_EXPR(offsetof(NMPUtilsEthtoolDriverInfo, version)
+                         == offsetof(struct ethtool_drvinfo, version));
+    G_STATIC_ASSERT_EXPR(offsetof(NMPUtilsEthtoolDriverInfo, fw_version)
+                         == offsetof(struct ethtool_drvinfo, fw_version));
+    G_STATIC_ASSERT_EXPR(sizeof(data->driver) == sizeof(drvinfo->driver));
+    G_STATIC_ASSERT_EXPR(sizeof(data->version) == sizeof(drvinfo->version));
+    G_STATIC_ASSERT_EXPR(sizeof(data->fw_version) == sizeof(drvinfo->fw_version));
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+    g_return_val_if_fail(data, FALSE);
+
+    drvinfo  = (struct ethtool_drvinfo *) data;
+    *drvinfo = (struct ethtool_drvinfo) {
+        .cmd = ETHTOOL_GDRVINFO,
+    };
+    return _ethtool_call_once(ifindex, drvinfo, sizeof(*drvinfo)) >= 0;
+}
+
+gboolean
+nmp_ethtool_ioctl_get_permanent_address(int ifindex, guint8 *buf, size_t *length)
+{
+    struct {
+        struct ethtool_perm_addr e;
+        guint8                   _extra_data[_NM_UTILS_HWADDR_LEN_MAX + 1];
+    } edata = {
+        .e.cmd  = ETHTOOL_GPERMADDR,
+        .e.size = _NM_UTILS_HWADDR_LEN_MAX,
+    };
+    const guint8 *pdata;
+
+    guint i;
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+
+    if (_ethtool_call_once(ifindex, &edata, sizeof(edata)) < 0)
+        return FALSE;
+
+    if (edata.e.size > _NM_UTILS_HWADDR_LEN_MAX)
+        return FALSE;
+    if (edata.e.size < 1)
+        return FALSE;
+
+    pdata = (const guint8 *) edata.e.data;
+
+    if (NM_IN_SET(pdata[0], 0, 0xFF)) {
+        /* Some drivers might return a permanent address of all zeros.
+         * Reject that (rh#1264024)
+         *
+         * Some drivers return a permanent address of all ones. Reject that too */
+        for (i = 1; i < edata.e.size; i++) {
+            if (pdata[0] != pdata[i])
+                goto not_all_0or1;
+        }
+        return FALSE;
+    }
+
+not_all_0or1:
+    memcpy(buf, pdata, edata.e.size);
+    *length = edata.e.size;
+    return TRUE;
+}
+
+gboolean
+nmp_ethtool_ioctl_supports_carrier_detect(int ifindex)
+{
+    struct ethtool_cmd edata = {.cmd = ETHTOOL_GLINK};
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+
+    /* We ignore the result. If the ETHTOOL_GLINK call succeeded, then we
+     * assume the device supports carrier-detect, otherwise we assume it
+     * doesn't.
+     */
+    return _ethtool_call_once(ifindex, &edata, sizeof(edata)) >= 0;
+}
+
+gboolean
+nmp_ethtool_ioctl_supports_vlans(int ifindex)
+{
+    nm_auto_socket_handle SocketHandle shandle       = SOCKET_HANDLE_INIT(ifindex);
+    gs_free struct ethtool_gfeatures  *features_free = NULL;
+    struct ethtool_gfeatures          *features;
+    gsize                              features_len;
+    int                                idx, block, bit, size;
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+
+    idx = ethtool_get_stringset_index(&shandle, ETH_SS_FEATURES, "vlan-challenged");
+    if (idx < 0) {
+        nm_log_dbg(LOGD_PLATFORM,
+                   "ethtool[%d]: vlan-challenged ethtool feature does not exist?",
+                   ifindex);
+        return FALSE;
+    }
+
+    block = idx / 32;
+    bit   = idx % 32;
+    size  = block + 1;
+
+    features_len   = sizeof(*features) + (size * sizeof(struct ethtool_get_features_block));
+    features       = nm_malloc0_maybe_a(300, features_len, &features_free);
+    features->cmd  = ETHTOOL_GFEATURES;
+    features->size = size;
+
+    if (_ethtool_call_handle(&shandle, features, features_len) < 0)
+        return FALSE;
+
+    return !(features->features[block].active & (1 << bit));
+}
+
+int
+nmp_ethtool_ioctl_get_peer_ifindex(int ifindex)
+{
+    nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT(ifindex);
+    gsize                              stats_len;
+    gs_free struct ethtool_stats      *stats_free = NULL;
+    struct ethtool_stats              *stats;
+    int                                peer_ifindex_stat;
+
+    g_return_val_if_fail(ifindex > 0, 0);
+
+    peer_ifindex_stat = ethtool_get_stringset_index(&shandle, ETH_SS_STATS, "peer_ifindex");
+    if (peer_ifindex_stat < 0) {
+        nm_log_dbg(LOGD_PLATFORM, "ethtool[%d]: peer_ifindex stat does not exist?", ifindex);
+        return FALSE;
+    }
+
+    stats_len      = sizeof(*stats) + (peer_ifindex_stat + 1) * sizeof(guint64);
+    stats          = nm_malloc0_maybe_a(300, stats_len, &stats_free);
+    stats->cmd     = ETHTOOL_GSTATS;
+    stats->n_stats = peer_ifindex_stat + 1;
+    if (_ethtool_call_handle(&shandle, stats, stats_len) < 0)
+        return 0;
+
+    return stats->data[peer_ifindex_stat];
+}
+
+gboolean
+nmp_ethtool_ioctl_get_wake_on_lan(int ifindex)
+{
+    struct ethtool_wolinfo wol = {
+        .cmd = ETHTOOL_GWOL,
+    };
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+
+    if (_ethtool_call_once(ifindex, &wol, sizeof(wol)) < 0)
+        return FALSE;
+
+    return wol.wolopts != 0;
+}
+
+gboolean
+nmp_ethtool_ioctl_set_wake_on_lan(int                      ifindex,
+                                  _NMSettingWiredWakeOnLan wol,
+                                  const char              *wol_password)
+{
+    struct ethtool_wolinfo wol_info = {
+        .cmd     = ETHTOOL_SWOL,
+        .wolopts = 0,
+    };
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+
+    if (wol == _NM_SETTING_WIRED_WAKE_ON_LAN_IGNORE)
+        return TRUE;
+
+    nm_log_dbg(LOGD_PLATFORM,
+               "ethtool[%d]: setting Wake-on-LAN options 0x%x, password '%s'",
+               ifindex,
+               (unsigned) wol,
+               wol_password);
+
+    if (NM_FLAGS_HAS(wol, _NM_SETTING_WIRED_WAKE_ON_LAN_PHY))
+        wol_info.wolopts |= WAKE_PHY;
+    if (NM_FLAGS_HAS(wol, _NM_SETTING_WIRED_WAKE_ON_LAN_UNICAST))
+        wol_info.wolopts |= WAKE_UCAST;
+    if (NM_FLAGS_HAS(wol, _NM_SETTING_WIRED_WAKE_ON_LAN_MULTICAST))
+        wol_info.wolopts |= WAKE_MCAST;
+    if (NM_FLAGS_HAS(wol, _NM_SETTING_WIRED_WAKE_ON_LAN_BROADCAST))
+        wol_info.wolopts |= WAKE_BCAST;
+    if (NM_FLAGS_HAS(wol, _NM_SETTING_WIRED_WAKE_ON_LAN_ARP))
+        wol_info.wolopts |= WAKE_ARP;
+    if (NM_FLAGS_HAS(wol, _NM_SETTING_WIRED_WAKE_ON_LAN_MAGIC))
+        wol_info.wolopts |= WAKE_MAGIC;
+
+    if (wol_password) {
+        if (!_nm_utils_hwaddr_aton_exact(wol_password, wol_info.sopass, ETH_ALEN)) {
+            nm_log_dbg(LOGD_PLATFORM,
+                       "ethtool[%d]: couldn't parse Wake-on-LAN password '%s'",
+                       ifindex,
+                       wol_password);
+            return FALSE;
+        }
+        wol_info.wolopts |= WAKE_MAGICSECURE;
+    }
+
+    return _ethtool_call_once(ifindex, &wol_info, sizeof(wol_info)) >= 0;
+}
+
+/*****************************************************************************/
+
+gboolean
+nmp_ethtool_ioctl_get_link_settings(int                       ifindex,
+                                    gboolean                 *out_autoneg,
+                                    guint32                  *out_speed,
+                                    NMPlatformLinkDuplexType *out_duplex)
+{
+    struct ethtool_cmd edata = {
+        .cmd = ETHTOOL_GSET,
+    };
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+
+    if (_ethtool_call_once(ifindex, &edata, sizeof(edata)) < 0)
+        return FALSE;
+
+    NM_SET_OUT(out_autoneg, (edata.autoneg == AUTONEG_ENABLE));
+
+    if (out_speed) {
+        guint32 speed;
+
+        speed = ethtool_cmd_speed(&edata);
+        if (speed == G_MAXUINT16 || speed == G_MAXUINT32)
+            speed = 0;
+
+        *out_speed = speed;
+    }
+
+    if (out_duplex) {
+        switch (edata.duplex) {
+        case DUPLEX_HALF:
+            *out_duplex = NM_PLATFORM_LINK_DUPLEX_HALF;
+            break;
+        case DUPLEX_FULL:
+            *out_duplex = NM_PLATFORM_LINK_DUPLEX_FULL;
+            break;
+        default: /* DUPLEX_UNKNOWN */
+            *out_duplex = NM_PLATFORM_LINK_DUPLEX_UNKNOWN;
+            break;
+        }
+    }
+
+    return TRUE;
+}
+
+#define ADVERTISED_INVALID 0
+
+static guint32
+get_baset_mode(guint32 speed, NMPlatformLinkDuplexType duplex)
+{
+    if (duplex == NM_PLATFORM_LINK_DUPLEX_UNKNOWN)
+        return ADVERTISED_INVALID;
+
+    if (duplex == NM_PLATFORM_LINK_DUPLEX_HALF) {
+        switch (speed) {
+        case 10:
+            return ADVERTISED_10baseT_Half;
+        case 100:
+            return ADVERTISED_100baseT_Half;
+        case 1000:
+            return ADVERTISED_1000baseT_Half;
+        default:
+            return ADVERTISED_INVALID;
+        }
+    } else {
+        switch (speed) {
+        case 10:
+            return ADVERTISED_10baseT_Full;
+        case 100:
+            return ADVERTISED_100baseT_Full;
+        case 1000:
+            return ADVERTISED_1000baseT_Full;
+        case 10000:
+            return ADVERTISED_10000baseT_Full;
+        default:
+            return ADVERTISED_INVALID;
+        }
+    }
+}
+
+static gboolean
+platform_link_duplex_type_to_native(NMPlatformLinkDuplexType duplex_type, guint8 *out_native)
+{
+    switch (duplex_type) {
+    case NM_PLATFORM_LINK_DUPLEX_HALF:
+        *out_native = DUPLEX_HALF;
+        return TRUE;
+    case NM_PLATFORM_LINK_DUPLEX_FULL:
+        *out_native = DUPLEX_FULL;
+        return TRUE;
+    case NM_PLATFORM_LINK_DUPLEX_UNKNOWN:
+        return FALSE;
+    default:
+        g_return_val_if_reached(FALSE);
+    }
+}
+
+static NMOptionBool
+set_link_settings_new(SocketHandle            *shandle,
+                      gboolean                 autoneg,
+                      guint32                  speed,
+                      NMPlatformLinkDuplexType duplex)
+{
+    struct ethtool_link_settings          edata0;
+    gs_free struct ethtool_link_settings *edata = NULL;
+    gsize                                 edata_size;
+    guint                                 nwords;
+    guint                                 i;
+
+    edata0 = (struct ethtool_link_settings) {
+        .cmd                    = ETHTOOL_GLINKSETTINGS,
+        .link_mode_masks_nwords = 0,
+    };
+
+    /* perform the handshake to find the size of masks */
+    if (_ethtool_call_handle(shandle, &edata0, sizeof(edata0)) < 0
+        || edata0.link_mode_masks_nwords >= 0) {
+        /* new API not supported */
+        return NM_OPTION_BOOL_DEFAULT;
+    }
+
+    nwords                        = -edata0.link_mode_masks_nwords;
+    edata_size                    = sizeof(*edata) + sizeof(guint32) * nwords * 3;
+    edata                         = g_malloc0(edata_size);
+    edata->cmd                    = ETHTOOL_GLINKSETTINGS;
+    edata->link_mode_masks_nwords = nwords;
+
+    /* retrieve first current settings */
+    if (_ethtool_call_handle(shandle, edata, edata_size) < 0)
+        return FALSE;
+
+    /* then change the needed ones */
+    edata->cmd = ETHTOOL_SLINKSETTINGS;
+
+    {
+        const guint32 *v_map_supported      = &edata->link_mode_masks[0];
+        guint32       *v_map_advertising    = &edata->link_mode_masks[nwords];
+        guint32       *v_map_lp_advertising = &edata->link_mode_masks[2 * nwords];
+
+        memcpy(v_map_advertising, v_map_supported, sizeof(guint32) * nwords);
+        (void) v_map_lp_advertising;
+
+        if (speed != 0) {
+            guint32 mode;
+
+            mode = get_baset_mode(speed, duplex);
+
+            if (mode == ADVERTISED_INVALID) {
+                if (!autoneg)
+                    goto set_autoneg;
+                nm_log_trace(LOGD_PLATFORM,
+                             "ethtool[%d]: %uBASE-T %s duplex mode cannot be advertised",
+                             shandle->ifindex,
+                             speed,
+                             nm_platform_link_duplex_type_to_string(duplex));
+                return FALSE;
+            }
+
+            if (!(v_map_supported[0] & mode)) {
+                if (!autoneg)
+                    goto set_autoneg;
+                nm_log_trace(LOGD_PLATFORM,
+                             "ethtool[%d]: device does not support %uBASE-T %s duplex mode",
+                             shandle->ifindex,
+                             speed,
+                             nm_platform_link_duplex_type_to_string(duplex));
+                return FALSE;
+            }
+
+            for (i = 0; i < (guint) G_N_ELEMENTS(_nmp_link_mode_all_advertised_modes); i++)
+                v_map_advertising[i] &= ~_nmp_link_mode_all_advertised_modes[i];
+            v_map_advertising[0] |= mode;
+        }
+    }
+
+set_autoneg:
+    if (autoneg)
+        edata->autoneg = AUTONEG_ENABLE;
+    else {
+        edata->autoneg = AUTONEG_DISABLE;
+
+        if (speed)
+            edata->speed = speed;
+
+        platform_link_duplex_type_to_native(duplex, &edata->duplex);
+    }
+
+    return _ethtool_call_handle(shandle, edata, edata_size) >= 0;
+}
+
+gboolean
+nmp_ethtool_ioctl_set_link_settings(int                      ifindex,
+                                    gboolean                 autoneg,
+                                    guint32                  speed,
+                                    NMPlatformLinkDuplexType duplex)
+{
+    nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT(ifindex);
+    struct ethtool_cmd                 edata   = {
+                          .cmd = ETHTOOL_GSET,
+    };
+    NMOptionBool ret;
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+    g_return_val_if_fail((speed && duplex != NM_PLATFORM_LINK_DUPLEX_UNKNOWN)
+                             || (!speed && duplex == NM_PLATFORM_LINK_DUPLEX_UNKNOWN),
+                         FALSE);
+
+    nm_log_trace(LOGD_PLATFORM,
+                 "ethtool[%d]: set link: autoneg=%d, speed=%d, duplex=%s",
+                 ifindex,
+                 autoneg,
+                 speed,
+                 nm_platform_link_duplex_type_to_string(duplex));
+
+    ret = set_link_settings_new(&shandle, autoneg, speed, duplex);
+    if (ret != NM_OPTION_BOOL_DEFAULT)
+        return ret;
+
+    /* new ETHTOOL_GLINKSETTINGS API not supported, fall back to GSET */
+
+    /* retrieve first current settings */
+    if (_ethtool_call_handle(&shandle, &edata, sizeof(edata)) < 0)
+        return FALSE;
+
+    /* then change the needed ones */
+    edata.cmd = ETHTOOL_SSET;
+
+    edata.advertising = edata.supported;
+    if (speed != 0) {
+        guint32 mode;
+
+        mode = get_baset_mode(speed, duplex);
+
+        if (mode == ADVERTISED_INVALID) {
+            if (!autoneg)
+                goto set_autoneg;
+            nm_log_trace(LOGD_PLATFORM,
+                         "ethtool[%d]: %uBASE-T %s duplex mode cannot be advertised",
+                         ifindex,
+                         speed,
+                         nm_platform_link_duplex_type_to_string(duplex));
+            return FALSE;
+        }
+        if (!(edata.supported & mode)) {
+            if (!autoneg)
+                goto set_autoneg;
+            nm_log_trace(LOGD_PLATFORM,
+                         "ethtool[%d]: device does not support %uBASE-T %s duplex mode",
+                         ifindex,
+                         speed,
+                         nm_platform_link_duplex_type_to_string(duplex));
+            return FALSE;
+        }
+        edata.advertising &= ~_nmp_link_mode_all_advertised_modes[0];
+        edata.advertising |= mode;
+    }
+
+set_autoneg:
+    if (autoneg)
+        edata.autoneg = AUTONEG_ENABLE;
+    else {
+        edata.autoneg = AUTONEG_DISABLE;
+
+        if (speed)
+            ethtool_cmd_speed_set(&edata, speed);
+
+        platform_link_duplex_type_to_native(duplex, &edata.duplex);
+    }
+
+    return _ethtool_call_handle(&shandle, &edata, sizeof(edata)) >= 0;
+}
+
+gboolean
+nmp_mii_ioctl_supports_carrier_detect(int ifindex)
+{
+    nm_auto_socket_handle SocketHandle shandle = SOCKET_HANDLE_INIT(ifindex);
+    int                                r;
+    struct ifreq                       ifr;
+    struct mii_ioctl_data             *mii;
+
+    g_return_val_if_fail(ifindex > 0, FALSE);
+
+    r = _ioctl_call("mii",
+                    "SIOCGMIIPHY",
+                    SIOCGMIIPHY,
+                    shandle.ifindex,
+                    &shandle.fd,
+                    shandle.ifname,
+                    IOCTL_CALL_DATA_TYPE_NONE,
+                    NULL,
+                    0,
+                    &ifr);
+    if (r < 0)
+        return FALSE;
+
+    /* If we can read the BMSR register, we assume that the card supports MII link detection */
+    mii          = (struct mii_ioctl_data *) &ifr.ifr_ifru;
+    mii->reg_num = MII_BMSR;
+
+    r = _ioctl_call("mii",
+                    "SIOCGMIIREG",
+                    SIOCGMIIREG,
+                    shandle.ifindex,
+                    &shandle.fd,
+                    shandle.ifname,
+                    IOCTL_CALL_DATA_TYPE_IFRU,
+                    mii,
+                    sizeof(*mii),
+                    &ifr);
+    if (r < 0)
+        return FALSE;
+
+    mii = (struct mii_ioctl_data *) &ifr.ifr_ifru;
+    nm_log_trace(LOGD_PLATFORM,
+                 "mii[%d,%s]: carrier-detect yes: SIOCGMIIREG result 0x%X",
+                 ifindex,
+                 shandle.ifname,
+                 mii->val_out);
+    return TRUE;
+}
diff --git a/src/libnm-platform/nmp-ethtool-ioctl.h b/src/libnm-platform/nmp-ethtool-ioctl.h
new file mode 100644
index 00000000..cb93e9cf
--- /dev/null
+++ b/src/libnm-platform/nmp-ethtool-ioctl.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#ifndef __NMP_ETHTOOL_IOCTL_H__
+#define __NMP_ETHTOOL_IOCTL_H__
+
+#include "libnm-platform/nmp-base.h"
+#include "libnm-platform/nm-netlink.h"
+
+gboolean nmp_ethtool_ioctl_supports_carrier_detect(int ifindex);
+
+gboolean nmp_ethtool_ioctl_supports_vlans(int ifindex);
+
+int nmp_ethtool_ioctl_get_peer_ifindex(int ifindex);
+
+gboolean nmp_ethtool_ioctl_get_wake_on_lan(int ifindex);
+
+gboolean nmp_ethtool_ioctl_set_wake_on_lan(int                      ifindex,
+                                           _NMSettingWiredWakeOnLan wol,
+                                           const char              *wol_password);
+
+gboolean nmp_ethtool_ioctl_get_link_settings(int                       ifindex,
+                                             gboolean                 *out_autoneg,
+                                             guint32                  *out_speed,
+                                             NMPlatformLinkDuplexType *out_duplex);
+gboolean nmp_ethtool_ioctl_set_link_settings(int                      ifindex,
+                                             gboolean                 autoneg,
+                                             guint32                  speed,
+                                             NMPlatformLinkDuplexType duplex);
+
+gboolean nmp_ethtool_ioctl_get_permanent_address(int ifindex, guint8 *buf, size_t *length);
+
+gboolean nmp_ethtool_ioctl_get_driver_info(int ifindex, NMPUtilsEthtoolDriverInfo *data);
+
+NMEthtoolFeatureStates *nmp_ethtool_ioctl_get_features(int ifindex);
+
+gboolean nmp_ethtool_ioctl_set_features(
+    int                           ifindex,
+    const NMEthtoolFeatureStates *features,
+    const NMOptionBool *requested /* indexed by NMEthtoolID - _NM_ETHTOOL_ID_FEATURE_FIRST */,
+    gboolean            do_set /* or reset */);
+
+gboolean nmp_ethtool_ioctl_get_coalesce(int ifindex, NMEthtoolCoalesceState *coalesce);
+
+gboolean nmp_ethtool_ioctl_set_coalesce(int ifindex, const NMEthtoolCoalesceState *coalesce);
+
+gboolean nmp_ethtool_ioctl_get_channels(int ifindex, NMEthtoolChannelsState *channels);
+
+gboolean nmp_ethtool_ioctl_set_channels(int ifindex, const NMEthtoolChannelsState *channels);
+
+gboolean nmp_ethtool_ioctl_get_fec_mode(int ifindex, uint32_t *fec_mode);
+
+gboolean nmp_ethtool_ioctl_set_fec_mode(int ifindex, uint32_t fec_mode);
+
+gboolean nmp_mii_ioctl_supports_carrier_detect(int ifindex);
+
+#endif /* __NMP_ETHTOOL_IOCTL_H__ */
diff --git a/src/libnm-platform/nmp-ethtool.c b/src/libnm-platform/nmp-ethtool.c
new file mode 100644
index 00000000..ed028f6a
--- /dev/null
+++ b/src/libnm-platform/nmp-ethtool.c
@@ -0,0 +1,559 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nmp-ethtool.h"
+
+#include "libnm-platform/nm-platform.h"
+#include "libnm-log-core/nm-logging.h"
+
+enum {
+    ETHTOOL_A_HEADER_UNSPEC,
+    ETHTOOL_A_HEADER_DEV_INDEX, /* u32 */
+    ETHTOOL_A_HEADER_DEV_NAME,  /* string */
+    ETHTOOL_A_HEADER_FLAGS,     /* u32 - ETHTOOL_FLAG_* */
+    ETHTOOL_A_HEADER_PHY_INDEX, /* u32 */
+
+    /* add new constants above here */
+    __ETHTOOL_A_HEADER_CNT,
+    ETHTOOL_A_HEADER_MAX = __ETHTOOL_A_HEADER_CNT - 1
+};
+
+enum {
+    ETHTOOL_MSG_USER_NONE  = 0,
+    ETHTOOL_MSG_STRSET_GET = 1,
+    ETHTOOL_MSG_LINKINFO_GET,
+    ETHTOOL_MSG_LINKINFO_SET,
+    ETHTOOL_MSG_LINKMODES_GET,
+    ETHTOOL_MSG_LINKMODES_SET,
+    ETHTOOL_MSG_LINKSTATE_GET,
+    ETHTOOL_MSG_DEBUG_GET,
+    ETHTOOL_MSG_DEBUG_SET,
+    ETHTOOL_MSG_WOL_GET,
+    ETHTOOL_MSG_WOL_SET,
+    ETHTOOL_MSG_FEATURES_GET,
+    ETHTOOL_MSG_FEATURES_SET,
+    ETHTOOL_MSG_PRIVFLAGS_GET,
+    ETHTOOL_MSG_PRIVFLAGS_SET,
+    ETHTOOL_MSG_RINGS_GET,
+    ETHTOOL_MSG_RINGS_SET,
+    ETHTOOL_MSG_CHANNELS_GET,
+    ETHTOOL_MSG_CHANNELS_SET,
+    ETHTOOL_MSG_COALESCE_GET,
+    ETHTOOL_MSG_COALESCE_SET,
+    ETHTOOL_MSG_PAUSE_GET,
+    ETHTOOL_MSG_PAUSE_SET,
+    ETHTOOL_MSG_EEE_GET,
+    ETHTOOL_MSG_EEE_SET,
+    ETHTOOL_MSG_TSINFO_GET,
+    ETHTOOL_MSG_CABLE_TEST_ACT,
+    ETHTOOL_MSG_CABLE_TEST_TDR_ACT,
+    ETHTOOL_MSG_TUNNEL_INFO_GET,
+    ETHTOOL_MSG_FEC_GET,
+    ETHTOOL_MSG_FEC_SET,
+
+    /* add new constants above here */
+    __ETHTOOL_MSG_USER_CNT,
+    ETHTOOL_MSG_USER_MAX = __ETHTOOL_MSG_USER_CNT - 1
+};
+
+#define ETHTOOL_GENL_VERSION 1
+
+#define _NMLOG_DOMAIN      LOGD_PLATFORM
+#define _NMLOG_PREFIX_NAME "ethtool"
+#define _NMLOG(_level, ...)                                   \
+    G_STMT_START                                              \
+    {                                                         \
+        int _ifindex = ifindex;                               \
+                                                              \
+        nm_log((_level),                                      \
+               (_NMLOG_DOMAIN),                               \
+               NULL,                                          \
+               NULL,                                          \
+               "%s[%d]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
+               _NMLOG_PREFIX_NAME,                            \
+               _ifindex _NM_UTILS_MACRO_REST(__VA_ARGS__));   \
+    }                                                         \
+    G_STMT_END
+
+#define CB_RESULT_PENDING 0
+#define CB_RESULT_OK      1
+
+static int
+ack_cb_handler(const struct nl_msg *msg, void *data)
+{
+    int *result = data;
+    *result     = CB_RESULT_OK;
+    return NL_STOP;
+}
+
+static int
+finish_cb_handler(const struct nl_msg *msg, void *data)
+{
+    int *result = data;
+    *result     = CB_RESULT_OK;
+    return NL_SKIP;
+}
+
+static int
+err_cb_handler(const struct sockaddr_nl *nla, const struct nlmsgerr *err, void *data)
+{
+    void      **args       = data;
+    int        *result     = args[0];
+    char      **err_msg    = args[1];
+    const char *extack_msg = NULL;
+
+    *result = err->error;
+    nlmsg_parse_error(nlmsg_undata(err), &extack_msg);
+
+    if (err_msg)
+        *err_msg = g_strdup(extack_msg ?: nm_strerror(err->error));
+
+    return NL_SKIP;
+}
+
+static int
+ethtool_send_and_recv(struct nl_sock *sock,
+                      int             ifindex,
+                      struct nl_msg  *msg,
+                      int (*valid_handler)(const struct nl_msg *, void *),
+                      void       *valid_data,
+                      char      **err_msg,
+                      const char *log_prefix)
+{
+    int                nle;
+    int                cb_result = CB_RESULT_PENDING;
+    void              *err_arg[] = {&cb_result, err_msg};
+    const struct nl_cb cb        = {
+               .err_cb     = err_cb_handler,
+               .err_arg    = err_arg,
+               .finish_cb  = finish_cb_handler,
+               .finish_arg = &cb_result,
+               .ack_cb     = ack_cb_handler,
+               .ack_arg    = &cb_result,
+               .valid_cb   = valid_handler,
+               .valid_arg  = valid_data,
+    };
+
+    g_return_val_if_fail(msg, -ENOMEM);
+
+    if (err_msg)
+        *err_msg = NULL;
+
+    nle = nl_send_auto(sock, msg);
+    if (nle < 0)
+        goto out;
+
+    while (cb_result == CB_RESULT_PENDING) {
+        nle = nl_recvmsgs(sock, &cb);
+        if (nle < 0 && nle != -EAGAIN) {
+            break;
+        }
+    }
+
+out:
+    if (nle < 0 && err_msg && *err_msg == NULL)
+        *err_msg = strdup(nm_strerror(nle));
+
+    if (nle >= 0 && cb_result < 0)
+        nle = cb_result;
+
+    if (nle < 0) {
+        _LOGT("%s: netlink error: %d (%s)", log_prefix, nle, err_msg && *err_msg ? *err_msg : "");
+    }
+
+    return nle;
+}
+
+static struct nl_msg *
+ethtool_create_msg(guint16     family_id,
+                   int         ifindex,
+                   guint8      cmd,
+                   int         header_attr,
+                   const char *log_prefix)
+{
+    nm_auto_nlmsg struct nl_msg *msg = NULL;
+    struct nlattr               *nest_header;
+
+    if (family_id == 0) {
+        _LOGT("%s: ethtool genl family not found", log_prefix);
+        return NULL;
+    }
+
+    msg = nlmsg_alloc(nlmsg_total_size(GENL_HDRLEN) + 200);
+
+    if (!genlmsg_put(msg,
+                     NL_AUTO_PORT,
+                     NL_AUTO_SEQ,
+                     family_id,
+                     0,
+                     NLM_F_REQUEST,
+                     cmd,
+                     ETHTOOL_GENL_VERSION))
+        goto nla_put_failure;
+
+    nest_header = nla_nest_start(msg, header_attr);
+    NLA_PUT_U32(msg, ETHTOOL_A_HEADER_DEV_INDEX, (guint32) ifindex);
+    NLA_NEST_END(msg, nest_header);
+    return g_steal_pointer(&msg);
+
+nla_put_failure:
+    g_return_val_if_reached(NULL);
+}
+
+/*****************************************************************************/
+/* PAUSE                                                                     */
+/*****************************************************************************/
+
+enum {
+    ETHTOOL_A_PAUSE_UNSPEC,
+    ETHTOOL_A_PAUSE_HEADER,
+    ETHTOOL_A_PAUSE_AUTONEG,
+    ETHTOOL_A_PAUSE_RX,
+    ETHTOOL_A_PAUSE_TX,
+
+    __ETHTOOL_A_PAUSE_CNT,
+    ETHTOOL_A_PAUSE_MAX = (__ETHTOOL_A_PAUSE_CNT - 1)
+};
+
+static int
+ethtool_parse_pause(const struct nl_msg *msg, void *data)
+{
+    NMEthtoolPauseState           *pause    = data;
+    static const struct nla_policy policy[] = {
+        [ETHTOOL_A_PAUSE_AUTONEG] = {.type = NLA_U8},
+        [ETHTOOL_A_PAUSE_RX]      = {.type = NLA_U8},
+        [ETHTOOL_A_PAUSE_TX]      = {.type = NLA_U8},
+    };
+    struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
+    struct nlattr     *tb[G_N_ELEMENTS(policy)];
+
+    *pause = (NMEthtoolPauseState) {};
+
+    if (nla_parse_arr(tb, genlmsg_attrdata(gnlh, 0), genlmsg_attrlen(gnlh, 0), policy) < 0)
+        return NL_SKIP;
+
+    if (tb[ETHTOOL_A_PAUSE_AUTONEG])
+        pause->autoneg = !!nla_get_u8(tb[ETHTOOL_A_PAUSE_AUTONEG]);
+    if (tb[ETHTOOL_A_PAUSE_RX])
+        pause->rx = !!nla_get_u8(tb[ETHTOOL_A_PAUSE_RX]);
+    if (tb[ETHTOOL_A_PAUSE_TX])
+        pause->tx = !!nla_get_u8(tb[ETHTOOL_A_PAUSE_TX]);
+
+    return NL_OK;
+}
+
+gboolean
+nmp_ethtool_get_pause(struct nl_sock      *genl_sock,
+                      guint16              family_id,
+                      int                  ifindex,
+                      NMEthtoolPauseState *pause)
+{
+    nm_auto_nlmsg struct nl_msg *msg     = NULL;
+    gs_free char                *err_msg = NULL;
+    int                          r;
+
+    g_return_val_if_fail(pause, FALSE);
+
+    _LOGT("get-pause: start");
+    *pause = (NMEthtoolPauseState) {};
+
+    msg = ethtool_create_msg(family_id,
+                             ifindex,
+                             ETHTOOL_MSG_PAUSE_GET,
+                             ETHTOOL_A_PAUSE_HEADER,
+                             "get-pause");
+    if (!msg)
+        return FALSE;
+
+    r = ethtool_send_and_recv(genl_sock,
+                              ifindex,
+                              msg,
+                              ethtool_parse_pause,
+                              pause,
+                              &err_msg,
+                              "get-pause");
+    if (r < 0)
+        return FALSE;
+
+    _LOGT("get-pause: autoneg %d rx %d tx %d", pause->autoneg, pause->rx, pause->tx);
+
+    return TRUE;
+}
+
+gboolean
+nmp_ethtool_set_pause(struct nl_sock            *genl_sock,
+                      guint16                    family_id,
+                      int                        ifindex,
+                      const NMEthtoolPauseState *pause)
+{
+    nm_auto_nlmsg struct nl_msg *msg     = NULL;
+    gs_free char                *err_msg = NULL;
+    int                          r;
+
+    g_return_val_if_fail(pause, FALSE);
+
+    _LOGT("set-pause: autoneg %d rx %d tx %d", pause->autoneg, pause->rx, pause->tx);
+
+    msg = ethtool_create_msg(family_id,
+                             ifindex,
+                             ETHTOOL_MSG_PAUSE_SET,
+                             ETHTOOL_A_PAUSE_HEADER,
+                             "set-pause");
+    if (!msg)
+        return FALSE;
+
+    NLA_PUT_U8(msg, ETHTOOL_A_PAUSE_AUTONEG, pause->autoneg);
+    NLA_PUT_U8(msg, ETHTOOL_A_PAUSE_RX, pause->rx);
+    NLA_PUT_U8(msg, ETHTOOL_A_PAUSE_TX, pause->tx);
+
+    r = ethtool_send_and_recv(genl_sock, ifindex, msg, NULL, NULL, &err_msg, "set-pause");
+    if (r < 0)
+        return FALSE;
+
+    _LOGT("set-pause: succeeded");
+
+    return TRUE;
+nla_put_failure:
+    g_return_val_if_reached(FALSE);
+}
+
+/*****************************************************************************/
+/* EEE                                                                       */
+/*****************************************************************************/
+
+enum {
+    ETHTOOL_A_EEE_UNSPEC,
+    ETHTOOL_A_EEE_HEADER,     /* nest - _A_HEADER_* */
+    ETHTOOL_A_EEE_MODES_OURS, /* bitset */
+    ETHTOOL_A_EEE_MODES_PEER, /* bitset */
+    ETHTOOL_A_EEE_ACTIVE,     /* u8 */
+    ETHTOOL_A_EEE_ENABLED,    /* u8 */
+
+    /* add new constants above here */
+    __ETHTOOL_A_EEE_CNT,
+    ETHTOOL_A_EEE_MAX = (__ETHTOOL_A_EEE_CNT - 1)
+};
+
+static int
+ethtool_parse_eee(const struct nl_msg *msg, void *data)
+{
+    NMEthtoolEEEState             *eee      = data;
+    static const struct nla_policy policy[] = {
+        [ETHTOOL_A_EEE_ENABLED] = {.type = NLA_U8},
+    };
+    struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
+    struct nlattr     *tb[G_N_ELEMENTS(policy)];
+
+    *eee = (NMEthtoolEEEState) {};
+
+    if (nla_parse_arr(tb, genlmsg_attrdata(gnlh, 0), genlmsg_attrlen(gnlh, 0), policy) < 0)
+        return NL_SKIP;
+
+    if (tb[ETHTOOL_A_EEE_ENABLED])
+        eee->enabled = !!nla_get_u8(tb[ETHTOOL_A_EEE_ENABLED]);
+
+    return NL_OK;
+}
+
+gboolean
+nmp_ethtool_get_eee(struct nl_sock    *genl_sock,
+                    guint16            family_id,
+                    int                ifindex,
+                    NMEthtoolEEEState *eee)
+{
+    nm_auto_nlmsg struct nl_msg *msg     = NULL;
+    gs_free char                *err_msg = NULL;
+    int                          r;
+
+    g_return_val_if_fail(eee, FALSE);
+
+    _LOGT("get-eee: start");
+    *eee = (NMEthtoolEEEState) {};
+
+    msg = ethtool_create_msg(family_id,
+                             ifindex,
+                             ETHTOOL_MSG_EEE_GET,
+                             ETHTOOL_A_EEE_HEADER,
+                             "get-eee");
+    if (!msg)
+        return FALSE;
+
+    r = ethtool_send_and_recv(genl_sock, ifindex, msg, ethtool_parse_eee, eee, &err_msg, "get-eee");
+    if (r < 0)
+        return FALSE;
+
+    _LOGT("get-eee: enabled %d", eee->enabled);
+
+    return TRUE;
+}
+
+gboolean
+nmp_ethtool_set_eee(struct nl_sock          *genl_sock,
+                    guint16                  family_id,
+                    int                      ifindex,
+                    const NMEthtoolEEEState *eee)
+{
+    nm_auto_nlmsg struct nl_msg *msg     = NULL;
+    gs_free char                *err_msg = NULL;
+    int                          r;
+
+    g_return_val_if_fail(eee, FALSE);
+
+    _LOGT("set-eee: enabled %d", eee->enabled);
+
+    msg = ethtool_create_msg(family_id,
+                             ifindex,
+                             ETHTOOL_MSG_EEE_SET,
+                             ETHTOOL_A_EEE_HEADER,
+                             "set-eee");
+    if (!msg)
+        return FALSE;
+
+    NLA_PUT_U8(msg, ETHTOOL_A_EEE_ENABLED, eee->enabled);
+
+    r = ethtool_send_and_recv(genl_sock, ifindex, msg, NULL, NULL, &err_msg, "set-eee");
+    if (r < 0)
+        return FALSE;
+
+    _LOGT("set-eee: succeeded");
+
+    return TRUE;
+nla_put_failure:
+    g_return_val_if_reached(FALSE);
+}
+
+/*****************************************************************************/
+/* RINGS                                                                     */
+/*****************************************************************************/
+
+enum {
+    ETHTOOL_A_RINGS_UNSPEC,
+    ETHTOOL_A_RINGS_HEADER,       /* nest - _A_HEADER_* */
+    ETHTOOL_A_RINGS_RX_MAX,       /* u32 */
+    ETHTOOL_A_RINGS_RX_MINI_MAX,  /* u32 */
+    ETHTOOL_A_RINGS_RX_JUMBO_MAX, /* u32 */
+    ETHTOOL_A_RINGS_TX_MAX,       /* u32 */
+    ETHTOOL_A_RINGS_RX,           /* u32 */
+    ETHTOOL_A_RINGS_RX_MINI,      /* u32 */
+    ETHTOOL_A_RINGS_RX_JUMBO,     /* u32 */
+    ETHTOOL_A_RINGS_TX,           /* u32 */
+
+    /* add new constants above here */
+    __ETHTOOL_A_RINGS_CNT,
+    ETHTOOL_A_RINGS_MAX = (__ETHTOOL_A_RINGS_CNT - 1)
+};
+
+static int
+ethtool_parse_ring(const struct nl_msg *msg, void *data)
+{
+    NMEthtoolRingState            *ring     = data;
+    static const struct nla_policy policy[] = {
+        [ETHTOOL_A_RINGS_RX]       = {.type = NLA_U32},
+        [ETHTOOL_A_RINGS_RX_MINI]  = {.type = NLA_U32},
+        [ETHTOOL_A_RINGS_RX_JUMBO] = {.type = NLA_U32},
+        [ETHTOOL_A_RINGS_TX]       = {.type = NLA_U32},
+    };
+    struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
+    struct nlattr     *tb[G_N_ELEMENTS(policy)];
+
+    *ring = (NMEthtoolRingState) {};
+
+    if (nla_parse_arr(tb, genlmsg_attrdata(gnlh, 0), genlmsg_attrlen(gnlh, 0), policy) < 0)
+        return NL_SKIP;
+
+    if (tb[ETHTOOL_A_RINGS_RX])
+        ring->rx_pending = nla_get_u32(tb[ETHTOOL_A_RINGS_RX]);
+    if (tb[ETHTOOL_A_RINGS_RX_MINI])
+        ring->rx_mini_pending = nla_get_u32(tb[ETHTOOL_A_RINGS_RX_MINI]);
+    if (tb[ETHTOOL_A_RINGS_RX_JUMBO])
+        ring->rx_jumbo_pending = nla_get_u32(tb[ETHTOOL_A_RINGS_RX_JUMBO]);
+    if (tb[ETHTOOL_A_RINGS_TX])
+        ring->tx_pending = nla_get_u32(tb[ETHTOOL_A_RINGS_TX]);
+
+    return NL_OK;
+}
+
+gboolean
+nmp_ethtool_get_ring(struct nl_sock     *genl_sock,
+                     guint16             family_id,
+                     int                 ifindex,
+                     NMEthtoolRingState *ring)
+{
+    nm_auto_nlmsg struct nl_msg *msg     = NULL;
+    gs_free char                *err_msg = NULL;
+    int                          r;
+
+    g_return_val_if_fail(ring, FALSE);
+
+    _LOGT("get-ring: start");
+    *ring = (NMEthtoolRingState) {};
+
+    msg = ethtool_create_msg(family_id,
+                             ifindex,
+                             ETHTOOL_MSG_RINGS_GET,
+                             ETHTOOL_A_RINGS_HEADER,
+                             "get-ring");
+    if (!msg)
+        return FALSE;
+
+    r = ethtool_send_and_recv(genl_sock,
+                              ifindex,
+                              msg,
+                              ethtool_parse_ring,
+                              ring,
+                              &err_msg,
+                              "get-ring");
+    if (r < 0)
+        return FALSE;
+
+    _LOGT("get-ring: rx %u rx-mini %u rx-jumbo %u tx %u",
+          ring->rx_pending,
+          ring->rx_mini_pending,
+          ring->rx_jumbo_pending,
+          ring->tx_pending);
+
+    return TRUE;
+}
+
+gboolean
+nmp_ethtool_set_ring(struct nl_sock           *genl_sock,
+                     guint16                   family_id,
+                     int                       ifindex,
+                     const NMEthtoolRingState *ring)
+{
+    nm_auto_nlmsg struct nl_msg *msg     = NULL;
+    gs_free char                *err_msg = NULL;
+    int                          r;
+
+    g_return_val_if_fail(ring, FALSE);
+
+    _LOGT("set-ring: rx %u rx-mini %u rx-jumbo %u tx %u",
+          ring->rx_pending,
+          ring->rx_mini_pending,
+          ring->rx_jumbo_pending,
+          ring->tx_pending);
+
+    msg = ethtool_create_msg(family_id,
+                             ifindex,
+                             ETHTOOL_MSG_RINGS_SET,
+                             ETHTOOL_A_RINGS_HEADER,
+                             "set-ring");
+    if (!msg)
+        return FALSE;
+
+    NLA_PUT_U32(msg, ETHTOOL_A_RINGS_RX, ring->rx_pending);
+    NLA_PUT_U32(msg, ETHTOOL_A_RINGS_RX_MINI, ring->rx_mini_pending);
+    NLA_PUT_U32(msg, ETHTOOL_A_RINGS_RX_JUMBO, ring->rx_jumbo_pending);
+    NLA_PUT_U32(msg, ETHTOOL_A_RINGS_TX, ring->tx_pending);
+
+    r = ethtool_send_and_recv(genl_sock, ifindex, msg, NULL, NULL, &err_msg, "set-ring");
+    if (r < 0)
+        return FALSE;
+
+    _LOGT("set-ring: succeeded");
+
+    return TRUE;
+nla_put_failure:
+    g_return_val_if_reached(FALSE);
+}
diff --git a/src/libnm-platform/nmp-ethtool.h b/src/libnm-platform/nmp-ethtool.h
new file mode 100644
index 00000000..88bb95b6
--- /dev/null
+++ b/src/libnm-platform/nmp-ethtool.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#ifndef __NMP_ETHTOOL_H__
+#define __NMP_ETHTOOL_H__
+
+#include "libnm-platform/nmp-base.h"
+#include "libnm-platform/nm-netlink.h"
+
+gboolean nmp_ethtool_get_pause(struct nl_sock      *genl_sock,
+                               guint16              family_id,
+                               int                  ifindex,
+                               NMEthtoolPauseState *pause);
+gboolean nmp_ethtool_set_pause(struct nl_sock            *genl_sock,
+                               guint16                    family_id,
+                               int                        ifindex,
+                               const NMEthtoolPauseState *pause);
+
+gboolean nmp_ethtool_get_eee(struct nl_sock    *genl_sock,
+                             guint16            family_id,
+                             int                ifindex,
+                             NMEthtoolEEEState *eee);
+gboolean nmp_ethtool_set_eee(struct nl_sock          *genl_sock,
+                             guint16                  family_id,
+                             int                      ifindex,
+                             const NMEthtoolEEEState *eee);
+
+gboolean nmp_ethtool_get_ring(struct nl_sock     *genl_sock,
+                              guint16             family_id,
+                              int                 ifindex,
+                              NMEthtoolRingState *ring);
+gboolean nmp_ethtool_set_ring(struct nl_sock           *genl_sock,
+                              guint16                   family_id,
+                              int                       ifindex,
+                              const NMEthtoolRingState *ring);
+
+#endif /* __NMP_ETHTOOL_H__ */
diff --git a/src/libnm-platform/nmp-object.c b/src/libnm-platform/nmp-object.c
index f41cc95a..e11bfb3f 100644
--- a/src/libnm-platform/nmp-object.c
+++ b/src/libnm-platform/nmp-object.c
@@ -16,6 +16,7 @@
 #include "libnm-platform/nm-platform-utils.h"
 #include "libnm-platform/wifi/nm-wifi-utils.h"
 #include "libnm-platform/wpan/nm-wpan-utils.h"
+#include "libnm-platform/nmp-ethtool-ioctl.h"
 
 /*****************************************************************************/
 
@@ -647,7 +648,7 @@ _link_get_driver(struct udev_device *udevice, const char *kind, int ifindex)
     if (ifindex > 0) {
         NMPUtilsEthtoolDriverInfo driver_info;
 
-        if (nmp_utils_ethtool_get_driver_info(ifindex, &driver_info)) {
+        if (nmp_ethtool_ioctl_get_driver_info(ifindex, &driver_info)) {
             if (driver_info.driver[0])
                 return g_intern_string(driver_info.driver);
         }
@@ -2006,8 +2007,8 @@ _vt_dedup_obj_clone(const NMDedupMultiObj *obj)
         .obj_destroy = _vt_dedup_obj_destroy,                                              \
         .obj_full_hash_update =                                                            \
             (void (*)(const NMDedupMultiObj *obj, NMHashState *h)) nmp_object_hash_update, \
-        .obj_full_equal = (gboolean(*)(const NMDedupMultiObj *obj_a,                       \
-                                       const NMDedupMultiObj *obj_b)) nmp_object_equal,    \
+        .obj_full_equal = (gboolean (*)(const NMDedupMultiObj *obj_a,                      \
+                                        const NMDedupMultiObj *obj_b)) nmp_object_equal,   \
     }
 
 /*****************************************************************************/