diff mbox series

[nft] Add tproxy support

Message ID 20180620104130.2217-3-ecklm94@gmail.com
State Changes Requested
Delegated to: Pablo Neira
Headers show
Series [nft] Add tproxy support | expand

Commit Message

Máté Eckl June 20, 2018, 10:41 a.m. UTC
This patch is built on the commit not applied yet with the title:
	evaluate: Detect address family in inet context

-- 8< --
This patch adds support for transparent proxy functionality which is
supported in ip, ip6 and inet tables.

The syntax is the following:
	tproxy [to [<ip address>][:<port>]]

Without arguments it looks for a socket listening on the ingress
interface and a port with the same destination port as the packet
destination port.
With the arguments specified, it looks for a socket listening on the
specified address or port.

In an inet table, a packet matches for both families until address is
specified.

Example ruleset:
	table inet x {
		chain y {
			type filter hook prerouting priority -150; policy accept;
			socket transparent 1 mark set 0x00000001 accept
			tproxy mark set 0x00000001 counter packets 611 bytes 46181
			meta l4proto tcp tproxy to :50080 mark set 0x00000001 counter packets 202 bytes 13600
		}
	}

Signed-off-by: Máté Eckl <ecklm94@gmail.com>
---
 include/linux/netfilter/nf_tables.h | 16 +++++++++
 include/statement.h                 | 10 ++++++
 src/evaluate.c                      | 44 ++++++++++++++++++++++++
 src/netlink_delinearize.c           | 52 +++++++++++++++++++++++++++++
 src/netlink_linearize.c             | 48 ++++++++++++++++++++++++++
 src/parser_bison.y                  | 27 +++++++++++++++
 src/scanner.l                       |  2 ++
 src/statement.c                     | 43 ++++++++++++++++++++++++
 8 files changed, 242 insertions(+)

Comments

Florian Westphal June 20, 2018, 11:29 a.m. UTC | #1
Máté Eckl <ecklm94@gmail.com> wrote:
> This patch is built on the commit not applied yet with the title:
> 	evaluate: Detect address family in inet context

You can add this ...

> Example ruleset:
> 	table inet x {
> 		chain y {
> 			type filter hook prerouting priority -150; policy accept;
> 			socket transparent 1 mark set 0x00000001 accept
> 			tproxy mark set 0x00000001 counter packets 611 bytes 46181
> 			meta l4proto tcp tproxy to :50080 mark set 0x00000001 counter packets 202 bytes 13600
> 		}
> 	}
> 
> Signed-off-by: Máté Eckl <ecklm94@gmail.com>
> ---

... here, as its removed automatically when using 'git am'.

The ruleset above seems a bit strange because it uses two 'tproxy'
ruleset.
Also, this patch should include a man page entry to document
socket/tproxy.

A patch to add test cases would also be nice (can be as followup
after patch has been applied of course).

> +	if (stmt->tproxy.addr) {
> +		addr_reg = get_register(ctx, NULL);
> +		registers++;
> +
> +		if (stmt->tproxy.addr->ops->type != EXPR_RANGE) {
> +			netlink_gen_expr(ctx, stmt->tproxy.addr, addr_reg);
> +			netlink_put_register(nle, NFTNL_EXPR_TPROXY_REG_ADDR,
> +					     addr_reg);
> +		}

This looks weird.  Why this != EXPR_RANGE check?
It should never happen, because in evaluate.c you do this for
ports, so why not also reject the unwanted expression type there?

> if (stmt->tproxy.port != NULL) {
>       if (stmt->tproxy.port->ops->type == EXPR_RANGE)
>            return -EOPNOTSUPP;

Might be a good idea to use stmt_error() though to indicate that this
doesn't work.

> +	if (stmt->tproxy.port) {
> +		port_reg = get_register(ctx, NULL);
> +		registers++;
> +
> +		if (stmt->tproxy.port->ops->type != EXPR_RANGE) {
> +			netlink_gen_expr(ctx, stmt->tproxy.port, port_reg);
> +			netlink_put_register(nle, nftnl_reg_port, port_reg);
> +		}

Likewise, this is always true, right?

> +tproxy_stmt		:	TPROXY
> +			{
> +				$$ = tproxy_stmt_alloc(&@$);
> +			}

I wonder what the use case for plain TPROXY is.
Do we need to support that?  How is it useful?

We should probably enforce at least the port number, no?

If not, it would be good to illustrate this with an example
in the nft man page at least.
--
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
Máté Eckl June 20, 2018, 3:09 p.m. UTC | #2
On Wed, Jun 20, 2018 at 01:29:51PM +0200, Florian Westphal wrote:
> Máté Eckl <ecklm94@gmail.com> wrote:
> > This patch is built on the commit not applied yet with the title:
> > 	evaluate: Detect address family in inet context
> 
> You can add this ...
> 
> > Example ruleset:
> > 	table inet x {
> > 		chain y {
> > 			type filter hook prerouting priority -150; policy accept;
> > 			socket transparent 1 mark set 0x00000001 accept
> > 			tproxy mark set 0x00000001 counter packets 611 bytes 46181
> > 			meta l4proto tcp tproxy to :50080 mark set 0x00000001 counter packets 202 bytes 13600
> > 		}
> > 	}
> > 
> > Signed-off-by: Máté Eckl <ecklm94@gmail.com>
> > ---
> 
> ... here, as its removed automatically when using 'git am'.

I don't understand. You mean that I should not include examples in commit
messages?

> 
> The ruleset above seems a bit strange because it uses two 'tproxy'
> ruleset.

Yeah, they were just sintactic examples, I will replace them with a meaningfull
one.

> Also, this patch should include a man page entry to document
> socket/tproxy.

As I didn't add man page for socket matching either, I thought that it could be
a separate commit, once the functionality and the code is accepted.

> 
> A patch to add test cases would also be nice (can be as followup
> after patch has been applied of course).
> 

I sent that.

> > +	if (stmt->tproxy.addr) {
> > +		addr_reg = get_register(ctx, NULL);
> > +		registers++;
> > +
> > +		if (stmt->tproxy.addr->ops->type != EXPR_RANGE) {
> > +			netlink_gen_expr(ctx, stmt->tproxy.addr, addr_reg);
> > +			netlink_put_register(nle, NFTNL_EXPR_TPROXY_REG_ADDR,
> > +					     addr_reg);
> > +		}
> 
> This looks weird. Why this != EXPR_RANGE check?
> It should never happen, because in evaluate.c you do this for
> ports, so why not also reject the unwanted expression type there?

Yes, you are right, I will remove this check.

> 
> > if (stmt->tproxy.port != NULL) {
> >       if (stmt->tproxy.port->ops->type == EXPR_RANGE)
> >            return -EOPNOTSUPP;
> 
> Might be a good idea to use stmt_error() though to indicate that this
> doesn't work.
> 
> > +	if (stmt->tproxy.port) {
> > +		port_reg = get_register(ctx, NULL);
> > +		registers++;
> > +
> > +		if (stmt->tproxy.port->ops->type != EXPR_RANGE) {
> > +			netlink_gen_expr(ctx, stmt->tproxy.port, port_reg);
> > +			netlink_put_register(nle, nftnl_reg_port, port_reg);
> > +		}
> 
> Likewise, this is always true, right?

Yes, removed along with the one above.

> 
> > +tproxy_stmt		:	TPROXY
> > +			{
> > +				$$ = tproxy_stmt_alloc(&@$);
> > +			}
> 
> I wonder what the use case for plain TPROXY is.
> Do we need to support that?  How is it useful?
> 
> We should probably enforce at least the port number, no?
> 
> If not, it would be good to illustrate this with an example
> in the nft man page at least.

I think, it is a useful one. If I want to make proxy working only for web
traffic this simple tproxy statement is sufficient:
	tcp dport 80 tproxy
if the proxy software is listening on port 80.
This would send packets to the proxy software as the original destination port
is used during socket lookup.
I can include this example in the man page.

This use-case seems quite meaningful to me.
--
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
Florian Westphal June 20, 2018, 3:24 p.m. UTC | #3
Máté Eckl <ecklm94@gmail.com> wrote:
> On Wed, Jun 20, 2018 at 01:29:51PM +0200, Florian Westphal wrote:
> > Máté Eckl <ecklm94@gmail.com> wrote:
> > > This patch is built on the commit not applied yet with the title:
> > > 	evaluate: Detect address family in inet context
> > 
> > You can add this ...
> > 
> > > Example ruleset:
> > > 	table inet x {
> > > 		chain y {
> > > 			type filter hook prerouting priority -150; policy accept;
> > > 			socket transparent 1 mark set 0x00000001 accept
> > > 			tproxy mark set 0x00000001 counter packets 611 bytes 46181
> > > 			meta l4proto tcp tproxy to :50080 mark set 0x00000001 counter packets 202 bytes 13600
> > > 		}
> > > 	}
> > > 
> > > Signed-off-by: Máté Eckl <ecklm94@gmail.com>
> > > ---
> > 
> > ... here, as its removed automatically when using 'git am'.
> 
> I don't understand. You mean that I should not include examples in commit
> messages?

Err, no, sorry, I was referring to the first two lines only.
("this patch is built ...").  Example is good to have.

> As I didn't add man page for socket matching either, I thought that it could be
> a separate commit, once the functionality and the code is accepted.

Thats fine as well.

> > A patch to add test cases would also be nice (can be as followup
> > after patch has been applied of course).
> > 
> 
> I sent that.

Yes, thanks, I saw it only after I had sent this email.

> I think, it is a useful one. If I want to make proxy working only for web
> traffic this simple tproxy statement is sufficient:
> 	tcp dport 80 tproxy
> if the proxy software is listening on port 80.

Yes, but in that case TPROXY isn't needed as all of it can
be done only by policy routing (i.e., use
   tcp dport 80 mark set mark 0x1
and add policy routing rule).

> This use-case seems quite meaningful to me.

Okay, but you don't need tproxy for this to work :-)

tproxy is only needed if packet for destination port x should end up
with socket on destination port y.
--
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
Máté Eckl June 21, 2018, 11:42 a.m. UTC | #4
On Wed, Jun 20, 2018 at 05:24:46PM +0200, Florian Westphal wrote:
> > I think, it is a useful one. If I want to make proxy working only for web
> > traffic this simple tproxy statement is sufficient:
> > 	tcp dport 80 tproxy
> > if the proxy software is listening on port 80.
> 
> Yes, but in that case TPROXY isn't needed as all of it can
> be done only by policy routing (i.e., use
>    tcp dport 80 mark set mark 0x1
> and add policy routing rule).
> 
> > This use-case seems quite meaningful to me.
> 
> Okay, but you don't need tproxy for this to work :-)
> 
> tproxy is only needed if packet for destination port x should end up
> with socket on destination port y.

Indeed :) I'll remove it then.
--
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
Máté Eckl June 29, 2018, 10:41 a.m. UTC | #5
Hi,

I have considered the protocol family specification, and I have a working
implementation for adding rules, but I still miss something from printing them.

What I'd like to achieve is not to print protocol family in ip/ip6 tables, but
do it in inet tables.

For example:
	table ip x {
		chain y {
			type filter hook prerouting priority -150; policy accept;
# Now I print this:
			ip protocol tcp tproxy ip to 1.1.1.1
# but I'd like this:
			ip protocol tcp tproxy to 1.1.1.1
		}
	}

Is there a way to do this?

Regards,
Máté
--
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
Florian Westphal June 29, 2018, 10:50 a.m. UTC | #6
Máté Eckl <ecklm94@gmail.com> wrote:
> For example:
> 	table ip x {
> 		chain y {
> 			type filter hook prerouting priority -150; policy accept;
> # Now I print this:
> 			ip protocol tcp tproxy ip to 1.1.1.1
> # but I'd like this:
> 			ip protocol tcp tproxy to 1.1.1.1
> 		}
> 	}
> 
> Is there a way to do this?

Yes, you can add a 'uint8_t family' to the tproxy expr
and fill it in netlink_delinarize:

expr_tproxy->tproxy.family = ctx->table->handle.family;
--
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 series

Patch

diff --git a/include/linux/netfilter/nf_tables.h b/include/linux/netfilter/nf_tables.h
index 88e0ca1..d98cebb 100644
--- a/include/linux/netfilter/nf_tables.h
+++ b/include/linux/netfilter/nf_tables.h
@@ -1231,6 +1231,22 @@  enum nft_nat_attributes {
 };
 #define NFTA_NAT_MAX		(__NFTA_NAT_MAX - 1)
 
+/**
+ * enum nft_tproxy_attributes - nf_tables tproxy expression netlink attributes
+ *
+ * NFTA_TPROXY_FAMILY: Target address family (NLA_U32: nft_registers)
+ * NFTA_TPROXY_REG_ADDR: Target address register (NLA_U32: nft_registers)
+ * NFTA_TPROXY_REG_PORT: Target port register (NLA_U32: nft_registers)
+ */
+enum nft_tproxy_attributes {
+	NFTA_TPROXY_UNSPEC,
+	NFTA_TPROXY_FAMILY,
+	NFTA_TPROXY_REG_ADDR,
+	NFTA_TPROXY_REG_PORT,
+	__NFTA_TPROXY_MAX
+};
+#define NFTA_TPROXY_MAX		(__NFTA_TPROXY_MAX - 1)
+
 /**
  * enum nft_masq_attributes - nf_tables masquerade expression attributes
  *
diff --git a/include/statement.h b/include/statement.h
index 5a907aa..edda98f 100644
--- a/include/statement.h
+++ b/include/statement.h
@@ -128,6 +128,14 @@  struct nat_stmt {
 extern struct stmt *nat_stmt_alloc(const struct location *loc,
 				   enum nft_nat_etypes type);
 
+struct tproxy_stmt {
+	struct expr	*addr;
+	struct expr	*port;
+	uint8_t			family;
+};
+
+extern struct stmt *tproxy_stmt_alloc(const struct location *loc);
+
 struct queue_stmt {
 	struct expr		*queue;
 	uint16_t		flags;
@@ -271,6 +279,7 @@  enum stmt_types {
 	STMT_LOG,
 	STMT_REJECT,
 	STMT_NAT,
+	STMT_TPROXY,
 	STMT_QUEUE,
 	STMT_CT,
 	STMT_SET,
@@ -337,6 +346,7 @@  struct stmt {
 		struct limit_stmt	limit;
 		struct reject_stmt	reject;
 		struct nat_stmt		nat;
+		struct tproxy_stmt	tproxy;
 		struct queue_stmt	queue;
 		struct quota_stmt	quota;
 		struct ct_stmt		ct;
diff --git a/src/evaluate.c b/src/evaluate.c
index 61b4697..f017ca8 100644
--- a/src/evaluate.c
+++ b/src/evaluate.c
@@ -2497,6 +2497,48 @@  static int stmt_evaluate_nat(struct eval_ctx *ctx, struct stmt *stmt)
 	return 0;
 }
 
+static int stmt_evaluate_tproxy(struct eval_ctx *ctx, struct stmt *stmt)
+{
+	int err;
+
+	switch (ctx->pctx.family) {
+	case NFPROTO_IPV4:
+	case NFPROTO_IPV6:
+	case NFPROTO_INET:
+		break;
+	default:
+		return stmt_error(ctx, stmt,
+				  "TPROXY is only supported for IPv4/IPv6/INET");
+	}
+	stmt->tproxy.family = ctx->pctx.family;
+
+	if (stmt->tproxy.addr != NULL) {
+		if (stmt->tproxy.addr->ops->type == EXPR_RANGE)
+			return -EOPNOTSUPP;
+		err = evaluate_addr(ctx, stmt, &stmt->tproxy.addr);
+		if (err < 0)
+			return err;
+		switch (stmt->tproxy.addr->dtype->type) {
+		case TYPE_IPADDR:
+			stmt->tproxy.family = NFPROTO_IPV4;
+			break;
+		case TYPE_IP6ADDR:
+			stmt->tproxy.family = NFPROTO_IPV6;
+			break;
+		}
+	}
+
+	if (stmt->tproxy.port != NULL) {
+		if (stmt->tproxy.port->ops->type == EXPR_RANGE)
+			return -EOPNOTSUPP;
+		err = nat_evaluate_transport(ctx, stmt, &stmt->tproxy.port);
+		if (err < 0)
+			return err;
+	}
+
+	return 0;
+}
+
 static int stmt_evaluate_dup(struct eval_ctx *ctx, struct stmt *stmt)
 {
 	int err;
@@ -2780,6 +2822,8 @@  int stmt_evaluate(struct eval_ctx *ctx, struct stmt *stmt)
 		return stmt_evaluate_reject(ctx, stmt);
 	case STMT_NAT:
 		return stmt_evaluate_nat(ctx, stmt);
+	case STMT_TPROXY:
+		return stmt_evaluate_tproxy(ctx, stmt);
 	case STMT_QUEUE:
 		return stmt_evaluate_queue(ctx, stmt);
 	case STMT_DUP:
diff --git a/src/netlink_delinearize.c b/src/netlink_delinearize.c
index 31d6242..6ede617 100644
--- a/src/netlink_delinearize.c
+++ b/src/netlink_delinearize.c
@@ -969,6 +969,49 @@  out_err:
 	xfree(stmt);
 }
 
+static void netlink_parse_tproxy(struct netlink_parse_ctx *ctx,
+			      const struct location *loc,
+			      const struct nftnl_expr *nle)
+{
+	struct stmt *stmt;
+	struct expr *addr, *port;
+	enum nft_registers reg;
+
+	stmt = tproxy_stmt_alloc(loc);
+	stmt->tproxy.family = nftnl_expr_get_u32(nle, NFTNL_EXPR_TPROXY_FAMILY);
+
+	reg = netlink_parse_register(nle, NFTNL_EXPR_TPROXY_REG_ADDR);
+	if (reg) {
+		addr = netlink_get_register(ctx, loc, reg);
+
+		switch (stmt->tproxy.family) {
+		case NFPROTO_IPV4:
+			expr_set_type(addr, &ipaddr_type, BYTEORDER_BIG_ENDIAN);
+			break;
+		case NFPROTO_IPV6:
+			expr_set_type(addr, &ip6addr_type, BYTEORDER_BIG_ENDIAN);
+			break;
+		default:
+			netlink_error(ctx, loc,
+				      "tproxy address must be IPv4 or IPv6");
+			goto err;
+		}
+		stmt->tproxy.addr = addr;
+	}
+
+	reg = netlink_parse_register(nle, NFTNL_EXPR_TPROXY_REG_PORT);
+	if (reg) {
+		port = netlink_get_register(ctx, loc, reg);
+		expr_set_type(port, &inet_service_type, BYTEORDER_BIG_ENDIAN);
+		stmt->tproxy.port = port;
+	}
+
+	ctx->stmt = stmt;
+	return;
+err:
+	xfree(stmt);
+}
+
 static void netlink_parse_masq(struct netlink_parse_ctx *ctx,
 			       const struct location *loc,
 			       const struct nftnl_expr *nle)
@@ -1362,6 +1405,7 @@  static const struct {
 	{ .name = "range",	.parse = netlink_parse_range },
 	{ .name = "reject",	.parse = netlink_parse_reject },
 	{ .name = "nat",	.parse = netlink_parse_nat },
+	{ .name = "tproxy",	.parse = netlink_parse_tproxy },
 	{ .name = "notrack",	.parse = netlink_parse_notrack },
 	{ .name = "masq",	.parse = netlink_parse_masq },
 	{ .name = "redir",	.parse = netlink_parse_redir },
@@ -2432,6 +2476,14 @@  static void rule_parse_postprocess(struct netlink_parse_ctx *ctx, struct rule *r
 				expr_postprocess(&rctx, &stmt->nat.proto);
 			}
 			break;
+		case STMT_TPROXY:
+			if (stmt->tproxy.addr)
+				expr_postprocess(&rctx, &stmt->tproxy.addr);
+			if (stmt->tproxy.port) {
+				payload_dependency_reset(&rctx.pdctx);
+				expr_postprocess(&rctx, &stmt->tproxy.port);
+			}
+			break;
 		case STMT_REJECT:
 			stmt_reject_postprocess(&rctx);
 			break;
diff --git a/src/netlink_linearize.c b/src/netlink_linearize.c
index 8471e83..33ec06d 100644
--- a/src/netlink_linearize.c
+++ b/src/netlink_linearize.c
@@ -1071,6 +1071,52 @@  static void netlink_gen_nat_stmt(struct netlink_linearize_ctx *ctx,
 	nftnl_rule_add_expr(ctx->nlr, nle);
 }
 
+static void netlink_gen_tproxy_stmt(struct netlink_linearize_ctx *ctx,
+				 const struct stmt *stmt)
+{
+	struct nftnl_expr *nle;
+	enum nft_registers addr_reg;
+	enum nft_registers port_reg;
+	int registers = 0;
+	const int family = stmt->tproxy.family;
+	int nftnl_reg_port;
+
+	nle = alloc_nft_expr("tproxy");
+
+	nftnl_expr_set_u32(nle, NFTNL_EXPR_TPROXY_FAMILY, family);
+
+	nftnl_reg_port = NFTNL_EXPR_TPROXY_REG_PORT;
+
+	if (stmt->tproxy.addr) {
+		addr_reg = get_register(ctx, NULL);
+		registers++;
+
+		if (stmt->tproxy.addr->ops->type != EXPR_RANGE) {
+			netlink_gen_expr(ctx, stmt->tproxy.addr, addr_reg);
+			netlink_put_register(nle, NFTNL_EXPR_TPROXY_REG_ADDR,
+					     addr_reg);
+		}
+
+	}
+
+	if (stmt->tproxy.port) {
+		port_reg = get_register(ctx, NULL);
+		registers++;
+
+		if (stmt->tproxy.port->ops->type != EXPR_RANGE) {
+			netlink_gen_expr(ctx, stmt->tproxy.port, port_reg);
+			netlink_put_register(nle, nftnl_reg_port, port_reg);
+		}
+	}
+
+	while (registers > 0) {
+		release_register(ctx, NULL);
+		registers--;
+	}
+
+	nftnl_rule_add_expr(ctx->nlr, nle);
+}
+
 static void netlink_gen_dup_stmt(struct netlink_linearize_ctx *ctx,
 				 const struct stmt *stmt)
 {
@@ -1301,6 +1347,8 @@  static void netlink_gen_stmt(struct netlink_linearize_ctx *ctx,
 		return netlink_gen_reject_stmt(ctx, stmt);
 	case STMT_NAT:
 		return netlink_gen_nat_stmt(ctx, stmt);
+	case STMT_TPROXY:
+		return netlink_gen_tproxy_stmt(ctx, stmt);
 	case STMT_DUP:
 		return netlink_gen_dup_stmt(ctx, stmt);
 	case STMT_QUEUE:
diff --git a/src/parser_bison.y b/src/parser_bison.y
index 98bfeba..c45d6e4 100644
--- a/src/parser_bison.y
+++ b/src/parser_bison.y
@@ -192,6 +192,8 @@  int nft_lex(void *, void *, void *);
 %token SOCKET			"socket"
 %token TRANSPARENT		"transparent"
 
+%token TPROXY			"tproxy"
+
 %token HOOK			"hook"
 %token DEVICE			"device"
 %token DEVICES			"devices"
@@ -572,6 +574,8 @@  int nft_lex(void *, void *, void *);
 %type <stmt>			nat_stmt nat_stmt_alloc masq_stmt masq_stmt_alloc redir_stmt redir_stmt_alloc
 %destructor { stmt_free($$); }	nat_stmt nat_stmt_alloc masq_stmt masq_stmt_alloc redir_stmt redir_stmt_alloc
 %type <val>			nf_nat_flags nf_nat_flag offset_opt
+%type <stmt>			tproxy_stmt
+%destructor { stmt_free($$); }	tproxy_stmt
 %type <stmt>			queue_stmt queue_stmt_alloc
 %destructor { stmt_free($$); }	queue_stmt queue_stmt_alloc
 %type <val>			queue_stmt_flags queue_stmt_flag
@@ -2082,6 +2086,7 @@  stmt			:	verdict_stmt
 			|	quota_stmt
 			|	reject_stmt
 			|	nat_stmt
+			|	tproxy_stmt
 			|	queue_stmt
 			|	ct_stmt
 			|	masq_stmt
@@ -2477,6 +2482,28 @@  nat_stmt_alloc		:	SNAT	{ $$ = nat_stmt_alloc(&@$, NFT_NAT_SNAT); }
 			|	DNAT	{ $$ = nat_stmt_alloc(&@$, NFT_NAT_DNAT); }
 			;
 
+tproxy_stmt		:	TPROXY
+			{
+				$$ = tproxy_stmt_alloc(&@$);
+			}
+			|	TPROXY TO stmt_expr
+			{
+				$$ = tproxy_stmt_alloc(&@$);
+				$$->tproxy.addr = $3;
+			}
+			|	TPROXY TO COLON stmt_expr
+			{
+				$$ = tproxy_stmt_alloc(&@$);
+				$$->tproxy.port = $4;
+			}
+			|	TPROXY TO stmt_expr COLON stmt_expr
+			{
+				$$ = tproxy_stmt_alloc(&@$);
+				$$->tproxy.addr = $3;
+				$$->tproxy.port = $5;
+			}
+			;
+
 primary_stmt_expr	:	symbol_expr		{ $$ = $1; }
 			|	integer_expr		{ $$ = $1; }
 			|	boolean_expr		{ $$ = $1; }
diff --git a/src/scanner.l b/src/scanner.l
index ed01b5e..703700f 100644
--- a/src/scanner.l
+++ b/src/scanner.l
@@ -261,6 +261,8 @@  addrstring	({macaddr}|{ip4addr}|{ip6addr})
 "socket"		{ return SOCKET; }
 "transparent"		{ return TRANSPARENT;}
 
+"tproxy"		{ return TPROXY; }
+
 "accept"		{ return ACCEPT; }
 "drop"			{ return DROP; }
 "continue"		{ return CONTINUE; }
diff --git a/src/statement.c b/src/statement.c
index 6f5e666..4537e45 100644
--- a/src/statement.c
+++ b/src/statement.c
@@ -619,6 +619,49 @@  struct stmt *nat_stmt_alloc(const struct location *loc,
 	return stmt;
 }
 
+static void tproxy_stmt_print(const struct stmt *stmt, struct output_ctx *octx)
+{
+	nft_print(octx, "tproxy");
+
+	if (stmt->tproxy.addr || stmt->tproxy.port)
+		nft_print(octx, " to");
+	if (stmt->tproxy.addr) {
+		nft_print(octx, " ");
+		if (stmt->tproxy.addr->ops->type == EXPR_VALUE &&
+		    stmt->tproxy.addr->dtype->type == TYPE_IP6ADDR) {
+			nft_print(octx, "[");
+			expr_print(stmt->tproxy.addr, octx);
+			nft_print(octx, "]");
+		} else {
+			expr_print(stmt->tproxy.addr, octx);
+		}
+	}
+	if (stmt->tproxy.port && stmt->tproxy.port->ops->type == EXPR_VALUE) {
+		if (!stmt->tproxy.addr)
+			nft_print(octx, " ");
+		nft_print(octx, ":");
+		expr_print(stmt->tproxy.port, octx);
+	}
+}
+
+static void tproxy_stmt_destroy(struct stmt *stmt)
+{
+	expr_free(stmt->tproxy.addr);
+	expr_free(stmt->tproxy.port);
+}
+
+static const struct stmt_ops tproxy_stmt_ops = {
+	.type		= STMT_TPROXY,
+	.name		= "tproxy",
+	.print		= tproxy_stmt_print,
+	.destroy	= tproxy_stmt_destroy,
+};
+
+struct stmt *tproxy_stmt_alloc(const struct location *loc)
+{
+	return stmt_alloc(loc, &tproxy_stmt_ops);
+}
+
 const char * const set_stmt_op_names[] = {
 	[NFT_DYNSET_OP_ADD]	= "add",
 	[NFT_DYNSET_OP_UPDATE]	= "update",