diff --git a/Makefile.am b/Makefile.am
index c51193ac92fd..70ded2ec8bc3 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -27,7 +27,7 @@ if ETHTOOL_ENABLE_NETLINK
 ethtool_SOURCES += \
 		  netlink/netlink.c netlink/netlink.h netlink/extapi.h \
 		  netlink/msgbuff.c netlink/msgbuff.h netlink/nlsock.c \
-		  netlink/nlsock.h \
+		  netlink/nlsock.h netlink/strset.c netlink/strset.h \
 		  uapi/linux/ethtool_netlink.h \
 		  uapi/linux/netlink.h uapi/linux/genetlink.h \
 		  uapi/linux/rtnetlink.h uapi/linux/if_link.h
diff --git a/netlink/netlink.c b/netlink/netlink.c
index 7cd7bef6eac9..60f7912181df 100644
--- a/netlink/netlink.c
+++ b/netlink/netlink.c
@@ -11,6 +11,7 @@
 #include "extapi.h"
 #include "msgbuff.h"
 #include "nlsock.h"
+#include "strset.h"
 
 /* Used as reply callback for requests where no reply is expected (e.g. most
  * "set" type commands)
@@ -40,6 +41,57 @@ int attr_cb(const struct nlattr *attr, void *data)
 	return MNL_CB_OK;
 }
 
+/* misc helpers */
+
+const char *get_dev_name(const struct nlattr *nest)
+{
+	const struct nlattr *tb[ETHTOOL_A_HEADER_MAX + 1] = {};
+	DECLARE_ATTR_TB_INFO(tb);
+	int ret;
+
+	if (!nest)
+		return NULL;
+	ret = mnl_attr_parse_nested(nest, attr_cb, &tb_info);
+	if (ret < 0 || !tb[ETHTOOL_A_HEADER_DEV_NAME])
+		return "(none)";
+	return mnl_attr_get_str(tb[ETHTOOL_A_HEADER_DEV_NAME]);
+}
+
+int get_dev_info(const struct nlattr *nest, int *ifindex, char *ifname)
+{
+	const struct nlattr *tb[ETHTOOL_A_HEADER_MAX + 1] = {};
+	const struct nlattr *index_attr;
+	const struct nlattr *name_attr;
+	DECLARE_ATTR_TB_INFO(tb);
+	int ret;
+
+	if (ifindex)
+		*ifindex = 0;
+	if (ifname)
+		memset(ifname, '\0', ALTIFNAMSIZ);
+
+	if (!nest)
+		return -EFAULT;
+	ret = mnl_attr_parse_nested(nest, attr_cb, &tb_info);
+	index_attr = tb[ETHTOOL_A_HEADER_DEV_INDEX];
+	name_attr = tb[ETHTOOL_A_HEADER_DEV_NAME];
+	if (ret < 0 || (ifindex && !index_attr) || (ifname && !name_attr))
+		return -EFAULT;
+
+	if (ifindex)
+		*ifindex = mnl_attr_get_u32(index_attr);
+	if (ifname) {
+		strncpy(ifname, mnl_attr_get_str(name_attr), ALTIFNAMSIZ);
+		if (ifname[ALTIFNAMSIZ - 1]) {
+			ifname[ALTIFNAMSIZ - 1] = '\0';
+			fprintf(stderr, "kernel device name too long: '%s'\n",
+				mnl_attr_get_str(name_attr));
+			return -EFAULT;
+		}
+	}
+	return 0;
+}
+
 /* initialization */
 
 struct fam_info {
@@ -153,4 +205,5 @@ void netlink_done(struct cmd_context *ctx)
 
 	free(ctx->nlctx);
 	ctx->nlctx = NULL;
+	cleanup_all_strings();
 }
diff --git a/netlink/netlink.h b/netlink/netlink.h
index 9ba03b05163f..04ad7bcd347c 100644
--- a/netlink/netlink.h
+++ b/netlink/netlink.h
@@ -39,6 +39,15 @@ struct attr_tb_info {
 int nomsg_reply_cb(const struct nlmsghdr *nlhdr, void *data);
 int attr_cb(const struct nlattr *attr, void *data);
 
+const char *get_dev_name(const struct nlattr *nest);
+int get_dev_info(const struct nlattr *nest, int *ifindex, char *ifname);
+
+static inline void copy_devname(char *dst, const char *src)
+{
+	strncpy(dst, src, ALTIFNAMSIZ);
+	dst[ALTIFNAMSIZ - 1] = '\0';
+}
+
 static inline int netlink_init_ethnl2_socket(struct nl_context *nlctx)
 {
 	if (nlctx->ethnl2_socket)
diff --git a/netlink/strset.c b/netlink/strset.c
new file mode 100644
index 000000000000..bc468ae5a88e
--- /dev/null
+++ b/netlink/strset.c
@@ -0,0 +1,295 @@
+/*
+ * strset.c - string set handling
+ *
+ * Implementation of local cache of ethtool string sets.
+ */
+
+#include <errno.h>
+#include <string.h>
+
+#include "../internal.h"
+#include "netlink.h"
+#include "nlsock.h"
+#include "msgbuff.h"
+
+struct stringset {
+	const char		**strings;
+	void			*raw_data;
+	unsigned int		count;
+	bool			loaded;
+};
+
+struct perdev_strings {
+	int			ifindex;
+	char			devname[ALTIFNAMSIZ];
+	struct stringset	strings[ETH_SS_COUNT];
+	struct perdev_strings	*next;
+};
+
+/* universal string sets */
+static struct stringset global_strings[ETH_SS_COUNT];
+/* linked list of string sets related to network devices */
+static struct perdev_strings *device_strings;
+
+static void drop_stringset(struct stringset *set)
+{
+	if (!set)
+		return;
+
+	free(set->strings);
+	free(set->raw_data);
+	memset(set, 0, sizeof(*set));
+}
+
+static int import_stringset(struct stringset *dest, const struct nlattr *nest)
+{
+	const struct nlattr *tb_stringset[ETHTOOL_A_STRINGSET_MAX + 1] = {};
+	DECLARE_ATTR_TB_INFO(tb_stringset);
+	const struct nlattr *string;
+	unsigned int size;
+	unsigned int count;
+	unsigned int idx;
+	int ret;
+
+	ret = mnl_attr_parse_nested(nest, attr_cb, &tb_stringset_info);
+	if (ret < 0)
+		return ret;
+	if (!tb_stringset[ETHTOOL_A_STRINGSET_ID] ||
+	    !tb_stringset[ETHTOOL_A_STRINGSET_COUNT] ||
+	    !tb_stringset[ETHTOOL_A_STRINGSET_STRINGS])
+		return -EFAULT;
+	idx = mnl_attr_get_u32(tb_stringset[ETHTOOL_A_STRINGSET_ID]);
+	if (idx >= ETH_SS_COUNT)
+		return 0;
+	if (dest[idx].loaded)
+		drop_stringset(&dest[idx]);
+	dest[idx].loaded = true;
+	count = mnl_attr_get_u32(tb_stringset[ETHTOOL_A_STRINGSET_COUNT]);
+	if (count == 0)
+		return 0;
+
+	size = mnl_attr_get_len(tb_stringset[ETHTOOL_A_STRINGSET_STRINGS]);
+	ret = -ENOMEM;
+	dest[idx].raw_data = malloc(size);
+	if (!dest[idx].raw_data)
+		goto err;
+	memcpy(dest[idx].raw_data, tb_stringset[ETHTOOL_A_STRINGSET_STRINGS],
+	       size);
+	dest[idx].strings = calloc(count, sizeof(dest[idx].strings[0]));
+	if (!dest[idx].strings)
+		goto err;
+	dest[idx].count = count;
+
+	nest = dest[idx].raw_data;
+	mnl_attr_for_each_nested(string, nest) {
+		const struct nlattr *tb[ETHTOOL_A_STRING_MAX + 1] = {};
+		DECLARE_ATTR_TB_INFO(tb);
+		unsigned int i;
+
+		if (mnl_attr_get_type(string) != ETHTOOL_A_STRINGS_STRING)
+			continue;
+		ret = mnl_attr_parse_nested(string, attr_cb, &tb_info);
+		if (ret < 0)
+			goto err;
+		ret = -EFAULT;
+		if (!tb[ETHTOOL_A_STRING_INDEX] || !tb[ETHTOOL_A_STRING_VALUE])
+			goto err;
+
+		i = mnl_attr_get_u32(tb[ETHTOOL_A_STRING_INDEX]);
+		if (i >= count)
+			goto err;
+		dest[idx].strings[i] =
+			mnl_attr_get_payload(tb[ETHTOOL_A_STRING_VALUE]);
+	}
+
+	return 0;
+err:
+	drop_stringset(&dest[idx]);
+	return ret;
+}
+
+static struct perdev_strings *get_perdev_by_ifindex(int ifindex)
+{
+	struct perdev_strings *perdev = device_strings;
+
+	while (perdev && perdev->ifindex != ifindex)
+		perdev = perdev->next;
+	if (perdev)
+		return perdev;
+
+	/* not found, allocate and insert into list */
+	perdev = calloc(sizeof(*perdev), 1);
+	if (!perdev)
+		return NULL;
+	perdev->ifindex = ifindex;
+	perdev->next = device_strings;
+	device_strings = perdev;
+
+	return perdev;
+}
+
+static int strset_reply_cb(const struct nlmsghdr *nlhdr, void *data)
+{
+	const struct nlattr *tb[ETHTOOL_A_STRSET_MAX + 1] = {};
+	DECLARE_ATTR_TB_INFO(tb);
+	struct nl_context *nlctx = data;
+	char devname[ALTIFNAMSIZ] = "";
+	struct stringset *dest;
+	struct nlattr *attr;
+	int ifindex = 0;
+	int ret;
+
+	ret = mnl_attr_parse(nlhdr, GENL_HDRLEN, attr_cb, &tb_info);
+	if (ret < 0)
+		return ret;
+	if (tb[ETHTOOL_A_STRSET_HEADER]) {
+		ret = get_dev_info(tb[ETHTOOL_A_STRSET_HEADER], &ifindex,
+				   devname);
+		if (ret < 0)
+			return MNL_CB_OK;
+		nlctx->devname = devname;
+	}
+
+	if (ifindex) {
+		struct perdev_strings *perdev;
+
+		perdev = get_perdev_by_ifindex(ifindex);
+		if (!perdev)
+			return MNL_CB_OK;
+		copy_devname(perdev->devname, devname);
+		dest = perdev->strings;
+	} else {
+		dest = global_strings;
+	}
+
+	if (!tb[ETHTOOL_A_STRSET_STRINGSETS])
+		return MNL_CB_OK;
+	mnl_attr_for_each_nested(attr, tb[ETHTOOL_A_STRSET_STRINGSETS]) {
+		if (mnl_attr_get_type(attr) ==
+		    ETHTOOL_A_STRINGSETS_STRINGSET)
+			import_stringset(dest, attr);
+	}
+
+	return MNL_CB_OK;
+}
+
+static int fill_stringset_id(struct nl_msg_buff *msgbuff, unsigned int type)
+{
+	struct nlattr *nest_sets;
+	struct nlattr *nest_set;
+
+	nest_sets = ethnla_nest_start(msgbuff, ETHTOOL_A_STRSET_STRINGSETS);
+	if (!nest_sets)
+		return -EMSGSIZE;
+	nest_set = ethnla_nest_start(msgbuff, ETHTOOL_A_STRINGSETS_STRINGSET);
+	if (!nest_set)
+		goto err;
+	if (ethnla_put_u32(msgbuff, ETHTOOL_A_STRINGSET_ID, type))
+		goto err;
+	ethnla_nest_end(msgbuff, nest_set);
+	ethnla_nest_end(msgbuff, nest_sets);
+	return 0;
+
+err:
+	ethnla_nest_cancel(msgbuff, nest_sets);
+	return -EMSGSIZE;
+}
+
+static int stringset_load_request(struct nl_socket *nlsk, const char *devname,
+				  int type, bool is_dump)
+{
+	struct nl_msg_buff *msgbuff = &nlsk->msgbuff;
+	int ret;
+
+	ret = msg_init(nlsk->nlctx, msgbuff, ETHTOOL_MSG_STRSET_GET,
+		       NLM_F_REQUEST | NLM_F_ACK | (is_dump ? NLM_F_DUMP : 0));
+	if (ret < 0)
+		return ret;
+	if (ethnla_fill_header(msgbuff, ETHTOOL_A_STRSET_HEADER, devname, 0))
+		return -EMSGSIZE;
+	if (type >= 0) {
+		ret = fill_stringset_id(msgbuff, type);
+		if (ret < 0)
+			return ret;
+	}
+
+	ret = nlsock_send_get_request(nlsk, strset_reply_cb);
+	return ret;
+}
+
+/* interface */
+
+const struct stringset *global_stringset(unsigned int type,
+					 struct nl_socket *nlsk)
+{
+	int ret;
+
+	if (type >= ETH_SS_COUNT)
+		return NULL;
+	if (global_strings[type].loaded)
+		return &global_strings[type];
+	ret = stringset_load_request(nlsk, NULL, type, false);
+	return ret < 0 ? NULL : &global_strings[type];
+}
+
+const struct stringset *perdev_stringset(const char *devname, unsigned int type,
+					 struct nl_socket *nlsk)
+{
+	const struct perdev_strings *p;
+	int ret;
+
+	if (type >= ETH_SS_COUNT)
+		return NULL;
+	for (p = device_strings; p; p = p->next)
+		if (!strcmp(p->devname, devname))
+			return &p->strings[type];
+
+	ret = stringset_load_request(nlsk, devname, type, false);
+	if (ret < 0)
+		return NULL;
+	for (p = device_strings; p; p = p->next)
+		if (!strcmp(p->devname, devname))
+			return &p->strings[type];
+
+	return NULL;
+}
+
+unsigned int get_count(const struct stringset *set)
+{
+	return set->count;
+}
+
+const char *get_string(const struct stringset *set, unsigned int idx)
+{
+	if (!set || idx >= set->count)
+		return NULL;
+	return set->strings[idx];
+}
+
+int preload_global_strings(struct nl_socket *nlsk)
+{
+	return stringset_load_request(nlsk, NULL, -1, false);
+}
+
+int preload_perdev_strings(struct nl_socket *nlsk, const char *dev)
+{
+	return stringset_load_request(nlsk, dev, -1, !dev);
+}
+
+void cleanup_all_strings(void)
+{
+	struct perdev_strings *perdev;
+	unsigned int i;
+
+	for (i = 0; i < ETH_SS_COUNT; i++)
+		drop_stringset(&global_strings[i]);
+
+	perdev = device_strings;
+	while (perdev) {
+		device_strings = perdev->next;
+		for (i = 0; i < ETH_SS_COUNT; i++)
+			drop_stringset(&perdev->strings[i]);
+		free(perdev);
+		perdev = device_strings;
+	}
+}
diff --git a/netlink/strset.h b/netlink/strset.h
new file mode 100644
index 000000000000..72a4a3929244
--- /dev/null
+++ b/netlink/strset.h
@@ -0,0 +1,25 @@
+/*
+ * strset.h - string set handling
+ *
+ * Interface for local cache of ethtool string sets.
+ */
+
+#ifndef ETHTOOL_NETLINK_STRSET_H__
+#define ETHTOOL_NETLINK_STRSET_H__
+
+struct nl_socket;
+struct stringset;
+
+const struct stringset *global_stringset(unsigned int type,
+					 struct nl_socket *nlsk);
+const struct stringset *perdev_stringset(const char *dev, unsigned int type,
+					 struct nl_socket *nlsk);
+
+unsigned int get_count(const struct stringset *set);
+const char *get_string(const struct stringset *set, unsigned int idx);
+
+int preload_global_strings(struct nl_socket *nlsk);
+int preload_perdev_strings(struct nl_socket *nlsk, const char *dev);
+void cleanup_all_strings(void);
+
+#endif /* ETHTOOL_NETLINK_STRSET_H__ */
