diff --git a/tools/include/uapi/linux/ethtool.h b/tools/include/uapi/linux/ethtool.h
index c86c3e942df9..cf3041d302e4 100644
--- a/tools/include/uapi/linux/ethtool.h
+++ b/tools/include/uapi/linux/ethtool.h
@@ -48,4 +48,48 @@ struct ethtool_channels {
 	__u32	combined_count;
 };
 
+#define ETH_GSTRING_LEN		32
+
+/**
+ * enum ethtool_stringset - string set ID
+ * @ETH_SS_TEST: Self-test result names, for use with %ETHTOOL_TEST
+ * @ETH_SS_STATS: Statistic names, for use with %ETHTOOL_GSTATS
+ * @ETH_SS_PRIV_FLAGS: Driver private flag names, for use with
+ *	%ETHTOOL_GPFLAGS and %ETHTOOL_SPFLAGS
+ * @ETH_SS_NTUPLE_FILTERS: Previously used with %ETHTOOL_GRXNTUPLE;
+ *	now deprecated
+ * @ETH_SS_FEATURES: Device feature names
+ * @ETH_SS_RSS_HASH_FUNCS: RSS hush function names
+ * @ETH_SS_PHY_STATS: Statistic names, for use with %ETHTOOL_GPHYSTATS
+ * @ETH_SS_PHY_TUNABLES: PHY tunable names
+ * @ETH_SS_LINK_MODES: link mode names
+ * @ETH_SS_MSG_CLASSES: debug message class names
+ * @ETH_SS_WOL_MODES: wake-on-lan modes
+ * @ETH_SS_SOF_TIMESTAMPING: SOF_TIMESTAMPING_* flags
+ * @ETH_SS_TS_TX_TYPES: timestamping Tx types
+ * @ETH_SS_TS_RX_FILTERS: timestamping Rx filters
+ * @ETH_SS_UDP_TUNNEL_TYPES: UDP tunnel types
+ */
+enum ethtool_stringset {
+	ETH_SS_TEST		= 0,
+	ETH_SS_STATS,
+	ETH_SS_PRIV_FLAGS,
+	ETH_SS_NTUPLE_FILTERS,
+	ETH_SS_FEATURES,
+	ETH_SS_RSS_HASH_FUNCS,
+	ETH_SS_TUNABLES,
+	ETH_SS_PHY_STATS,
+	ETH_SS_PHY_TUNABLES,
+	ETH_SS_LINK_MODES,
+	ETH_SS_MSG_CLASSES,
+	ETH_SS_WOL_MODES,
+	ETH_SS_SOF_TIMESTAMPING,
+	ETH_SS_TS_TX_TYPES,
+	ETH_SS_TS_RX_FILTERS,
+	ETH_SS_UDP_TUNNEL_TYPES,
+
+	/* add new constants above here */
+	ETH_SS_COUNT
+};
+
 #endif /* _UAPI_LINUX_ETHTOOL_H */
diff --git a/tools/lib/bpf/ethtool.h b/tools/lib/bpf/ethtool.h
new file mode 100644
index 000000000000..14b2ae47bc26
--- /dev/null
+++ b/tools/lib/bpf/ethtool.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
+
+/*
+ * Generic netlink ethtool family required defines
+ *
+ * Copyright (c) 2020 Intel
+ */
+
+#ifndef __LIBBPF_ETHTOOL_H_
+#define __LIBBPF_ETHTOOL_H_
+
+#include <linux/ethtool_netlink.h>
+
+#define DIV_ROUND_UP(n, d)  (((n) + (d) - 1) / (d))
+#define FEATURE_BITS_TO_BLOCKS(n_bits)      DIV_ROUND_UP(n_bits, 32U)
+
+#define FEATURE_WORD(blocks, index)  ((blocks)[(index) / 32U])
+#define FEATURE_FIELD_FLAG(index)       (1U << (index) % 32U)
+#define FEATURE_BIT_IS_SET(blocks, index)        \
+		(FEATURE_WORD(blocks, index) & FEATURE_FIELD_FLAG(index))
+
+#define NETDEV_XDP_STR			"xdp"
+#define NETDEV_XDP_LEN			4
+
+#define NETDEV_AF_XDP_ZC_STR		"af-xdp-zc"
+#define NETDEV_AF_XDP_ZC_LEN		10
+
+#define BUF_SIZE_4096			4096
+#define BUF_SIZE_8192			8192
+
+#define MAX_FEATURES			500
+
+struct ethnl_params {
+	const char *ifname;
+	const char *nl_family;
+	int features;
+	int xdp_idx;
+	int xdp_zc_idx;
+	int xdp_flags;
+	int xdp_zc_flags;
+	__u16 fam_id;
+};
+
+int libbpf_ethnl_get_ethtool_family_id(struct ethnl_params *param);
+int libbpf_ethnl_get_netdev_features(struct ethnl_params *param);
+int libbpf_ethnl_get_active_bits(struct ethnl_params *param);
+
+#endif /* __LIBBPF_ETHTOOL_H_ */
+
diff --git a/tools/lib/bpf/libbpf.h b/tools/lib/bpf/libbpf.h
index 6909ee81113a..4f0656716eee 100644
--- a/tools/lib/bpf/libbpf.h
+++ b/tools/lib/bpf/libbpf.h
@@ -41,6 +41,7 @@ enum libbpf_errno {
 	LIBBPF_ERRNO__WRNGPID,	/* Wrong pid in netlink message */
 	LIBBPF_ERRNO__INVSEQ,	/* Invalid netlink sequence */
 	LIBBPF_ERRNO__NLPARSE,	/* netlink parsing error */
+	LIBBPF_ERRNO__INVXDP,	/* Invalid XDP data */
 	__LIBBPF_ERRNO__END,
 };
 
diff --git a/tools/lib/bpf/netlink.c b/tools/lib/bpf/netlink.c
index 4dd73de00b6f..a5344401b842 100644
--- a/tools/lib/bpf/netlink.c
+++ b/tools/lib/bpf/netlink.c
@@ -6,6 +6,8 @@
 #include <unistd.h>
 #include <linux/bpf.h>
 #include <linux/rtnetlink.h>
+#include <linux/genetlink.h>
+#include <linux/if.h>
 #include <sys/socket.h>
 #include <errno.h>
 #include <time.h>
@@ -14,6 +16,7 @@
 #include "libbpf.h"
 #include "libbpf_internal.h"
 #include "nlattr.h"
+#include "ethtool.h"
 
 #ifndef SOL_NETLINK
 #define SOL_NETLINK 270
@@ -23,6 +26,11 @@ typedef int (*libbpf_dump_nlmsg_t)(void *cookie, void *msg, struct nlattr **tb);
 
 typedef int (*__dump_nlmsg_t)(struct nlmsghdr *nlmsg, libbpf_dump_nlmsg_t,
 			      void *cookie);
+struct ethnl_msg {
+	struct nlmsghdr nlh;
+	struct genlmsghdr genlhdr;
+	char msg[BUF_SIZE_4096];
+};
 
 struct xdp_id_md {
 	int ifindex;
@@ -30,7 +38,7 @@ struct xdp_id_md {
 	struct xdp_link_info info;
 };
 
-static int libbpf_netlink_open(__u32 *nl_pid)
+static int libbpf_netlink_open(__u32 *nl_pid, int protocol)
 {
 	struct sockaddr_nl sa;
 	socklen_t addrlen;
@@ -40,7 +48,7 @@ static int libbpf_netlink_open(__u32 *nl_pid)
 	memset(&sa, 0, sizeof(sa));
 	sa.nl_family = AF_NETLINK;
 
-	sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
+	sock = socket(AF_NETLINK, SOCK_RAW, protocol);
 	if (sock < 0)
 		return -errno;
 
@@ -143,7 +151,7 @@ static int __bpf_set_link_xdp_fd_replace(int ifindex, int fd, int old_fd,
 	} req;
 	__u32 nl_pid = 0;
 
-	sock = libbpf_netlink_open(&nl_pid);
+	sock = libbpf_netlink_open(&nl_pid, NETLINK_ROUTE);
 	if (sock < 0)
 		return sock;
 
@@ -302,7 +310,7 @@ int bpf_get_link_xdp_info(int ifindex, struct xdp_link_info *info,
 	if (flags && flags & mask)
 		return -EINVAL;
 
-	sock = libbpf_netlink_open(&nl_pid);
+	sock = libbpf_netlink_open(&nl_pid, NETLINK_ROUTE);
 	if (sock < 0)
 		return sock;
 
@@ -370,3 +378,366 @@ int libbpf_nl_get_link(int sock, unsigned int nl_pid,
 	return bpf_netlink_recv(sock, nl_pid, seq, __dump_link_nlmsg,
 				dump_link_nlmsg, cookie);
 }
+
+static int libbpf_ethtool_parse_feature_strings(struct nlattr *start, int elem,
+						int *xdp, int *xdp_zc)
+{
+	struct nlattr *tbs[__ETHTOOL_A_STRING_CNT + 1];
+	struct nlattr *tab[elem > 0 ? elem : 0];
+	struct libbpf_nla_policy policy[] = {
+		[ETHTOOL_A_STRING_UNSPEC] = {
+		.type = LIBBPF_NLA_UNSPEC,
+		.minlen = 0,
+		.maxlen = 0,
+		},
+		[ETHTOOL_A_STRING_INDEX] = {
+		.type = LIBBPF_NLA_U32,
+		.minlen = sizeof(uint32_t),
+		.maxlen = sizeof(uint32_t),
+		},
+		[ETHTOOL_A_STRING_VALUE] = {
+		.type = LIBBPF_NLA_STRING,
+		.minlen = 1,
+		.maxlen = ETH_GSTRING_LEN,
+		}
+	};
+	const char *f;
+	int n = 0;
+	__u32 v;
+	int ret;
+	int i;
+
+	if (!xdp || !xdp_zc || !start || elem <= 0)
+		return -EINVAL;
+
+	*xdp = -1;
+	*xdp_zc = -1;
+
+	ret = libbpf_nla_parse_table(tab, elem, start, 0, NULL);
+	if (ret)
+		goto cleanup;
+
+	for (i = 0; tab[i] && i < elem; ++i) {
+		ret = libbpf_nla_parse_nested(tbs, __ETHTOOL_A_STRING_CNT, tab[i], policy);
+		if (ret)
+			break;
+
+		if (tbs[ETHTOOL_A_STRING_INDEX] && tbs[ETHTOOL_A_STRING_VALUE]) {
+			f = libbpf_nla_getattr_str(tbs[ETHTOOL_A_STRING_VALUE]);
+			v = libbpf_nla_getattr_u32(tbs[ETHTOOL_A_STRING_INDEX]);
+
+			if (!strncmp(NETDEV_XDP_STR, f, NETDEV_XDP_LEN)) {
+				*xdp = v;
+				n++;
+			}
+
+			if (!strncmp(NETDEV_AF_XDP_ZC_STR, f, NETDEV_AF_XDP_ZC_LEN)) {
+				*xdp_zc = v;
+				n++;
+			}
+		} else {
+			ret = -LIBBPF_ERRNO__NLPARSE;
+			break;
+		}
+	}
+
+cleanup:
+	/* If error occurred return it. */
+	if (ret)
+		return ret;
+
+	/*
+	 * If zero or two xdp flags found that is okay.
+	 * Zero means older kernel without any xdp flags added.
+	 * Two means newer kernel with xdp flags added.
+	 * Both flags were added in single commit, so that
+	 * n == 1 is a faulty value.
+	 */
+	if (n == 2 || n == 0)
+		return 0;
+
+	/* If no error and one or more than 2 xdp flags found return error */
+	return -LIBBPF_ERRNO__INVXDP;
+}
+
+static int libbpf_ethnl_send(int sock, __u32 seq, __u32 nl_pid, struct ethnl_msg *req)
+{
+	ssize_t written;
+
+	req->nlh.nlmsg_pid = nl_pid;
+	req->nlh.nlmsg_seq = seq;
+
+	written = send(sock, req, req->nlh.nlmsg_len, 0);
+	if (written < 0)
+		return -errno;
+
+	if (written == req->nlh.nlmsg_len)
+		return 0;
+	else
+		return -errno;
+}
+
+static int libbpf_ethnl_validate(int len, __u16 fam_id, __u32 nl_pid, __u32 seq,
+				 struct ethnl_msg *req)
+{
+	if (!NLMSG_OK(&req->nlh, (unsigned int)len))
+		return -ENOMSG;
+
+	if (req->nlh.nlmsg_pid != nl_pid)
+		return -LIBBPF_ERRNO__WRNGPID;
+
+	if (req->nlh.nlmsg_seq != seq)
+		return -LIBBPF_ERRNO__INVSEQ;
+
+	if (req->nlh.nlmsg_type != fam_id) {
+		int ret = -ENOMSG;
+
+		if (req->nlh.nlmsg_type == NLMSG_ERROR) {
+			struct nlmsgerr *err = (struct nlmsgerr *)&req->genlhdr;
+
+			if (err->error)
+				ret = err->error;
+			libbpf_nla_dump_errormsg(&req->nlh);
+		}
+		return ret;
+	}
+
+	return 0;
+}
+
+static int libbpf_ethnl_send_recv(struct ethnl_msg *req, struct ethnl_params *param)
+{
+	__u32 nl_pid;
+	__u32 seq;
+	int sock;
+	int ret;
+	int len;
+
+	sock = libbpf_netlink_open(&nl_pid, NETLINK_GENERIC);
+	if (sock < 0) {
+		ret = sock;
+		goto cleanup;
+	}
+
+	seq = time(NULL);
+	ret = libbpf_ethnl_send(sock, seq, nl_pid, req);
+	if (ret)
+		goto cleanup;
+
+	len = recv(sock, req, sizeof(struct ethnl_msg), 0);
+	if (len < 0) {
+		ret = -errno;
+		goto cleanup;
+	}
+
+	ret = libbpf_ethnl_validate(len, param->fam_id, nl_pid, seq, req);
+	if (ret < 0)
+		goto cleanup;
+
+	ret = len;
+
+cleanup:
+	if (sock >= 0)
+		close(sock);
+
+	return ret;
+}
+
+int libbpf_ethnl_get_netdev_features(struct ethnl_params *param)
+{
+	struct ethnl_msg req = {
+		.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct genlmsghdr)),
+		.nlh.nlmsg_flags = NLM_F_REQUEST,
+		.nlh.nlmsg_type = param->fam_id,
+		.nlh.nlmsg_pid = 0,
+		.genlhdr.version = ETHTOOL_GENL_VERSION,
+		.genlhdr.cmd = ETHTOOL_MSG_STRSET_GET,
+		.genlhdr.reserved = 0,
+	};
+	struct nlattr *tbn[__ETHTOOL_A_STRINGSETS_CNT + 1];
+	struct nlattr *tbnn[__ETHTOOL_A_STRINGSET_CNT + 1];
+	struct nlattr *tb[__ETHTOOL_A_STRSET_CNT + 1];
+	struct nlattr *nla, *nla_next, *nla_set;
+	int string_set = ETH_SS_FEATURES;
+	int ret;
+	int len;
+
+	memset(&req.msg, 0, BUF_SIZE_4096);
+
+	nla = (struct nlattr *)req.msg;
+	nla_next = libbpf_nla_nest_start(nla, ETHTOOL_A_STRSET_HEADER);
+	nla_next = libbpf_nla_put_str(nla_next, ETHTOOL_A_HEADER_DEV_NAME,
+				      param->ifname, IFNAMSIZ);
+	libbpf_nla_nest_end(nla, nla_next);
+
+	nla = nla_next;
+	nla_set = libbpf_nla_nest_start(nla, ETHTOOL_A_STRSET_STRINGSETS);
+	nla_next = libbpf_nla_nest_start(nla_set, ETHTOOL_A_STRINGSETS_STRINGSET);
+	nla_next = libbpf_nla_put_u32(nla_next, ETHTOOL_A_STRINGSET_ID, string_set);
+	libbpf_nla_nest_end(nla_set, nla_next);
+	libbpf_nla_nest_end(nla, nla_next);
+	if (!param->features)
+		nla_next = libbpf_nla_put_flag(nla_next, ETHTOOL_A_STRSET_COUNTS_ONLY);
+
+	req.nlh.nlmsg_len += libbpf_nla_attrs_length((struct nlattr *)req.msg, nla_next);
+
+	len = libbpf_ethnl_send_recv(&req, param);
+	if (len < 0)
+		return len;
+
+	/* set parsing error, and change if succeeded */
+	ret = -LIBBPF_ERRNO__NLPARSE;
+	nla = (struct nlattr *)req.msg;
+	len = len - NLMSG_HDRLEN - GENL_HDRLEN;
+
+	if (libbpf_nla_parse(tb, __ETHTOOL_A_STRSET_CNT, nla, len, NULL))
+		return ret;
+
+	if (!tb[ETHTOOL_A_STRSET_STRINGSETS])
+		return ret;
+
+	if (libbpf_nla_parse_nested(tbn, __ETHTOOL_A_STRINGSETS_CNT,
+				    tb[ETHTOOL_A_STRSET_STRINGSETS], NULL))
+		return ret;
+
+	if (!tbn[ETHTOOL_A_STRINGSETS_STRINGSET])
+		return ret;
+
+	if (libbpf_nla_parse_nested(tbnn, __ETHTOOL_A_STRINGSET_CNT,
+				    tbn[ETHTOOL_A_STRINGSETS_STRINGSET], NULL))
+		return ret;
+
+	if (param->features == 0) {
+		if (!tbnn[ETHTOOL_A_STRINGSET_COUNT])
+			return ret;
+
+		param->features = libbpf_nla_getattr_u32(tbnn[ETHTOOL_A_STRINGSET_COUNT]);
+
+		/* success */
+		ret = 0;
+	} else if (param->features > 0) {
+		if (!tbnn[ETHTOOL_A_STRINGSET_STRINGS])
+			return ret;
+
+		/*
+		 * Upper boundary is known, but it is input from socket stream.
+		 * Let's perform upper limit check anyway, and limit it up to
+		 * MAX_FEATURES (which is still far more than is actually needed).
+		 */
+		if (param->features > MAX_FEATURES)
+			param->features = MAX_FEATURES;
+
+		/* success if returns 0 */
+		ret = libbpf_ethtool_parse_feature_strings(tbnn[ETHTOOL_A_STRINGSET_STRINGS],
+							   param->features, &param->xdp_idx,
+							   &param->xdp_zc_idx);
+	}
+
+	return ret;
+}
+
+int libbpf_ethnl_get_ethtool_family_id(struct ethnl_params *param)
+{
+	struct ethnl_msg req = {
+		.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct genlmsghdr)),
+		.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,
+		.nlh.nlmsg_type = GENL_ID_CTRL,
+		.nlh.nlmsg_pid = 0,
+		.genlhdr.version = ETHTOOL_GENL_VERSION,
+		.genlhdr.cmd = CTRL_CMD_GETFAMILY,
+		.genlhdr.reserved = 0,
+	};
+	struct nlattr *tb[__CTRL_ATTR_MAX + 1] = {0};
+	struct nlattr *nla, *nla_next;
+	int ret = -1;
+	int len;
+
+	memset(&req.msg, 0, BUF_SIZE_4096);
+	param->fam_id = GENL_ID_CTRL;
+
+	nla = (struct nlattr *)req.msg;
+	nla_next = libbpf_nla_put_str(nla, CTRL_ATTR_FAMILY_NAME, param->nl_family, GENL_NAMSIZ);
+	req.nlh.nlmsg_len += libbpf_nla_attrs_length(nla, nla_next);
+
+	len = libbpf_ethnl_send_recv(&req, param);
+	if (len < 0)
+		return len;
+
+	/* set parsing error, and change if succeeded */
+	ret = -LIBBPF_ERRNO__NLPARSE;
+	len = len - NLMSG_HDRLEN - GENL_HDRLEN;
+	if (!libbpf_nla_parse(tb, __CTRL_ATTR_MAX, nla, len, NULL)) {
+		if (tb[CTRL_ATTR_FAMILY_ID]) {
+			param->fam_id = libbpf_nla_getattr_u16(tb[CTRL_ATTR_FAMILY_ID]);
+			ret = 0;
+		}
+	}
+
+	return ret;
+}
+
+int libbpf_ethnl_get_active_bits(struct ethnl_params *param)
+{
+	struct ethnl_msg req = {
+		.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct genlmsghdr)),
+		.nlh.nlmsg_flags = NLM_F_REQUEST,
+		.nlh.nlmsg_type = param->fam_id,
+		.nlh.nlmsg_pid = 0,
+		.genlhdr.cmd = ETHTOOL_MSG_FEATURES_GET,
+		.genlhdr.version = ETHTOOL_GENL_VERSION,
+		.genlhdr.reserved = 0,
+	};
+	__u32  active[FEATURE_BITS_TO_BLOCKS(param->features)];
+	struct nlattr *tb[__ETHTOOL_A_FEATURES_CNT + 1];
+	struct nlattr *tbn[__ETHTOOL_A_BITSET_CNT + 1];
+	int flags = ETHTOOL_FLAG_COMPACT_BITSETS;
+	struct nlattr *nla, *nla_next;
+	int ret = -1;
+	int len;
+
+	memset(&req.msg, 0, BUF_SIZE_4096);
+
+	nla = (struct nlattr *)req.msg;
+	nla_next = libbpf_nla_nest_start(nla, ETHTOOL_A_FEATURES_HEADER);
+	nla_next = libbpf_nla_put_str(nla_next, ETHTOOL_A_HEADER_DEV_NAME, param->ifname, IFNAMSIZ);
+	nla_next = libbpf_nla_put_u32(nla_next, ETHTOOL_A_HEADER_FLAGS, flags);
+	libbpf_nla_nest_end(nla, nla_next);
+	req.nlh.nlmsg_len += libbpf_nla_attrs_length(nla, nla_next);
+
+	len = libbpf_ethnl_send_recv(&req, param);
+	if (len < 0)
+		return len;
+
+	ret = -LIBBPF_ERRNO__NLPARSE;
+	nla = (struct nlattr *)req.msg;
+	len = len - NLMSG_HDRLEN - GENL_HDRLEN;
+	if (libbpf_nla_parse(tb, __ETHTOOL_A_FEATURES_CNT, nla, len, NULL))
+		return ret;
+
+	if (!tb[ETHTOOL_A_FEATURES_ACTIVE])
+		return ret;
+
+	if (libbpf_nla_parse_nested(tbn, __ETHTOOL_A_BITSET_CNT,
+				    tb[ETHTOOL_A_FEATURES_ACTIVE], NULL))
+		return ret;
+
+	if (!tbn[ETHTOOL_A_BITSET_VALUE])
+		return ret;
+
+	for (unsigned int i = 0; i < FEATURE_BITS_TO_BLOCKS(param->features); ++i)
+		active[i]  = libbpf_nla_getattr_u32(tbn[ETHTOOL_A_BITSET_VALUE] + i);
+
+	/* mark successful parsing */
+	ret = 0;
+	if (FEATURE_BIT_IS_SET(active, param->xdp_idx)) {
+		param->xdp_flags = 1;
+		if (FEATURE_BIT_IS_SET(active, param->xdp_zc_idx))
+			param->xdp_zc_flags = 1;
+	} else {
+		/* zero copy without driver mode makes no sense */
+		if (FEATURE_BIT_IS_SET(active, param->xdp_zc_idx))
+			ret = -LIBBPF_ERRNO__INVXDP;
+	}
+
+	return ret;
+}
