diff mbox

[RFC,v2] ethtool: implement [GS]FEATURES handling

Message ID 08e06fc4d7b6e4dba466422a7cc93b2e2630c978.1327739670.git.mirq-linux@rere.qmqm.pl
State Not Applicable, archived
Delegated to: David Miller
Headers show

Commit Message

Michał Mirosław Jan. 28, 2012, 8:37 a.m. UTC
v2: fix missing change CMDL_FLAG -> CMDL_BOOL for old offloads parsing

Signed-off-by: Michał Mirosław <mirq-linux@rere.qmqm.pl>
---
 ethtool.c |  613 ++++++++++++++++++++++++++++++++++++++++++++++++------------
 1 files changed, 490 insertions(+), 123 deletions(-)

Comments

Eric Dumazet Jan. 28, 2012, 10:02 a.m. UTC | #1
Le samedi 28 janvier 2012 à 09:37 +0100, Michał Mirosław a écrit :
> v2: fix missing change CMDL_FLAG -> CMDL_BOOL for old offloads parsing
> 
> Signed-off-by: Michał Mirosław <mirq-linux@rere.qmqm.pl>
> ---
>  ethtool.c |  613 ++++++++++++++++++++++++++++++++++++++++++++++++------------
>  1 files changed, 490 insertions(+), 123 deletions(-)

Seems fine to me, here is the result for the new section :

I can now 'see' my tg3 doesnt support tx-tcp6-segmentation without
having to read driver source...

Extended offload state for eth3:
tx-scatter-gather: on (changeable)
tx-checksum-ipv4: on (changeable)
tx-checksum-ip-generic: off (driver-controlled)
tx-checksum-ipv6: off (driver-controlled)
highdma: on (changeable)
tx-scatter-gather-fraglist: off (driver-controlled)
tx-vlan-hw-insert: on (changeable)
rx-vlan-hw-parse: on (changeable)
rx-vlan-filter: off (driver-controlled)
vlan-challenged: off (fixed)
tx-generic-segmentation: on (changeable)
tx-lockless: off (fixed)
netns-local: off (fixed)
rx-gro: on (changeable)
rx-lro: off (driver-controlled)
tx-tcp-segmentation: on (changeable)
tx-udp-fragmentation: off (driver-controlled)
tx-gso-robust: off (driver-controlled)
tx-tcp-ecn-segmentation: off (driver-controlled)
tx-tcp6-segmentation: off (driver-controlled)
tx-fcoe-segmentation: off (driver-controlled)
tx-checksum-fcoe-crc: off (driver-controlled)
tx-checksum-sctp: off (driver-controlled)
fcoe-mtu: off (driver-controlled)
rx-ntuple-filter: off (driver-controlled)
rx-hashing: off (driver-controlled)
rx-checksum: on (changeable)
tx-nocache-copy: on (changeable)
loopback: off (changeable)

Thanks !


--
To unsubscribe from this list: send the line "unsubscribe netdev" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
diff mbox

Patch

diff --git a/ethtool.c b/ethtool.c
index d0cc7d4..8df2ce8 100644
--- a/ethtool.c
+++ b/ethtool.c
@@ -1036,9 +1036,15 @@  static int dump_coalesce(const struct ethtool_coalesce *ecoal)
 	return 0;
 }
 
-static int dump_offload(int rx, int tx, int sg, int tso, int ufo, int gso,
-			int gro, int lro, int rxvlan, int txvlan, int ntuple,
-			int rxhash)
+struct offload_state {
+	int rx, tx, sg, tso, ufo, gso, gro, lro, rxvlan, txvlan, ntuple, rxhash;
+};
+
+static const char *const old_feature_names[] = {
+	"rx", "tx", "sg", "tso", "ufo", "gso", "gro", "lro", "rxvlan", "txvlan", "ntuple", "rxhash"
+};
+
+static int dump_offload(const struct offload_state *offload)
 {
 	fprintf(stdout,
 		"rx-checksumming: %s\n"
@@ -1053,18 +1059,18 @@  static int dump_offload(int rx, int tx, int sg, int tso, int ufo, int gso,
 		"tx-vlan-offload: %s\n"
 		"ntuple-filters: %s\n"
 		"receive-hashing: %s\n",
-		rx ? "on" : "off",
-		tx ? "on" : "off",
-		sg ? "on" : "off",
-		tso ? "on" : "off",
-		ufo ? "on" : "off",
-		gso ? "on" : "off",
-		gro ? "on" : "off",
-		lro ? "on" : "off",
-		rxvlan ? "on" : "off",
-		txvlan ? "on" : "off",
-		ntuple ? "on" : "off",
-		rxhash ? "on" : "off");
+		offload->rx ? "on" : "off",
+		offload->tx ? "on" : "off",
+		offload->sg ? "on" : "off",
+		offload->tso ? "on" : "off",
+		offload->ufo ? "on" : "off",
+		offload->gso ? "on" : "off",
+		offload->gro ? "on" : "off",
+		offload->lro ? "on" : "off",
+		offload->rxvlan ? "on" : "off",
+		offload->txvlan ? "on" : "off",
+		offload->ntuple ? "on" : "off",
+		offload->rxhash ? "on" : "off");
 
 	return 0;
 }
@@ -1547,24 +1553,20 @@  static int do_scoalesce(struct cmd_context *ctx)
 	return 0;
 }
 
-static int do_goffload(struct cmd_context *ctx)
+static int send_goffloads(struct cmd_context *ctx,
+	struct offload_state *offload)
 {
 	struct ethtool_value eval;
-	int err, allfail = 1, rx = 0, tx = 0, sg = 0;
-	int tso = 0, ufo = 0, gso = 0, gro = 0, lro = 0, rxvlan = 0, txvlan = 0,
-	    ntuple = 0, rxhash = 0;
-
-	if (ctx->argc != 0)
-		exit_bad_args();
+	int err, allfail = 1;
 
-	fprintf(stdout, "Offload parameters for %s:\n", ctx->devname);
+	memset(offload, 0, sizeof(*offload));
 
 	eval.cmd = ETHTOOL_GRXCSUM;
 	err = send_ioctl(ctx, &eval);
 	if (err)
 		perror("Cannot get device rx csum settings");
 	else {
-		rx = eval.data;
+		offload->rx = eval.data;
 		allfail = 0;
 	}
 
@@ -1573,7 +1575,7 @@  static int do_goffload(struct cmd_context *ctx)
 	if (err)
 		perror("Cannot get device tx csum settings");
 	else {
-		tx = eval.data;
+		offload->tx = eval.data;
 		allfail = 0;
 	}
 
@@ -1582,7 +1584,7 @@  static int do_goffload(struct cmd_context *ctx)
 	if (err)
 		perror("Cannot get device scatter-gather settings");
 	else {
-		sg = eval.data;
+		offload->sg = eval.data;
 		allfail = 0;
 	}
 
@@ -1591,7 +1593,7 @@  static int do_goffload(struct cmd_context *ctx)
 	if (err)
 		perror("Cannot get device tcp segmentation offload settings");
 	else {
-		tso = eval.data;
+		offload->tso = eval.data;
 		allfail = 0;
 	}
 
@@ -1600,7 +1602,7 @@  static int do_goffload(struct cmd_context *ctx)
 	if (err)
 		perror("Cannot get device udp large send offload settings");
 	else {
-		ufo = eval.data;
+		offload->ufo = eval.data;
 		allfail = 0;
 	}
 
@@ -1609,7 +1611,7 @@  static int do_goffload(struct cmd_context *ctx)
 	if (err)
 		perror("Cannot get device generic segmentation offload settings");
 	else {
-		gso = eval.data;
+		offload->gso = eval.data;
 		allfail = 0;
 	}
 
@@ -1618,11 +1620,11 @@  static int do_goffload(struct cmd_context *ctx)
 	if (err) {
 		perror("Cannot get device flags");
 	} else {
-		lro = (eval.data & ETH_FLAG_LRO) != 0;
-		rxvlan = (eval.data & ETH_FLAG_RXVLAN) != 0;
-		txvlan = (eval.data & ETH_FLAG_TXVLAN) != 0;
-		ntuple = (eval.data & ETH_FLAG_NTUPLE) != 0;
-		rxhash = (eval.data & ETH_FLAG_RXHASH) != 0;
+		offload->lro = (eval.data & ETH_FLAG_LRO) != 0;
+		offload->rxvlan = (eval.data & ETH_FLAG_RXVLAN) != 0;
+		offload->txvlan = (eval.data & ETH_FLAG_TXVLAN) != 0;
+		offload->ntuple = (eval.data & ETH_FLAG_NTUPLE) != 0;
+		offload->rxhash = (eval.data & ETH_FLAG_RXHASH) != 0;
 		allfail = 0;
 	}
 
@@ -1631,7 +1633,7 @@  static int do_goffload(struct cmd_context *ctx)
 	if (err)
 		perror("Cannot get device GRO settings");
 	else {
-		gro = eval.data;
+		offload->gro = eval.data;
 		allfail = 0;
 	}
 
@@ -1640,144 +1642,509 @@  static int do_goffload(struct cmd_context *ctx)
 		return 83;
 	}
 
-	return dump_offload(rx, tx, sg, tso, ufo, gso, gro, lro, rxvlan, txvlan,
-			    ntuple, rxhash);
+	return 0;
 }
 
-static int do_soffload(struct cmd_context *ctx)
+static int send_soffloads(struct cmd_context *ctx,
+	const struct offload_state *wanted)
 {
-	int goffload_changed = 0;
-	int off_csum_rx_wanted = -1;
-	int off_csum_tx_wanted = -1;
-	int off_sg_wanted = -1;
-	int off_tso_wanted = -1;
-	int off_ufo_wanted = -1;
-	int off_gso_wanted = -1;
+	struct ethtool_value eval;
+	int changed = 0, err;
 	u32 off_flags_wanted = 0;
 	u32 off_flags_mask = 0;
-	int off_gro_wanted = -1;
-	struct cmdline_info cmdline_offload[] = {
-		{ "rx", CMDL_BOOL, &off_csum_rx_wanted, NULL },
-		{ "tx", CMDL_BOOL, &off_csum_tx_wanted, NULL },
-		{ "sg", CMDL_BOOL, &off_sg_wanted, NULL },
-		{ "tso", CMDL_BOOL, &off_tso_wanted, NULL },
-		{ "ufo", CMDL_BOOL, &off_ufo_wanted, NULL },
-		{ "gso", CMDL_BOOL, &off_gso_wanted, NULL },
-		{ "lro", CMDL_FLAG, &off_flags_wanted, NULL,
-		  ETH_FLAG_LRO, &off_flags_mask },
-		{ "gro", CMDL_BOOL, &off_gro_wanted, NULL },
-		{ "rxvlan", CMDL_FLAG, &off_flags_wanted, NULL,
-		  ETH_FLAG_RXVLAN, &off_flags_mask },
-		{ "txvlan", CMDL_FLAG, &off_flags_wanted, NULL,
-		  ETH_FLAG_TXVLAN, &off_flags_mask },
-		{ "ntuple", CMDL_FLAG, &off_flags_wanted, NULL,
-		  ETH_FLAG_NTUPLE, &off_flags_mask },
-		{ "rxhash", CMDL_FLAG, &off_flags_wanted, NULL,
-		  ETH_FLAG_RXHASH, &off_flags_mask },
-	};
-	struct ethtool_value eval;
-	int err, changed = 0;
 
-	parse_generic_cmdline(ctx, &goffload_changed,
-			      cmdline_offload, ARRAY_SIZE(cmdline_offload));
+	if (wanted->lro >= 0) {
+		off_flags_mask |= ETH_FLAG_LRO;
+		if (wanted->lro)
+			off_flags_wanted |= ETH_FLAG_LRO;
+	}
+
+	if (wanted->rxvlan >= 0) {
+		off_flags_mask |= ETH_FLAG_RXVLAN;
+		if (wanted->rxvlan)
+			off_flags_wanted |= ETH_FLAG_RXVLAN;
+	}
 
-	if (off_csum_rx_wanted >= 0) {
-		changed = 1;
+	if (wanted->txvlan >= 0) {
+		off_flags_mask |= ETH_FLAG_TXVLAN;
+		if (wanted->txvlan)
+			off_flags_wanted |= ETH_FLAG_TXVLAN;
+	}
+
+	if (wanted->ntuple >= 0) {
+		off_flags_mask |= ETH_FLAG_NTUPLE;
+		if (wanted->ntuple)
+			off_flags_wanted |= ETH_FLAG_NTUPLE;
+	}
+
+	if (wanted->rxhash >= 0) {
+		off_flags_mask |= ETH_FLAG_RXHASH;
+		if (wanted->rxhash)
+			off_flags_wanted |= ETH_FLAG_RXHASH;
+	}
+
+	if (wanted->rx >= 0) {
 		eval.cmd = ETHTOOL_SRXCSUM;
-		eval.data = (off_csum_rx_wanted == 1);
+		eval.data = !!wanted->rx;
 		err = send_ioctl(ctx, &eval);
-		if (err) {
+		if (err)
 			perror("Cannot set device rx csum settings");
-			return 84;
-		}
+		else
+			changed = 1;
 	}
 
-	if (off_csum_tx_wanted >= 0) {
-		changed = 1;
+	if (wanted->tx >= 0) {
 		eval.cmd = ETHTOOL_STXCSUM;
-		eval.data = (off_csum_tx_wanted == 1);
+		eval.data = !!wanted->tx;
 		err = send_ioctl(ctx, &eval);
-		if (err) {
+		if (err)
 			perror("Cannot set device tx csum settings");
-			return 85;
-		}
+		else
+			changed = 1;
 	}
 
-	if (off_sg_wanted >= 0) {
-		changed = 1;
+	if (wanted->sg >= 0) {
 		eval.cmd = ETHTOOL_SSG;
-		eval.data = (off_sg_wanted == 1);
+		eval.data = !!wanted->sg;
 		err = send_ioctl(ctx, &eval);
-		if (err) {
+		if (err)
 			perror("Cannot set device scatter-gather settings");
-			return 86;
-		}
+		else
+			changed = 1;
 	}
 
-	if (off_tso_wanted >= 0) {
-		changed = 1;
+	if (wanted->tso >= 0) {
 		eval.cmd = ETHTOOL_STSO;
-		eval.data = (off_tso_wanted == 1);
+		eval.data = !!wanted->tso;
 		err = send_ioctl(ctx, &eval);
-		if (err) {
+		if (err)
 			perror("Cannot set device tcp segmentation offload settings");
-			return 88;
-		}
+		else
+			changed = 1;
 	}
-	if (off_ufo_wanted >= 0) {
-		changed = 1;
+	if (wanted->ufo >= 0) {
 		eval.cmd = ETHTOOL_SUFO;
-		eval.data = (off_ufo_wanted == 1);
+		eval.data = !!wanted->ufo;
 		err = send_ioctl(ctx, &eval);
-		if (err) {
+		if (err)
 			perror("Cannot set device udp large send offload settings");
-			return 89;
-		}
+		else
+			changed = 1;
 	}
-	if (off_gso_wanted >= 0) {
-		changed = 1;
+	if (wanted->gso >= 0) {
 		eval.cmd = ETHTOOL_SGSO;
-		eval.data = (off_gso_wanted == 1);
+		eval.data = !!wanted->gso;
 		err = send_ioctl(ctx, &eval);
-		if (err) {
+		if (err)
 			perror("Cannot set device generic segmentation offload settings");
-			return 90;
-		}
+		else
+			changed = 1;
 	}
 	if (off_flags_mask) {
-		changed = 1;
 		eval.cmd = ETHTOOL_GFLAGS;
 		eval.data = 0;
 		err = send_ioctl(ctx, &eval);
-		if (err) {
+		if (err)
 			perror("Cannot get device flag settings");
-			return 91;
-		}
-
-		eval.cmd = ETHTOOL_SFLAGS;
-		eval.data = ((eval.data & ~off_flags_mask) |
-			     off_flags_wanted);
-
-		err = send_ioctl(ctx, &eval);
-		if (err) {
-			perror("Cannot set device flag settings");
-			return 92;
+		else {
+			eval.cmd = ETHTOOL_SFLAGS;
+			eval.data = ((eval.data & ~off_flags_mask) |
+				     off_flags_wanted);
+
+			err = send_ioctl(ctx, &eval);
+			if (err)
+				perror("Cannot set device flag settings");
+			else
+				changed = 1;
 		}
 	}
-	if (off_gro_wanted >= 0) {
-		changed = 1;
+	if (wanted->gro >= 0) {
 		eval.cmd = ETHTOOL_SGRO;
-		eval.data = (off_gro_wanted == 1);
+		eval.data = !!wanted->gro;
 		err = send_ioctl(ctx, &eval);
-		if (err) {
+		if (err)
 			perror("Cannot set device GRO settings");
-			return 93;
+		else
+			changed = 1;
+	}
+
+	return changed;
+}
+
+static int n_feature_strings;
+static const char **feature_strings;
+
+static int init_feature_strings(struct cmd_context *ctx)
+{
+	struct ethtool_gstrings *strings;
+	int i, n;
+
+	if (feature_strings)
+		return n_feature_strings;
+
+	strings = get_stringset(ctx, ETH_SS_FEATURES, 0);
+	if (!strings)
+		return -100;
+
+	n = n_feature_strings = strings->len;
+	feature_strings = calloc(n, sizeof(*feature_strings));
+	if (!feature_strings) {
+		fprintf(stderr, "no memory available for string table [size=%d]\n", n);
+		exit(95);
+	}
+
+	for (i = 0; i < n; ++i) {
+		if (!strings->data[i*ETH_GSTRING_LEN])
+			continue;
+
+		feature_strings[i] = strndup(
+			(const char *)&strings->data[i * ETH_GSTRING_LEN],
+			ETH_GSTRING_LEN);
+
+		if (!feature_strings[i]) {
+			fprintf(stderr, "no memory available for a string\n");
+			exit(95);
 		}
 	}
 
+	free(strings);
+	return n;
+}
+
+static void parse_sfeatures_args(struct cmd_context *ctx,
+	struct offload_state *offload,
+	struct ethtool_sfeatures **features_req_p)
+{
+	struct ethtool_sfeatures *features_req;
+	struct cmdline_info *cmdline_desc, *cp;
+	int sz_features, i;
+	int changed = 0;
+
+	struct cmdline_info cmdline_offload[] = {
+		{ "rx", CMDL_BOOL, &offload->rx, NULL },
+		{ "tx", CMDL_BOOL, &offload->tx, NULL },
+		{ "sg", CMDL_BOOL, &offload->sg, NULL },
+		{ "tso", CMDL_BOOL, &offload->tso, NULL },
+		{ "ufo", CMDL_BOOL, &offload->ufo, NULL },
+		{ "gso", CMDL_BOOL, &offload->gso, NULL },
+		{ "lro", CMDL_BOOL, &offload->lro, NULL },
+		{ "gro", CMDL_BOOL, &offload->gro, NULL },
+		{ "rxvlan", CMDL_BOOL, &offload->rxvlan, NULL },
+		{ "txvlan", CMDL_BOOL, &offload->txvlan, NULL },
+		{ "ntuple", CMDL_BOOL, &offload->ntuple, NULL },
+		{ "rxhash", CMDL_BOOL, &offload->rxhash, NULL },
+	};
+
+	for (i = 0; i < ARRAY_SIZE(old_feature_names); ++i)
+		((int *)offload)[i] = -1;
+	*features_req_p = NULL;
+
+	if (init_feature_strings(ctx) < 0) {
+		/* ETHTOOL_GFEATURES unavailable */
+		parse_generic_cmdline(ctx, &changed,
+			cmdline_offload, ARRAY_SIZE(cmdline_offload));
+		return;
+	}
+
+	sz_features = sizeof(*features_req->features) * ((n_feature_strings + 31) / 32);
+
+	cp = cmdline_desc = calloc(n_feature_strings + ARRAY_SIZE(cmdline_offload),
+		sizeof(*cmdline_desc));
+	memcpy(cp, cmdline_offload, sizeof(cmdline_offload));
+	cp += ARRAY_SIZE(cmdline_offload);
+
+	features_req = calloc(1, sizeof(*features_req) + sz_features);
+	if (!cmdline_desc || !features_req) {
+		fprintf(stderr, "no memory available\n");
+		exit(95);
+	}
+
+	features_req->size = (n_feature_strings + 31) / 32;
+
+	for (i = 0; i < n_feature_strings; ++i) {
+		if (!feature_strings[i])
+			continue;
+
+		cp->name = feature_strings[i];
+		cp->type = CMDL_FLAG;
+		cp->flag_val = 1 << (i % 32);
+		cp->wanted_val = &features_req->features[i / 32].requested;
+		cp->seen_val = &features_req->features[i / 32].valid;
+		++cp;
+	}
+
+	parse_generic_cmdline(ctx, &changed, cmdline_desc, cp - cmdline_desc);
+
+	free(cmdline_desc);
+
+	if (changed)
+		*features_req_p = features_req;
+	else
+		free(features_req);
+}
+
+static int send_gfeatures(struct cmd_context *ctx,
+	struct ethtool_gfeatures **features_p)
+{
+	struct ethtool_gfeatures *features;
+	int err, sz_features;
+
+	sz_features = sizeof(*features->features) * ((n_feature_strings + 31) / 32);
+	features = calloc(1, sz_features + sizeof(*features));
+	if (!features) {
+		fprintf(stderr, "no memory available\n");
+		return 95;
+	}
+
+	features->cmd = ETHTOOL_GFEATURES;
+	features->size = (n_feature_strings + 31) / 32;
+	err = send_ioctl(ctx, features);
+
+	if (err < 0) {
+		perror("Cannot get feature status");
+		free(features);
+		return 97;
+	}
+
+	*features_p = features;
+	return 0;
+}
+
+static const char *get_feature_state(struct ethtool_get_features_block *gfb,
+	uint32_t bit)
+{
+	if (gfb->never_changed & bit)
+		return "fixed";
+	if (!(gfb->available & bit))
+		return "driver-controlled";
+
+	if ((gfb->active ^ gfb->requested) & bit)
+		return (gfb->requested & bit) ? "requested on" : "requested off";
+	else
+		return "changeable";
+}
+
+static int do_gfeatures(struct cmd_context *ctx)
+{
+	struct ethtool_get_features_block *gfb;
+	struct ethtool_gfeatures *features;
+	uint32_t bit;
+	int err, i;
+
+	err = init_feature_strings(ctx);
+	if (err < 0)
+		return -err;
+
+	err = send_gfeatures(ctx, &features);
+	if (err)
+		return err;
+
+	fprintf(stdout, "\nExtended offload state for %s:\n", ctx->devname);
+	for (i = 0; i < n_feature_strings; i++) {
+		if (!feature_strings[i])
+			continue;	/* empty */
+
+		gfb = features->features + i / 32;
+		bit = 1 << (i % 32);
+
+		fprintf(stdout, "%s: %s (%s)\n", feature_strings[i],
+			(gfb->active & bit) ? "on" : "off",
+			get_feature_state(gfb, bit));
+	}
+	free(features);
+
+	return 0;
+}
+
+static void print_gfeatures_diff(
+	const struct ethtool_get_features_block *expected,
+	const struct ethtool_get_features_block *set,
+	const char **strings, int n_strings)
+{
+	int i;
+
+	if (n_strings > 32)
+		n_strings = 32;
+
+	for (i = 0; i < n_strings; ++i) {
+		u32 mask = 1 << i;
+
+		if (!strings[i])
+			continue;
+
+		if (!((expected->active ^ set->active) & mask))
+			continue;
+
+		fprintf(stdout, "feature %.*s is %s (expected: %s, saved: %s)\n",
+			ETH_GSTRING_LEN, strings[i],
+			set->active & mask ? "enabled" : "disabled",
+			expected->active & mask ? "enabled" : "disabled",
+			!(set->available & mask) ? "not user-changeable" :
+				set->requested & mask ? "enabled" : "disabled"
+		);
+	}
+}
+
+static int get_offload_state(struct cmd_context *ctx,
+	struct ethtool_gfeatures **features,
+	struct offload_state *offload)
+{
+	int err, allfail;
+
+	if (ctx->argc != 0)
+		exit_bad_args();
+
+	allfail = send_goffloads(ctx, offload);
+
+	err = init_feature_strings(ctx);
+	if (err < 0)
+		return allfail ? err : 0;
+
+	err = send_gfeatures(ctx, features);
+	if (err)
+		perror("Cannot read features");
+
+	return allfail ? -err : 0;
+}
+
+static int send_sfeatures(struct cmd_context *ctx,
+	struct ethtool_sfeatures *features)
+{
+	int err;
+
+	features->cmd = ETHTOOL_SFEATURES;
+	err = send_ioctl(ctx, features);
+	if (err < 0) {
+		perror("Cannot change features");
+		return 97;
+	}
+
+	return 0;
+}
+
+static void compare_offload_state(struct offload_state *offload0,
+	struct offload_state *offload_req, struct offload_state *offload1)
+{
+	int *old = (int *)offload0;
+	int *req = (int *)offload_req;
+	int *new = (int *)offload1;
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(old_feature_names); i++) {
+		if (req[i] < 0)
+			req[i] = old[i];
+		if (req[i] == new[i])
+			continue;
+
+		fprintf(stdout, "feature group %s is %s (expected: %s)\n",
+			old_feature_names[i],
+			new[i] ? "enabled" : "disabled",
+			req[i] ? "enabled" : "disabled"
+		);
+	}
+}
+
+static void compare_features(struct ethtool_gfeatures *features0,
+	struct ethtool_sfeatures *features_req,
+	struct ethtool_gfeatures *features1)
+{
+	int i;
+
+	if (features_req) {
+		/* make features0 .active what we expect to be set */
+		i = (n_feature_strings + 31) / 32;
+		while (i--) {
+			features0->features[i].active &= ~features_req->features[i].valid;
+			features0->features[i].active |=
+				features_req->features[i].requested &
+				features_req->features[i].valid;
+		}
+	}
+
+	for (i = 0; i < n_feature_strings; i += 32)
+		print_gfeatures_diff(&features0->features[i / 32],
+			&features1->features[i / 32],
+			feature_strings + i,
+			n_feature_strings - i);
+}
+
+static int do_goffload(struct cmd_context *ctx)
+{
+	struct offload_state offload;
+	int err, allfail;
+
+	allfail = send_goffloads(ctx, &offload);
+
+	if (!allfail) {
+		fprintf(stdout, "Offload parameters for %s:\n", ctx->devname);
+
+		dump_offload(&offload);
+	}
+
+	err = do_gfeatures(ctx);
+	if (!err)
+		allfail = 0;
+
+	if (allfail) {
+		fprintf(stdout, "no offload info available\n");
+		return 83;
+	}
+
+	return 0;
+}
+
+static int do_soffload(struct cmd_context *ctx)
+{
+	struct ethtool_gfeatures *features_old, *features_new;
+	struct ethtool_sfeatures *features_req;
+	struct offload_state offload_old, offload_new, offload_req;
+	int err, changed;
+
+	parse_sfeatures_args(ctx, &offload_req, &features_req);
+
+	err = get_offload_state(ctx, &features_old, &offload_old);
+	if (err)
+		return -err;
+
+	changed = send_soffloads(ctx, &offload_req);
+
+	if (features_req) {
+		err = send_sfeatures(ctx, features_req);
+		if (!err)
+			changed = 1;
+	}
+
 	if (!changed) {
 		fprintf(stdout, "no offload settings changed\n");
+		return err;
+	}
+
+	err = get_offload_state(ctx, &features_new, &offload_new);
+	if (err) {
+		perror("can't verify offload setting");
+		return 101;
+	}
+
+	if ((!features_old) ^ (!features_new)) {
+		fprintf(stderr, "can't compare features (one GFEATURES failed)\n");
+		if (features_old) {
+			free(features_old);
+			features_old = NULL;
+		}
+		if (features_new) {
+			free(features_new);
+			features_new = NULL;
+		}
+	}
+
+	compare_offload_state(&offload_old, &offload_req, &offload_new);
+	if (features_old) {
+		compare_features(features_old, features_req, features_new);
+		free(features_old);
+		free(features_new);
 	}
+	if (features_req)
+		free(features_req);
 
 	return 0;
 }