diff --git a/drivers/net/sfc/sfc.h b/drivers/net/sfc/sfc.h
index 079216c1fb..4ff3af6aaa 100644
--- a/drivers/net/sfc/sfc.h
+++ b/drivers/net/sfc/sfc.h
@@ -27,6 +27,7 @@
 #include "sfc_debug.h"
 #include "sfc_log.h"
 #include "sfc_filter.h"
+#include "sfc_flow_tunnel.h"
 #include "sfc_sriov.h"
 #include "sfc_mae.h"
 #include "sfc_dp.h"
@@ -258,6 +259,8 @@ struct sfc_adapter {
 	struct sfc_intr			intr;
 	struct sfc_port			port;
 	struct sfc_sw_xstats		sw_xstats;
+	/* Registry of tunnel offload contexts */
+	struct sfc_flow_tunnel		flow_tunnels[SFC_FT_MAX_NTUNNELS];
 	struct sfc_filter		filter;
 	struct sfc_mae			mae;
 
diff --git a/drivers/net/sfc/sfc_flow.c b/drivers/net/sfc/sfc_flow.c
index fb91ce8dbe..d07425f61e 100644
--- a/drivers/net/sfc/sfc_flow.c
+++ b/drivers/net/sfc/sfc_flow.c
@@ -2549,15 +2549,46 @@ sfc_flow_parse_rte_to_mae(struct rte_eth_dev *dev,
 	struct sfc_flow_spec_mae *spec_mae = &spec->mae;
 	int rc;
 
+	/*
+	 * If the flow is meant to be a JUMP rule in tunnel offload,
+	 * preparse its actions and save its properties in spec_mae.
+	 */
+	rc = sfc_flow_tunnel_detect_jump_rule(sa, actions, spec_mae, error);
+	if (rc != 0)
+		goto fail;
+
 	rc = sfc_mae_rule_parse_pattern(sa, pattern, spec_mae, error);
 	if (rc != 0)
-		return rc;
+		goto fail;
+
+	if (spec_mae->ft_rule_type == SFC_FT_RULE_JUMP) {
+		/*
+		 * This flow is represented solely by the outer rule.
+		 * It is supposed to mark and count matching packets.
+		 */
+		goto skip_action_rule;
+	}
 
 	rc = sfc_mae_rule_parse_actions(sa, actions, spec_mae, error);
 	if (rc != 0)
-		return rc;
+		goto fail;
+
+skip_action_rule:
+	if (spec_mae->ft != NULL) {
+		if (spec_mae->ft_rule_type == SFC_FT_RULE_JUMP)
+			spec_mae->ft->jump_rule_is_set = B_TRUE;
+
+		++(spec_mae->ft->refcnt);
+	}
 
 	return 0;
+
+fail:
+	/* Reset these values to avoid confusing sfc_mae_flow_cleanup(). */
+	spec_mae->ft_rule_type = SFC_FT_RULE_NONE;
+	spec_mae->ft = NULL;
+
+	return rc;
 }
 
 static int
diff --git a/drivers/net/sfc/sfc_flow.h b/drivers/net/sfc/sfc_flow.h
index 3435d53ba4..ada3d563ad 100644
--- a/drivers/net/sfc/sfc_flow.h
+++ b/drivers/net/sfc/sfc_flow.h
@@ -63,8 +63,20 @@ struct sfc_flow_spec_filter {
 	struct sfc_flow_rss rss_conf;
 };
 
+/* Indicates the role of a given flow in tunnel offload */
+enum sfc_flow_tunnel_rule_type {
+	/* The flow has nothing to do with tunnel offload */
+	SFC_FT_RULE_NONE = 0,
+	/* The flow represents a JUMP rule */
+	SFC_FT_RULE_JUMP,
+};
+
 /* MAE-specific flow specification */
 struct sfc_flow_spec_mae {
+	/* FLow Tunnel (FT) rule type (or NONE) */
+	enum sfc_flow_tunnel_rule_type	ft_rule_type;
+	/* Flow Tunnel (FT) context (or NULL) */
+	struct sfc_flow_tunnel		*ft;
 	/* Desired priority level */
 	unsigned int			priority;
 	/* Outer rule registry entry */
diff --git a/drivers/net/sfc/sfc_flow_tunnel.c b/drivers/net/sfc/sfc_flow_tunnel.c
index 2a0cf299d1..8e02e52d81 100644
--- a/drivers/net/sfc/sfc_flow_tunnel.c
+++ b/drivers/net/sfc/sfc_flow_tunnel.c
@@ -7,6 +7,7 @@
 #include <stdint.h>
 
 #include "sfc.h"
+#include "sfc_flow.h"
 #include "sfc_dp_rx.h"
 #include "sfc_flow_tunnel.h"
 #include "sfc_mae.h"
@@ -28,3 +29,118 @@ sfc_flow_tunnel_is_active(struct sfc_adapter *sa)
 	return ((sa->negotiated_rx_metadata &
 		 RTE_ETH_RX_METADATA_TUNNEL_ID) != 0);
 }
+
+struct sfc_flow_tunnel *
+sfc_flow_tunnel_pick(struct sfc_adapter *sa, uint32_t ft_mark)
+{
+	uint32_t tunnel_mark = SFC_FT_GET_TUNNEL_MARK(ft_mark);
+
+	SFC_ASSERT(sfc_adapter_is_locked(sa));
+
+	if (tunnel_mark != SFC_FT_TUNNEL_MARK_INVALID) {
+		sfc_ft_id_t ft_id = SFC_FT_TUNNEL_MARK_TO_ID(tunnel_mark);
+		struct sfc_flow_tunnel *ft = &sa->flow_tunnels[ft_id];
+
+		ft->id = ft_id;
+
+		return ft;
+	}
+
+	return NULL;
+}
+
+int
+sfc_flow_tunnel_detect_jump_rule(struct sfc_adapter *sa,
+				 const struct rte_flow_action *actions,
+				 struct sfc_flow_spec_mae *spec,
+				 struct rte_flow_error *error)
+{
+	const struct rte_flow_action_mark *action_mark = NULL;
+	const struct rte_flow_action_jump *action_jump = NULL;
+	struct sfc_flow_tunnel *ft;
+	uint32_t ft_mark = 0;
+	int rc = 0;
+
+	SFC_ASSERT(sfc_adapter_is_locked(sa));
+
+	if (!sfc_flow_tunnel_is_active(sa)) {
+		/* Tunnel-related actions (if any) will be turned down later. */
+		return 0;
+	}
+
+	if (actions == NULL) {
+		rte_flow_error_set(error, EINVAL,
+				   RTE_FLOW_ERROR_TYPE_ACTION_NUM, NULL,
+				   "NULL actions");
+		return -rte_errno;
+	}
+
+	for (; actions->type != RTE_FLOW_ACTION_TYPE_END; ++actions) {
+		if (actions->type == RTE_FLOW_ACTION_TYPE_VOID)
+			continue;
+
+		if (actions->conf == NULL) {
+			rc = EINVAL;
+			continue;
+		}
+
+		switch (actions->type) {
+		case RTE_FLOW_ACTION_TYPE_MARK:
+			if (action_mark == NULL) {
+				action_mark = actions->conf;
+				ft_mark = action_mark->id;
+			} else {
+				rc = EINVAL;
+			}
+			break;
+		case RTE_FLOW_ACTION_TYPE_JUMP:
+			if (action_jump == NULL) {
+				action_jump = actions->conf;
+				if (action_jump->group != 0)
+					rc = EINVAL;
+			} else {
+				rc = EINVAL;
+			}
+			break;
+		default:
+			rc = ENOTSUP;
+			break;
+		}
+	}
+
+	ft = sfc_flow_tunnel_pick(sa, ft_mark);
+	if (ft != NULL && action_jump != 0) {
+		sfc_dbg(sa, "tunnel offload: JUMP: detected");
+
+		if (rc != 0) {
+			/* The loop above might have spotted wrong actions. */
+			sfc_err(sa, "tunnel offload: JUMP: invalid actions: %s",
+				strerror(rc));
+			goto fail;
+		}
+
+		if (ft->refcnt == 0) {
+			sfc_err(sa, "tunnel offload: JUMP: tunnel=%u does not exist",
+				ft->id);
+			rc = ENOENT;
+			goto fail;
+		}
+
+		if (ft->jump_rule_is_set) {
+			sfc_err(sa, "tunnel offload: JUMP: already exists in tunnel=%u",
+				ft->id);
+			rc = EEXIST;
+			goto fail;
+		}
+
+		spec->ft_rule_type = SFC_FT_RULE_JUMP;
+		spec->ft = ft;
+	}
+
+	return 0;
+
+fail:
+	return rte_flow_error_set(error, rc,
+				  RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				  "tunnel offload: JUMP: preparsing failed");
+}
diff --git a/drivers/net/sfc/sfc_flow_tunnel.h b/drivers/net/sfc/sfc_flow_tunnel.h
index fec891fdad..6a81b29438 100644
--- a/drivers/net/sfc/sfc_flow_tunnel.h
+++ b/drivers/net/sfc/sfc_flow_tunnel.h
@@ -10,6 +10,8 @@
 #include <stdbool.h>
 #include <stdint.h>
 
+#include "efx.h"
+
 #ifdef __cplusplus
 extern "C" {
 #endif
@@ -26,12 +28,41 @@ typedef uint8_t sfc_ft_id_t;
 #define SFC_FT_USER_MARK_MASK \
 	RTE_LEN2MASK(SFC_FT_USER_MARK_BITS, uint32_t)
 
+#define SFC_FT_GET_TUNNEL_MARK(_mark) \
+	((_mark) >> SFC_FT_USER_MARK_BITS)
+
+#define SFC_FT_TUNNEL_MARK_INVALID	(0)
+
+#define SFC_FT_TUNNEL_MARK_TO_ID(_tunnel_mark) \
+	((_tunnel_mark) - 1)
+
+#define SFC_FT_ID_TO_TUNNEL_MARK(_id) \
+	((_id) + 1)
+
+#define SFC_FT_MAX_NTUNNELS \
+	(RTE_LEN2MASK(SFC_FT_TUNNEL_MARK_BITS, uint8_t) - 1)
+
+struct sfc_flow_tunnel {
+	bool				jump_rule_is_set;
+	efx_tunnel_protocol_t		encap_type;
+	unsigned int			refcnt;
+	sfc_ft_id_t			id;
+};
+
 struct sfc_adapter;
 
 bool sfc_flow_tunnel_is_supported(struct sfc_adapter *sa);
 
 bool sfc_flow_tunnel_is_active(struct sfc_adapter *sa);
 
+struct sfc_flow_tunnel *sfc_flow_tunnel_pick(struct sfc_adapter *sa,
+					     uint32_t ft_mark);
+
+int sfc_flow_tunnel_detect_jump_rule(struct sfc_adapter *sa,
+				     const struct rte_flow_action *actions,
+				     struct sfc_flow_spec_mae *spec,
+				     struct rte_flow_error *error);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/drivers/net/sfc/sfc_mae.c b/drivers/net/sfc/sfc_mae.c
index c8659fa456..d8ebef4cc1 100644
--- a/drivers/net/sfc/sfc_mae.c
+++ b/drivers/net/sfc/sfc_mae.c
@@ -269,6 +269,9 @@ sfc_mae_outer_rule_enable(struct sfc_adapter *sa,
 		}
 	}
 
+	if (match_spec_action == NULL)
+		goto skip_action_rule;
+
 	rc = efx_mae_match_spec_outer_rule_id_set(match_spec_action,
 						  &fw_rsrc->rule_id);
 	if (rc != 0) {
@@ -283,6 +286,7 @@ sfc_mae_outer_rule_enable(struct sfc_adapter *sa,
 		return rc;
 	}
 
+skip_action_rule:
 	if (fw_rsrc->refcnt == 0) {
 		sfc_dbg(sa, "enabled outer_rule=%p: OR_ID=0x%08x",
 			rule, fw_rsrc->rule_id.id);
@@ -806,6 +810,14 @@ sfc_mae_flow_cleanup(struct sfc_adapter *sa,
 
 	spec_mae = &spec->mae;
 
+	if (spec_mae->ft != NULL) {
+		if (spec_mae->ft_rule_type == SFC_FT_RULE_JUMP)
+			spec_mae->ft->jump_rule_is_set = B_FALSE;
+
+		SFC_ASSERT(spec_mae->ft->refcnt != 0);
+		--(spec_mae->ft->refcnt);
+	}
+
 	SFC_ASSERT(spec_mae->rule_id.id == EFX_MAE_RSRC_ID_INVALID);
 
 	if (spec_mae->outer_rule != NULL)
@@ -2146,6 +2158,16 @@ sfc_mae_rule_process_outer(struct sfc_adapter *sa,
 	ctx->match_spec_outer = NULL;
 
 no_or_id:
+	switch (ctx->ft_rule_type) {
+	case SFC_FT_RULE_NONE:
+		break;
+	case SFC_FT_RULE_JUMP:
+		/* No action rule */
+		return 0;
+	default:
+		SFC_ASSERT(B_FALSE);
+	}
+
 	/*
 	 * In MAE, lookup sequence comprises outer parse, outer rule lookup,
 	 * inner parse (when some outer rule is hit) and action rule lookup.
@@ -2183,6 +2205,7 @@ sfc_mae_rule_encap_parse_init(struct sfc_adapter *sa,
 			      struct rte_flow_error *error)
 {
 	struct sfc_mae *mae = &sa->mae;
+	uint8_t recirc_id = 0;
 	int rc;
 
 	if (pattern == NULL) {
@@ -2222,34 +2245,71 @@ sfc_mae_rule_encap_parse_init(struct sfc_adapter *sa,
 		break;
 	}
 
-	if (pattern->type == RTE_FLOW_ITEM_TYPE_END)
-		return 0;
+	switch (ctx->ft_rule_type) {
+	case SFC_FT_RULE_NONE:
+		if (pattern->type == RTE_FLOW_ITEM_TYPE_END)
+			return 0;
+		break;
+	case SFC_FT_RULE_JUMP:
+		if (pattern->type != RTE_FLOW_ITEM_TYPE_END) {
+			return rte_flow_error_set(error, ENOTSUP,
+						  RTE_FLOW_ERROR_TYPE_ITEM,
+						  pattern, "tunnel offload: JUMP: invalid item");
+		}
+		ctx->encap_type = ctx->ft->encap_type;
+		break;
+	default:
+		SFC_ASSERT(B_FALSE);
+		break;
+	}
 
 	if ((mae->encap_types_supported & (1U << ctx->encap_type)) == 0) {
 		return rte_flow_error_set(error, ENOTSUP,
-					  RTE_FLOW_ERROR_TYPE_ITEM,
-					  pattern, "Unsupported tunnel item");
+					  RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+					  "OR: unsupported tunnel type");
 	}
 
-	if (ctx->priority >= mae->nb_outer_rule_prios_max) {
-		return rte_flow_error_set(error, ENOTSUP,
-					  RTE_FLOW_ERROR_TYPE_ATTR_PRIORITY,
-					  NULL, "Unsupported priority level");
-	}
+	switch (ctx->ft_rule_type) {
+	case SFC_FT_RULE_JUMP:
+		recirc_id = SFC_FT_ID_TO_TUNNEL_MARK(ctx->ft->id);
+		/* FALLTHROUGH */
+	case SFC_FT_RULE_NONE:
+		if (ctx->priority >= mae->nb_outer_rule_prios_max) {
+			return rte_flow_error_set(error, ENOTSUP,
+					RTE_FLOW_ERROR_TYPE_ATTR_PRIORITY,
+					NULL, "OR: unsupported priority level");
+		}
 
-	rc = efx_mae_match_spec_init(sa->nic, EFX_MAE_RULE_OUTER, ctx->priority,
-				     &ctx->match_spec_outer);
-	if (rc != 0) {
-		return rte_flow_error_set(error, rc,
-			RTE_FLOW_ERROR_TYPE_ITEM, pattern,
-			"Failed to initialise outer rule match specification");
-	}
+		rc = efx_mae_match_spec_init(sa->nic,
+					     EFX_MAE_RULE_OUTER, ctx->priority,
+					     &ctx->match_spec_outer);
+		if (rc != 0) {
+			return rte_flow_error_set(error, rc,
+				RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				"OR: failed to initialise the match specification");
+		}
+
+		/*
+		 * Outermost items comprise a match
+		 * specification of type OUTER.
+		 */
+		ctx->match_spec = ctx->match_spec_outer;
 
-	/* Outermost items comprise a match specification of type OUTER. */
-	ctx->match_spec = ctx->match_spec_outer;
+		/* Outermost items use "ENC" EFX MAE field IDs. */
+		ctx->field_ids_remap = field_ids_remap_to_encap;
 
-	/* Outermost items use "ENC" EFX MAE field IDs. */
-	ctx->field_ids_remap = field_ids_remap_to_encap;
+		rc = efx_mae_outer_rule_recirc_id_set(ctx->match_spec,
+						      recirc_id);
+		if (rc != 0) {
+			return rte_flow_error_set(error, rc,
+					RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+					"OR: failed to initialise RECIRC_ID");
+		}
+		break;
+	default:
+		SFC_ASSERT(B_FALSE);
+		break;
+	}
 
 	return 0;
 }
@@ -2276,17 +2336,29 @@ sfc_mae_rule_parse_pattern(struct sfc_adapter *sa,
 	int rc;
 
 	memset(&ctx_mae, 0, sizeof(ctx_mae));
+	ctx_mae.ft_rule_type = spec->ft_rule_type;
 	ctx_mae.priority = spec->priority;
+	ctx_mae.ft = spec->ft;
 	ctx_mae.sa = sa;
 
-	rc = efx_mae_match_spec_init(sa->nic, EFX_MAE_RULE_ACTION,
-				     spec->priority,
-				     &ctx_mae.match_spec_action);
-	if (rc != 0) {
-		rc = rte_flow_error_set(error, rc,
-			RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
-			"Failed to initialise action rule match specification");
-		goto fail_init_match_spec_action;
+	switch (ctx_mae.ft_rule_type) {
+	case SFC_FT_RULE_JUMP:
+		/* No action rule */
+		break;
+	case SFC_FT_RULE_NONE:
+		rc = efx_mae_match_spec_init(sa->nic, EFX_MAE_RULE_ACTION,
+					     spec->priority,
+					     &ctx_mae.match_spec_action);
+		if (rc != 0) {
+			rc = rte_flow_error_set(error, rc,
+				RTE_FLOW_ERROR_TYPE_UNSPECIFIED, NULL,
+				"AR: failed to initialise the match specification");
+			goto fail_init_match_spec_action;
+		}
+		break;
+	default:
+		SFC_ASSERT(B_FALSE);
+		break;
 	}
 
 	/*
@@ -2320,7 +2392,8 @@ sfc_mae_rule_parse_pattern(struct sfc_adapter *sa,
 	if (rc != 0)
 		goto fail_process_outer;
 
-	if (!efx_mae_match_spec_is_valid(sa->nic, ctx_mae.match_spec_action)) {
+	if (ctx_mae.match_spec_action != NULL &&
+	    !efx_mae_match_spec_is_valid(sa->nic, ctx_mae.match_spec_action)) {
 		rc = rte_flow_error_set(error, ENOTSUP,
 					RTE_FLOW_ERROR_TYPE_ITEM, NULL,
 					"Inconsistent pattern");
@@ -2338,7 +2411,8 @@ sfc_mae_rule_parse_pattern(struct sfc_adapter *sa,
 	sfc_mae_rule_encap_parse_fini(sa, &ctx_mae);
 
 fail_encap_parse_init:
-	efx_mae_match_spec_fini(sa->nic, ctx_mae.match_spec_action);
+	if (ctx_mae.match_spec_action != NULL)
+		efx_mae_match_spec_fini(sa->nic, ctx_mae.match_spec_action);
 
 fail_init_match_spec_action:
 	return rc;
@@ -3254,6 +3328,9 @@ sfc_mae_action_rule_class_verify(struct sfc_adapter *sa,
 {
 	const struct rte_flow *entry;
 
+	if (spec->match_spec == NULL)
+		return 0;
+
 	TAILQ_FOREACH_REVERSE(entry, &sa->flow_list, sfc_flow_list, entries) {
 		const struct sfc_flow_spec *entry_spec = &entry->spec;
 		const struct sfc_flow_spec_mae *es_mae = &entry_spec->mae;
@@ -3325,11 +3402,10 @@ sfc_mae_flow_insert(struct sfc_adapter *sa,
 	struct sfc_flow_spec_mae *spec_mae = &spec->mae;
 	struct sfc_mae_outer_rule *outer_rule = spec_mae->outer_rule;
 	struct sfc_mae_action_set *action_set = spec_mae->action_set;
-	struct sfc_mae_fw_rsrc *fw_rsrc = &action_set->fw_rsrc;
+	struct sfc_mae_fw_rsrc *fw_rsrc;
 	int rc;
 
 	SFC_ASSERT(spec_mae->rule_id.id == EFX_MAE_RSRC_ID_INVALID);
-	SFC_ASSERT(action_set != NULL);
 
 	if (outer_rule != NULL) {
 		rc = sfc_mae_outer_rule_enable(sa, outer_rule,
@@ -3338,6 +3414,11 @@ sfc_mae_flow_insert(struct sfc_adapter *sa,
 			goto fail_outer_rule_enable;
 	}
 
+	if (action_set == NULL) {
+		sfc_dbg(sa, "enabled flow=%p (no AR)", flow);
+		return 0;
+	}
+
 	rc = sfc_mae_action_set_enable(sa, action_set);
 	if (rc != 0)
 		goto fail_action_set_enable;
@@ -3351,6 +3432,8 @@ sfc_mae_flow_insert(struct sfc_adapter *sa,
 		}
 	}
 
+	fw_rsrc = &action_set->fw_rsrc;
+
 	rc = efx_mae_action_rule_insert(sa->nic, spec_mae->match_spec,
 					NULL, &fw_rsrc->aset_id,
 					&spec_mae->rule_id);
@@ -3384,8 +3467,12 @@ sfc_mae_flow_remove(struct sfc_adapter *sa,
 	struct sfc_mae_outer_rule *outer_rule = spec_mae->outer_rule;
 	int rc;
 
+	if (action_set == NULL) {
+		sfc_dbg(sa, "disabled flow=%p (no AR)", flow);
+		goto skip_action_rule;
+	}
+
 	SFC_ASSERT(spec_mae->rule_id.id != EFX_MAE_RSRC_ID_INVALID);
-	SFC_ASSERT(action_set != NULL);
 
 	rc = efx_mae_action_rule_remove(sa->nic, &spec_mae->rule_id);
 	if (rc != 0) {
@@ -3398,6 +3485,7 @@ sfc_mae_flow_remove(struct sfc_adapter *sa,
 
 	sfc_mae_action_set_disable(sa, action_set);
 
+skip_action_rule:
 	if (outer_rule != NULL)
 		sfc_mae_outer_rule_disable(sa, outer_rule);
 
@@ -3416,7 +3504,7 @@ sfc_mae_query_counter(struct sfc_adapter *sa,
 	unsigned int i;
 	int rc;
 
-	if (action_set->n_counters == 0) {
+	if (action_set == NULL || action_set->n_counters == 0) {
 		return rte_flow_error_set(error, EINVAL,
 			RTE_FLOW_ERROR_TYPE_ACTION, action,
 			"Queried flow rule does not have count actions");
diff --git a/drivers/net/sfc/sfc_mae.h b/drivers/net/sfc/sfc_mae.h
index 7e3b6a7a97..907e292dd1 100644
--- a/drivers/net/sfc/sfc_mae.h
+++ b/drivers/net/sfc/sfc_mae.h
@@ -285,9 +285,11 @@ struct sfc_mae_parse_ctx {
 	size_t				tunnel_def_mask_size;
 	const void			*tunnel_def_mask;
 	bool				match_mport_set;
+	enum sfc_flow_tunnel_rule_type	ft_rule_type;
 	struct sfc_mae_pattern_data	pattern_data;
 	efx_tunnel_protocol_t		encap_type;
 	unsigned int			priority;
+	struct sfc_flow_tunnel		*ft;
 };
 
 int sfc_mae_attach(struct sfc_adapter *sa);
