diff --git a/Documentation/netlink/specs/ethtool.yaml b/Documentation/netlink/specs/ethtool.yaml
index 4e0790648913..280b090b5f7c 100644
--- a/Documentation/netlink/specs/ethtool.yaml
+++ b/Documentation/netlink/specs/ethtool.yaml
@@ -16,6 +16,11 @@ definitions:
     name: stringset
     type: enum
     entries: []
+  -
+    name: phy-upstream-type
+    enum-name:
+    type: enum
+    entries: [ mac, phy ]
 
 attribute-sets:
   -
@@ -942,6 +947,45 @@ attribute-sets:
       -
         name: burst-tmr
         type: u32
+  -
+    name: phy-upstream
+    attributes:
+      -
+        name: index
+        type: u32
+      -
+        name: sfp-name
+        type: string
+  -
+    name: phy
+    attributes:
+      -
+        name: header
+        type: nest
+        nested-attributes: header
+      -
+        name: index
+        type: u32
+      -
+        name: drvname
+        type: string
+      -
+        name: name
+        type: string
+      -
+        name: upstream-type
+        type: u8
+        enum: phy-upstream-type
+      -
+        name: upstream
+        type: nest
+        nested-attributes: phy-upstream
+      -
+        name: downstream-sfp-name
+        type: string
+      -
+        name: id
+        type: u32
 
 operations:
   enum-model: directional
@@ -1692,3 +1736,24 @@ operations:
       name: mm-ntf
       doc: Notification for change in MAC Merge configuration.
       notify: mm-get
+    -
+      name: phy-get
+      doc: Get PHY devices attached to an interface
+
+      attribute-set: phy
+
+      do: &phy-get-op
+        request:
+          attributes:
+            - header
+        reply:
+          attributes:
+            - header
+            - index
+            - drvname
+            - name
+            - upstream-type
+            - upstream
+            - downstream-sfp-name
+            - id
+      dump: *phy-get-op
diff --git a/tools/net/ynl/generated/ethtool-user.c b/tools/net/ynl/generated/ethtool-user.c
index 295661eb3a3e..b63ba2d2e25e 100644
--- a/tools/net/ynl/generated/ethtool-user.c
+++ b/tools/net/ynl/generated/ethtool-user.c
@@ -59,6 +59,7 @@ static const char * const ethtool_op_strmap[] = {
 	[41] = "plca-ntf",
 	[ETHTOOL_MSG_MM_GET] = "mm-get",
 	[43] = "mm-ntf",
+	[ETHTOOL_MSG_PHY_GET] = "phy-get",
 };
 
 const char *ethtool_op_str(int op)
@@ -91,6 +92,18 @@ const char *ethtool_stringset_str(enum ethtool_stringset value)
 	return ethtool_stringset_strmap[value];
 }
 
+static const char * const ethtool_phy_upstream_type_strmap[] = {
+	[0] = "mac",
+	[1] = "phy",
+};
+
+const char *ethtool_phy_upstream_type_str(int value)
+{
+	if (value < 0 || value >= (int)MNL_ARRAY_SIZE(ethtool_phy_upstream_type_strmap))
+		return NULL;
+	return ethtool_phy_upstream_type_strmap[value];
+}
+
 /* Policies */
 struct ynl_policy_attr ethtool_header_policy[ETHTOOL_A_HEADER_MAX + 1] = {
 	[ETHTOOL_A_HEADER_DEV_INDEX] = { .name = "dev-index", .type = YNL_PT_U32, },
@@ -154,6 +167,16 @@ struct ynl_policy_nest ethtool_mm_stat_nest = {
 	.table = ethtool_mm_stat_policy,
 };
 
+struct ynl_policy_attr ethtool_phy_upstream_policy[ETHTOOL_A_PHY_UPSTREAM_MAX + 1] = {
+	[ETHTOOL_A_PHY_UPSTREAM_INDEX] = { .name = "index", .type = YNL_PT_U32, },
+	[ETHTOOL_A_PHY_UPSTREAM_SFP_NAME] = { .name = "sfp-name", .type = YNL_PT_NUL_STR, },
+};
+
+struct ynl_policy_nest ethtool_phy_upstream_nest = {
+	.max_attr = ETHTOOL_A_PHY_UPSTREAM_MAX,
+	.table = ethtool_phy_upstream_policy,
+};
+
 struct ynl_policy_attr ethtool_cable_result_policy[ETHTOOL_A_CABLE_RESULT_MAX + 1] = {
 	[ETHTOOL_A_CABLE_RESULT_PAIR] = { .name = "pair", .type = YNL_PT_U8, },
 	[ETHTOOL_A_CABLE_RESULT_CODE] = { .name = "code", .type = YNL_PT_U8, },
@@ -667,6 +690,22 @@ struct ynl_policy_nest ethtool_mm_nest = {
 	.table = ethtool_mm_policy,
 };
 
+struct ynl_policy_attr ethtool_phy_policy[ETHTOOL_A_PHY_MAX + 1] = {
+	[ETHTOOL_A_PHY_HEADER] = { .name = "header", .type = YNL_PT_NEST, .nest = &ethtool_header_nest, },
+	[ETHTOOL_A_PHY_INDEX] = { .name = "index", .type = YNL_PT_U32, },
+	[ETHTOOL_A_PHY_DRVNAME] = { .name = "drvname", .type = YNL_PT_NUL_STR, },
+	[ETHTOOL_A_PHY_NAME] = { .name = "name", .type = YNL_PT_NUL_STR, },
+	[ETHTOOL_A_PHY_UPSTREAM_TYPE] = { .name = "upstream-type", .type = YNL_PT_U8, },
+	[ETHTOOL_A_PHY_UPSTREAM] = { .name = "upstream", .type = YNL_PT_NEST, .nest = &ethtool_phy_upstream_nest, },
+	[ETHTOOL_A_PHY_DOWNSTREAM_SFP_NAME] = { .name = "downstream-sfp-name", .type = YNL_PT_NUL_STR, },
+	[ETHTOOL_A_PHY_ID] = { .name = "id", .type = YNL_PT_U32, },
+};
+
+struct ynl_policy_nest ethtool_phy_nest = {
+	.max_attr = ETHTOOL_A_PHY_MAX,
+	.table = ethtool_phy_policy,
+};
+
 /* Common nested types */
 void ethtool_header_free(struct ethtool_header *obj)
 {
@@ -899,6 +938,42 @@ int ethtool_mm_stat_parse(struct ynl_parse_arg *yarg,
 	return 0;
 }
 
+void ethtool_phy_upstream_free(struct ethtool_phy_upstream *obj)
+{
+	free(obj->sfp_name);
+}
+
+int ethtool_phy_upstream_parse(struct ynl_parse_arg *yarg,
+			       const struct nlattr *nested)
+{
+	struct ethtool_phy_upstream *dst = yarg->data;
+	const struct nlattr *attr;
+
+	mnl_attr_for_each_nested(attr, nested) {
+		unsigned int type = mnl_attr_get_type(attr);
+
+		if (type == ETHTOOL_A_PHY_UPSTREAM_INDEX) {
+			if (ynl_attr_validate(yarg, attr))
+				return MNL_CB_ERROR;
+			dst->_present.index = 1;
+			dst->index = mnl_attr_get_u32(attr);
+		} else if (type == ETHTOOL_A_PHY_UPSTREAM_SFP_NAME) {
+			unsigned int len;
+
+			if (ynl_attr_validate(yarg, attr))
+				return MNL_CB_ERROR;
+
+			len = strnlen(mnl_attr_get_str(attr), mnl_attr_get_payload_len(attr));
+			dst->_present.sfp_name_len = len;
+			dst->sfp_name = malloc(len + 1);
+			memcpy(dst->sfp_name, mnl_attr_get_str(attr), len);
+			dst->sfp_name[len] = 0;
+		}
+	}
+
+	return 0;
+}
+
 void ethtool_cable_result_free(struct ethtool_cable_result *obj)
 {
 }
@@ -6158,6 +6233,188 @@ int ethtool_mm_set(struct ynl_sock *ys, struct ethtool_mm_set_req *req)
 	return 0;
 }
 
+/* ============== ETHTOOL_MSG_PHY_GET ============== */
+/* ETHTOOL_MSG_PHY_GET - do */
+void ethtool_phy_get_req_free(struct ethtool_phy_get_req *req)
+{
+	ethtool_header_free(&req->header);
+	free(req);
+}
+
+void ethtool_phy_get_rsp_free(struct ethtool_phy_get_rsp *rsp)
+{
+	ethtool_header_free(&rsp->header);
+	free(rsp->drvname);
+	free(rsp->name);
+	ethtool_phy_upstream_free(&rsp->upstream);
+	free(rsp->downstream_sfp_name);
+	free(rsp);
+}
+
+int ethtool_phy_get_rsp_parse(const struct nlmsghdr *nlh, void *data)
+{
+	struct ynl_parse_arg *yarg = data;
+	struct ethtool_phy_get_rsp *dst;
+	const struct nlattr *attr;
+	struct ynl_parse_arg parg;
+
+	dst = yarg->data;
+	parg.ys = yarg->ys;
+
+	mnl_attr_for_each(attr, nlh, sizeof(struct genlmsghdr)) {
+		unsigned int type = mnl_attr_get_type(attr);
+
+		if (type == ETHTOOL_A_PHY_HEADER) {
+			if (ynl_attr_validate(yarg, attr))
+				return MNL_CB_ERROR;
+			dst->_present.header = 1;
+
+			parg.rsp_policy = &ethtool_header_nest;
+			parg.data = &dst->header;
+			if (ethtool_header_parse(&parg, attr))
+				return MNL_CB_ERROR;
+		} else if (type == ETHTOOL_A_PHY_INDEX) {
+			if (ynl_attr_validate(yarg, attr))
+				return MNL_CB_ERROR;
+			dst->_present.index = 1;
+			dst->index = mnl_attr_get_u32(attr);
+		} else if (type == ETHTOOL_A_PHY_DRVNAME) {
+			unsigned int len;
+
+			if (ynl_attr_validate(yarg, attr))
+				return MNL_CB_ERROR;
+
+			len = strnlen(mnl_attr_get_str(attr), mnl_attr_get_payload_len(attr));
+			dst->_present.drvname_len = len;
+			dst->drvname = malloc(len + 1);
+			memcpy(dst->drvname, mnl_attr_get_str(attr), len);
+			dst->drvname[len] = 0;
+		} else if (type == ETHTOOL_A_PHY_NAME) {
+			unsigned int len;
+
+			if (ynl_attr_validate(yarg, attr))
+				return MNL_CB_ERROR;
+
+			len = strnlen(mnl_attr_get_str(attr), mnl_attr_get_payload_len(attr));
+			dst->_present.name_len = len;
+			dst->name = malloc(len + 1);
+			memcpy(dst->name, mnl_attr_get_str(attr), len);
+			dst->name[len] = 0;
+		} else if (type == ETHTOOL_A_PHY_UPSTREAM_TYPE) {
+			if (ynl_attr_validate(yarg, attr))
+				return MNL_CB_ERROR;
+			dst->_present.upstream_type = 1;
+			dst->upstream_type = mnl_attr_get_u8(attr);
+		} else if (type == ETHTOOL_A_PHY_UPSTREAM) {
+			if (ynl_attr_validate(yarg, attr))
+				return MNL_CB_ERROR;
+			dst->_present.upstream = 1;
+
+			parg.rsp_policy = &ethtool_phy_upstream_nest;
+			parg.data = &dst->upstream;
+			if (ethtool_phy_upstream_parse(&parg, attr))
+				return MNL_CB_ERROR;
+		} else if (type == ETHTOOL_A_PHY_DOWNSTREAM_SFP_NAME) {
+			unsigned int len;
+
+			if (ynl_attr_validate(yarg, attr))
+				return MNL_CB_ERROR;
+
+			len = strnlen(mnl_attr_get_str(attr), mnl_attr_get_payload_len(attr));
+			dst->_present.downstream_sfp_name_len = len;
+			dst->downstream_sfp_name = malloc(len + 1);
+			memcpy(dst->downstream_sfp_name, mnl_attr_get_str(attr), len);
+			dst->downstream_sfp_name[len] = 0;
+		} else if (type == ETHTOOL_A_PHY_ID) {
+			if (ynl_attr_validate(yarg, attr))
+				return MNL_CB_ERROR;
+			dst->_present.id = 1;
+			dst->id = mnl_attr_get_u32(attr);
+		}
+	}
+
+	return MNL_CB_OK;
+}
+
+struct ethtool_phy_get_rsp *
+ethtool_phy_get(struct ynl_sock *ys, struct ethtool_phy_get_req *req)
+{
+	struct ynl_req_state yrs = { .yarg = { .ys = ys, }, };
+	struct ethtool_phy_get_rsp *rsp;
+	struct nlmsghdr *nlh;
+	int err;
+
+	nlh = ynl_gemsg_start_req(ys, ys->family_id, ETHTOOL_MSG_PHY_GET, 1);
+	ys->req_policy = &ethtool_phy_nest;
+	yrs.yarg.rsp_policy = &ethtool_phy_nest;
+
+	if (req->_present.header)
+		ethtool_header_put(nlh, ETHTOOL_A_PHY_HEADER, &req->header);
+
+	rsp = calloc(1, sizeof(*rsp));
+	yrs.yarg.data = rsp;
+	yrs.cb = ethtool_phy_get_rsp_parse;
+	yrs.rsp_cmd = ETHTOOL_MSG_PHY_GET;
+
+	err = ynl_exec(ys, nlh, &yrs);
+	if (err < 0)
+		goto err_free;
+
+	return rsp;
+
+err_free:
+	ethtool_phy_get_rsp_free(rsp);
+	return NULL;
+}
+
+/* ETHTOOL_MSG_PHY_GET - dump */
+void ethtool_phy_get_list_free(struct ethtool_phy_get_list *rsp)
+{
+	struct ethtool_phy_get_list *next = rsp;
+
+	while ((void *)next != YNL_LIST_END) {
+		rsp = next;
+		next = rsp->next;
+
+		ethtool_header_free(&rsp->obj.header);
+		free(rsp->obj.drvname);
+		free(rsp->obj.name);
+		ethtool_phy_upstream_free(&rsp->obj.upstream);
+		free(rsp->obj.downstream_sfp_name);
+		free(rsp);
+	}
+}
+
+struct ethtool_phy_get_list *
+ethtool_phy_get_dump(struct ynl_sock *ys, struct ethtool_phy_get_req_dump *req)
+{
+	struct ynl_dump_state yds = {};
+	struct nlmsghdr *nlh;
+	int err;
+
+	yds.ys = ys;
+	yds.alloc_sz = sizeof(struct ethtool_phy_get_list);
+	yds.cb = ethtool_phy_get_rsp_parse;
+	yds.rsp_cmd = ETHTOOL_MSG_PHY_GET;
+	yds.rsp_policy = &ethtool_phy_nest;
+
+	nlh = ynl_gemsg_start_dump(ys, ys->family_id, ETHTOOL_MSG_PHY_GET, 1);
+	ys->req_policy = &ethtool_phy_nest;
+
+	if (req->_present.header)
+		ethtool_header_put(nlh, ETHTOOL_A_PHY_HEADER, &req->header);
+
+	err = ynl_exec_dump(ys, nlh, &yds);
+	if (err < 0)
+		goto free_list;
+
+	return yds.first;
+
+free_list:
+	ethtool_phy_get_list_free(yds.first);
+	return NULL;
+}
+
 /* ETHTOOL_MSG_CABLE_TEST_NTF - event */
 int ethtool_cable_test_ntf_rsp_parse(const struct nlmsghdr *nlh, void *data)
 {
diff --git a/tools/net/ynl/generated/ethtool-user.h b/tools/net/ynl/generated/ethtool-user.h
index 97c079c0f332..59ebb0a1a09f 100644
--- a/tools/net/ynl/generated/ethtool-user.h
+++ b/tools/net/ynl/generated/ethtool-user.h
@@ -20,6 +20,7 @@ extern const struct ynl_family ynl_ethtool_family;
 const char *ethtool_op_str(int op);
 const char *ethtool_udp_tunnel_type_str(int value);
 const char *ethtool_stringset_str(enum ethtool_stringset value);
+const char *ethtool_phy_upstream_type_str(int value);
 
 /* Common nested types */
 struct ethtool_header {
@@ -90,6 +91,16 @@ struct ethtool_mm_stat {
 	__u64 hold_count;
 };
 
+struct ethtool_phy_upstream {
+	struct {
+		__u32 index:1;
+		__u32 sfp_name_len;
+	} _present;
+
+	__u32 index;
+	char *sfp_name;
+};
+
 struct ethtool_cable_result {
 	struct {
 		__u32 pair:1;
@@ -6018,6 +6029,148 @@ ethtool_mm_set_req_set_tx_min_frag_size(struct ethtool_mm_set_req *req,
  */
 int ethtool_mm_set(struct ynl_sock *ys, struct ethtool_mm_set_req *req);
 
+/* ============== ETHTOOL_MSG_PHY_GET ============== */
+/* ETHTOOL_MSG_PHY_GET - do */
+struct ethtool_phy_get_req {
+	struct {
+		__u32 header:1;
+	} _present;
+
+	struct ethtool_header header;
+};
+
+static inline struct ethtool_phy_get_req *ethtool_phy_get_req_alloc(void)
+{
+	return calloc(1, sizeof(struct ethtool_phy_get_req));
+}
+void ethtool_phy_get_req_free(struct ethtool_phy_get_req *req);
+
+static inline void
+ethtool_phy_get_req_set_header_dev_index(struct ethtool_phy_get_req *req,
+					 __u32 dev_index)
+{
+	req->_present.header = 1;
+	req->header._present.dev_index = 1;
+	req->header.dev_index = dev_index;
+}
+static inline void
+ethtool_phy_get_req_set_header_dev_name(struct ethtool_phy_get_req *req,
+					const char *dev_name)
+{
+	free(req->header.dev_name);
+	req->header._present.dev_name_len = strlen(dev_name);
+	req->header.dev_name = malloc(req->header._present.dev_name_len + 1);
+	memcpy(req->header.dev_name, dev_name, req->header._present.dev_name_len);
+	req->header.dev_name[req->header._present.dev_name_len] = 0;
+}
+static inline void
+ethtool_phy_get_req_set_header_flags(struct ethtool_phy_get_req *req,
+				     __u32 flags)
+{
+	req->_present.header = 1;
+	req->header._present.flags = 1;
+	req->header.flags = flags;
+}
+static inline void
+ethtool_phy_get_req_set_header_phy_index(struct ethtool_phy_get_req *req,
+					 __u32 phy_index)
+{
+	req->_present.header = 1;
+	req->header._present.phy_index = 1;
+	req->header.phy_index = phy_index;
+}
+
+struct ethtool_phy_get_rsp {
+	struct {
+		__u32 header:1;
+		__u32 index:1;
+		__u32 drvname_len;
+		__u32 name_len;
+		__u32 upstream_type:1;
+		__u32 upstream:1;
+		__u32 downstream_sfp_name_len;
+		__u32 id:1;
+	} _present;
+
+	struct ethtool_header header;
+	__u32 index;
+	char *drvname;
+	char *name;
+	__u8 upstream_type;
+	struct ethtool_phy_upstream upstream;
+	char *downstream_sfp_name;
+	__u32 id;
+};
+
+void ethtool_phy_get_rsp_free(struct ethtool_phy_get_rsp *rsp);
+
+/*
+ * Get PHY devices attached to an interface
+ */
+struct ethtool_phy_get_rsp *
+ethtool_phy_get(struct ynl_sock *ys, struct ethtool_phy_get_req *req);
+
+/* ETHTOOL_MSG_PHY_GET - dump */
+struct ethtool_phy_get_req_dump {
+	struct {
+		__u32 header:1;
+	} _present;
+
+	struct ethtool_header header;
+};
+
+static inline struct ethtool_phy_get_req_dump *
+ethtool_phy_get_req_dump_alloc(void)
+{
+	return calloc(1, sizeof(struct ethtool_phy_get_req_dump));
+}
+void ethtool_phy_get_req_dump_free(struct ethtool_phy_get_req_dump *req);
+
+static inline void
+ethtool_phy_get_req_dump_set_header_dev_index(struct ethtool_phy_get_req_dump *req,
+					      __u32 dev_index)
+{
+	req->_present.header = 1;
+	req->header._present.dev_index = 1;
+	req->header.dev_index = dev_index;
+}
+static inline void
+ethtool_phy_get_req_dump_set_header_dev_name(struct ethtool_phy_get_req_dump *req,
+					     const char *dev_name)
+{
+	free(req->header.dev_name);
+	req->header._present.dev_name_len = strlen(dev_name);
+	req->header.dev_name = malloc(req->header._present.dev_name_len + 1);
+	memcpy(req->header.dev_name, dev_name, req->header._present.dev_name_len);
+	req->header.dev_name[req->header._present.dev_name_len] = 0;
+}
+static inline void
+ethtool_phy_get_req_dump_set_header_flags(struct ethtool_phy_get_req_dump *req,
+					  __u32 flags)
+{
+	req->_present.header = 1;
+	req->header._present.flags = 1;
+	req->header.flags = flags;
+}
+static inline void
+ethtool_phy_get_req_dump_set_header_phy_index(struct ethtool_phy_get_req_dump *req,
+					      __u32 phy_index)
+{
+	req->_present.header = 1;
+	req->header._present.phy_index = 1;
+	req->header.phy_index = phy_index;
+}
+
+struct ethtool_phy_get_list {
+	struct ethtool_phy_get_list *next;
+	struct ethtool_phy_get_rsp obj __attribute__((aligned(8)));
+};
+
+void ethtool_phy_get_list_free(struct ethtool_phy_get_list *rsp);
+
+struct ethtool_phy_get_list *
+ethtool_phy_get_dump(struct ynl_sock *ys, struct ethtool_phy_get_req_dump *req);
+
 /* ETHTOOL_MSG_CABLE_TEST_NTF - event */
 struct ethtool_cable_test_ntf_rsp {
 	struct {
