From nobody Mon Feb 9 00:31:15 2026 Received: from smtpout-04.galae.net (smtpout-04.galae.net [185.171.202.116]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id D759435F8A8 for ; Tue, 27 Jan 2026 13:42:52 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.171.202.116 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1769521374; cv=none; b=BIFrQ3NLq9EbGW+6a1FIxno40TsUIX/lKP4a+yOzjEdT/6njHFt+YcpXIFvpV61e/OPc48VoZVBYEeXqQKw8MAWaPOE4RmErohfEJKSKCsbyGDv0/9tugXpzYJlkzwUhO5gmg6u8wrO/COEY5uckwTpnEXB4OlnrWakxkyd1lwU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1769521374; c=relaxed/simple; bh=pGmGlCKaaXi6n3kzu6pebk8nKqAgx61Vp2G+3MzMqzk=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=KtdY/BDFvZHAHn4gHLbGIIyiUTfl+OsL0U5UJX/fm0Rku0q9qsbmvuVtcaL7OeE6CVEKrDN9r0DDttOvW/IDUAhF23KRrA0rWoLoCpXSi3pL2iQltMsQzq9t6H90G/twoo/Z161P3aULt42VLKhza85RgpVSkWjfLVlZfTzuyd4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=bootlin.com; spf=pass smtp.mailfrom=bootlin.com; dkim=pass (2048-bit key) header.d=bootlin.com header.i=@bootlin.com header.b=lGbouBSY; arc=none smtp.client-ip=185.171.202.116 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=bootlin.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=bootlin.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=bootlin.com header.i=@bootlin.com header.b="lGbouBSY" Received: from smtpout-01.galae.net (smtpout-01.galae.net [212.83.139.233]) by smtpout-04.galae.net (Postfix) with ESMTPS id CB5CBC211C0; Tue, 27 Jan 2026 13:42:53 +0000 (UTC) Received: from mail.galae.net (mail.galae.net [212.83.136.155]) by smtpout-01.galae.net (Postfix) with ESMTPS id B8CF9606F5; Tue, 27 Jan 2026 13:42:51 +0000 (UTC) Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with ESMTPSA id 2931A119A864D; Tue, 27 Jan 2026 14:42:48 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bootlin.com; s=dkim; t=1769521370; h=from:subject:date:message-id:to:cc:mime-version: content-transfer-encoding:in-reply-to:references; bh=jZWnm/yA0XYyoiDagHR7AQL8Azf/LmljhrPAtB/4/yU=; b=lGbouBSY/2O+Q6kwm0zJvG+0f3lRnQVUF3oz2jxxNluHtlkQwU5p5gkjsIddEa5YSMct+P A65lCu9cRCSFaXmhokUjZDGoosBcdb2O5quYoKpFLyfTb6hZBhxolWa34qjAZRvUAl5T6e bu/lakRw6r8VV/oU3k6+KPMG4AjTMj2SwQAzMKcvw81I+ZOgQ4lql910x5AdYmlEEllo76 fVZbQr4DHDFWRlfAzCQ9KSMtBlrKlLqhtIanoWuJ/kBfsxiiuaHx1ALyBIWJyU7VST/pki ljlVrSWzZK/oMHvW3V3Prs4NF934G3cURHy53L4SgOpBcllja28pCUWyHQmVyg== From: Maxime Chevallier To: davem@davemloft.net, Andrew Lunn , Jakub Kicinski , Eric Dumazet , Paolo Abeni , Russell King , Heiner Kallweit Cc: Maxime Chevallier , netdev@vger.kernel.org, linux-kernel@vger.kernel.org, thomas.petazzoni@bootlin.com, Christophe Leroy , Herve Codina , Florian Fainelli , Vladimir Oltean , =?UTF-8?q?K=C3=B6ry=20Maincent?= , =?UTF-8?q?Marek=20Beh=C3=BAn?= , Oleksij Rempel , =?UTF-8?q?Nicol=C3=B2=20Veronese?= , Simon Horman , mwojtas@chromium.org, Romain Gantois , Daniel Golle , Dimitri Fedrau Subject: [PATCH net-next 13/13] net: ethtool: Introduce ethtool command to list ports Date: Tue, 27 Jan 2026 14:42:01 +0100 Message-ID: <20260127134202.8208-14-maxime.chevallier@bootlin.com> X-Mailer: git-send-email 2.49.0 In-Reply-To: <20260127134202.8208-1-maxime.chevallier@bootlin.com> References: <20260127134202.8208-1-maxime.chevallier@bootlin.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Last-TLS-Session-Version: TLSv1.3 Content-Type: text/plain; charset="utf-8" Expose the phy_port information to userspace, so that we can know how many ports are available on a given interface, as well as their capabilities. For MDI ports, we report the list of supported linkmodes based on what the PHY that drives this port says. For MII ports, i.e. empty SFP cages, we report the MII linkmodes that we can output on this port. Signed-off-by: Maxime Chevallier --- MAINTAINERS | 1 + net/ethtool/Makefile | 2 +- net/ethtool/netlink.c | 11 ++ net/ethtool/netlink.h | 5 + net/ethtool/port.c | 373 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 net/ethtool/port.c diff --git a/MAINTAINERS b/MAINTAINERS index c3df85fd5acd..a8272e169888 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -18226,6 +18226,7 @@ F: Documentation/devicetree/bindings/net/ethernet-c= onnector.yaml F: Documentation/networking/phy-port.rst F: drivers/net/phy/phy_port.c F: include/linux/phy_port.h +F: net/ethtool/port.c K: struct\s+phy_port|phy_port_ =20 NETWORKING [GENERAL] diff --git a/net/ethtool/Makefile b/net/ethtool/Makefile index 629c10916670..9b5b09670008 100644 --- a/net/ethtool/Makefile +++ b/net/ethtool/Makefile @@ -9,4 +9,4 @@ ethtool_nl-y :=3D netlink.o bitset.o strset.o linkinfo.o li= nkmodes.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 port.o diff --git a/net/ethtool/netlink.c b/net/ethtool/netlink.c index 6e5f0f4f815a..82d94a2da55c 100644 --- a/net/ethtool/netlink.c +++ b/net/ethtool/netlink.c @@ -421,6 +421,7 @@ ethnl_default_requests[__ETHTOOL_MSG_USER_CNT] =3D { [ETHTOOL_MSG_TSCONFIG_SET] =3D ðnl_tsconfig_request_ops, [ETHTOOL_MSG_PHY_GET] =3D ðnl_phy_request_ops, [ETHTOOL_MSG_MSE_GET] =3D ðnl_mse_request_ops, + [ETHTOOL_MSG_PORT_GET] =3D ðnl_port_request_ops, }; =20 static struct ethnl_dump_ctx *ethnl_dump_context(struct netlink_callback *= cb) @@ -1544,6 +1545,16 @@ static const struct genl_ops ethtool_genl_ops[] =3D { .policy =3D ethnl_mse_get_policy, .maxattr =3D ARRAY_SIZE(ethnl_mse_get_policy) - 1, }, + { + .cmd =3D ETHTOOL_MSG_PORT_GET, + .doit =3D ethnl_default_doit, + .start =3D ethnl_port_dump_start, + .dumpit =3D ethnl_port_dumpit, + .done =3D ethnl_port_dump_done, + .policy =3D ethnl_port_get_policy, + .maxattr =3D ARRAY_SIZE(ethnl_port_get_policy) - 1, + }, + }; =20 static const struct genl_multicast_group ethtool_nl_mcgrps[] =3D { diff --git a/net/ethtool/netlink.h b/net/ethtool/netlink.h index 89010eaa67df..ea033992ba56 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_port_request_ops; =20 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,7 @@ extern const struct nla_policy ethnl_phy_get_policy[ETH= TOOL_A_PHY_HEADER + 1]; extern const struct nla_policy ethnl_tsconfig_get_policy[ETHTOOL_A_TSCONFI= G_HEADER + 1]; extern const struct nla_policy ethnl_tsconfig_set_policy[ETHTOOL_A_TSCONFI= G_MAX + 1]; extern const struct nla_policy ethnl_mse_get_policy[ETHTOOL_A_MSE_HEADER += 1]; +extern const struct nla_policy ethnl_port_get_policy[ETHTOOL_A_PORT_ID + 1= ]; =20 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); @@ -514,6 +516,9 @@ int ethnl_tsinfo_dumpit(struct sk_buff *skb, struct net= link_callback *cb); int ethnl_tsinfo_done(struct netlink_callback *cb); int ethnl_rss_create_doit(struct sk_buff *skb, struct genl_info *info); int ethnl_rss_delete_doit(struct sk_buff *skb, struct genl_info *info); +int ethnl_port_dump_start(struct netlink_callback *cb); +int ethnl_port_dumpit(struct sk_buff *skb, struct netlink_callback *cb); +int ethnl_port_dump_done(struct netlink_callback *cb); =20 extern const char stats_std_names[__ETHTOOL_STATS_CNT][ETH_GSTRING_LEN]; extern const char stats_eth_phy_names[__ETHTOOL_A_STATS_ETH_PHY_CNT][ETH_G= STRING_LEN]; diff --git a/net/ethtool/port.c b/net/ethtool/port.c new file mode 100644 index 000000000000..efad1b88d909 --- /dev/null +++ b/net/ethtool/port.c @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2026 Bootlin + * + */ +#include "common.h" +#include "bitset.h" +#include "netlink.h" + +#include +#include +#include +#include + +struct port_req_info { + struct ethnl_req_info base; + u32 port_id; +}; + +struct port_reply_data { + struct ethnl_reply_data base; + __ETHTOOL_DECLARE_LINK_MODE_MASK(supported); + DECLARE_PHY_INTERFACE_MASK(interfaces); + u32 port_id; + bool mii; + bool sfp; + bool occupied; +}; + +#define PORT_REQINFO(__req_base) \ + container_of(__req_base, struct port_req_info, base) + +#define PORT_REPDATA(__reply_base) \ + container_of(__reply_base, struct port_reply_data, base) + +const struct nla_policy ethnl_port_get_policy[ETHTOOL_A_PORT_ID + 1] =3D { + [ETHTOOL_A_PORT_HEADER] =3D NLA_POLICY_NESTED(ethnl_header_policy), + [ETHTOOL_A_PORT_ID] =3D { .type =3D NLA_U32}, +}; + +static int port_parse_request(struct ethnl_req_info *req_info, + struct nlattr **tb, + struct netlink_ext_ack *extack) +{ + struct port_req_info *request =3D PORT_REQINFO(req_info); + + /* PORT id is required for GET requests */ + if (tb[ETHTOOL_A_PORT_ID]) + request->port_id =3D nla_get_u32(tb[ETHTOOL_A_PORT_ID]); + + if (!request->port_id) { + NL_SET_ERR_MSG(extack, "port id missing"); + return -ENODEV; + } + + return 0; +} + +static int port_prepare_data(const struct ethnl_req_info *req_info, + struct ethnl_reply_data *reply_data, + const struct genl_info *info) +{ + struct port_reply_data *reply =3D PORT_REPDATA(reply_data); + struct port_req_info *request =3D PORT_REQINFO(req_info); + struct phy_port *port; + + /* RTNL must be held while holding a ref to the phy_port. Here, caller + * holds RTNL. + */ + port =3D phy_link_topo_get_port(req_info->dev, request->port_id); + if (!port) + return -ENODEV; + + linkmode_copy(reply->supported, port->supported); + phy_interface_copy(reply->interfaces, port->interfaces); + reply->port_id =3D port->id; + reply->mii =3D port->is_mii; + reply->sfp =3D port->is_sfp; + reply->occupied =3D port->occupied; + + return 0; +} + +static int port_reply_size(const struct ethnl_req_info *req_info, + const struct ethnl_reply_data *reply_data) +{ + bool compact =3D req_info->flags & ETHTOOL_FLAG_COMPACT_BITSETS; + struct port_reply_data *reply =3D PORT_REPDATA(reply_data); + size_t size =3D 0; + int ret; + + /* ETHTOOL_A_PORT_ID */ + size +=3D nla_total_size(sizeof(u32)); + + if (!reply->mii) { + /* ETHTOOL_A_PORT_SUPPORTED_MODES */ + ret =3D ethnl_bitset_size(reply->supported, NULL, + __ETHTOOL_LINK_MODE_MASK_NBITS, + link_mode_names, compact); + if (ret < 0) + return ret; + + size +=3D ret; + } else { + /* ETHTOOL_A_PORT_SUPPORTED_INTERFACES */ + ret =3D ethnl_bitset_size(reply->interfaces, NULL, + PHY_INTERFACE_MODE_MAX, + phy_interface_names, compact); + if (ret < 0) + return ret; + } + + /* ETHTOOL_A_PORT_TYPE */ + size +=3D nla_total_size(sizeof(u8)); + + /* ETHTOOL_A_PORT_OCCUPIED */ + size +=3D nla_total_size(sizeof(u8)); + + return size; +} + +static int port_fill_reply(struct sk_buff *skb, + const struct ethnl_req_info *req_info, + const struct ethnl_reply_data *reply_data) +{ + bool compact =3D req_info->flags & ETHTOOL_FLAG_COMPACT_BITSETS; + struct port_reply_data *reply =3D PORT_REPDATA(reply_data); + int ret, port_type =3D ETHTOOL_PORT_TYPE_MDI; + + if (nla_put_u32(skb, ETHTOOL_A_PORT_ID, reply->port_id)) + return -EMSGSIZE; + + if (!reply->mii) { + ret =3D ethnl_put_bitset(skb, ETHTOOL_A_PORT_SUPPORTED_MODES, + reply->supported, NULL, + __ETHTOOL_LINK_MODE_MASK_NBITS, + link_mode_names, compact); + if (ret < 0) + return -EMSGSIZE; + } else { + ret =3D ethnl_put_bitset(skb, ETHTOOL_A_PORT_SUPPORTED_INTERFACES, + reply->interfaces, NULL, + PHY_INTERFACE_MODE_MAX, + phy_interface_names, compact); + if (ret < 0) + return -EMSGSIZE; + } + + if (reply->mii || reply->sfp) + port_type =3D ETHTOOL_PORT_TYPE_SFP; + + if (nla_put_u8(skb, ETHTOOL_A_PORT_TYPE, port_type) || + nla_put_u8(skb, ETHTOOL_A_PORT_OCCUPIED, reply->occupied)) + return -EMSGSIZE; + + return 0; +} + +struct port_dump_ctx { + struct port_req_info *req_info; + struct port_reply_data *reply_data; + unsigned long ifindex; + unsigned long pos_portid; +}; + +static struct port_dump_ctx * +port_dump_ctx_get(struct netlink_callback *cb) +{ + return (struct port_dump_ctx *)cb->ctx; +} + +int ethnl_port_dump_start(struct netlink_callback *cb) +{ + const struct genl_dumpit_info *info =3D genl_dumpit_info(cb); + struct port_dump_ctx *ctx =3D port_dump_ctx_get(cb); + struct nlattr **tb =3D info->info.attrs; + struct port_reply_data *reply_data; + struct port_req_info *req_info; + int ret; + + BUILD_BUG_ON(sizeof(*ctx) > sizeof(cb->ctx)); + + req_info =3D kzalloc(sizeof(*req_info), GFP_KERNEL); + if (!req_info) + return -ENOMEM; + + reply_data =3D kmalloc(sizeof(*reply_data), GFP_KERNEL); + if (!reply_data) { + ret =3D -ENOMEM; + goto free_req_info; + } + + ret =3D ethnl_parse_header_dev_get(&req_info->base, tb[ETHTOOL_A_PORT_HEA= DER], + genl_info_net(&info->info), + info->info.extack, false); + if (ret < 0) + return ret; + + ctx->ifindex =3D 0; + + /* For filtered DUMP requests, let's just store the ifindex. We'll check + * again if the netdev is still there when looping over the netdev list + * in the DUMP loop. + */ + if (req_info->base.dev) { + ctx->ifindex =3D req_info->base.dev->ifindex; + netdev_put(req_info->base.dev, &req_info->base.dev_tracker); + req_info->base.dev =3D NULL; + } + + ctx->req_info =3D req_info; + ctx->reply_data =3D reply_data; + + return 0; + + kfree(reply_data); +free_req_info: + kfree(req_info); + + return ret; +} + +static int port_dump_one(struct sk_buff *skb, struct net_device *dev, + struct netlink_callback *cb) +{ + struct port_dump_ctx *ctx =3D port_dump_ctx_get(cb); + void *ehdr; + int ret; + + ehdr =3D ethnl_dump_put(skb, cb, ETHTOOL_A_PORT_HEADER); + if (!ehdr) + return -EMSGSIZE; + + memset(ctx->reply_data, 0, sizeof(struct port_reply_data)); + ctx->reply_data->base.dev =3D dev; + + rtnl_lock(); + netdev_lock_ops(dev); + + ret =3D port_prepare_data(&ctx->req_info->base, &ctx->reply_data->base, + genl_info_dump(cb)); + + netdev_unlock_ops(dev); + rtnl_unlock(); + + if (ret < 0) + goto out; + + ret =3D ethnl_fill_reply_header(skb, dev, ETHTOOL_A_PORT_HEADER); + if (ret < 0) + goto out; + + ret =3D port_fill_reply(skb, &ctx->req_info->base, &ctx->reply_data->base= ); + +out: + ctx->reply_data->base.dev =3D NULL; + if (ret < 0) + genlmsg_cancel(skb, ehdr); + else + genlmsg_end(skb, ehdr); + + return ret; +} + +static int port_dump_one_dev(struct sk_buff *skb, struct netlink_callback = *cb) +{ + struct port_dump_ctx *ctx =3D port_dump_ctx_get(cb); + struct net_device *dev; + struct phy_port *port; + int ret; + + dev =3D ctx->req_info->base.dev; + + if (!dev->link_topo) + return 0; + + xa_for_each_start(&dev->link_topo->ports, ctx->pos_portid, port, + ctx->pos_portid) { + ctx->req_info->port_id =3D ctx->pos_portid; + + ret =3D port_dump_one(skb, dev, cb); + if (ret) + return ret; + } + + ctx->pos_portid =3D 0; + + return 0; +} + +static int port_dump_all_dev(struct sk_buff *skb, struct netlink_callback = *cb) +{ + struct port_dump_ctx *ctx =3D port_dump_ctx_get(cb); + struct net *net =3D sock_net(skb->sk); + netdevice_tracker dev_tracker; + struct net_device *dev; + int ret =3D 0; + + rcu_read_lock(); + for_each_netdev_dump(net, dev, ctx->ifindex) { + netdev_hold(dev, &dev_tracker, GFP_ATOMIC); + rcu_read_unlock(); + + ctx->req_info->base.dev =3D dev; + ret =3D port_dump_one_dev(skb, cb); + + rcu_read_lock(); + netdev_put(dev, &dev_tracker); + ctx->req_info->base.dev =3D NULL; + + if (ret < 0 && ret !=3D -EOPNOTSUPP) { + if (likely(skb->len)) + ret =3D skb->len; + break; + } + ret =3D 0; + } + rcu_read_unlock(); + + return ret; +} + +int ethnl_port_dumpit(struct sk_buff *skb, struct netlink_callback *cb) +{ + const struct genl_dumpit_info *info =3D genl_dumpit_info(cb); + struct port_dump_ctx *ctx =3D port_dump_ctx_get(cb); + int ret =3D 0; + + if (ctx->ifindex) { + netdevice_tracker dev_tracker; + struct net_device *dev; + + dev =3D netdev_get_by_index(genl_info_net(&info->info), + ctx->ifindex, &dev_tracker, + GFP_KERNEL); + if (!dev) + return -ENODEV; + + ctx->req_info->base.dev =3D dev; + ret =3D port_dump_one_dev(skb, cb); + if (ret < 0 && ret !=3D -EOPNOTSUPP && likely(skb->len)) + ret =3D skb->len; + + netdev_put(dev, &dev_tracker); + } else { + ret =3D port_dump_all_dev(skb, cb); + } + + return ret; +} + +int ethnl_port_dump_done(struct netlink_callback *cb) +{ + struct port_dump_ctx *ctx =3D port_dump_ctx_get(cb); + + kfree(ctx->req_info); + kfree(ctx->reply_data); + + return 0; +} + +const struct ethnl_request_ops ethnl_port_request_ops =3D { + .request_cmd =3D ETHTOOL_MSG_PORT_GET, + .reply_cmd =3D ETHTOOL_MSG_PORT_GET_REPLY, + .hdr_attr =3D ETHTOOL_A_PORT_HEADER, + .req_info_size =3D sizeof(struct port_req_info), + .reply_data_size =3D sizeof(struct port_reply_data), + + .parse_request =3D port_parse_request, + .prepare_data =3D port_prepare_data, + .reply_size =3D port_reply_size, + .fill_reply =3D port_fill_reply, +}; --=20 2.49.0