diff --git a/common.c b/common.c
index 59897acd2512..db9a360ffae6 100644
--- a/common.c
+++ b/common.c
@@ -283,3 +283,33 @@ void dump_mdix(u8 mdix, u8 mdix_ctrl)
 		fprintf(stdout, "\n");
 	}
 }
+
+const struct off_flag_def off_flag_def[OFF_FLAG_DEF_SIZE] = {
+	{ "rx",     "rx-checksumming",		    "rx-checksum",
+	  ETHTOOL_GRXCSUM, ETHTOOL_SRXCSUM, ETH_FLAG_RXCSUM,	0 },
+	{ "tx",     "tx-checksumming",		    "tx-checksum-*",
+	  ETHTOOL_GTXCSUM, ETHTOOL_STXCSUM, ETH_FLAG_TXCSUM,	0 },
+	{ "sg",     "scatter-gather",		    "tx-scatter-gather*",
+	  ETHTOOL_GSG,	   ETHTOOL_SSG,     ETH_FLAG_SG,	0 },
+	{ "tso",    "tcp-segmentation-offload",	    "tx-tcp*-segmentation",
+	  ETHTOOL_GTSO,	   ETHTOOL_STSO,    ETH_FLAG_TSO,	0 },
+	{ "ufo",    "udp-fragmentation-offload",    "tx-udp-fragmentation",
+	  ETHTOOL_GUFO,	   ETHTOOL_SUFO,    ETH_FLAG_UFO,	0 },
+	{ "gso",    "generic-segmentation-offload", "tx-generic-segmentation",
+	  ETHTOOL_GGSO,	   ETHTOOL_SGSO,    ETH_FLAG_GSO,	0 },
+	{ "gro",    "generic-receive-offload",	    "rx-gro",
+	  ETHTOOL_GGRO,	   ETHTOOL_SGRO,    ETH_FLAG_GRO,	0 },
+	{ "lro",    "large-receive-offload",	    "rx-lro",
+	  0,		   0,		    ETH_FLAG_LRO,
+	  KERNEL_VERSION(2,6,24) },
+	{ "rxvlan", "rx-vlan-offload",		    "rx-vlan-hw-parse",
+	  0,		   0,		    ETH_FLAG_RXVLAN,
+	  KERNEL_VERSION(2,6,37) },
+	{ "txvlan", "tx-vlan-offload",		    "tx-vlan-hw-insert",
+	  0,		   0,		    ETH_FLAG_TXVLAN,
+	  KERNEL_VERSION(2,6,37) },
+	{ "ntuple", "ntuple-filters",		    "rx-ntuple-filter",
+	  0,		   0,		    ETH_FLAG_NTUPLE,	0 },
+	{ "rxhash", "receive-hashing",		    "rx-hashing",
+	  0,		   0,		    ETH_FLAG_RXHASH,	0 },
+};
diff --git a/common.h b/common.h
index a980b844a0ed..36f96ddaa3bd 100644
--- a/common.h
+++ b/common.h
@@ -1,6 +1,8 @@
 #ifndef ETHTOOL_COMMON_H__
 #define ETHTOOL_COMMON_H__
 
+#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))
+
 struct flag_info {
 	const char *name;
 	u32 value;
@@ -25,6 +27,25 @@ struct link_mode_info {
 	u8			duplex;
 };
 
+struct off_flag_def {
+	const char *short_name;
+	const char *long_name;
+	const char *kernel_name;
+	u32 get_cmd, set_cmd;
+	u32 value;
+	/* For features exposed through ETHTOOL_GFLAGS, the oldest
+	 * kernel version for which we can trust the result.  Where
+	 * the flag was added at the same time the kernel started
+	 * supporting the feature, this is 0 (to allow for backports).
+	 * Where the feature was supported before the flag was added,
+	 * it is the version that introduced the flag.
+	 */
+	u32 min_kernel_ver;
+};
+
+#define OFF_FLAG_DEF_SIZE 12
+extern const struct off_flag_def off_flag_def[OFF_FLAG_DEF_SIZE];
+
 extern const struct link_mode_info link_modes[];
 extern const unsigned int link_modes_count;
 
diff --git a/ethtool.c b/ethtool.c
index 0b40b96cf9bf..b29086bbabde 100644
--- a/ethtool.c
+++ b/ethtool.c
@@ -61,8 +61,6 @@
 #define NETLINK_GENERIC	16
 #endif
 
-#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c))
-
 static void exit_bad_args(void) __attribute__((noreturn));
 
 static void exit_bad_args(void)
@@ -104,51 +102,6 @@ struct cmdline_info {
 	void *seen_val;
 };
 
-struct off_flag_def {
-	const char *short_name;
-	const char *long_name;
-	const char *kernel_name;
-	u32 get_cmd, set_cmd;
-	u32 value;
-	/* For features exposed through ETHTOOL_GFLAGS, the oldest
-	 * kernel version for which we can trust the result.  Where
-	 * the flag was added at the same time the kernel started
-	 * supporting the feature, this is 0 (to allow for backports).
-	 * Where the feature was supported before the flag was added,
-	 * it is the version that introduced the flag.
-	 */
-	u32 min_kernel_ver;
-};
-static const struct off_flag_def off_flag_def[] = {
-	{ "rx",     "rx-checksumming",		    "rx-checksum",
-	  ETHTOOL_GRXCSUM, ETHTOOL_SRXCSUM, ETH_FLAG_RXCSUM,	0 },
-	{ "tx",     "tx-checksumming",		    "tx-checksum-*",
-	  ETHTOOL_GTXCSUM, ETHTOOL_STXCSUM, ETH_FLAG_TXCSUM,	0 },
-	{ "sg",     "scatter-gather",		    "tx-scatter-gather*",
-	  ETHTOOL_GSG,	   ETHTOOL_SSG,     ETH_FLAG_SG,	0 },
-	{ "tso",    "tcp-segmentation-offload",	    "tx-tcp*-segmentation",
-	  ETHTOOL_GTSO,	   ETHTOOL_STSO,    ETH_FLAG_TSO,	0 },
-	{ "ufo",    "udp-fragmentation-offload",    "tx-udp-fragmentation",
-	  ETHTOOL_GUFO,	   ETHTOOL_SUFO,    ETH_FLAG_UFO,	0 },
-	{ "gso",    "generic-segmentation-offload", "tx-generic-segmentation",
-	  ETHTOOL_GGSO,	   ETHTOOL_SGSO,    ETH_FLAG_GSO,	0 },
-	{ "gro",    "generic-receive-offload",	    "rx-gro",
-	  ETHTOOL_GGRO,	   ETHTOOL_SGRO,    ETH_FLAG_GRO,	0 },
-	{ "lro",    "large-receive-offload",	    "rx-lro",
-	  0,		   0,		    ETH_FLAG_LRO,
-	  KERNEL_VERSION(2,6,24) },
-	{ "rxvlan", "rx-vlan-offload",		    "rx-vlan-hw-parse",
-	  0,		   0,		    ETH_FLAG_RXVLAN,
-	  KERNEL_VERSION(2,6,37) },
-	{ "txvlan", "tx-vlan-offload",		    "tx-vlan-hw-insert",
-	  0,		   0,		    ETH_FLAG_TXVLAN,
-	  KERNEL_VERSION(2,6,37) },
-	{ "ntuple", "ntuple-filters",		    "rx-ntuple-filter",
-	  0,		   0,		    ETH_FLAG_NTUPLE,	0 },
-	{ "rxhash", "receive-hashing",		    "rx-hashing",
-	  0,		   0,		    ETH_FLAG_RXHASH,	0 },
-};
-
 struct feature_def {
 	char name[ETH_GSTRING_LEN];
 	int off_flag_index; /* index in off_flag_def; negative if none match */
@@ -157,7 +110,7 @@ struct feature_def {
 struct feature_defs {
 	size_t n_features;
 	/* Number of features each offload flag is associated with */
-	unsigned int off_flag_matched[ARRAY_SIZE(off_flag_def)];
+	unsigned int off_flag_matched[OFF_FLAG_DEF_SIZE];
 	/* Name and offload flag index for each feature */
 	struct feature_def def[0];
 };
@@ -1325,7 +1278,7 @@ static void dump_features(const struct feature_defs *defs,
 	int indent;
 	int i, j;
 
-	for (i = 0; i < ARRAY_SIZE(off_flag_def); i++) {
+	for (i = 0; i < OFF_FLAG_DEF_SIZE; i++) {
 		/* Don't show features whose state is unknown on this
 		 * kernel version
 		 */
@@ -1640,7 +1593,7 @@ static struct feature_defs *get_feature_defs(struct cmd_context *ctx)
 		defs->def[i].off_flag_index = -1;
 
 		for (j = 0;
-		     j < ARRAY_SIZE(off_flag_def) &&
+		     j < OFF_FLAG_DEF_SIZE &&
 			     defs->def[i].off_flag_index < 0;
 		     j++) {
 			const char *pattern =
@@ -2082,7 +2035,7 @@ get_features(struct cmd_context *ctx, const struct feature_defs *defs)
 
 	state->off_flags = 0;
 
-	for (i = 0; i < ARRAY_SIZE(off_flag_def); i++) {
+	for (i = 0; i < OFF_FLAG_DEF_SIZE; i++) {
 		value = off_flag_def[i].value;
 		if (!off_flag_def[i].get_cmd)
 			continue;
@@ -2198,14 +2151,14 @@ static int do_sfeatures(struct cmd_context *ctx)
 	/* Generate cmdline_info for legacy flags and kernel-named
 	 * features, and parse our arguments.
 	 */
-	cmdline_features = calloc(ARRAY_SIZE(off_flag_def) + defs->n_features,
+	cmdline_features = calloc(OFF_FLAG_DEF_SIZE + defs->n_features,
 				  sizeof(cmdline_features[0]));
 	if (!cmdline_features) {
 		perror("Cannot parse arguments");
 		rc = 1;
 		goto err;
 	}
-	for (i = 0; i < ARRAY_SIZE(off_flag_def); i++)
+	for (i = 0; i < OFF_FLAG_DEF_SIZE; i++)
 		flag_to_cmdline_info(off_flag_def[i].short_name,
 				     off_flag_def[i].value,
 				     &off_flags_wanted, &off_flags_mask,
@@ -2215,9 +2168,9 @@ static int do_sfeatures(struct cmd_context *ctx)
 			defs->def[i].name, FEATURE_FIELD_FLAG(i),
 			&FEATURE_WORD(efeatures->features, i, requested),
 			&FEATURE_WORD(efeatures->features, i, valid),
-			&cmdline_features[ARRAY_SIZE(off_flag_def) + i]);
+			&cmdline_features[OFF_FLAG_DEF_SIZE + i]);
 	parse_generic_cmdline(ctx, &any_changed, cmdline_features,
-			      ARRAY_SIZE(off_flag_def) + defs->n_features);
+			      OFF_FLAG_DEF_SIZE + defs->n_features);
 	free(cmdline_features);
 
 	if (!any_changed) {
@@ -2237,7 +2190,7 @@ static int do_sfeatures(struct cmd_context *ctx)
 		 * related features that the user did not specify and that
 		 * are not fixed.  Warn if all related features are fixed.
 		 */
-		for (i = 0; i < ARRAY_SIZE(off_flag_def); i++) {
+		for (i = 0; i < OFF_FLAG_DEF_SIZE; i++) {
 			int fixed = 1;
 
 			if (!(off_flags_mask & off_flag_def[i].value))
@@ -2278,7 +2231,7 @@ static int do_sfeatures(struct cmd_context *ctx)
 			goto err;
 		}
 	} else {
-		for (i = 0; i < ARRAY_SIZE(off_flag_def); i++) {
+		for (i = 0; i < OFF_FLAG_DEF_SIZE; i++) {
 			if (!off_flag_def[i].set_cmd)
 				continue;
 			if (off_flags_mask & off_flag_def[i].value) {
@@ -4924,6 +4877,7 @@ static int show_usage(struct cmd_context *ctx);
 #define nl_gdrv		NULL
 #define nl_gset		NULL
 #define nl_sset		NULL
+#define nl_gfeatures	NULL
 #endif
 
 static const struct option {
@@ -4988,7 +4942,7 @@ static const struct option {
 	  "		[ rx-mini N ]\n"
 	  "		[ rx-jumbo N ]\n"
 	  "		[ tx N ]\n" },
-	{ "-k|--show-features|--show-offload", 1, do_gfeatures, NULL,
+	{ "-k|--show-features|--show-offload", 1, do_gfeatures, nl_gfeatures,
 	  "Get state of protocol offload and other features" },
 	{ "-K|--features|--offload", 1, do_sfeatures, NULL,
 	  "Set protocol offload and other features",
diff --git a/netlink/extapi.h b/netlink/extapi.h
index 66573f0b4304..c656ff862687 100644
--- a/netlink/extapi.h
+++ b/netlink/extapi.h
@@ -16,6 +16,7 @@ int netlink_done(struct cmd_context *ctx);
 int nl_gdrv(struct cmd_context *ctx);
 int nl_gset(struct cmd_context *ctx);
 int nl_sset(struct cmd_context *ctx);
+int nl_gfeatures(struct cmd_context *ctx);
 int nl_monitor(struct cmd_context *ctx);
 
 void monitor_usage();
diff --git a/netlink/monitor.c b/netlink/monitor.c
index 32d842611011..e3b8bb46f561 100644
--- a/netlink/monitor.c
+++ b/netlink/monitor.c
@@ -127,6 +127,12 @@ static struct monitor_option monitor_opts[] = {
 				  ETH_SETTINGS_IM_WOLINFO |
 				  ETH_SETTINGS_IM_LINK,
 	},
+	{
+		.pattern	= "-k|--show-features|--show-offload"
+				  "|-K|--features|--offload",
+		.cmd		= ETHNL_CMD_SET_SETTINGS,
+		.info_mask	= ETH_SETTINGS_IM_FEATURES,
+	},
 };
 
 static bool pattern_match(const char *s, const char *pattern)
diff --git a/netlink/settings.c b/netlink/settings.c
index b077a96325e5..51a01b400224 100644
--- a/netlink/settings.c
+++ b/netlink/settings.c
@@ -5,6 +5,7 @@
 #include "../internal.h"
 #include "../common.h"
 #include "netlink.h"
+#include "strset.h"
 #include "parser.h"
 
 /* GET_SETTINGS */
@@ -34,6 +35,219 @@ err:
 	return ret;
 }
 
+uint32_t *get_compact_bitset_value(const struct nlattr *bitset)
+{
+        const struct nlattr *tb[ETHA_BITSET_MAX + 1] = {};
+        DECLARE_ATTR_TB_INFO(tb);
+	unsigned int count;
+        int ret;
+
+        ret = mnl_attr_parse_nested(bitset, attr_cb, &tb_info);
+        if (ret < 0 || !tb[ETHA_BITSET_SIZE] || !tb[ETHA_BITSET_VALUE])
+		return NULL;
+	count = mnl_attr_get_u32(tb[ETHA_BITSET_SIZE]);
+	if (32 * mnl_attr_get_payload_len(tb[ETHA_BITSET_VALUE]) < count)
+		return NULL;
+
+	return mnl_attr_get_payload(tb[ETHA_BITSET_VALUE]);
+}
+
+uint32_t *get_compact_bitset_mask(const struct nlattr *bitset)
+{
+        const struct nlattr *tb[ETHA_BITSET_MAX + 1] = {};
+        DECLARE_ATTR_TB_INFO(tb);
+	unsigned int count;
+        int ret;
+
+        ret = mnl_attr_parse_nested(bitset, attr_cb, &tb_info);
+        if (ret < 0 || !tb[ETHA_BITSET_SIZE] || !tb[ETHA_BITSET_MASK])
+		return NULL;
+	count = mnl_attr_get_u32(tb[ETHA_BITSET_SIZE]);
+	if (32 * mnl_attr_get_payload_len(tb[ETHA_BITSET_MASK]) < count)
+		return NULL;
+
+	return mnl_attr_get_payload(tb[ETHA_BITSET_MASK]);
+}
+
+struct feature_results {
+	uint32_t	*hw;
+	uint32_t	*wanted;
+	uint32_t	*active;
+	uint32_t	*nochange;
+	unsigned int	count;
+	unsigned int	words;
+};
+
+static int prepare_feature_results(const struct nlattr *bitset,
+				   struct feature_results *dest)
+{
+	const struct nlattr *tb[ETHA_FEATURES_MAX + 1] = {};
+	DECLARE_ATTR_TB_INFO(tb);
+	unsigned int count;
+	int ret;
+
+	memset(dest, '\0', sizeof(*dest));
+	ret = mnl_attr_parse_nested(bitset, attr_cb, &tb_info);
+	if (ret < 0)
+		return ret;
+	if (!tb[ETHA_FEATURES_HW] || !tb[ETHA_FEATURES_WANTED] ||
+	    !tb[ETHA_FEATURES_ACTIVE] || !tb[ETHA_FEATURES_NOCHANGE])
+		return -EFAULT;
+	count = bitset_get_count(tb[ETHA_FEATURES_HW], &ret);
+	if (ret < 0)
+		return -EFAULT;
+	if ((bitset_get_count(tb[ETHA_FEATURES_WANTED], &ret) != count) ||
+	    (bitset_get_count(tb[ETHA_FEATURES_ACTIVE], &ret) != count) ||
+	    (bitset_get_count(tb[ETHA_FEATURES_NOCHANGE], &ret) != count))
+		return -EFAULT;
+	dest->hw = get_compact_bitset_value(tb[ETHA_FEATURES_HW]);
+	dest->wanted = get_compact_bitset_value(tb[ETHA_FEATURES_WANTED]);
+	dest->active = get_compact_bitset_value(tb[ETHA_FEATURES_ACTIVE]);
+	dest->nochange = get_compact_bitset_value(tb[ETHA_FEATURES_NOCHANGE]);
+	if (!dest->hw || !dest->wanted || !dest->active || !dest->nochange)
+		return -EFAULT;
+	dest->count = count;
+	dest->words = (count + 31) / 32;
+
+	return 0;
+}
+
+static bool feature_on(const uint32_t *bitmap, unsigned int idx)
+{
+	return bitmap[idx / 32] & (1 << (idx % 32));
+}
+
+static void dump_feature(const struct feature_results *results,
+			 const uint32_t *ref, const uint32_t *ref_mask,
+			 unsigned int idx, const char *name, const char *prefix)
+{
+	const char *suffix = "";
+
+	if (!name || !*name)
+		return;
+	if (ref) {
+		if (ref_mask && !feature_on(ref_mask, idx))
+			return;
+		if ((!ref_mask || feature_on(ref_mask, idx)) &&
+		    (feature_on(results->active, idx) == feature_on(ref, idx)))
+			return;
+	}
+
+	if (!feature_on(results->hw, idx) || feature_on(results->nochange, idx))
+		suffix = " [fixed]";
+	else if (feature_on(results->active, idx) !=
+		 feature_on(results->wanted, idx))
+		suffix = feature_on(results->wanted, idx) ?
+			 " [requested on]" : " [requested off]";
+	printf("%s%s: %s%s\n", prefix, name,
+	       feature_on(results->active, idx) ? "on" : "off", suffix);
+}
+
+/* this assumes pattern contains no more than one asterisk */
+static bool _flag_pattern_match(const char *name, const char *pattern)
+{
+	const char *p_ast = strchr(pattern, '*');
+
+	if (p_ast) {
+		size_t name_len = strlen(name);
+		size_t pattern_len = strlen(pattern);
+
+		if (name_len + 1 < pattern_len)
+			return false;
+		if (strncmp(name, pattern, p_ast - pattern))
+			return false;
+		pattern_len -= (p_ast - pattern) + 1;
+		name += name_len  - pattern_len;
+		pattern = p_ast + 1;
+	}
+	return !strcmp(name, pattern);
+}
+
+static bool flag_pattern_match(const char *name, const char *pattern)
+{
+	bool ret = _flag_pattern_match(name, pattern);
+
+	return ret;
+}
+
+int dump_features(const struct nlattr *bitset)
+{
+	const struct stringset *feature_names;
+	struct feature_results results;
+	unsigned int i, j;
+	int *feature_flags = NULL;
+	int ret;
+
+	ret = prepare_feature_results(bitset, &results);
+	if (ret < 0)
+		return -EFAULT;
+
+	ret = -ENOMEM;
+	feature_flags = calloc(results.count, sizeof(feature_flags[0]));
+	if (!feature_flags)
+	       goto out_free;
+	feature_names = global_stringset(ETH_SS_FEATURES);
+
+	/* map netdev features to legacy flags */
+	for (i = 0; i < results.count; i++) {
+		const char *name = get_string(feature_names, i);
+		feature_flags[i] = -1;
+
+		if (!name || !*name)
+			continue;
+		for (j = 0; j < OFF_FLAG_DEF_SIZE; j++) {
+			const char *flag_name = off_flag_def[j].kernel_name;
+
+			if (flag_pattern_match(name, flag_name)) {
+				feature_flags[i] = j;
+				break;
+			}
+		}
+	}
+	/* show legacy flags and their matching features first */
+	for (i = 0; i < OFF_FLAG_DEF_SIZE; i++) {
+		unsigned int n_match = 0;
+		bool flag_value = false;
+
+		for (j = 0; j < results.count; j++) {
+			if (feature_flags[j] == i) {
+				n_match++;
+				flag_value = flag_value ||
+					     feature_on(results.active, j);
+			}
+		}
+		if (n_match != 1)
+			printf("%s: %s\n", off_flag_def[i].long_name,
+			       flag_value ? "on" : "off");
+		if (n_match == 0)
+			continue;
+		for (j = 0; j < results.count; j++) {
+			const char *name = get_string(feature_names, j);
+
+			if (feature_flags[j] != i)
+				continue;
+			if (n_match == 1)
+				dump_feature(&results, NULL, NULL, j,
+					     off_flag_def[i].long_name, "");
+			else
+				dump_feature(&results, NULL, NULL, j, name,
+					     "\t");
+		}
+	}
+	/* and, finally, remaining netdev_features not matching legacy flags */
+	for (i = 0; i < results.count; i++) {
+		const char *name = get_string(feature_names, i);
+
+		if (!name || !*name || feature_flags[i] >= 0)
+			continue;
+		dump_feature(&results, NULL, NULL, i, name, "");
+	}
+
+out_free:
+	free(feature_flags);
+	return 0;
+}
+
 int settings_reply_cb(const struct nlmsghdr *nlhdr, void *data)
 {
 	const struct nlattr *tb[ETHA_SETTINGS_MAX + 1] = {};
@@ -262,6 +476,15 @@ int settings_reply_cb(const struct nlmsghdr *nlhdr, void *data)
 		printf("\tLink detected: %s\n", link ? "yes" : "no");
 		allfail = false;
 	};
+	if (tb[ETHA_SETTINGS_FEATURES] &&
+	    mask_ok(nlctx, ETH_SETTINGS_IM_FEATURES)) {
+		int ret;
+
+		printf("Features for %s:\n", nlctx->devname);
+		ret = dump_features(tb[ETHA_SETTINGS_FEATURES]);
+		if (ret == 0)
+			allfail = false;
+	}
 
 	if (allfail && !nlctx->is_monitor) {
 		fputs("No data available\n", stdout);
@@ -299,6 +522,9 @@ int nl_gset(struct cmd_context *ctx)
 				     ETH_SETTINGS_IM_LINK);
 }
 
+int nl_gfeatures(struct cmd_context *ctx)
+{
+	return settings_request(ctx, ETH_SETTINGS_IM_FEATURES);
 }
 
 /* SET_SETTINGS */
