diff --git a/app/test-pmd/cmdline_flow.c b/app/test-pmd/cmdline_flow.c
index 890c36d8e..38aa6099d 100644
--- a/app/test-pmd/cmdline_flow.c
+++ b/app/test-pmd/cmdline_flow.c
@@ -167,6 +167,10 @@ enum index {
 	ACTION_DUP,
 	ACTION_DUP_INDEX,
 	ACTION_RSS,
+	ACTION_RSS_KEY,
+	ACTION_RSS_KEY_LEN,
+	ACTION_RSS_TYPES,
+	ACTION_RSS_TYPE,
 	ACTION_RSS_QUEUES,
 	ACTION_RSS_QUEUE,
 	ACTION_PF,
@@ -223,6 +227,9 @@ struct context {
 struct arg {
 	uint32_t hton:1; /**< Use network byte ordering. */
 	uint32_t sign:1; /**< Value is signed. */
+	uint32_t bounded:1; /**< Value is bounded. */
+	uintmax_t min; /**< Minimum value if bounded. */
+	uintmax_t max; /**< Maximum value if bounded. */
 	uint32_t offset; /**< Relative offset from ctx->object. */
 	uint32_t size; /**< Field size. */
 	const uint8_t *mask; /**< Bit-mask to use instead of offset/size. */
@@ -329,6 +336,16 @@ struct token {
 		.size = (s), \
 	})
 
+/** Same as ARGS_ENTRY_ARB() with bounded values. */
+#define ARGS_ENTRY_ARB_BOUNDED(o, s, i, a) \
+	(&(const struct arg){ \
+		.bounded = 1, \
+		.min = (i), \
+		.max = (a), \
+		.offset = (o), \
+		.size = (s), \
+	})
+
 /** Same as ARGS_ENTRY() using network byte ordering. */
 #define ARGS_ENTRY_HTON(s, f) \
 	(&(const struct arg){ \
@@ -635,6 +652,9 @@ static const enum index action_dup[] = {
 };
 
 static const enum index action_rss[] = {
+	ACTION_RSS_KEY,
+	ACTION_RSS_KEY_LEN,
+	ACTION_RSS_TYPES,
 	ACTION_RSS_QUEUES,
 	ACTION_NEXT,
 	ZERO,
@@ -666,6 +686,9 @@ static int parse_vc_conf(struct context *, const struct token *,
 static int parse_vc_action_rss(struct context *, const struct token *,
 			       const char *, unsigned int, void *,
 			       unsigned int);
+static int parse_vc_action_rss_type(struct context *, const struct token *,
+				    const char *, unsigned int, void *,
+				    unsigned int);
 static int parse_vc_action_rss_queue(struct context *, const struct token *,
 				     const char *, unsigned int, void *,
 				     unsigned int);
@@ -721,6 +744,8 @@ static int comp_port(struct context *, const struct token *,
 		     unsigned int, char *, unsigned int);
 static int comp_rule_id(struct context *, const struct token *,
 			unsigned int, char *, unsigned int);
+static int comp_vc_action_rss_type(struct context *, const struct token *,
+				   unsigned int, char *, unsigned int);
 static int comp_vc_action_rss_queue(struct context *, const struct token *,
 				    unsigned int, char *, unsigned int);
 
@@ -1593,6 +1618,43 @@ static const struct token token_list[] = {
 		.next = NEXT(action_rss),
 		.call = parse_vc_action_rss,
 	},
+	[ACTION_RSS_KEY] = {
+		.name = "key",
+		.help = "RSS hash key",
+		.next = NEXT(action_rss, NEXT_ENTRY(STRING)),
+		.args = ARGS(ARGS_ENTRY_ARB
+			     (((uintptr_t)&((union action_rss_data *)0)->
+			       s.rss_conf.rss_key_len),
+			      sizeof(((struct rte_eth_rss_conf *)0)->
+				     rss_key_len)),
+			     ARGS_ENTRY_ARB
+			     (((uintptr_t)((union action_rss_data *)0)->
+			       s.rss_key),
+			      RSS_HASH_KEY_LENGTH)),
+	},
+	[ACTION_RSS_KEY_LEN] = {
+		.name = "key_len",
+		.help = "RSS hash key length in bytes",
+		.next = NEXT(action_rss, NEXT_ENTRY(UNSIGNED)),
+		.args = ARGS(ARGS_ENTRY_ARB_BOUNDED
+			     (((uintptr_t)&((union action_rss_data *)0)->
+			       s.rss_conf.rss_key_len),
+			      sizeof(((struct rte_eth_rss_conf *)0)->
+				     rss_key_len),
+			      0,
+			      RSS_HASH_KEY_LENGTH)),
+	},
+	[ACTION_RSS_TYPES] = {
+		.name = "types",
+		.help = "RSS hash types",
+		.next = NEXT(action_rss, NEXT_ENTRY(ACTION_RSS_TYPE)),
+	},
+	[ACTION_RSS_TYPE] = {
+		.name = "{type}",
+		.help = "RSS hash type",
+		.call = parse_vc_action_rss_type,
+		.comp = comp_vc_action_rss_type,
+	},
 	[ACTION_RSS_QUEUES] = {
 		.name = "queues",
 		.help = "queue indices to use",
@@ -2079,6 +2141,50 @@ parse_vc_action_rss(struct context *ctx, const struct token *token,
 }
 
 /**
+ * Parse type field for RSS action.
+ *
+ * Valid tokens are type field names and the "end" token.
+ */
+static int
+parse_vc_action_rss_type(struct context *ctx, const struct token *token,
+			  const char *str, unsigned int len,
+			  void *buf, unsigned int size)
+{
+	static const enum index next[] = NEXT_ENTRY(ACTION_RSS_TYPE);
+	union action_rss_data *action_rss_data;
+	unsigned int i;
+
+	(void)token;
+	(void)buf;
+	(void)size;
+	if (ctx->curr != ACTION_RSS_TYPE)
+		return -1;
+	if (!(ctx->objdata >> 16) && ctx->object) {
+		action_rss_data = ctx->object;
+		action_rss_data->s.rss_conf.rss_hf = 0;
+	}
+	if (!strcmp_partial("end", str, len)) {
+		ctx->objdata &= 0xffff;
+		return len;
+	}
+	for (i = 0; rss_type_table[i].str; ++i)
+		if (!strcmp_partial(rss_type_table[i].str, str, len))
+			break;
+	if (!rss_type_table[i].str)
+		return -1;
+	ctx->objdata = 1 << 16 | (ctx->objdata & 0xffff);
+	/* Repeat token. */
+	if (ctx->next_num == RTE_DIM(ctx->next))
+		return -1;
+	ctx->next[ctx->next_num++] = next;
+	if (!ctx->object)
+		return len;
+	action_rss_data = ctx->object;
+	action_rss_data->s.rss_conf.rss_hf |= rss_type_table[i].rss_type;
+	return len;
+}
+
+/**
  * Parse queue field for RSS action.
  *
  * Valid tokens are queue indices and the "end" token.
@@ -2344,6 +2450,11 @@ parse_int(struct context *ctx, const struct token *token,
 		strtoumax(str, &end, 0);
 	if (errno || (size_t)(end - str) != len)
 		goto error;
+	if (arg->bounded &&
+	    ((arg->sign && ((intmax_t)u < (intmax_t)arg->min ||
+			    (intmax_t)u > (intmax_t)arg->max)) ||
+	     (!arg->sign && (u < arg->min || u > arg->max))))
+		goto error;
 	if (!ctx->object)
 		return len;
 	if (arg->mask) {
@@ -2437,7 +2548,7 @@ parse_string(struct context *ctx, const struct token *token,
 	buf = (uint8_t *)ctx->object + arg_data->offset;
 	/* Output buffer is not necessarily NUL-terminated. */
 	memcpy(buf, str, len);
-	memset((uint8_t *)buf + len, 0x55, size - len);
+	memset((uint8_t *)buf + len, 0x00, size - len);
 	if (ctx->objmask)
 		memset((uint8_t *)ctx->objmask + arg_data->offset, 0xff, len);
 	return len;
@@ -2733,6 +2844,26 @@ comp_rule_id(struct context *ctx, const struct token *token,
 	return i;
 }
 
+/** Complete type field for RSS action. */
+static int
+comp_vc_action_rss_type(struct context *ctx, const struct token *token,
+			unsigned int ent, char *buf, unsigned int size)
+{
+	unsigned int i;
+
+	(void)ctx;
+	(void)token;
+	for (i = 0; rss_type_table[i].str; ++i)
+		;
+	if (!buf)
+		return i + 1;
+	if (ent < i)
+		return snprintf(buf, size, "%s", rss_type_table[ent].str);
+	if (ent == i)
+		return snprintf(buf, size, "end");
+	return -1;
+}
+
 /** Complete queue field for RSS action. */
 static int
 comp_vc_action_rss_queue(struct context *ctx, const struct token *token,
diff --git a/app/test-pmd/config.c b/app/test-pmd/config.c
index a4b54f86a..7ade30f47 100644
--- a/app/test-pmd/config.c
+++ b/app/test-pmd/config.c
@@ -73,12 +73,7 @@ static const struct {
 	},
 };
 
-struct rss_type_info {
-	char str[32];
-	uint64_t rss_type;
-};
-
-static const struct rss_type_info rss_type_table[] = {
+const struct rss_type_info rss_type_table[] = {
 	{ "ipv4", ETH_RSS_IPV4 },
 	{ "ipv4-frag", ETH_RSS_FRAG_IPV4 },
 	{ "ipv4-tcp", ETH_RSS_NONFRAG_IPV4_TCP },
@@ -99,7 +94,12 @@ static const struct rss_type_info rss_type_table[] = {
 	{ "vxlan", ETH_RSS_VXLAN },
 	{ "geneve", ETH_RSS_GENEVE },
 	{ "nvgre", ETH_RSS_NVGRE },
-
+	{ "ip", ETH_RSS_IP },
+	{ "udp", ETH_RSS_UDP },
+	{ "tcp", ETH_RSS_TCP },
+	{ "sctp", ETH_RSS_SCTP },
+	{ "tunnel", ETH_RSS_TUNNEL },
+	{ NULL, 0 },
 };
 
 static void
@@ -1820,7 +1820,7 @@ port_rss_hash_conf_show(portid_t port_id, char rss_info[], int show_rss_key)
 	}
 
 	rss_conf.rss_hf = 0;
-	for (i = 0; i < RTE_DIM(rss_type_table); i++) {
+	for (i = 0; rss_type_table[i].str; i++) {
 		if (!strcmp(rss_info, rss_type_table[i].str))
 			rss_conf.rss_hf = rss_type_table[i].rss_type;
 	}
@@ -1849,7 +1849,7 @@ port_rss_hash_conf_show(portid_t port_id, char rss_info[], int show_rss_key)
 		return;
 	}
 	printf("RSS functions:\n ");
-	for (i = 0; i < RTE_DIM(rss_type_table); i++) {
+	for (i = 0; rss_type_table[i].str; i++) {
 		if (rss_hf & rss_type_table[i].rss_type)
 			printf("%s ", rss_type_table[i].str);
 	}
@@ -1873,7 +1873,7 @@ port_rss_hash_key_update(portid_t port_id, char rss_type[], uint8_t *hash_key,
 	rss_conf.rss_key = NULL;
 	rss_conf.rss_key_len = hash_key_len;
 	rss_conf.rss_hf = 0;
-	for (i = 0; i < RTE_DIM(rss_type_table); i++) {
+	for (i = 0; rss_type_table[i].str; i++) {
 		if (!strcmp(rss_type_table[i].str, rss_type))
 			rss_conf.rss_hf = rss_type_table[i].rss_type;
 	}
diff --git a/app/test-pmd/testpmd.h b/app/test-pmd/testpmd.h
index 153abea05..593ae2160 100644
--- a/app/test-pmd/testpmd.h
+++ b/app/test-pmd/testpmd.h
@@ -79,6 +79,19 @@ struct pkt_burst_stats {
 };
 #endif
 
+/** Information for a given RSS type. */
+struct rss_type_info {
+	const char *str; /**< Type name. */
+	uint64_t rss_type; /**< Type value. */
+};
+
+/**
+ * RSS type information table.
+ *
+ * An entry with a NULL type name terminates the list.
+ */
+extern const struct rss_type_info rss_type_table[];
+
 /**
  * The data structure associated with a forwarding stream between a receive
  * port/queue and a transmit port/queue.
diff --git a/doc/guides/testpmd_app_ug/testpmd_funcs.rst b/doc/guides/testpmd_app_ug/testpmd_funcs.rst
index a766ac795..f85628109 100644
--- a/doc/guides/testpmd_app_ug/testpmd_funcs.rst
+++ b/doc/guides/testpmd_app_ug/testpmd_funcs.rst
@@ -3406,6 +3406,14 @@ This section lists supported actions and their attributes, if any.
 
 - ``rss``: spread packets among several queues.
 
+  - ``key {string}``: RSS hash key, overrides ``key_len``.
+
+  - ``key_len {unsigned}``: RSS hash key length in bytes, can be used in
+    conjunction with ``key`` to pad or truncate it.
+
+  - ``types [{RSS hash type} [...]] end``: RSS hash types, allowed tokens
+    are the same as `set_hash_input_set`_, an empty list means none (0).
+
   - ``queues [{unsigned} [...]] end``: queue indices to use.
 
 - ``pf``: redirect packets to physical device function.
