about summary refs log tree commit diff
path: root/src/libnm-platform
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2024-02-22 17:21:11 +0100
committerMichael Biebl <biebl@debian.org>2024-02-22 17:21:11 +0100
commitbba2e4b4de668db525cbfdfc35292e5a0b51671a (patch)
tree38d20cddfcc6f71572b9e169deefab5fa96e8d0c /src/libnm-platform
parent6681f77b757bbc42ce5c8868ee9142b7ebc8c059 (diff)
New upstream version 1.46.0 upstream/1.46.0
Diffstat (limited to 'src/libnm-platform')
-rw-r--r--src/libnm-platform/devlink/nm-devlink.c365
-rw-r--r--src/libnm-platform/devlink/nm-devlink.h30
-rw-r--r--src/libnm-platform/meson.build1
-rw-r--r--src/libnm-platform/nm-linux-platform.c440
-rw-r--r--src/libnm-platform/nm-platform.c19
-rw-r--r--src/libnm-platform/nm-platform.h15
6 files changed, 768 insertions, 102 deletions
diff --git a/src/libnm-platform/devlink/nm-devlink.c b/src/libnm-platform/devlink/nm-devlink.c
new file mode 100644
index 00000000..f06697cf
--- /dev/null
+++ b/src/libnm-platform/devlink/nm-devlink.c
@@ -0,0 +1,365 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Red Hat, Inc.
+ */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-devlink.h"
+
+#include <linux/if.h>
+#include <linux/devlink.h>
+
+#include "libnm-log-core/nm-logging.h"
+#include "libnm-platform/nm-netlink.h"
+#include "libnm-platform/nm-platform.h"
+#include "libnm-platform/nm-platform-utils.h"
+
+#define _NMLOG_PREFIX_NAME "devlink"
+#define _NMLOG_DOMAIN      LOGD_PLATFORM | LOGD_DEVICE
+#define _NMLOG(level, ...)                                                                        \
+    G_STMT_START                                                                                  \
+    {                                                                                             \
+        char        _ifname_buf[IFNAMSIZ];                                                        \
+        const char *_ifname = self ? nmp_utils_if_indextoname(self->ifindex, _ifname_buf) : NULL; \
+                                                                                                  \
+        nm_log((level),                                                                           \
+               _NMLOG_DOMAIN,                                                                     \
+               _ifname ?: NULL,                                                                   \
+               NULL,                                                                              \
+               "%s%s%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                                   \
+               _NMLOG_PREFIX_NAME,                                                                \
+               NM_PRINT_FMT_QUOTED(_ifname, " (", _ifname, ")", "")                               \
+                   _NM_UTILS_MACRO_REST(__VA_ARGS__));                                            \
+    }                                                                                             \
+    G_STMT_END
+
+#define CB_RESULT_PENDING 0
+#define CB_RESULT_OK      1
+
+struct _NMDevlink {
+    NMPlatform     *plat;
+    struct nl_sock *genl_sock_sync;
+    guint16         genl_family_id;
+    int             ifindex;
+};
+
+/**
+ * nm_devlink_new:
+ * @platform: the #NMPlatform that will use this #NMDevlink instance
+ * @genl_sock_sync: the netlink socket (will be used synchronously)
+ * @ifindex: the kernel's netdev ifindex corresponding to the devlink device
+ *
+ * Create a new #NMDevlink instance to make devlink queries regarding a specific
+ * device.
+ *
+ * Returns: (transfer full): the allocated new #NMDevlink
+ */
+NMDevlink *
+nm_devlink_new(NMPlatform *platform, struct nl_sock *genl_sock_sync, int ifindex)
+{
+    NMDevlink *self = g_new(NMDevlink, 1);
+
+    self->plat           = platform;
+    self->genl_sock_sync = genl_sock_sync;
+    self->genl_family_id = nm_platform_genl_get_family_id(platform, NMP_GENL_FAMILY_TYPE_DEVLINK);
+    self->ifindex        = ifindex;
+    return self;
+}
+
+/**
+ * nm_devlink_get_dev_identifier:
+ * @self: the #NMDevlink
+ * @out_bus: (out): the "bus_name" part of the devlink device identifier
+ * @out_addr: (out): the "bus_addr" part of the devlink device identifier
+ * @error: (optional): the error location
+ *
+ * Get the devlink device identifier of the device for which the #NMDevlink was
+ * created (with the @ifindex argument of nm_devlink_get_new()). A devlink device
+ * is identified as "bus_name/bus_addr" (i.e. "pci/0000:65:00.0"). This function
+ * provides both parts separately.
+ *
+ * Note that here we only get the potential devlink device identifier. The real devlink
+ * device might not even exist if the hw doesn't implement devlink or the netdev
+ * doesn't have a 1-1 corresponding devlink device (i.e. because it's a VF or
+ * because the hw uses a "one eswitch for many ports" model).
+ *
+ * Also note that currently only PCI devices are supported, an error will be
+ * returned for other kind of devices.
+ *
+ * Returns: FALSE in case of error, TRUE otherwise
+ */
+gboolean
+nm_devlink_get_dev_identifier(NMDevlink *self, char **out_bus, char **out_addr, GError **error)
+{
+    const char               *bus;
+    char                      sbuf[IFNAMSIZ];
+    NMPUtilsEthtoolDriverInfo ethtool_driver_info;
+
+    nm_assert(out_bus != NULL && out_addr != NULL);
+    nm_assert(!error || !*error);
+
+    if (!nm_platform_link_get_udev_property(self->plat, self->ifindex, "ID_BUS", &bus)) {
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    "Can't get udev info for device '%s'",
+                    nmp_utils_if_indextoname(self->ifindex, sbuf));
+        return FALSE;
+    }
+
+    if (!nm_streq0(bus, "pci")) {
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    "Devlink is only supported for PCI but device '%s' has bus name '%s'",
+                    nmp_utils_if_indextoname(self->ifindex, sbuf),
+                    bus);
+        return FALSE;
+    }
+
+    if (!nmp_utils_ethtool_get_driver_info(self->ifindex, &ethtool_driver_info)) {
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    "Can't get ethtool driver info for device '%s'",
+                    nmp_utils_if_indextoname(self->ifindex, sbuf));
+        return FALSE;
+    }
+
+    *out_bus  = g_strdup("pci");
+    *out_addr = g_strdup(ethtool_driver_info._private_bus_info);
+    return TRUE;
+}
+
+static struct nl_msg *
+devlink_alloc_msg(NMDevlink *self, uint8_t cmd, uint16_t flags)
+{
+    nm_auto_nlmsg struct nl_msg *msg = nlmsg_alloc(0);
+    if (!msg)
+        return NULL;
+
+    genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, self->genl_family_id, 0, flags, cmd, 0);
+    return g_steal_pointer(&msg);
+}
+
+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;
+    NMDevlink  *self       = args[0];
+    int        *result     = args[1];
+    char      **err_msg    = args[2];
+    const char *extack_msg = NULL;
+
+    *result = err->error;
+    nlmsg_parse_error(nlmsg_undata(err), &extack_msg);
+
+    _LOGT("error response (%d - %s)", err->error, extack_msg ?: nm_strerror(err->error));
+
+    if (err_msg)
+        *err_msg = g_strdup(extack_msg ?: nm_strerror(err->error));
+
+    return NL_SKIP;
+}
+
+static int
+devlink_send_and_recv(NMDevlink     *self,
+                      struct nl_msg *msg,
+                      int (*valid_handler)(const struct nl_msg *, void *),
+                      void  *valid_data,
+                      char **err_msg)
+{
+    int                nle;
+    int                cb_result = CB_RESULT_PENDING;
+    void              *err_arg[] = {self, &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 != NULL, -ENOMEM);
+
+    if (err_msg)
+        *err_msg = NULL;
+
+    nle = nl_send_auto(self->genl_sock_sync, msg);
+    if (nle < 0)
+        goto out;
+
+    while (cb_result == CB_RESULT_PENDING) {
+        nle = nl_recvmsgs(self->genl_sock_sync, &cb);
+        if (nle < 0 && nle != -EAGAIN) {
+            _LOGW("nl_recvmsgs() error (%d - %s)", nle, nm_strerror(nle));
+            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;
+    return nle;
+}
+
+static int
+devlink_parse_eswitch_mode(const struct nl_msg *msg, void *data)
+{
+    static const struct nla_policy eswitch_policy[] = {
+        [DEVLINK_ATTR_ESWITCH_MODE]        = {.type = NLA_U16},
+        [DEVLINK_ATTR_ESWITCH_INLINE_MODE] = {.type = NLA_U8},
+        [DEVLINK_ATTR_ESWITCH_ENCAP_MODE]  = {.type = NLA_U8},
+    };
+    NMDevlinkEswitchParams *params = data;
+    struct genlmsghdr      *gnlh   = nlmsg_data(nlmsg_hdr(msg));
+    struct nlattr          *tb[G_N_ELEMENTS(eswitch_policy)];
+    struct nlattr          *nla;
+
+    if (nla_parse_arr(tb, genlmsg_attrdata(gnlh, 0), genlmsg_attrlen(gnlh, 0), eswitch_policy) < 0)
+        return NL_SKIP;
+
+    nla          = tb[DEVLINK_ATTR_ESWITCH_MODE];
+    params->mode = nla ? (_NMSriovEswitchMode) nla_get_u16(nla) : _NM_SRIOV_ESWITCH_MODE_UNKNOWN;
+
+    nla = tb[DEVLINK_ATTR_ESWITCH_INLINE_MODE];
+    params->inline_mode =
+        nla ? (_NMSriovEswitchInlineMode) nla_get_u8(nla) : _NM_SRIOV_ESWITCH_INLINE_MODE_UNKNOWN;
+
+    nla = tb[DEVLINK_ATTR_ESWITCH_ENCAP_MODE];
+    params->encap_mode =
+        nla ? (_NMSriovEswitchEncapMode) nla_get_u8(nla) : _NM_SRIOV_ESWITCH_ENCAP_MODE_UNKNOWN;
+
+    return NL_OK;
+}
+
+/*
+ * nm_devlink_get_eswitch_params:
+ * @self: the #NMDevlink
+ * @out_params: the eswitch parameters read via Devlink
+ * @error: the error location
+ *
+ * Get the eswitch configuration of the device related to the #NMDevlink instance. Note
+ * that this might be unsupported by the device (see nm_devlink_get_dev()).
+ *
+ * Returns: FALSE in case of error, TRUE otherwise
+ */
+gboolean
+nm_devlink_get_eswitch_params(NMDevlink *self, NMDevlinkEswitchParams *out_params, GError **error)
+{
+    nm_auto_nlmsg struct nl_msg *msg     = NULL;
+    gs_free char                *bus     = NULL;
+    gs_free char                *addr    = NULL;
+    gs_free char                *err_msg = NULL;
+    int                          rc;
+
+    nm_assert(out_params);
+
+    if (!nm_devlink_get_dev_identifier(self, &bus, &addr, error))
+        return FALSE;
+
+    msg = devlink_alloc_msg(self, DEVLINK_CMD_ESWITCH_GET, 0);
+    NLA_PUT_STRING(msg, DEVLINK_ATTR_BUS_NAME, bus);
+    NLA_PUT_STRING(msg, DEVLINK_ATTR_DEV_NAME, addr);
+
+    rc = devlink_send_and_recv(self, msg, devlink_parse_eswitch_mode, out_params, &err_msg);
+    if (rc < 0) {
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    "devlink: eswitch get failed (%d - %s)",
+                    rc,
+                    err_msg);
+        return FALSE;
+    }
+
+    _LOGD("eswitch get success");
+
+    return TRUE;
+
+nla_put_failure:
+    g_return_val_if_reached(FALSE);
+}
+
+/*
+ * nm_devlink_set_eswitch_params:
+ * @self: the #NMDevlink
+ * @params: the eswitch parameters to set
+ * @error: the error location
+ *
+ * Set the eswitch configuration of the device related to the #NMDevlink instance. Note
+ * that this might be unsupported by the device (see nm_devlink_get_dev()).
+ *
+ * If any of the eswitch parameters is set to "preserve" it won't be modified.
+ *
+ * Returns: FALSE in case of error, TRUE otherwise
+ */
+gboolean
+nm_devlink_set_eswitch_params(NMDevlink *self, NMDevlinkEswitchParams params, GError **error)
+{
+    nm_auto_nlmsg struct nl_msg *msg     = NULL;
+    gs_free char                *bus     = NULL;
+    gs_free char                *addr    = NULL;
+    gs_free char                *err_msg = NULL;
+    int                          rc;
+
+    if (params.mode == _NM_SRIOV_ESWITCH_MODE_PRESERVE
+        && params.inline_mode == _NM_SRIOV_ESWITCH_INLINE_MODE_PRESERVE
+        && params.encap_mode == _NM_SRIOV_ESWITCH_ENCAP_MODE_PRESERVE)
+        return TRUE;
+
+    if (!nm_devlink_get_dev_identifier(self, &bus, &addr, error))
+        return FALSE;
+
+    msg = devlink_alloc_msg(self, DEVLINK_CMD_ESWITCH_SET, 0);
+    NLA_PUT_STRING(msg, DEVLINK_ATTR_BUS_NAME, bus);
+    NLA_PUT_STRING(msg, DEVLINK_ATTR_DEV_NAME, addr);
+
+    if (params.mode != _NM_SRIOV_ESWITCH_MODE_PRESERVE)
+        NLA_PUT_U16(msg, DEVLINK_ATTR_ESWITCH_MODE, params.mode);
+    if (params.inline_mode != _NM_SRIOV_ESWITCH_INLINE_MODE_PRESERVE)
+        NLA_PUT_U8(msg, DEVLINK_ATTR_ESWITCH_INLINE_MODE, params.inline_mode);
+    if (params.encap_mode != _NM_SRIOV_ESWITCH_ENCAP_MODE_PRESERVE)
+        NLA_PUT_U8(msg, DEVLINK_ATTR_ESWITCH_ENCAP_MODE, params.encap_mode);
+
+    rc = devlink_send_and_recv(self, msg, NULL, NULL, &err_msg);
+    if (rc < 0) {
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    "devlink: eswitch set failed (%d - %s)",
+                    rc,
+                    err_msg);
+        return FALSE;
+    }
+
+    _LOGD("eswitch set success");
+
+    return TRUE;
+
+nla_put_failure:
+    g_return_val_if_reached(FALSE);
+}
diff --git a/src/libnm-platform/devlink/nm-devlink.h b/src/libnm-platform/devlink/nm-devlink.h
new file mode 100644
index 00000000..c626a120
--- /dev/null
+++ b/src/libnm-platform/devlink/nm-devlink.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Red Hat, Inc.
+ */
+
+#ifndef __NMP_DEVLINK_H__
+#define __NMP_DEVLINK_H__
+
+#include "libnm-base/nm-base.h"
+#include <linux/devlink.h>
+
+struct nl_sock;
+typedef struct _NMPlatform NMPlatform;
+typedef struct _NMDevlink  NMDevlink;
+
+typedef struct {
+    _NMSriovEswitchMode       mode;
+    _NMSriovEswitchInlineMode inline_mode;
+    _NMSriovEswitchEncapMode  encap_mode;
+} NMDevlinkEswitchParams;
+
+NMDevlink *nm_devlink_new(NMPlatform *platform, struct nl_sock *genl_sock_sync, int ifindex);
+gboolean
+nm_devlink_get_dev_identifier(NMDevlink *self, char **out_bus, char **out_addr, GError **error);
+gboolean
+nm_devlink_get_eswitch_params(NMDevlink *self, NMDevlinkEswitchParams *out_params, GError **error);
+gboolean
+nm_devlink_set_eswitch_params(NMDevlink *self, NMDevlinkEswitchParams params, GError **error);
+
+#endif /* __NMP_DEVLINK_H__ */
\ No newline at end of file
diff --git a/src/libnm-platform/meson.build b/src/libnm-platform/meson.build
index 696ca1a6..7b6ad042 100644
--- a/src/libnm-platform/meson.build
+++ b/src/libnm-platform/meson.build
@@ -12,6 +12,7 @@ libnm_platform = static_library(
     'nmp-netns.c',
     'nmp-object.c',
     'nmp-plobj.c',
+    'devlink/nm-devlink.c',
     'wifi/nm-wifi-utils-nl80211.c',
     'wifi/nm-wifi-utils.c',
     'wpan/nm-wpan-utils.c',
diff --git a/src/libnm-platform/nm-linux-platform.c b/src/libnm-platform/nm-linux-platform.c
index a7078280..9ecac2d9 100644
--- a/src/libnm-platform/nm-linux-platform.c
+++ b/src/libnm-platform/nm-linux-platform.c
@@ -41,6 +41,7 @@
 #include "libnm-platform/nm-netlink.h"
 #include "libnm-platform/nm-platform-utils.h"
 #include "libnm-platform/nmp-netns.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"
 #include "libnm-platform/wpan/nm-wpan-utils.h"
@@ -8881,141 +8882,394 @@ nla_put_failure:
     g_return_val_if_reached(FALSE);
 }
 
+static gint64
+sriov_read_sysctl_uint(NMPlatform *platform,
+                       int         dirfd,
+                       const char *ifname,
+                       const char *dev_file,
+                       GError    **error)
+{
+    const char *path;
+    gint64      val;
+
+    nm_assert(NM_STRLEN("device/%s") + strlen(dev_file));
+
+    path = nm_sprintf_bufa(256, "device/%s", dev_file);
+    val  = nm_platform_sysctl_get_int_checked(platform,
+                                             NMP_SYSCTL_PATHID_NETDIR_UNSAFE_A(dirfd, ifname, path),
+                                             10,
+                                             0,
+                                             G_MAXUINT,
+                                             -1);
+
+    if (val < 0) {
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    "couldn't read %s: %s",
+                    dev_file,
+                    nm_strerror_native(errno));
+        return -errno;
+    }
+
+    return val;
+}
+
+static gboolean
+sriov_set_autoprobe(NMPlatform  *platform,
+                    int          dirfd,
+                    const char  *ifname,
+                    NMOptionBool autoprobe,
+                    GError     **error)
+{
+    int current_autoprobe =
+        (int) sriov_read_sysctl_uint(platform, dirfd, ifname, "sriov_drivers_autoprobe", error);
+
+    if (current_autoprobe == -ENOENT) {
+        /* older kernel versions don't have this sysctl. Assume the value is "1". */
+        current_autoprobe = 1;
+        g_clear_error(error);
+    }
+
+    if (current_autoprobe < 0)
+        return FALSE;
+
+    if (autoprobe != NM_OPTION_BOOL_DEFAULT && current_autoprobe != autoprobe) {
+        if (!nm_platform_sysctl_set(
+                platform,
+                NMP_SYSCTL_PATHID_NETDIR_A(dirfd, ifname, "device/sriov_drivers_autoprobe"),
+                autoprobe == 1 ? "1" : "0")) {
+            g_set_error(error,
+                        NM_UTILS_ERROR,
+                        NM_UTILS_ERROR_UNKNOWN,
+                        "couldn't set SR-IOV drivers-autoprobe to %d: %s",
+                        (int) autoprobe,
+                        nm_strerror_native(errno));
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+#define _SRIOV_ASYNC_MAX_STEPS 4
+
+typedef struct _SriovAsyncState {
+    NMPlatform           *platform;
+    int                   ifindex;
+    NMPlatformSriovParams sriov_params;
+    void (*steps[_SRIOV_ASYNC_MAX_STEPS])(struct _SriovAsyncState *);
+    int                     current_step;
+    NMPlatformAsyncCallback callback;
+    gpointer                data;
+    GCancellable           *cancellable;
+} SriovAsyncState;
+
 static void
-sriov_idle_cb(gpointer user_data, GCancellable *cancellable)
+sriov_async_invoke_callback(gpointer user_data, GCancellable *cancellable)
 {
-    gs_unref_object NMPlatform *platform        = NULL;
-    gs_free_error GError       *cancelled_error = NULL;
-    gs_free_error GError       *error           = NULL;
-    NMPlatformAsyncCallback     callback;
-    gpointer                    callback_data;
+    gs_free_error GError   *cancelled_error = NULL;
+    gs_free_error GError   *error           = NULL;
+    NMPlatformAsyncCallback callback;
+    gpointer                callback_data;
 
     g_cancellable_set_error_if_cancelled(cancellable, &cancelled_error);
-    nm_utils_user_data_unpack(user_data, &platform, &error, &callback, &callback_data);
+    nm_utils_user_data_unpack(user_data, &error, &callback, &callback_data);
     callback(cancelled_error ?: error, callback_data);
 }
 
 static void
+sriov_async_finish_err(SriovAsyncState *async_state, GError *error)
+{
+    NMPlatform *platform = async_state->platform;
+
+    _LOGD("finished configuring SR-IOV, error: %s", error ? error->message : "none");
+
+    if (async_state->callback) {
+        /* nm_platform_link_set_sriov_params() promises to always call the callback,
+         * and always asynchronously. We might have reached here without doing
+         * any asynchronous task, so invoke the user's callback in the idle task
+         * to make it asynchronous. Actually, let's make it simple and do it
+         * always in this way, even if asynchronous tasks were made.
+         */
+        gpointer packed = nm_utils_user_data_pack(g_steal_pointer(&error),
+                                                  async_state->callback,
+                                                  async_state->data);
+        nm_utils_invoke_on_idle(async_state->cancellable, sriov_async_invoke_callback, packed);
+    }
+
+    g_object_unref(async_state->platform);
+    g_object_unref(async_state->cancellable);
+    g_free(async_state);
+    g_free(error);
+}
+
+static void
+sriov_async_call_next_step(SriovAsyncState *async_state)
+{
+    if (g_cancellable_is_cancelled(async_state->cancellable)) {
+        sriov_async_finish_err(async_state, NULL); /* The error will be set later */
+        return;
+    }
+
+    async_state->current_step++;
+
+    nm_assert(async_state->current_step >= 0);
+    nm_assert(async_state->current_step < _SRIOV_ASYNC_MAX_STEPS);
+    nm_assert(async_state->steps[async_state->current_step] != NULL);
+
+    async_state->steps[async_state->current_step](async_state);
+}
+
+static void
+sriov_async_sysctl_done_cb(GError *error, gpointer data)
+{
+    SriovAsyncState *async_state = data;
+
+    if (error)
+        sriov_async_finish_err(async_state, g_error_copy(error));
+    else
+        sriov_async_call_next_step(async_state);
+}
+
+static void
+sriov_async_set_num_vfs(SriovAsyncState *async_state, const char *val)
+{
+    NMPlatform           *platform = async_state->platform;
+    const char           *values[] = {val, NULL};
+    nm_auto_close int     dirfd    = -1;
+    char                  ifname[IFNAMSIZ];
+    gs_free_error GError *error = NULL;
+
+    dirfd = nm_platform_sysctl_open_netdir(platform, async_state->ifindex, ifname);
+    if (!dirfd) {
+        g_set_error(&error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    "couldn't open netdir for device with ifindex %d",
+                    async_state->ifindex);
+        sriov_async_finish_err(async_state, g_steal_pointer(&error));
+        return;
+    }
+
+    sysctl_set_async(platform,
+                     NMP_SYSCTL_PATHID_NETDIR_A(dirfd, ifname, "device/sriov_numvfs"),
+                     values,
+                     sriov_async_sysctl_done_cb,
+                     async_state,
+                     async_state->cancellable);
+}
+
+static void
+sriov_async_step1_destroy_vfs(SriovAsyncState *async_state)
+{
+    NMPlatform *platform = async_state->platform;
+
+    _LOGD("destroying VFs before configuring SR-IOV");
+
+    sriov_async_set_num_vfs(async_state, "0");
+}
+
+static void
+sriov_async_step2_set_eswitch_mode(SriovAsyncState *async_state)
+{
+    NMPlatform             *platform       = async_state->platform;
+    NMLinuxPlatformPrivate *priv           = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+    gs_free NMDevlink      *devlink        = NULL;
+    gs_free_error GError   *error          = NULL;
+    NMDevlinkEswitchParams  eswitch_params = {
+         .mode        = async_state->sriov_params.eswitch_mode,
+         .inline_mode = async_state->sriov_params.eswitch_inline_mode,
+         .encap_mode  = async_state->sriov_params.eswitch_encap_mode,
+    };
+
+    _LOGD("setting eswitch params (mode=%d, inline-mode=%d, encap-mode=%d)",
+          (int) eswitch_params.mode,
+          (int) eswitch_params.inline_mode,
+          (int) eswitch_params.encap_mode);
+
+    /* We set eswitch mode as a sriov_async step because it's in the middle of
+     * other steps that are async. However, this step itself is synchronous. */
+    devlink = nm_devlink_new(platform, priv->sk_genl_sync, async_state->ifindex);
+    if (!nm_devlink_set_eswitch_params(devlink, eswitch_params, &error)) {
+        sriov_async_finish_err(async_state, g_steal_pointer(&error));
+        return;
+    }
+
+    sriov_async_call_next_step(async_state);
+}
+
+static void
+sriov_async_step3_create_vfs(SriovAsyncState *async_state)
+{
+    NMPlatform *platform = async_state->platform;
+    const char *val      = nm_sprintf_bufa(32, "%u", async_state->sriov_params.num_vfs);
+
+    _LOGD("setting sriov_numvfs to %u", async_state->sriov_params.num_vfs);
+
+    sriov_async_set_num_vfs(async_state, val);
+}
+
+static void
+sriov_async_step_finish_ok(SriovAsyncState *async_state)
+{
+    sriov_async_finish_err(async_state, NULL);
+}
+
+static int
+sriov_eswitch_get_needs_change(SriovAsyncState *async_state,
+                               gboolean        *out_needs_change,
+                               GError         **error)
+{
+    NMPlatform               *platform    = async_state->platform;
+    NMLinuxPlatformPrivate   *priv        = NM_LINUX_PLATFORM_GET_PRIVATE(platform);
+    _NMSriovEswitchMode       mode        = async_state->sriov_params.eswitch_mode;
+    _NMSriovEswitchInlineMode inline_mode = async_state->sriov_params.eswitch_inline_mode;
+    _NMSriovEswitchEncapMode  encap_mode  = async_state->sriov_params.eswitch_encap_mode;
+    NMDevlinkEswitchParams    current_params;
+    gs_free NMDevlink        *devlink = NULL;
+
+    nm_assert(out_needs_change);
+
+    if (mode == _NM_SRIOV_ESWITCH_MODE_PRESERVE
+        && inline_mode == _NM_SRIOV_ESWITCH_INLINE_MODE_PRESERVE
+        && encap_mode == _NM_SRIOV_ESWITCH_ENCAP_MODE_PRESERVE) {
+        *out_needs_change = FALSE;
+        return 0;
+    }
+
+    devlink = nm_devlink_new(platform, priv->sk_genl_sync, async_state->ifindex);
+
+    if (!nm_devlink_get_eswitch_params(devlink, &current_params, error))
+        return -1;
+
+    *out_needs_change = (mode != _NM_SRIOV_ESWITCH_MODE_PRESERVE && mode != current_params.mode)
+                        || (inline_mode != _NM_SRIOV_ESWITCH_INLINE_MODE_PRESERVE
+                            && inline_mode != current_params.inline_mode)
+                        || (encap_mode != _NM_SRIOV_ESWITCH_ENCAP_MODE_PRESERVE
+                            && encap_mode != current_params.encap_mode);
+    return 0;
+}
+
+/*
+ * Take special care when setting new values:
+ *  - don't touch anything if the right values are already set
+ *  - to change the number of VFs, eswitch mode or autoprobe we need to destroy existing VFs
+ *  - the autoprobe setting is irrelevant when numvfs is zero
+ */
+static void
 link_set_sriov_params_async(NMPlatform             *platform,
                             int                     ifindex,
-                            guint                   num_vfs,
-                            NMOptionBool            autoprobe,
+                            NMPlatformSriovParams   sriov_params,
                             NMPlatformAsyncCallback callback,
                             gpointer                data,
                             GCancellable           *cancellable)
 {
+    SriovAsyncState            *async_state;
     nm_auto_pop_netns NMPNetns *netns = NULL;
     gs_free_error GError       *error = NULL;
     nm_auto_close int           dirfd = -1;
-    int                         current_autoprobe;
-    guint                       i, total;
-    gint64                      current_num;
     char                        ifname[IFNAMSIZ];
-    gpointer                    packed;
-    const char                 *values[3];
-    char                        buf[64];
+    int                         max_vfs;
+    int                         current_num_vfs;
+    gboolean                    need_change_eswitch_params;
+    gboolean                    need_change_vfs;
+    gboolean                    need_destroy_vfs;
+    gboolean                    need_create_vfs;
+    int                         i;
 
     g_return_if_fail(callback || !data);
     g_return_if_fail(cancellable);
 
+    async_state               = g_new0(SriovAsyncState, 1);
+    async_state->platform     = g_object_ref(platform);
+    async_state->ifindex      = ifindex;
+    async_state->sriov_params = sriov_params;
+    async_state->current_step = -1;
+    async_state->callback     = callback;
+    async_state->data         = data;
+    async_state->cancellable  = g_object_ref(cancellable);
+
     if (!nm_platform_netns_push(platform, &netns)) {
         g_set_error_literal(&error,
                             NM_UTILS_ERROR,
                             NM_UTILS_ERROR_UNKNOWN,
-                            "couldn't change namespace");
-        goto out_idle;
+                            "couldn't change network namespace");
+        sriov_async_finish_err(async_state, g_steal_pointer(&error));
+        return;
     }
 
     dirfd = nm_platform_sysctl_open_netdir(platform, ifindex, ifname);
     if (!dirfd) {
-        g_set_error_literal(&error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "couldn't open netdir");
-        goto out_idle;
+        g_set_error(&error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    "couldn't open netdir for device with ifindex %d",
+                    ifindex);
+        sriov_async_finish_err(async_state, g_steal_pointer(&error));
+        return;
     }
 
-    total = nm_platform_sysctl_get_int_checked(
-        platform,
-        NMP_SYSCTL_PATHID_NETDIR_A(dirfd, ifname, "device/sriov_totalvfs"),
-        10,
-        0,
-        G_MAXUINT,
-        0);
-    if (!errno && num_vfs > total) {
-        _LOGW("link: %d only supports %u VFs (requested %u)", ifindex, total, num_vfs);
-        num_vfs = total;
+    current_num_vfs = sriov_read_sysctl_uint(platform, dirfd, ifname, "sriov_numvfs", &error);
+    if (current_num_vfs < 0) {
+        sriov_async_finish_err(async_state, g_steal_pointer(&error));
+        return;
     }
 
-    /*
-     * Take special care when setting new values:
-     *  - don't touch anything if the right values are already set
-     *  - to change the number of VFs or autoprobe we need to destroy existing VFs
-     *  - the autoprobe setting is irrelevant when numvfs is zero
-     */
-    current_num = nm_platform_sysctl_get_int_checked(
-        platform,
-        NMP_SYSCTL_PATHID_NETDIR_A(dirfd, ifname, "device/sriov_numvfs"),
-        10,
-        0,
-        G_MAXUINT,
-        -1);
-    current_autoprobe = nm_platform_sysctl_get_int_checked(
-        platform,
-        NMP_SYSCTL_PATHID_NETDIR_A(dirfd, ifname, "device/sriov_drivers_autoprobe"),
-        10,
-        0,
-        1,
-        -1);
-
-    if (current_autoprobe == -1 && errno == ENOENT) {
-        /* older kernel versions don't have this sysctl. Assume the value is
-         * "1". */
-        current_autoprobe = 1;
+    max_vfs = sriov_read_sysctl_uint(platform, dirfd, ifname, "sriov_totalvfs", &error);
+    if (max_vfs < 0) {
+        _LOGD("link: can't read max VFs (%s)", error->message);
+        g_clear_error(&error);
+        max_vfs = sriov_params.num_vfs; /* Try to create all */
     }
 
-    if (current_num == num_vfs
-        && (autoprobe == NM_OPTION_BOOL_DEFAULT || current_autoprobe == autoprobe))
-        goto out_idle;
+    if (sriov_params.num_vfs > max_vfs) {
+        _LOGW("link: device %d only supports %u VFs (requested %u)",
+              ifindex,
+              max_vfs,
+              sriov_params.num_vfs);
+        _LOGW("link: reducing num_vfs to %u for device %d", max_vfs, ifindex);
+        sriov_params.num_vfs              = max_vfs;
+        async_state->sriov_params.num_vfs = max_vfs;
+    }
 
-    if (NM_IN_SET(autoprobe, NM_OPTION_BOOL_TRUE, NM_OPTION_BOOL_FALSE)
-        && current_autoprobe != autoprobe
-        && !nm_platform_sysctl_set(
-            platform,
-            NMP_SYSCTL_PATHID_NETDIR_A(dirfd, ifname, "device/sriov_drivers_autoprobe"),
-            nm_sprintf_buf(buf, "%d", (int) autoprobe))) {
-        g_set_error(&error,
-                    NM_UTILS_ERROR,
-                    NM_UTILS_ERROR_UNKNOWN,
-                    "couldn't set SR-IOV drivers-autoprobe to %d: %s",
-                    (int) autoprobe,
-                    nm_strerror_native(errno));
-        goto out_idle;
+    /* Setting autoprobe goes first, we can do it synchronously */
+    if (sriov_params.num_vfs > 0
+        && !sriov_set_autoprobe(platform, dirfd, ifname, sriov_params.autoprobe, &error)) {
+        sriov_async_finish_err(async_state, g_steal_pointer(&error));
+        return;
     }
 
-    if (current_num == 0 && num_vfs == 0)
-        goto out_idle;
+    /* Decide what actions we must do. Note that we might need to destroy the VFs even
+     * if num_vfs == current_num_vfs, for example to change the eswitch mode. Because of
+     * that, we might need to create VFs even if num_vfs == current_num_vfs.
+     * Steps in order (unnecessary steps are skipped):
+     *   1. Destroy VFs
+     *   2. Set eswitch mode
+     *   3. Create VFs
+     *   4. Invoke caller's callback
+     */
+    if (sriov_eswitch_get_needs_change(async_state, &need_change_eswitch_params, &error) < 0) {
+        sriov_async_finish_err(async_state, g_steal_pointer(&error));
+        return;
+    }
+    need_change_vfs  = sriov_params.num_vfs != current_num_vfs;
+    need_destroy_vfs = current_num_vfs > 0 && (need_change_eswitch_params || need_change_vfs);
+    need_create_vfs  = (current_num_vfs == 0 || need_destroy_vfs) && sriov_params.num_vfs > 0;
 
     i = 0;
-    if (current_num != 0)
-        values[i++] = "0";
-    if (num_vfs != 0)
-        values[i++] = nm_sprintf_bufa(32, "%u", num_vfs);
-    values[i++] = NULL;
+    if (need_destroy_vfs)
+        async_state->steps[i++] = sriov_async_step1_destroy_vfs;
+    if (need_change_eswitch_params)
+        async_state->steps[i++] = sriov_async_step2_set_eswitch_mode;
+    if (need_create_vfs)
+        async_state->steps[i++] = sriov_async_step3_create_vfs;
 
-    sysctl_set_async(platform,
-                     NMP_SYSCTL_PATHID_NETDIR_A(dirfd, ifname, "device/sriov_numvfs"),
-                     values,
-                     callback,
-                     data,
-                     cancellable);
-    return;
-
-out_idle:
-    if (callback) {
-        packed = nm_utils_user_data_pack(g_object_ref(platform),
-                                         g_steal_pointer(&error),
-                                         callback,
-                                         data);
-        nm_utils_invoke_on_idle(cancellable, sriov_idle_cb, packed);
-    }
+    nm_assert(i < _SRIOV_ASYNC_MAX_STEPS);
+
+    async_state->steps[i] = sriov_async_step_finish_ok;
+
+    sriov_async_call_next_step(async_state);
 }
 
 static gboolean
diff --git a/src/libnm-platform/nm-platform.c b/src/libnm-platform/nm-platform.c
index 1411fe9e..b89b0359 100644
--- a/src/libnm-platform/nm-platform.c
+++ b/src/libnm-platform/nm-platform.c
@@ -452,6 +452,10 @@ _nm_platform_kernel_support_init(NMPlatformKernelSupportType type, int value)
 /*****************************************************************************/
 
 const NMPGenlFamilyInfo nmp_genl_family_infos[_NMP_GENL_FAMILY_TYPE_NUM] = {
+    [NMP_GENL_FAMILY_TYPE_DEVLINK] =
+        {
+            .name = "devlink",
+        },
     [NMP_GENL_FAMILY_TYPE_ETHTOOL] =
         {
             .name = "ethtool",
@@ -2018,8 +2022,7 @@ nm_platform_link_supports_sriov(NMPlatform *self, int ifindex)
 void
 nm_platform_link_set_sriov_params_async(NMPlatform             *self,
                                         int                     ifindex,
-                                        guint                   num_vfs,
-                                        NMOptionBool            autoprobe,
+                                        NMPlatformSriovParams   sriov_params,
                                         NMPlatformAsyncCallback callback,
                                         gpointer                callback_data,
                                         GCancellable           *cancellable)
@@ -2028,11 +2031,17 @@ nm_platform_link_set_sriov_params_async(NMPlatform             *self,
 
     g_return_if_fail(ifindex > 0);
 
-    _LOG3D("link: setting %u total VFs and autoprobe %d", num_vfs, (int) autoprobe);
+    _LOG3D("link: setting SR-IOV params (numvfs=%u, autoprobe=%d, eswitch mode=%d inline-mode=%d "
+           "encap-mode=%d)",
+           sriov_params.num_vfs,
+           (int) sriov_params.autoprobe,
+           (int) sriov_params.eswitch_mode,
+           (int) sriov_params.eswitch_inline_mode,
+           (int) sriov_params.eswitch_encap_mode);
+
     klass->link_set_sriov_params_async(self,
                                        ifindex,
-                                       num_vfs,
-                                       autoprobe,
+                                       sriov_params,
                                        callback,
                                        callback_data,
                                        cancellable);
diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h
index a6e60bd4..f6a6ba08 100644
--- a/src/libnm-platform/nm-platform.h
+++ b/src/libnm-platform/nm-platform.h
@@ -993,6 +993,14 @@ typedef struct {
     guint8  public_key[NMP_WIREGUARD_PUBLIC_KEY_LEN];
 } _nm_alignas(NMPlatformObject) NMPlatformLnkWireGuard;
 
+typedef struct {
+    guint                     num_vfs;
+    NMOptionBool              autoprobe;
+    _NMSriovEswitchMode       eswitch_mode;
+    _NMSriovEswitchInlineMode eswitch_inline_mode;
+    _NMSriovEswitchEncapMode  eswitch_encap_mode;
+} NMPlatformSriovParams;
+
 typedef enum {
     NM_PLATFORM_WIREGUARD_CHANGE_FLAG_NONE            = 0,
     NM_PLATFORM_WIREGUARD_CHANGE_FLAG_REPLACE_PEERS   = (1LL << 0),
@@ -1084,6 +1092,7 @@ nm_platform_kernel_support_get(NMPlatformKernelSupportType type)
 }
 
 typedef enum {
+    NMP_GENL_FAMILY_TYPE_DEVLINK,
     NMP_GENL_FAMILY_TYPE_ETHTOOL,
     NMP_GENL_FAMILY_TYPE_MPTCP_PM,
     NMP_GENL_FAMILY_TYPE_NL80211,
@@ -1171,8 +1180,7 @@ typedef struct {
     gboolean (*link_set_name)(NMPlatform *self, int ifindex, const char *name);
     void (*link_set_sriov_params_async)(NMPlatform             *self,
                                         int                     ifindex,
-                                        guint                   num_vfs,
-                                        NMOptionBool            autoprobe,
+                                        NMPlatformSriovParams   sriov_params,
                                         NMPlatformAsyncCallback callback,
                                         gpointer                callback_data,
                                         GCancellable           *cancellable);
@@ -2034,8 +2042,7 @@ gboolean nm_platform_link_set_name(NMPlatform *self, int ifindex, const char *na
 
 void nm_platform_link_set_sriov_params_async(NMPlatform             *self,
                                              int                     ifindex,
-                                             guint                   num_vfs,
-                                             NMOptionBool            autoprobe,
+                                             NMPlatformSriovParams   sriov_params,
                                              NMPlatformAsyncCallback callback,
                                              gpointer                callback_data,
                                              GCancellable           *cancellable);