diff mbox

[nft] src: add tee statement

Message ID 1434709594-6460-1-git-send-email-pablo@netfilter.org
State Changes Requested
Delegated to: Pablo Neira
Headers show

Commit Message

Pablo Neira Ayuso June 19, 2015, 10:26 a.m. UTC
This allows you to clone packets to some destination, eg.

... tee gateway 172.20.0.2
... tee oifname tap0 gateway ip saddr map { 192.168.0.2 : 172.20.0.2, ... }

Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
 include/statement.h       |    9 +++++++++
 src/evaluate.c            |   21 +++++++++++++++++++--
 src/netlink_delinearize.c |   39 +++++++++++++++++++++++++++++++++++++++
 src/netlink_linearize.c   |   21 +++++++++++++++++++++
 src/parser_bison.y        |   18 ++++++++++++++++++
 src/scanner.l             |    2 ++
 src/statement.c           |   29 +++++++++++++++++++++++++++++
 7 files changed, 137 insertions(+), 2 deletions(-)

Comments

Patrick McHardy June 19, 2015, 12:57 p.m. UTC | #1
On 19.06, Pablo Neira Ayuso wrote:
> This allows you to clone packets to some destination, eg.
> 
> ... tee gateway 172.20.0.2
> ... tee oifname tap0 gateway ip saddr map { 192.168.0.2 : 172.20.0.2, ... }

Is tee a name we want to use for userspace syntax? It's not particulary
descriptive for people who don't know what "tee" is, which I guess are
quite a few. Alternative suggestion of the top of my head would be "dup"
or "duplicate".

> +struct tee_stmt {
> +	struct expr		*gw;
> +	const char		*oifname;

I'd suggest to use an expr as well to allow use of symbolic variables.
BTW, have you considered using an ifindex? I mean, we do it for matches
as well, and in case of tee its rather unlikely to be used with a
dynamic network device.

> +	reg1 = netlink_parse_register(nle, NFT_EXPR_TEE_SREG_GW);
> +	if (reg1) {
> +		addr = netlink_get_register(ctx, loc, reg1);
> +		if (addr == NULL)
> +			return netlink_error(ctx, loc,
> +					     "TEE statement has no address "
> +					     "expression");
> +
> +		if (ctx->table->handle.family == NFPROTO_IPV4)
> +			expr_set_type(addr, &ipaddr_type, BYTEORDER_BIG_ENDIAN);
> +		else
> +			expr_set_type(addr, &ip6addr_type,
> +				      BYTEORDER_BIG_ENDIAN);
> +		stmt->tee.gw = addr;
> +	}
> +
> +	if (nft_rule_expr_is_set(nle, NFT_EXPR_TEE_OIFNAME)) {
> +		stmt->tee.oifname =
> +			strdup(nft_rule_expr_get_str(nle, NFT_EXPR_TEE_OIFNAME));

Please use consistent braces, IOW none for single statements.
--
To unsubscribe from this list: send the line "unsubscribe netfilter-devel" in
Patrick Schaaf June 19, 2015, 1:17 p.m. UTC | #2
On Friday 19 June 2015 14:57:24 Patrick McHardy wrote:
>
> Is tee a name we want to use for userspace syntax? It's not particulary
> descriptive for people who don't know what "tee" is, which I guess are
> quite a few.

Naming the thing "tee" was a conscious decision when I invented the it as an 
option to the ROUTE target back in 2004  - because it is a huge foot gun, and 
obscurity seemed appropriate.

best regards
  Patrick
--
To unsubscribe from this list: send the line "unsubscribe netfilter-devel" in
Jan Engelhardt June 19, 2015, 1:24 p.m. UTC | #3
On Friday 2015-06-19 15:17, Patrick Schaaf wrote:
>On Friday 19 June 2015 14:57:24 Patrick McHardy wrote:
>>
>> Is tee a name we want to use for userspace syntax? It's not particulary
>> descriptive for people who don't know what "tee" is, which I guess are
>> quite a few.
>
>Naming the thing "tee" was a conscious decision when I invented the it as an 
>option to the ROUTE target back in 2004  - because it is a huge foot gun, and 
>obscurity seemed appropriate.

Well, "clone" is the term that should be used if one really wants to
avoid "tee". But tee is now used for the kernel module and for the
iptables module, so out of consistency, you want the same in nft.

Besides, tee is inspired from /usr/bin/tee and therefore ought to be
rather well-known already: it's what people are usually learning in
the same chapter as cat(1) when they make their first steps in Shell.
--
To unsubscribe from this list: send the line "unsubscribe netfilter-devel" in
Patrick McHardy June 19, 2015, 1:29 p.m. UTC | #4
On 19.06, Patrick Schaaf wrote:
> On Friday 19 June 2015 14:57:24 Patrick McHardy wrote:
> >
> > Is tee a name we want to use for userspace syntax? It's not particulary
> > descriptive for people who don't know what "tee" is, which I guess are
> > quite a few.
> 
> Naming the thing "tee" was a conscious decision when I invented the it as an 
> option to the ROUTE target back in 2004  - because it is a huge foot gun, and 
> obscurity seemed appropriate.

Yeah, but we're trying to have a descriptive language. Its not that
"duplicate" doesn't also sound dangerous.
--
To unsubscribe from this list: send the line "unsubscribe netfilter-devel" in
Patrick McHardy June 19, 2015, 1:37 p.m. UTC | #5
On 19.06, Jan Engelhardt wrote:
> 
> On Friday 2015-06-19 15:17, Patrick Schaaf wrote:
> >On Friday 19 June 2015 14:57:24 Patrick McHardy wrote:
> >>
> >> Is tee a name we want to use for userspace syntax? It's not particulary
> >> descriptive for people who don't know what "tee" is, which I guess are
> >> quite a few.
> >
> >Naming the thing "tee" was a conscious decision when I invented the it as an 
> >option to the ROUTE target back in 2004  - because it is a huge foot gun, and 
> >obscurity seemed appropriate.
> 
> Well, "clone" is the term that should be used if one really wants to
> avoid "tee". But tee is now used for the kernel module and for the
> iptables module, so out of consistency, you want the same in nft.

We are free to use whatever we want in the nft syntax. There's no need
to use the kernel internal names.

> Besides, tee is inspired from /usr/bin/tee and therefore ought to be
> rather well-known already: it's what people are usually learning in
> the same chapter as cat(1) when they make their first steps in Shell.

I'm aware of that, but I don't think it is necessarily well known among
firewall admins. That was my whole point.
--
To unsubscribe from this list: send the line "unsubscribe netfilter-devel" in
Pablo Neira Ayuso June 19, 2015, 1:37 p.m. UTC | #6
On Fri, Jun 19, 2015 at 02:57:24PM +0200, Patrick McHardy wrote:
> On 19.06, Pablo Neira Ayuso wrote:
> > This allows you to clone packets to some destination, eg.
> > 
> > ... tee gateway 172.20.0.2
> > ... tee oifname tap0 gateway ip saddr map { 192.168.0.2 : 172.20.0.2, ... }
> 
> Is tee a name we want to use for userspace syntax? It's not particulary
> descriptive for people who don't know what "tee" is, which I guess are
> quite a few. Alternative suggestion of the top of my head would be "dup"
> or "duplicate".

I can do so, yes. Do you want to me rename the kernel part to
nft_dup.c as well? The core name can be net/netfilter/nf_dup.c instead
of net/netfilter/nf_tee.c

> > +struct tee_stmt {
> > +	struct expr		*gw;
> > +	const char		*oifname;
> 
> I'd suggest to use an expr as well to allow use of symbolic variables.
> BTW, have you considered using an ifindex? I mean, we do it for matches
> as well, and in case of tee its rather unlikely to be used with a
> dynamic network device.

Note sure about this, we now have tapX in place in VM environments
that can go up and down.

However, when implementing tee (now dup) for the new netdev and bridge
family we'll indicate the physical device to duplicate packets, it
should be good to allow the use ifindef if we want maps in place
there.

I think we can change to ifindex, if someone needs with ifname, we can
add it later on, OK?

> > +	reg1 = netlink_parse_register(nle, NFT_EXPR_TEE_SREG_GW);
> > +	if (reg1) {
> > +		addr = netlink_get_register(ctx, loc, reg1);
> > +		if (addr == NULL)
> > +			return netlink_error(ctx, loc,
> > +					     "TEE statement has no address "
> > +					     "expression");
> > +
> > +		if (ctx->table->handle.family == NFPROTO_IPV4)
> > +			expr_set_type(addr, &ipaddr_type, BYTEORDER_BIG_ENDIAN);
> > +		else
> > +			expr_set_type(addr, &ip6addr_type,
> > +				      BYTEORDER_BIG_ENDIAN);
> > +		stmt->tee.gw = addr;
> > +	}
> > +
> > +	if (nft_rule_expr_is_set(nle, NFT_EXPR_TEE_OIFNAME)) {
> > +		stmt->tee.oifname =
> > +			strdup(nft_rule_expr_get_str(nle, NFT_EXPR_TEE_OIFNAME));
> 
> Please use consistent braces, IOW none for single statements.

I'll do it, but I think it's unclear wrt. kernel netdev coding style,
since the line split can be considered as two lines.
--
To unsubscribe from this list: send the line "unsubscribe netfilter-devel" in
Patrick McHardy June 19, 2015, 1:41 p.m. UTC | #7
On 19.06, Pablo Neira Ayuso wrote:
> On Fri, Jun 19, 2015 at 02:57:24PM +0200, Patrick McHardy wrote:
> > On 19.06, Pablo Neira Ayuso wrote:
> > > This allows you to clone packets to some destination, eg.
> > > 
> > > ... tee gateway 172.20.0.2
> > > ... tee oifname tap0 gateway ip saddr map { 192.168.0.2 : 172.20.0.2, ... }
> > 
> > Is tee a name we want to use for userspace syntax? It's not particulary
> > descriptive for people who don't know what "tee" is, which I guess are
> > quite a few. Alternative suggestion of the top of my head would be "dup"
> > or "duplicate".
> 
> I can do so, yes. Do you want to me rename the kernel part to
> nft_dup.c as well? The core name can be net/netfilter/nf_dup.c instead
> of net/netfilter/nf_tee.c

Would be fine with me, however I don't have as strong an opinion about
that as for the userspace syntax.

> > > +struct tee_stmt {
> > > +	struct expr		*gw;
> > > +	const char		*oifname;
> > 
> > I'd suggest to use an expr as well to allow use of symbolic variables.
> > BTW, have you considered using an ifindex? I mean, we do it for matches
> > as well, and in case of tee its rather unlikely to be used with a
> > dynamic network device.
> 
> Note sure about this, we now have tapX in place in VM environments
> that can go up and down.

Good point.

> However, when implementing tee (now dup) for the new netdev and bridge
> family we'll indicate the physical device to duplicate packets, it
> should be good to allow the use ifindef if we want maps in place
> there.
> 
> I think we can change to ifindex, if someone needs with ifname, we can
> add it later on, OK?

Yeah, that sound good to me. Perhaps we even have something that can take
care of all the ifindices until then.

> > > +	reg1 = netlink_parse_register(nle, NFT_EXPR_TEE_SREG_GW);
> > > +	if (reg1) {
> > > +		addr = netlink_get_register(ctx, loc, reg1);
> > > +		if (addr == NULL)
> > > +			return netlink_error(ctx, loc,
> > > +					     "TEE statement has no address "
> > > +					     "expression");
> > > +
> > > +		if (ctx->table->handle.family == NFPROTO_IPV4)
> > > +			expr_set_type(addr, &ipaddr_type, BYTEORDER_BIG_ENDIAN);
> > > +		else
> > > +			expr_set_type(addr, &ip6addr_type,
> > > +				      BYTEORDER_BIG_ENDIAN);
> > > +		stmt->tee.gw = addr;
> > > +	}
> > > +
> > > +	if (nft_rule_expr_is_set(nle, NFT_EXPR_TEE_OIFNAME)) {
> > > +		stmt->tee.oifname =
> > > +			strdup(nft_rule_expr_get_str(nle, NFT_EXPR_TEE_OIFNAME));
> > 
> > Please use consistent braces, IOW none for single statements.
> 
> I'll do it, but I think it's unclear wrt. kernel netdev coding style,
> since the line split can be considered as two lines.

For the kernel the coding style states to now use them for single statements.
This is what we also have in most places in nft.
--
To unsubscribe from this list: send the line "unsubscribe netfilter-devel" in
Pablo Neira Ayuso June 19, 2015, 1:56 p.m. UTC | #8
On Fri, Jun 19, 2015 at 03:41:29PM +0200, Patrick McHardy wrote:
> On 19.06, Pablo Neira Ayuso wrote:
> > On Fri, Jun 19, 2015 at 02:57:24PM +0200, Patrick McHardy wrote:
> > > On 19.06, Pablo Neira Ayuso wrote:
> > > > This allows you to clone packets to some destination, eg.
> > > > 
> > > > ... tee gateway 172.20.0.2
> > > > ... tee oifname tap0 gateway ip saddr map { 192.168.0.2 : 172.20.0.2, ... }
> > > 
> > > Is tee a name we want to use for userspace syntax? It's not particulary
> > > descriptive for people who don't know what "tee" is, which I guess are
> > > quite a few. Alternative suggestion of the top of my head would be "dup"
> > > or "duplicate".
> > 
> > I can do so, yes. Do you want to me rename the kernel part to
> > nft_dup.c as well? The core name can be net/netfilter/nf_dup.c instead
> > of net/netfilter/nf_tee.c
> 
> Would be fine with me, however I don't have as strong an opinion about
> that as for the userspace syntax.

I'll rename nft_tee.c to nft_dup.c for consistency.

> > > > +struct tee_stmt {
> > > > +	struct expr		*gw;
> > > > +	const char		*oifname;
> > > 
> > > I'd suggest to use an expr as well to allow use of symbolic variables.
> > > BTW, have you considered using an ifindex? I mean, we do it for matches
> > > as well, and in case of tee its rather unlikely to be used with a
> > > dynamic network device.
> > 
> > Note sure about this, we now have tapX in place in VM environments
> > that can go up and down.
> 
> Good point.
> 
> > However, when implementing tee (now dup) for the new netdev and bridge
> > family we'll indicate the physical device to duplicate packets, it
> > should be good to allow the use ifindef if we want maps in place
> > there.
> > 
> > I think we can change to ifindex, if someone needs with ifname, we can
> > add it later on, OK?
> 
> Yeah, that sound good to me. Perhaps we even have something that can take
> care of all the ifindices until then.

Will do so then.
--
To unsubscribe from this list: send the line "unsubscribe netfilter-devel" in
Jan Engelhardt June 19, 2015, 2:07 p.m. UTC | #9
On Friday 2015-06-19 15:37, Patrick McHardy wrote:
>
>[...] I don't think [tee] is necessarily well known among
>firewall admins.

I think different.


TEE has been in iptables for a long time (a little over five years),
the manpage for it has the important keywords "clone" and "redirect"
in it, and there are sufficiently many blog posts and StackExchange
on the matter (including duplication), and has become an established
name, like masquerade - another symbolic-mnemonic name - once did.
--
To unsubscribe from this list: send the line "unsubscribe netfilter-devel" in
Bjørnar Ness June 26, 2015, 11:47 a.m. UTC | #10
On Jun 19, 2015 4:14 PM, "Jan Engelhardt" <jengelh@inai.de> wrote:
>
>
> On Friday 2015-06-19 15:37, Patrick McHardy wrote:
> >
> >[...] I don't think [tee] is necessarily well known among
> >firewall admins.
>
> I think different.
>
>
> TEE has been in iptables for a long time (a little over five years),
> the manpage for it has the important keywords "clone" and "redirect"
> in it, and there are sufficiently many blog posts and StackExchange
> on the matter (including duplication), and has become an established
> name, like masquerade - another symbolic-mnemonic name - once did.

I agree. Tee is a historic name, and should be good reasons to change.
--
To unsubscribe from this list: send the line "unsubscribe netfilter-devel" 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/include/statement.h b/include/statement.h
index 48e6130..5c8e08d 100644
--- a/include/statement.h
+++ b/include/statement.h
@@ -103,6 +103,12 @@  struct ct_stmt {
 extern struct stmt *ct_stmt_alloc(const struct location *loc,
 				  enum nft_ct_keys key,
 				  struct expr *expr);
+struct tee_stmt {
+	struct expr		*gw;
+	const char		*oifname;
+};
+
+struct stmt *tee_stmt_alloc(const struct location *loc);
 
 struct set_stmt {
 	struct expr		*set;
@@ -129,6 +135,7 @@  extern struct stmt *set_stmt_alloc(const struct location *loc);
  * @STMT_QUEUE:		QUEUE statement
  * @STMT_CT:		conntrack statement
  * @STMT_SET:		set statement
+ * @STMT_TEE:		tee statement
  */
 enum stmt_types {
 	STMT_INVALID,
@@ -145,6 +152,7 @@  enum stmt_types {
 	STMT_QUEUE,
 	STMT_CT,
 	STMT_SET,
+	STMT_TEE,
 };
 
 /**
@@ -195,6 +203,7 @@  struct stmt {
 		struct queue_stmt	queue;
 		struct ct_stmt		ct;
 		struct set_stmt		set;
+		struct tee_stmt		tee;
 	};
 };
 
diff --git a/src/evaluate.c b/src/evaluate.c
index d99b38f..f29c716 100644
--- a/src/evaluate.c
+++ b/src/evaluate.c
@@ -1557,7 +1557,7 @@  static int nat_evaluate_family(struct eval_ctx *ctx, struct stmt *stmt)
 	}
 }
 
-static int nat_evaluate_addr(struct eval_ctx *ctx, struct stmt *stmt,
+static int evaluate_addr(struct eval_ctx *ctx, struct stmt *stmt,
 			     struct expr **expr)
 {
 	struct proto_ctx *pctx = &ctx->pctx;
@@ -1599,7 +1599,7 @@  static int stmt_evaluate_nat(struct eval_ctx *ctx, struct stmt *stmt)
 		return err;
 
 	if (stmt->nat.addr != NULL) {
-		err = nat_evaluate_addr(ctx, stmt, &stmt->nat.addr);
+		err = evaluate_addr(ctx, stmt, &stmt->nat.addr);
 		if (err < 0)
 			return err;
 	}
@@ -1643,6 +1643,21 @@  static int stmt_evaluate_redir(struct eval_ctx *ctx, struct stmt *stmt)
 	return 0;
 }
 
+static int stmt_evaluate_tee(struct eval_ctx *ctx, struct stmt *stmt)
+{
+	int err;
+
+	err = nat_evaluate_family(ctx, stmt);
+	if (err < 0)
+		return err;
+
+	err = evaluate_addr(ctx, stmt, &stmt->tee.gw);
+	if (err < 0)
+		return err;
+
+	return 0;
+}
+
 static int stmt_evaluate_queue(struct eval_ctx *ctx, struct stmt *stmt)
 {
 	if (stmt->queue.queue != NULL) {
@@ -1726,6 +1741,8 @@  int stmt_evaluate(struct eval_ctx *ctx, struct stmt *stmt)
 		return stmt_evaluate_redir(ctx, stmt);
 	case STMT_QUEUE:
 		return stmt_evaluate_queue(ctx, stmt);
+	case STMT_TEE:
+		return stmt_evaluate_tee(ctx, stmt);
 	case STMT_SET:
 		return stmt_evaluate_set(ctx, stmt);
 	default:
diff --git a/src/netlink_delinearize.c b/src/netlink_delinearize.c
index 6d60be3..d0f9154 100644
--- a/src/netlink_delinearize.c
+++ b/src/netlink_delinearize.c
@@ -747,6 +747,40 @@  static void netlink_parse_redir(struct netlink_parse_ctx *ctx,
 	list_add_tail(&stmt->list, &ctx->rule->stmts);
 }
 
+static void netlink_parse_tee(struct netlink_parse_ctx *ctx,
+			      const struct location *loc,
+			      const struct nft_rule_expr *nle)
+{
+	struct stmt *stmt;
+	struct expr *addr;
+	enum nft_registers reg1;
+
+	stmt = tee_stmt_alloc(loc);
+
+	reg1 = netlink_parse_register(nle, NFT_EXPR_TEE_SREG_GW);
+	if (reg1) {
+		addr = netlink_get_register(ctx, loc, reg1);
+		if (addr == NULL)
+			return netlink_error(ctx, loc,
+					     "TEE statement has no address "
+					     "expression");
+
+		if (ctx->table->handle.family == NFPROTO_IPV4)
+			expr_set_type(addr, &ipaddr_type, BYTEORDER_BIG_ENDIAN);
+		else
+			expr_set_type(addr, &ip6addr_type,
+				      BYTEORDER_BIG_ENDIAN);
+		stmt->tee.gw = addr;
+	}
+
+	if (nft_rule_expr_is_set(nle, NFT_EXPR_TEE_OIFNAME)) {
+		stmt->tee.oifname =
+			strdup(nft_rule_expr_get_str(nle, NFT_EXPR_TEE_OIFNAME));
+	}
+
+	list_add_tail(&stmt->list, &ctx->rule->stmts);
+}
+
 static void netlink_parse_queue(struct netlink_parse_ctx *ctx,
 			      const struct location *loc,
 			      const struct nft_rule_expr *nle)
@@ -835,6 +869,7 @@  static const struct {
 	{ .name = "nat",	.parse = netlink_parse_nat },
 	{ .name = "masq",	.parse = netlink_parse_masq },
 	{ .name = "redir",	.parse = netlink_parse_redir },
+	{ .name = "tee",	.parse = netlink_parse_tee },
 	{ .name = "queue",	.parse = netlink_parse_queue },
 	{ .name = "dynset",	.parse = netlink_parse_dynset },
 };
@@ -1365,6 +1400,10 @@  static void rule_parse_postprocess(struct netlink_parse_ctx *ctx, struct rule *r
 		case STMT_SET:
 			expr_postprocess(&rctx, &stmt->set.key);
 			break;
+		case STMT_TEE:
+			if (stmt->tee.gw!= NULL)
+				expr_postprocess(&rctx, &stmt->tee.gw);
+			break;
 		default:
 			break;
 		}
diff --git a/src/netlink_linearize.c b/src/netlink_linearize.c
index bf1e56b..2a4f567 100644
--- a/src/netlink_linearize.c
+++ b/src/netlink_linearize.c
@@ -802,6 +802,25 @@  static void netlink_gen_redir_stmt(struct netlink_linearize_ctx *ctx,
 	nft_rule_add_expr(ctx->nlr, nle);
 }
 
+static void netlink_gen_tee_stmt(struct netlink_linearize_ctx *ctx,
+				 const struct stmt *stmt)
+{
+	struct nft_rule_expr *nle;
+	const char *oifname = stmt->tee.oifname;
+	enum nft_registers sreg_gw;
+
+	nle = alloc_nft_expr("tee");
+
+	sreg_gw = get_register(ctx, stmt->tee.gw);
+	netlink_gen_expr(ctx, stmt->tee.gw, sreg_gw);
+	netlink_put_register(nle, NFT_EXPR_TEE_SREG_GW, sreg_gw);
+	release_register(ctx, stmt->tee.gw);
+
+	if (oifname != NULL)
+		nft_rule_expr_set_str(nle, NFT_EXPR_TEE_OIFNAME, oifname);
+	nft_rule_add_expr(ctx->nlr, nle);
+}
+
 static void netlink_gen_queue_stmt(struct netlink_linearize_ctx *ctx,
 				 const struct stmt *stmt)
 {
@@ -892,6 +911,8 @@  static void netlink_gen_stmt(struct netlink_linearize_ctx *ctx,
 		return netlink_gen_masq_stmt(ctx, stmt);
 	case STMT_REDIR:
 		return netlink_gen_redir_stmt(ctx, stmt);
+	case STMT_TEE:
+		return netlink_gen_tee_stmt(ctx, stmt);
 	case STMT_QUEUE:
 		return netlink_gen_queue_stmt(ctx, stmt);
 	case STMT_CT:
diff --git a/src/parser_bison.y b/src/parser_bison.y
index 5c4e272..16c17e6 100644
--- a/src/parser_bison.y
+++ b/src/parser_bison.y
@@ -390,6 +390,8 @@  static void location_update(struct location *loc, struct location *rhs, int n)
 %token BYPASS			"bypass"
 %token FANOUT			"fanout"
 
+%token TEE			"tee"
+
 %token POSITION			"position"
 %token COMMENT			"comment"
 
@@ -457,6 +459,8 @@  static void location_update(struct location *loc, struct location *rhs, int n)
 %type <stmt>			queue_stmt queue_stmt_alloc
 %destructor { stmt_free($$); }	queue_stmt queue_stmt_alloc
 %type <val>			queue_stmt_flags queue_stmt_flag
+%type <stmt>			tee_stmt
+%destructor { stmt_free($$); }	tee_stmt
 %type <stmt>			set_stmt
 %destructor { stmt_free($$); }	set_stmt
 %type <val>			set_stmt_op
@@ -1275,6 +1279,7 @@  stmt			:	verdict_stmt
 			|	ct_stmt
 			|	masq_stmt
 			|	redir_stmt
+			|	tee_stmt
 			|	set_stmt
 			;
 
@@ -1538,6 +1543,19 @@  redir_stmt_arg		:	TO	expr
 			}
 			;
 
+tee_stmt		:	TEE	GATEWAY	expr
+			{
+				$$ = tee_stmt_alloc(&@$);
+				$$->tee.gw = $3;
+			}
+			|	TEE	OIFNAME	string	GATEWAY expr
+			{
+				$$ = tee_stmt_alloc(&@$);
+				$$->tee.oifname = strdup($3);
+				$$->tee.gw = $5;
+			}
+			;
+
 nf_nat_flags		:	nf_nat_flag
 			|	nf_nat_flags	COMMA	nf_nat_flag
 			{
diff --git a/src/scanner.l b/src/scanner.l
index 985ea2a..d4f706f 100644
--- a/src/scanner.l
+++ b/src/scanner.l
@@ -454,6 +454,8 @@  addrstring	({macaddr}|{ip4addr}|{ip6addr})
 "proto-dst"		{ return PROTO_DST; }
 "label"			{ return LABEL; }
 
+"tee"			{ return TEE; }
+
 "xml"			{ return XML; }
 "json"			{ return JSON; }
 
diff --git a/src/statement.c b/src/statement.c
index 9ebc593..267be5e 100644
--- a/src/statement.c
+++ b/src/statement.c
@@ -408,3 +408,32 @@  struct stmt *set_stmt_alloc(const struct location *loc)
 {
 	return stmt_alloc(loc, &set_stmt_ops);
 }
+
+static void tee_stmt_print(const struct stmt *stmt)
+{
+	printf("tee");
+	if (stmt->tee.oifname != NULL)
+		printf(" oifname %s", stmt->tee.oifname);
+
+	printf(" gateway ");
+	expr_print(stmt->tee.gw);
+}
+
+static void tee_stmt_destroy(struct stmt *stmt)
+{
+	expr_free(stmt->tee.gw);
+	if (stmt->tee.oifname != NULL)
+		xfree(stmt->tee.oifname);
+}
+
+static const struct stmt_ops tee_stmt_ops = {
+	.type		= STMT_TEE,
+	.name		= "tee",
+	.print		= tee_stmt_print,
+	.destroy	= tee_stmt_destroy,
+};
+
+struct stmt *tee_stmt_alloc(const struct location *loc)
+{
+	return stmt_alloc(loc, &tee_stmt_ops);
+}