diff --git a/drivers/net/phy/Makefile b/drivers/net/phy/Makefile
index c945ed9bd14b..baa95d9f24e4 100644
--- a/drivers/net/phy/Makefile
+++ b/drivers/net/phy/Makefile
@@ -2,7 +2,7 @@
 # Makefile for Linux PHY drivers
 
 libphy-y			:= phy.o phy-c45.o phy-core.o phy_device.o \
-				   linkmode.o
+				   linkmode.o phy_ns.o
 mdio-bus-y			+= mdio_bus.o mdio_device.o
 
 ifdef CONFIG_MDIO_DEVICE
diff --git a/drivers/net/phy/phy_device.c b/drivers/net/phy/phy_device.c
index 2ce74593d6e4..0c029ae5130a 100644
--- a/drivers/net/phy/phy_device.c
+++ b/drivers/net/phy/phy_device.c
@@ -29,6 +29,7 @@
 #include <linux/phy.h>
 #include <linux/phylib_stubs.h>
 #include <linux/phy_led_triggers.h>
+#include <linux/phy_ns.h>
 #include <linux/pse-pd/pse.h>
 #include <linux/property.h>
 #include <linux/rtnetlink.h>
@@ -265,6 +266,14 @@ static void phy_mdio_device_remove(struct mdio_device *mdiodev)
 
 static struct phy_driver genphy_driver;
 
+static struct phy_namespace *phy_get_ns(struct phy_device *phydev)
+{
+	if (phydev->attached_dev)
+		return &phydev->attached_dev->phy_ns;
+
+	return NULL;
+}
+
 static LIST_HEAD(phy_fixup_list);
 static DEFINE_MUTEX(phy_fixup_lock);
 
@@ -677,6 +686,7 @@ struct phy_device *phy_device_create(struct mii_bus *bus, int addr, u32 phy_id,
 
 	dev->state = PHY_DOWN;
 	INIT_LIST_HEAD(&dev->leds);
+	INIT_LIST_HEAD(&dev->node);
 
 	mutex_init(&dev->lock);
 	INIT_DELAYED_WORK(&dev->state_queue, phy_state_machine);
@@ -1489,6 +1499,8 @@ int phy_attach_direct(struct net_device *dev, struct phy_device *phydev,
 
 		if (phydev->sfp_bus_attached)
 			dev->sfp_bus = phydev->sfp_bus;
+
+		phy_ns_add_phy(&dev->phy_ns, phydev);
 	}
 
 	/* Some Ethernet drivers try to connect to a PHY device before
@@ -1814,6 +1826,7 @@ void phy_detach(struct phy_device *phydev)
 	if (dev) {
 		phydev->attached_dev->phydev = NULL;
 		phydev->attached_dev = NULL;
+		phy_ns_del_phy(&dev->phy_ns, phydev);
 	}
 	phydev->phylink = NULL;
 
diff --git a/drivers/net/phy/phy_ns.c b/drivers/net/phy/phy_ns.c
new file mode 100644
index 000000000000..d7865028ab20
--- /dev/null
+++ b/drivers/net/phy/phy_ns.c
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Infrastructure to handle all PHY devices connected to a given netdev,
+ * either directly or indirectly attached.
+ *
+ * Copyright (c) 2023 Maxime Chevallier<maxime.chevallier@bootlin.com>
+ */
+
+#include <linux/phy.h>
+#include <linux/phy_ns.h>
+
+static int phy_ns_next_phyindex(struct phy_namespace *phy_ns)
+{
+	int phyindex = phy_ns->last_attributed_index;
+
+	for (;;) {
+		if (++phyindex <= 0)
+			phyindex = 1;
+		if (!phy_ns_get_by_index(phy_ns, phyindex))
+			return phy_ns->last_attributed_index = phyindex;
+	}
+}
+
+struct phy_device *phy_ns_get_by_index(struct phy_namespace *phy_ns,
+				       int phyindex)
+{
+	struct phy_device *phy;
+
+	mutex_lock(&phy_ns->ns_lock);
+	list_for_each_entry(phy, &phy_ns->phys, node)
+		if (phy->phyindex == phyindex)
+			goto unlock;
+
+	phy = NULL;
+unlock:
+	mutex_unlock(&phy_ns->ns_lock);
+	return phy;
+}
+EXPORT_SYMBOL_GPL(phy_ns_get_by_index);
+
+void phy_ns_add_phy(struct phy_namespace *phy_ns, struct phy_device *phy)
+{
+	/* PHYs can be attached and detached, they will keep their id */
+	if (!phy->phyindex)
+		phy->phyindex = phy_ns_next_phyindex(phy_ns);
+
+	mutex_lock(&phy_ns->ns_lock);
+	list_add(&phy->node, &phy_ns->phys);
+	mutex_unlock(&phy_ns->ns_lock);
+}
+EXPORT_SYMBOL_GPL(phy_ns_add_phy);
+
+void phy_ns_del_phy(struct phy_namespace *phy_ns, struct phy_device *phy)
+{
+	mutex_lock(&phy_ns->ns_lock);
+	list_del(&phy->node);
+	mutex_unlock(&phy_ns->ns_lock);
+}
+EXPORT_SYMBOL_GPL(phy_ns_del_phy);
+
+void phy_ns_init(struct phy_namespace *phy_ns)
+{
+	INIT_LIST_HEAD(&phy_ns->phys);
+	mutex_init(&phy_ns->ns_lock);
+}
diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
index 0896aaa91dd7..ef86cb87a38a 100644
--- a/include/linux/netdevice.h
+++ b/include/linux/netdevice.h
@@ -43,6 +43,7 @@
 
 #include <linux/netdev_features.h>
 #include <linux/neighbour.h>
+#include <linux/phy_ns.h>
 #include <uapi/linux/netdevice.h>
 #include <uapi/linux/if_bonding.h>
 #include <uapi/linux/pkt_cls.h>
@@ -2380,6 +2381,7 @@ struct net_device {
 	struct netprio_map __rcu *priomap;
 #endif
 	struct phy_device	*phydev;
+	struct phy_namespace	phy_ns;
 	struct sfp_bus		*sfp_bus;
 	struct lock_class_key	*qdisc_tx_busylock;
 	bool			proto_down;
diff --git a/include/linux/phy.h b/include/linux/phy.h
index 1351b802ffcf..b12fd33aa84a 100644
--- a/include/linux/phy.h
+++ b/include/linux/phy.h
@@ -543,6 +543,8 @@ struct macsec_ops;
  * @drv: Pointer to the driver for this PHY instance
  * @devlink: Create a link between phy dev and mac dev, if the external phy
  *           used by current mac interface is managed by another mac interface.
+ * @phyindex: Unique id across the phy's parent tree of phys to address the PHY
+ *	      from userspace, similar to ifindex. It's never recycled.
  * @phy_id: UID for this device found during discovery
  * @c45_ids: 802.3-c45 Device Identifiers if is_c45.
  * @is_c45:  Set to true if this PHY uses clause 45 addressing.
@@ -640,6 +642,7 @@ struct phy_device {
 
 	struct device_link *devlink;
 
+	int phyindex;
 	u32 phy_id;
 
 	struct phy_c45_device_ids c45_ids;
@@ -761,6 +764,7 @@ struct phy_device {
 	/* MACsec management functions */
 	const struct macsec_ops *macsec_ops;
 #endif
+	struct list_head node;
 };
 
 /* Generic phy_device::dev_flags */
diff --git a/include/linux/phy_ns.h b/include/linux/phy_ns.h
new file mode 100644
index 000000000000..ae173e637c62
--- /dev/null
+++ b/include/linux/phy_ns.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * PHY device namespaces allow maintaining a list of PHY devices that are
+ * part of a netdevice's link topology. PHYs can for example be chained,
+ * as is the case when using a PHY that exposes an SFP module, on which an
+ * SFP transceiver that embeds a PHY is connected.
+ *
+ * This list can then be used by userspace to leverage individual PHY
+ * capabilities.
+ */
+#ifndef __PHY_NS_H
+#define __PHY_NS_H
+
+struct mutex;
+
+struct phy_namespace {
+	struct list_head phys;
+	int last_attributed_index;
+
+	/* Protects the .phys list */
+	struct mutex ns_lock;
+};
+
+struct phy_device *phy_ns_get_by_index(struct phy_namespace *phy_ns,
+				       int phyindex);
+void phy_ns_add_phy(struct phy_namespace *phy_ns, struct phy_device *phy);
+void phy_ns_del_phy(struct phy_namespace *phy_ns, struct phy_device *phy);
+void phy_ns_init(struct phy_namespace *phy_ns);
+
+#endif /* __PHY_NS_H */
diff --git a/net/core/dev.c b/net/core/dev.c
index ccff2b6ef958..aa8b924269d7 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -10729,6 +10729,9 @@ struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
 	INIT_LIST_HEAD(&dev->net_notifier_list);
 #ifdef CONFIG_NET_SCHED
 	hash_init(dev->qdisc_hash);
+#endif
+#ifdef CONFIG_PHYLIB
+	phy_ns_init(&dev->phy_ns);
 #endif
 	dev->priv_flags = IFF_XMIT_DST_RELEASE | IFF_XMIT_DST_RELEASE_PERM;
 	setup(dev);
