diff --git a/include/expression.h b/include/expression.h
index 0a0e178fe468..8f9da1b1c7c9 100644
--- a/include/expression.h
+++ b/include/expression.h
@@ -407,6 +407,8 @@ extern struct expr *prefix_expr_alloc(const struct location *loc,
 extern struct expr *range_expr_alloc(const struct location *loc,
 				     struct expr *low, struct expr *high);
 
+extern struct expr *compound_expr_alloc(const struct location *loc,
+					const struct expr_ops *ops);
 extern void compound_expr_add(struct expr *compound, struct expr *expr);
 extern void compound_expr_remove(struct expr *compound, struct expr *expr);
 extern void list_expr_sort(struct list_head *head);
diff --git a/include/mnl.h b/include/mnl.h
index 4475e7f872d9..470b29787fa6 100644
--- a/include/mnl.h
+++ b/include/mnl.h
@@ -92,6 +92,10 @@ int mnl_nft_obj_batch_del(struct nftnl_obj *nln, struct nftnl_batch *batch,
 struct nftnl_flowtable_list *
 mnl_nft_flowtable_dump(struct netlink_ctx *ctx, int family, const char *table);
 
+int mnl_nft_flowtable_batch_add(struct nftnl_flowtable *flo,
+				struct nftnl_batch *batch, unsigned int flags,
+				uint32_t seqnum);
+
 struct nftnl_ruleset *mnl_nft_ruleset_dump(struct netlink_ctx *ctx,
 					   uint32_t family);
 int mnl_nft_event_listener(struct mnl_socket *nf_sock, unsigned int debug_mask,
diff --git a/include/netlink.h b/include/netlink.h
index bca59bbd9ebe..b80acbabe80f 100644
--- a/include/netlink.h
+++ b/include/netlink.h
@@ -7,6 +7,7 @@
 #include <libnftnl/expr.h>
 #include <libnftnl/set.h>
 #include <libnftnl/object.h>
+#include <libnftnl/flowtable.h>
 
 #include <linux/netlink.h>
 #include <linux/netfilter/nf_tables.h>
@@ -184,6 +185,9 @@ extern int netlink_delete_obj(struct netlink_ctx *ctx, const struct handle *h,
 extern int netlink_list_flowtables(struct netlink_ctx *ctx,
 				   const struct handle *h,
 				   const struct location *loc);
+extern int netlink_add_flowtable(struct netlink_ctx *ctx,
+				 const struct handle *h, struct flowtable *ft,
+				 uint32_t flags);
 
 extern void netlink_dump_chain(const struct nftnl_chain *nlc,
 			       struct netlink_ctx *ctx);
diff --git a/include/rule.h b/include/rule.h
index 5ee5389a6c36..de84cfc0ae64 100644
--- a/include/rule.h
+++ b/include/rule.h
@@ -320,10 +320,13 @@ uint32_t obj_type_to_cmd(uint32_t type);
 struct flowtable {
 	struct list_head	list;
 	struct handle		handle;
+	struct scope		scope;
 	struct location		location;
+	const char *		hookstr;
 	unsigned int		hooknum;
 	int			priority;
 	const char		**dev_array;
+	struct expr		*dev_expr;
 	int			dev_array_len;
 	unsigned int		refcnt;
 };
@@ -381,6 +384,8 @@ enum cmd_ops {
  * @CMD_OBJ_CHAIN:	chain
  * @CMD_OBJ_CHAINS:	multiple chains
  * @CMD_OBJ_TABLE:	table
+ * @CMD_OBJ_FLOWTABLE:	flowtable
+ * @CMD_OBJ_FLOWTABLES:	flowtables
  * @CMD_OBJ_RULESET:	ruleset
  * @CMD_OBJ_EXPR:	expression
  * @CMD_OBJ_MONITOR:	monitor
@@ -420,6 +425,7 @@ enum cmd_obj {
 	CMD_OBJ_CT_HELPERS,
 	CMD_OBJ_LIMIT,
 	CMD_OBJ_LIMITS,
+	CMD_OBJ_FLOWTABLE,
 	CMD_OBJ_FLOWTABLES,
 };
 
@@ -479,6 +485,7 @@ struct cmd {
 		struct rule	*rule;
 		struct chain	*chain;
 		struct table	*table;
+		struct flowtable *flowtable;
 		struct monitor	*monitor;
 		struct markup	*markup;
 		struct obj	*object;
diff --git a/src/evaluate.c b/src/evaluate.c
index a58fca9e6362..70a61c72838a 100644
--- a/src/evaluate.c
+++ b/src/evaluate.c
@@ -2894,6 +2894,24 @@ static int set_evaluate(struct eval_ctx *ctx, struct set *set)
 	return 0;
 }
 
+static uint32_t str2hooknum(uint32_t family, const char *hook);
+
+static int flowtable_evaluate(struct eval_ctx *ctx, struct flowtable *ft)
+{
+	struct table *table;
+
+	table = table_lookup_global(ctx);
+	if (table == NULL)
+		return cmd_error(ctx, "Could not process rule: Table '%s' does not exist",
+				 ctx->cmd->handle.table);
+
+	ft->hooknum = str2hooknum(NFPROTO_NETDEV, ft->hookstr);
+	if (ft->hooknum == NF_INET_NUMHOOKS)
+		return chain_error(ctx, ft, "invalid hook %s", ft->hookstr);
+
+	return 0;
+}
+
 static int rule_evaluate(struct eval_ctx *ctx, struct rule *rule)
 {
 	struct stmt *stmt, *tstmt = NULL;
@@ -3066,6 +3084,14 @@ static int cmd_evaluate_add(struct eval_ctx *ctx, struct cmd *cmd)
 		return chain_evaluate(ctx, cmd->chain);
 	case CMD_OBJ_TABLE:
 		return table_evaluate(ctx, cmd->table);
+	case CMD_OBJ_FLOWTABLE:
+		ret = cache_update(ctx->nf_sock, ctx->cache, cmd->op,
+				   ctx->msgs, ctx->debug_mask & NFT_DEBUG_NETLINK, ctx->octx);
+		if (ret < 0)
+			return ret;
+
+		handle_merge(&cmd->flowtable->handle, &cmd->handle);
+		return flowtable_evaluate(ctx, cmd->flowtable);
 	case CMD_OBJ_COUNTER:
 	case CMD_OBJ_QUOTA:
 	case CMD_OBJ_CT_HELPER:
diff --git a/src/expression.c b/src/expression.c
index 393c1b2b2cfe..7da69442bf03 100644
--- a/src/expression.c
+++ b/src/expression.c
@@ -663,8 +663,8 @@ struct expr *range_expr_alloc(const struct location *loc,
 	return expr;
 }
 
-static struct expr *compound_expr_alloc(const struct location *loc,
-					const struct expr_ops *ops)
+struct expr *compound_expr_alloc(const struct location *loc,
+				 const struct expr_ops *ops)
 {
 	struct expr *expr;
 
diff --git a/src/mnl.c b/src/mnl.c
index e70b0cdeb723..be6e05da5936 100644
--- a/src/mnl.c
+++ b/src/mnl.c
@@ -1011,6 +1011,22 @@ err:
 	return NULL;
 }
 
+int mnl_nft_flowtable_batch_add(struct nftnl_flowtable *flo,
+				struct nftnl_batch *batch, unsigned int flags,
+				uint32_t seqnum)
+{
+	struct nlmsghdr *nlh;
+
+	nlh = nftnl_nlmsg_build_hdr(nftnl_batch_buffer(batch),
+				    NFT_MSG_NEWFLOWTABLE,
+				    nftnl_flowtable_get_u32(flo, NFTNL_FLOWTABLE_FAMILY),
+				    NLM_F_CREATE | flags, seqnum);
+	nftnl_flowtable_nlmsg_build_payload(nlh, flo);
+	mnl_nft_batch_continue(batch);
+
+	return 0;
+}
+
 /*
  * ruleset
  */
diff --git a/src/netlink.c b/src/netlink.c
index 7b8e48dc341d..89513584a50f 100644
--- a/src/netlink.c
+++ b/src/netlink.c
@@ -1762,6 +1762,64 @@ static struct obj *netlink_delinearize_obj(struct netlink_ctx *ctx,
 	return obj;
 }
 
+static struct nftnl_flowtable *alloc_nftnl_flowtable(const struct handle *h,
+						     const struct flowtable *ft)
+{
+	struct nftnl_flowtable *flo;
+
+	flo = nftnl_flowtable_alloc();
+	if (flo == NULL)
+		memory_allocation_error();
+
+	nftnl_flowtable_set_u32(flo, NFTNL_FLOWTABLE_FAMILY, h->family);
+	nftnl_flowtable_set_str(flo, NFTNL_FLOWTABLE_TABLE, h->table);
+	if (h->flowtable != NULL)
+		nftnl_flowtable_set_str(flo, NFTNL_FLOWTABLE_NAME, h->flowtable);
+
+	return flo;
+}
+
+static void netlink_dump_flowtable(struct nftnl_flowtable *flo,
+				   struct netlink_ctx *ctx)
+{
+	FILE *fp = ctx->octx->output_fp;
+
+	if (!(ctx->debug_mask & NFT_DEBUG_NETLINK) || !fp)
+		return;
+
+	nftnl_flowtable_fprintf(fp, flo, 0, 0);
+	fprintf(fp, "\n");
+}
+
+int netlink_add_flowtable(struct netlink_ctx *ctx, const struct handle *h,
+			  struct flowtable *ft, uint32_t flags)
+{
+	struct nftnl_flowtable *flo;
+	const char *dev_array[8];
+	struct expr *expr;
+	int i = 0, err;
+
+	flo = alloc_nftnl_flowtable(h, ft);
+	nftnl_flowtable_set_u32(flo, NFTNL_FLOWTABLE_HOOKNUM, ft->hooknum);
+	nftnl_flowtable_set_u32(flo, NFTNL_FLOWTABLE_PRIO, ft->priority);
+
+	list_for_each_entry(expr, &ft->dev_expr->expressions, list)
+		dev_array[i++] = expr->identifier;
+
+	dev_array[i] = NULL;
+	nftnl_flowtable_set_array(flo, NFTNL_FLOWTABLE_DEVICES, dev_array);
+
+	netlink_dump_flowtable(flo, ctx);
+
+	err = mnl_nft_flowtable_batch_add(flo, ctx->batch, flags, ctx->seqnum);
+	if (err < 0)
+		netlink_io_error(ctx, &ft->location, "Could not add flowtable: %s",
+				 strerror(errno));
+	nftnl_flowtable_free(flo);
+
+	return err;
+}
+
 static int list_obj_cb(struct nftnl_obj *nls, void *arg)
 {
 	struct netlink_ctx *ctx = arg;
diff --git a/src/parser_bison.y b/src/parser_bison.y
index 4cfa8076cfc4..45cc3b4114ff 100644
--- a/src/parser_bison.y
+++ b/src/parser_bison.y
@@ -145,6 +145,7 @@ int nft_lex(void *, void *, void *);
 	struct expr		*expr;
 	struct set		*set;
 	struct obj		*obj;
+	struct flowtable	*flowtable;
 	struct counter		*counter;
 	struct quota		*quota;
 	struct ct		*ct;
@@ -189,6 +190,7 @@ int nft_lex(void *, void *, void *);
 
 %token HOOK			"hook"
 %token DEVICE			"device"
+%token DEVICES			"devices"
 %token TABLE			"table"
 %token TABLES			"tables"
 %token CHAIN			"chain"
@@ -200,6 +202,7 @@ int nft_lex(void *, void *, void *);
 %token ELEMENT			"element"
 %token MAP			"map"
 %token MAPS			"maps"
+%token FLOWTABLE		"flowtable"
 %token HANDLE			"handle"
 %token RULESET			"ruleset"
 %token TRACE			"trace"
@@ -499,9 +502,9 @@ int nft_lex(void *, void *, void *);
 %type <cmd>			base_cmd add_cmd replace_cmd create_cmd insert_cmd delete_cmd list_cmd reset_cmd flush_cmd rename_cmd export_cmd monitor_cmd describe_cmd import_cmd
 %destructor { cmd_free($$); }	base_cmd add_cmd replace_cmd create_cmd insert_cmd delete_cmd list_cmd reset_cmd flush_cmd rename_cmd export_cmd monitor_cmd describe_cmd import_cmd
 
-%type <handle>			table_spec chain_spec chain_identifier ruleid_spec handle_spec position_spec rule_position ruleset_spec
-%destructor { handle_free(&$$); } table_spec chain_spec chain_identifier ruleid_spec handle_spec position_spec rule_position ruleset_spec
-%type <handle>			set_spec set_identifier obj_spec obj_identifier
+%type <handle>			table_spec chain_spec flowtable_spec chain_identifier ruleid_spec handle_spec position_spec rule_position ruleset_spec
+%destructor { handle_free(&$$); } table_spec chain_spec flowtable_spec chain_identifier ruleid_spec handle_spec position_spec rule_position ruleset_spec
+%type <handle>			set_spec set_identifier flowtable_identifier obj_spec obj_identifier
 %destructor { handle_free(&$$); } set_spec set_identifier obj_spec obj_identifier
 %type <val>			family_spec family_spec_explicit chain_policy prio_spec
 
@@ -525,6 +528,9 @@ int nft_lex(void *, void *, void *);
 %type <set>			map_block_alloc map_block
 %destructor { set_free($$); }	map_block_alloc
 
+%type <flowtable>		flowtable_block_alloc flowtable_block
+%destructor { flowtable_free($$); }	flowtable_block_alloc
+
 %type <obj>			obj_block_alloc counter_block quota_block ct_helper_block limit_block
 %destructor { obj_free($$); }	obj_block_alloc
 
@@ -605,8 +611,8 @@ int nft_lex(void *, void *, void *);
 %type <expr>			verdict_map_expr verdict_map_list_expr verdict_map_list_member_expr
 %destructor { expr_free($$); }	verdict_map_expr verdict_map_list_expr verdict_map_list_member_expr
 
-%type <expr>			set_expr set_block_expr set_list_expr set_list_member_expr
-%destructor { expr_free($$); }	set_expr set_block_expr set_list_expr set_list_member_expr
+%type <expr>			set_expr set_block_expr set_list_expr set_list_member_expr flowtable_expr flowtable_list_expr flowtable_expr_member
+%destructor { expr_free($$); }	set_expr set_block_expr set_list_expr set_list_member_expr flowtable_expr flowtable_list_expr flowtable_expr_member
 %type <expr>			set_elem_expr set_elem_expr_alloc set_lhs_expr set_rhs_expr
 %destructor { expr_free($$); }	set_elem_expr set_elem_expr_alloc set_lhs_expr set_rhs_expr
 %type <expr>			set_elem_expr_stmt set_elem_expr_stmt_alloc
@@ -871,6 +877,13 @@ add_cmd			:	TABLE		table_spec
 			{
 				$$ = cmd_alloc(CMD_ADD, CMD_OBJ_SETELEM, &$2, &@$, $3);
 			}
+			|	FLOWTABLE	flowtable_spec	flowtable_block_alloc
+						'{'	flowtable_block	'}'
+			{
+				$5->location = @5;
+				handle_merge(&$3->handle, &$2);
+				$$ = cmd_alloc(CMD_ADD, CMD_OBJ_FLOWTABLE, &$2, &@$, $5);
+			}
 			|	COUNTER		obj_spec
 			{
 				struct obj *obj;
@@ -946,6 +959,13 @@ create_cmd		:	TABLE		table_spec
 			{
 				$$ = cmd_alloc(CMD_CREATE, CMD_OBJ_SETELEM, &$2, &@$, $3);
 			}
+			|	FLOWTABLE	flowtable_spec	flowtable_block_alloc
+						'{'	flowtable_block	'}'
+			{
+				$5->location = @5;
+				handle_merge(&$3->handle, &$2);
+				$$ = cmd_alloc(CMD_CREATE, CMD_OBJ_FLOWTABLE, &$2, &@$, $5);
+			}
 			|	COUNTER		obj_spec
 			{
 				struct obj *obj;
@@ -1316,6 +1336,17 @@ table_block		:	/* empty */	{ $$ = $<table>-1; }
 				list_add_tail(&$4->list, &$1->sets);
 				$$ = $1;
 			}
+
+			|	table_block	FLOWTABLE	flowtable_identifier
+					flowtable_block_alloc	'{'	flowtable_block	'}'
+					stmt_separator
+			{
+				$4->location = @3;
+				handle_merge(&$4->handle, &$3);
+				handle_free(&$3);
+				list_add_tail(&$4->list, &$1->flowtables);
+				$$ = $1;
+			}
 			|	table_block	COUNTER		obj_identifier
 					obj_block_alloc	'{'	counter_block	'}'
 					stmt_separator
@@ -1506,6 +1537,62 @@ set_policy_spec		:	PERFORMANCE	{ $$ = NFT_SET_POL_PERFORMANCE; }
 			|	MEMORY		{ $$ = NFT_SET_POL_MEMORY; }
 			;
 
+flowtable_block_alloc	:	/* empty */
+			{
+				$$ = flowtable_alloc(NULL);
+			}
+			;
+
+flowtable_block		:	/* empty */	{ $$ = $<flowtable>-1; }
+			|	flowtable_block	common_block
+			|	flowtable_block	stmt_separator
+			|	flowtable_block	HOOK		STRING	PRIORITY        prio_spec	stmt_separator
+			{
+				$$->hookstr	= chain_hookname_lookup($3);
+				if ($$->hookstr == NULL) {
+					erec_queue(error(&@3, "unknown chain hook %s", $3),
+						   state->msgs);
+					xfree($3);
+					YYERROR;
+				}
+				xfree($3);
+
+				$$->priority = $5;
+			}
+			|	flowtable_block	DEVICES		'='	flowtable_expr	stmt_separator
+			{
+				$$->dev_expr = $4;
+			}
+			;
+
+flowtable_expr		:	'{'	flowtable_list_expr	'}'
+			{
+				$2->location = @$;
+				$$ = $2;
+			}
+			;
+
+flowtable_list_expr	:	flowtable_expr_member
+			{
+				$$ = compound_expr_alloc(&@$, NULL);
+				compound_expr_add($$, $1);
+			}
+			|	flowtable_list_expr	COMMA	flowtable_expr_member
+			{
+				compound_expr_add($1, $3);
+				$$ = $1;
+			}
+			|	flowtable_list_expr	COMMA	opt_newline
+			;
+
+flowtable_expr_member	:	STRING
+			{
+				$$ = symbol_expr_alloc(&@$, SYMBOL_VALUE,
+						       current_scope(state),
+						       $1);
+			}
+			;
+
 data_type_atom_expr	:	type_identifier
 			{
 				const struct datatype *dtype = datatype_lookup_byname($1);
@@ -1714,6 +1801,21 @@ set_identifier		:	identifier
 			}
 			;
 
+
+flowtable_spec		:	table_spec	identifier
+			{
+				$$		= $1;
+				$$.flowtable	= $2;
+			}
+			;
+
+flowtable_identifier	:	identifier
+			{
+				memset(&$$, 0, sizeof($$));
+				$$.flowtable	= $1;
+			}
+			;
+
 obj_spec		:	table_spec	identifier
 			{
 				$$		= $1;
diff --git a/src/rule.c b/src/rule.c
index 08ea2c3c9fb5..8a38bcc66a66 100644
--- a/src/rule.c
+++ b/src/rule.c
@@ -45,6 +45,8 @@ void handle_merge(struct handle *dst, const struct handle *src)
 		dst->chain = xstrdup(src->chain);
 	if (dst->set == NULL && src->set != NULL)
 		dst->set = xstrdup(src->set);
+	if (dst->flowtable == NULL && src->flowtable != NULL)
+		dst->flowtable = xstrdup(src->flowtable);
 	if (dst->obj == NULL && src->obj != NULL)
 		dst->obj = xstrdup(src->obj);
 	if (dst->handle.id == 0)
@@ -854,6 +856,7 @@ struct cmd *cmd_alloc(enum cmd_ops op, enum cmd_obj obj,
 void nft_cmd_expand(struct cmd *cmd)
 {
 	struct list_head new_cmds;
+	struct flowtable *ft;
 	struct table *table;
 	struct chain *chain;
 	struct rule *rule;
@@ -893,6 +896,14 @@ void nft_cmd_expand(struct cmd *cmd)
 					&set->location, set_get(set));
 			list_add_tail(&new->list, &new_cmds);
 		}
+		list_for_each_entry(ft, &table->flowtables, list) {
+			handle_merge(&ft->handle, &table->handle);
+			memset(&h, 0, sizeof(h));
+			handle_merge(&h, &ft->handle);
+			new = cmd_alloc(CMD_ADD, CMD_OBJ_FLOWTABLE, &h,
+					&ft->location, flowtable_get(ft));
+			list_add_tail(&new->list, &new_cmds);
+		}
 		list_for_each_entry(chain, &table->chains, list) {
 			list_for_each_entry(rule, &chain->rules, list) {
 				memset(&h, 0, sizeof(h));
@@ -979,6 +990,9 @@ void cmd_free(struct cmd *cmd)
 		case CMD_OBJ_LIMIT:
 			obj_free(cmd->object);
 			break;
+		case CMD_OBJ_FLOWTABLE:
+			flowtable_free(cmd->flowtable);
+			break;
 		default:
 			BUG("invalid command object type %u\n", cmd->obj);
 		}
@@ -1068,6 +1082,9 @@ static int do_command_add(struct netlink_ctx *ctx, struct cmd *cmd, bool excl)
 	case CMD_OBJ_CT_HELPER:
 	case CMD_OBJ_LIMIT:
 		return netlink_add_obj(ctx, &cmd->handle, cmd->object, flags);
+	case CMD_OBJ_FLOWTABLE:
+		return netlink_add_flowtable(ctx, &cmd->handle, cmd->flowtable,
+					     flags);
 	default:
 		BUG("invalid command object type %u\n", cmd->obj);
 	}
diff --git a/src/scanner.l b/src/scanner.l
index 6a1f2f685615..ca74650cdd1f 100644
--- a/src/scanner.l
+++ b/src/scanner.l
@@ -238,6 +238,7 @@ addrstring	({macaddr}|{ip4addr}|{ip6addr})
 
 "hook"			{ return HOOK; }
 "device"		{ return DEVICE; }
+"devices"		{ return DEVICES; }
 "table"			{ return TABLE; }
 "tables"		{ return TABLES; }
 "chain"			{ return CHAIN; }
@@ -249,6 +250,7 @@ addrstring	({macaddr}|{ip4addr}|{ip6addr})
 "element"		{ return ELEMENT; }
 "map"			{ return MAP; }
 "maps"			{ return MAPS; }
+"flowtable"		{ return FLOWTABLE; }
 "handle"		{ return HANDLE; }
 "ruleset"		{ return RULESET; }
 "trace"			{ return TRACE; }
