platform: add ethtool netlink implementation

Introduce some basic infrastructure to perform ethtool operations via
netlink. As a proof of concept, implement the pause settings.

Netlink has some advantages over ioctl():

 - it can be easily extended with new attributes;

 - it can return descriptive error messages via the extended ack
   mechanism. For example, when setting the ring parameters to a value
   outside the allowed range, userspace receives error code -EINVAL
   and message "requested ring size exceeds maximum". ioctl() gets
   only -EINVAL, which is shared among many error reasons;

 - since it's possible to specify an ifindex in the request, there are
   no race conditions when the interface name changes;

New ethtool API is available only via netlink; however it makes sense
to start using netlink also for the old API that NM is already using
(pause, eee, rings, etc.) over ioctl() because of the advantages
described above.
This commit is contained in:
Beniamino Galvani
2025-03-19 13:47:53 +01:00
parent e8a3cd611e
commit 79ba228c59
3 changed files with 337 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ 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',

View File

@@ -0,0 +1,319 @@
/* 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);
}

View File

@@ -0,0 +1,17 @@
/* 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);
#endif /* __NMP_ETHTOOL_H__ */