[RFC net-next v2 2/6] ethtool: Add loopback GET/SET netlink implementation
From: Björn Töpel
Date: Sun Mar 08 2026 - 08:42:47 EST
Add the kernel-side ETHTOOL_MSG_LOOPBACK_GET,
ETHTOOL_MSG_LOOPBACK_SET, and ETHTOOL_MSG_LOOPBACK_NTF handlers using
the standard ethnl_request_ops infrastructure.
GET collects loopback entries from per-component helpers via
loopback_get_entries(). SET parses the nested entry attributes,
dispatches each to loopback_set_one(), and only sends a notification
when the state is changed.
No components are wired yet.
Signed-off-by: Björn Töpel <bjorn@xxxxxxxxxx>
---
include/linux/ethtool.h | 28 +++++
net/ethtool/Makefile | 2 +-
net/ethtool/loopback.c | 246 ++++++++++++++++++++++++++++++++++++++++
net/ethtool/netlink.c | 20 ++++
net/ethtool/netlink.h | 3 +
5 files changed, 298 insertions(+), 1 deletion(-)
create mode 100644 net/ethtool/loopback.c
diff --git a/include/linux/ethtool.h b/include/linux/ethtool.h
index 83c375840835..b1ebfe22c355 100644
--- a/include/linux/ethtool.h
+++ b/include/linux/ethtool.h
@@ -846,6 +846,34 @@ void ethtool_mmsv_set_mm(struct ethtool_mmsv *mmsv, struct ethtool_mm_cfg *cfg);
void ethtool_mmsv_init(struct ethtool_mmsv *mmsv, struct net_device *dev,
const struct ethtool_mmsv_ops *ops);
+/**
+ * struct ethtool_loopback_entry - Per-component loopback configuration
+ * @id: Optional component instance identifier, 0 means not specified
+ * @supported: Bitmask of supported directions
+ * @component: Loopback component
+ * @direction: Current loopback direction, 0 means disabled
+ * @name: Subsystem-specific name for the loopback point
+ */
+struct ethtool_loopback_entry {
+ enum ethtool_loopback_component component;
+ u32 id;
+ u32 supported;
+ u32 direction;
+ char name[ETH_GSTRING_LEN];
+};
+
+#define ETHTOOL_LOOPBACK_MAX_ENTRIES 16
+
+/**
+ * struct ethtool_loopback_cfg - Loopback configuration
+ * @entries: Array of per-component loopback configurations
+ * @n_entries: Number of valid entries in the array
+ */
+struct ethtool_loopback_cfg {
+ struct ethtool_loopback_entry entries[ETHTOOL_LOOPBACK_MAX_ENTRIES];
+ u32 n_entries;
+};
+
/**
* struct ethtool_rxfh_param - RXFH (RSS) parameters
* @hfunc: Defines the current RSS hash function used by HW (or to be set to).
diff --git a/net/ethtool/Makefile b/net/ethtool/Makefile
index 629c10916670..ef534b55d724 100644
--- a/net/ethtool/Makefile
+++ b/net/ethtool/Makefile
@@ -9,4 +9,4 @@ ethtool_nl-y := netlink.o bitset.o strset.o linkinfo.o linkmodes.o rss.o \
channels.o coalesce.o pause.o eee.o tsinfo.o cabletest.o \
tunnels.o fec.o eeprom.o stats.o phc_vclocks.o mm.o \
module.o cmis_fw_update.o cmis_cdb.o pse-pd.o plca.o \
- phy.o tsconfig.o mse.o
+ phy.o tsconfig.o mse.o loopback.o
diff --git a/net/ethtool/loopback.c b/net/ethtool/loopback.c
new file mode 100644
index 000000000000..1c6d27857f8a
--- /dev/null
+++ b/net/ethtool/loopback.c
@@ -0,0 +1,246 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include "netlink.h"
+#include "common.h"
+
+struct loopback_req_info {
+ struct ethnl_req_info base;
+};
+
+struct loopback_reply_data {
+ struct ethnl_reply_data base;
+ struct ethtool_loopback_cfg cfg;
+};
+
+#define LOOPBACK_REPDATA(__reply_base) \
+ container_of(__reply_base, struct loopback_reply_data, base)
+
+/* GET */
+
+static const struct nla_policy
+ethnl_loopback_entry_policy[ETHTOOL_A_LOOPBACK_ENTRY_MAX + 1] = {
+ [ETHTOOL_A_LOOPBACK_ENTRY_COMPONENT] =
+ NLA_POLICY_MAX(NLA_U32, ETHTOOL_LOOPBACK_COMPONENT_MODULE),
+ [ETHTOOL_A_LOOPBACK_ENTRY_ID] =
+ NLA_POLICY_MIN(NLA_U32, 1),
+ [ETHTOOL_A_LOOPBACK_ENTRY_NAME] =
+ { .type = NLA_NUL_STRING, .len = ETH_GSTRING_LEN - 1 },
+ [ETHTOOL_A_LOOPBACK_ENTRY_DIRECTION] =
+ NLA_POLICY_MASK(NLA_U32, ETHTOOL_LOOPBACK_DIRECTION_NEAR_END |
+ ETHTOOL_LOOPBACK_DIRECTION_FAR_END),
+};
+
+const struct nla_policy ethnl_loopback_get_policy[] = {
+ [ETHTOOL_A_LOOPBACK_HEADER] = NLA_POLICY_NESTED(ethnl_header_policy),
+};
+
+static int loopback_get_entries(struct net_device *dev,
+ struct ethtool_loopback_cfg *cfg)
+{
+ return 0;
+}
+
+static int loopback_prepare_data(const struct ethnl_req_info *req_base,
+ struct ethnl_reply_data *reply_base,
+ const struct genl_info *info)
+{
+ struct loopback_reply_data *data = LOOPBACK_REPDATA(reply_base);
+ struct net_device *dev = reply_base->dev;
+ int ret;
+
+ ret = ethnl_ops_begin(dev);
+ if (ret < 0)
+ return ret;
+
+ ret = loopback_get_entries(dev, &data->cfg);
+
+ ethnl_ops_complete(dev);
+
+ return ret;
+}
+
+static int loopback_reply_size(const struct ethnl_req_info *req_base,
+ const struct ethnl_reply_data *reply_base)
+{
+ const struct loopback_reply_data *data = LOOPBACK_REPDATA(reply_base);
+ int entry_size;
+
+ /* Per-entry: nest + component + id + name + supported + direction */
+ entry_size = nla_total_size(0) + /* nest */
+ nla_total_size(sizeof(u32)) + /* component */
+ nla_total_size(sizeof(u32)) + /* id */
+ nla_total_size(sizeof(u32)) + /* supported */
+ nla_total_size(sizeof(u32)) + /* direction */
+ nla_total_size(ETH_GSTRING_LEN); /* name */
+
+ return data->cfg.n_entries * entry_size;
+}
+
+static int loopback_fill_entry(struct sk_buff *skb,
+ const struct ethtool_loopback_entry *entry)
+{
+ struct nlattr *nest;
+
+ nest = nla_nest_start(skb, ETHTOOL_A_LOOPBACK_ENTRY);
+ if (!nest)
+ return -EMSGSIZE;
+
+ if (nla_put_u32(skb, ETHTOOL_A_LOOPBACK_ENTRY_COMPONENT,
+ entry->component))
+ goto err_cancel;
+
+ if (entry->id &&
+ nla_put_u32(skb, ETHTOOL_A_LOOPBACK_ENTRY_ID, entry->id))
+ goto err_cancel;
+
+ if (nla_put_u32(skb, ETHTOOL_A_LOOPBACK_ENTRY_SUPPORTED,
+ entry->supported) ||
+ nla_put_u32(skb, ETHTOOL_A_LOOPBACK_ENTRY_DIRECTION,
+ entry->direction) ||
+ nla_put_string(skb, ETHTOOL_A_LOOPBACK_ENTRY_NAME,
+ entry->name))
+ goto err_cancel;
+
+ nla_nest_end(skb, nest);
+ return 0;
+
+err_cancel:
+ nla_nest_cancel(skb, nest);
+ return -EMSGSIZE;
+}
+
+static int loopback_fill_reply(struct sk_buff *skb,
+ const struct ethnl_req_info *req_base,
+ const struct ethnl_reply_data *reply_base)
+{
+ const struct loopback_reply_data *data = LOOPBACK_REPDATA(reply_base);
+ const struct ethtool_loopback_cfg *cfg = &data->cfg;
+ u32 i;
+
+ for (i = 0; i < cfg->n_entries; i++) {
+ int ret = loopback_fill_entry(skb, &cfg->entries[i]);
+
+ if (ret < 0)
+ return ret;
+ }
+
+ return 0;
+}
+
+/* SET */
+
+const struct nla_policy ethnl_loopback_set_policy[ETHTOOL_A_LOOPBACK_ENTRY + 1] = {
+ [ETHTOOL_A_LOOPBACK_HEADER] = NLA_POLICY_NESTED(ethnl_header_policy),
+ [ETHTOOL_A_LOOPBACK_ENTRY] = NLA_POLICY_NESTED(ethnl_loopback_entry_policy),
+};
+
+static int loopback_parse_entry(struct nlattr *attr,
+ struct ethtool_loopback_entry *entry,
+ struct netlink_ext_ack *extack)
+{
+ struct nlattr *tb[ETHTOOL_A_LOOPBACK_ENTRY_MAX + 1];
+ int ret;
+
+ ret = nla_parse_nested(tb, ETHTOOL_A_LOOPBACK_ENTRY_MAX, attr,
+ ethnl_loopback_entry_policy, extack);
+ if (ret < 0)
+ return ret;
+
+ if (!tb[ETHTOOL_A_LOOPBACK_ENTRY_COMPONENT]) {
+ NL_SET_ERR_MSG_ATTR(extack, attr,
+ "loopback component is required");
+ return -EINVAL;
+ }
+
+ entry->component = nla_get_u32(tb[ETHTOOL_A_LOOPBACK_ENTRY_COMPONENT]);
+
+ if (tb[ETHTOOL_A_LOOPBACK_ENTRY_ID])
+ entry->id = nla_get_u32(tb[ETHTOOL_A_LOOPBACK_ENTRY_ID]);
+
+ if (!tb[ETHTOOL_A_LOOPBACK_ENTRY_NAME]) {
+ NL_SET_ERR_MSG_ATTR(extack, attr,
+ "loopback name is required");
+ return -EINVAL;
+ }
+ nla_strscpy(entry->name, tb[ETHTOOL_A_LOOPBACK_ENTRY_NAME],
+ sizeof(entry->name));
+
+ if (!tb[ETHTOOL_A_LOOPBACK_ENTRY_DIRECTION]) {
+ NL_SET_ERR_MSG_ATTR(extack, attr,
+ "loopback direction is required");
+ return -EINVAL;
+ }
+
+ entry->direction = nla_get_u32(tb[ETHTOOL_A_LOOPBACK_ENTRY_DIRECTION]);
+
+ return 0;
+}
+
+static int loopback_set_one(struct net_device *dev,
+ const struct ethtool_loopback_entry *entry,
+ struct netlink_ext_ack *extack)
+{
+ switch (entry->component) {
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ethnl_set_loopback(struct ethnl_req_info *req_info,
+ struct genl_info *info)
+{
+ struct net_device *dev = req_info->dev;
+ struct ethtool_loopback_cfg cfg = {};
+ int rem, ret, mod = 0;
+ struct nlattr *attr;
+ u32 i;
+
+ nla_for_each_attr(attr, genlmsg_data(info->genlhdr),
+ genlmsg_len(info->genlhdr), rem) {
+ if (nla_type(attr) != ETHTOOL_A_LOOPBACK_ENTRY)
+ continue;
+
+ if (cfg.n_entries >= ETHTOOL_LOOPBACK_MAX_ENTRIES) {
+ NL_SET_ERR_MSG(info->extack,
+ "too many loopback entries");
+ return -EINVAL;
+ }
+
+ ret = loopback_parse_entry(attr, &cfg.entries[cfg.n_entries],
+ info->extack);
+ if (ret < 0)
+ return ret;
+
+ cfg.n_entries++;
+ }
+
+ if (!cfg.n_entries) {
+ NL_SET_ERR_MSG(info->extack, "no loopback entries specified");
+ return -EINVAL;
+ }
+
+ for (i = 0; i < cfg.n_entries; i++) {
+ ret = loopback_set_one(dev, &cfg.entries[i], info->extack);
+ if (ret < 0)
+ return ret;
+ if (ret > 0)
+ mod = 1;
+ }
+
+ return mod;
+}
+
+const struct ethnl_request_ops ethnl_loopback_request_ops = {
+ .request_cmd = ETHTOOL_MSG_LOOPBACK_GET,
+ .reply_cmd = ETHTOOL_MSG_LOOPBACK_GET_REPLY,
+ .hdr_attr = ETHTOOL_A_LOOPBACK_HEADER,
+ .req_info_size = sizeof(struct loopback_req_info),
+ .reply_data_size = sizeof(struct loopback_reply_data),
+
+ .prepare_data = loopback_prepare_data,
+ .reply_size = loopback_reply_size,
+ .fill_reply = loopback_fill_reply,
+
+ .set = ethnl_set_loopback,
+ .set_ntf_cmd = ETHTOOL_MSG_LOOPBACK_NTF,
+};
diff --git a/net/ethtool/netlink.c b/net/ethtool/netlink.c
index 6e5f0f4f815a..c438828ea072 100644
--- a/net/ethtool/netlink.c
+++ b/net/ethtool/netlink.c
@@ -421,6 +421,8 @@ ethnl_default_requests[__ETHTOOL_MSG_USER_CNT] = {
[ETHTOOL_MSG_TSCONFIG_SET] = ðnl_tsconfig_request_ops,
[ETHTOOL_MSG_PHY_GET] = ðnl_phy_request_ops,
[ETHTOOL_MSG_MSE_GET] = ðnl_mse_request_ops,
+ [ETHTOOL_MSG_LOOPBACK_GET] = ðnl_loopback_request_ops,
+ [ETHTOOL_MSG_LOOPBACK_SET] = ðnl_loopback_request_ops,
};
static struct ethnl_dump_ctx *ethnl_dump_context(struct netlink_callback *cb)
@@ -962,6 +964,7 @@ ethnl_default_notify_ops[ETHTOOL_MSG_KERNEL_MAX + 1] = {
[ETHTOOL_MSG_MM_NTF] = ðnl_mm_request_ops,
[ETHTOOL_MSG_RSS_NTF] = ðnl_rss_request_ops,
[ETHTOOL_MSG_RSS_CREATE_NTF] = ðnl_rss_request_ops,
+ [ETHTOOL_MSG_LOOPBACK_NTF] = ðnl_loopback_request_ops,
};
/* default notification handler */
@@ -1070,6 +1073,7 @@ static const ethnl_notify_handler_t ethnl_notify_handlers[] = {
[ETHTOOL_MSG_MM_NTF] = ethnl_default_notify,
[ETHTOOL_MSG_RSS_NTF] = ethnl_default_notify,
[ETHTOOL_MSG_RSS_CREATE_NTF] = ethnl_default_notify,
+ [ETHTOOL_MSG_LOOPBACK_NTF] = ethnl_default_notify,
};
void ethnl_notify(struct net_device *dev, unsigned int cmd,
@@ -1544,6 +1548,22 @@ static const struct genl_ops ethtool_genl_ops[] = {
.policy = ethnl_mse_get_policy,
.maxattr = ARRAY_SIZE(ethnl_mse_get_policy) - 1,
},
+ {
+ .cmd = ETHTOOL_MSG_LOOPBACK_GET,
+ .doit = ethnl_default_doit,
+ .start = ethnl_default_start,
+ .dumpit = ethnl_default_dumpit,
+ .done = ethnl_default_done,
+ .policy = ethnl_loopback_get_policy,
+ .maxattr = ARRAY_SIZE(ethnl_loopback_get_policy) - 1,
+ },
+ {
+ .cmd = ETHTOOL_MSG_LOOPBACK_SET,
+ .flags = GENL_UNS_ADMIN_PERM,
+ .doit = ethnl_default_set_doit,
+ .policy = ethnl_loopback_set_policy,
+ .maxattr = ARRAY_SIZE(ethnl_loopback_set_policy) - 1,
+ },
};
static const struct genl_multicast_group ethtool_nl_mcgrps[] = {
diff --git a/net/ethtool/netlink.h b/net/ethtool/netlink.h
index 89010eaa67df..5660ce494916 100644
--- a/net/ethtool/netlink.h
+++ b/net/ethtool/netlink.h
@@ -443,6 +443,7 @@ extern const struct ethnl_request_ops ethnl_mm_request_ops;
extern const struct ethnl_request_ops ethnl_phy_request_ops;
extern const struct ethnl_request_ops ethnl_tsconfig_request_ops;
extern const struct ethnl_request_ops ethnl_mse_request_ops;
+extern const struct ethnl_request_ops ethnl_loopback_request_ops;
extern const struct nla_policy ethnl_header_policy[ETHTOOL_A_HEADER_FLAGS + 1];
extern const struct nla_policy ethnl_header_policy_stats[ETHTOOL_A_HEADER_FLAGS + 1];
@@ -499,6 +500,8 @@ extern const struct nla_policy ethnl_phy_get_policy[ETHTOOL_A_PHY_HEADER + 1];
extern const struct nla_policy ethnl_tsconfig_get_policy[ETHTOOL_A_TSCONFIG_HEADER + 1];
extern const struct nla_policy ethnl_tsconfig_set_policy[ETHTOOL_A_TSCONFIG_MAX + 1];
extern const struct nla_policy ethnl_mse_get_policy[ETHTOOL_A_MSE_HEADER + 1];
+extern const struct nla_policy ethnl_loopback_get_policy[ETHTOOL_A_LOOPBACK_HEADER + 1];
+extern const struct nla_policy ethnl_loopback_set_policy[ETHTOOL_A_LOOPBACK_ENTRY + 1];
int ethnl_set_features(struct sk_buff *skb, struct genl_info *info);
int ethnl_act_cable_test(struct sk_buff *skb, struct genl_info *info);
--
2.53.0