netfilter: exthdr: add support for tcp option removal
authorFlorian Westphal <fw@strlen.de>
Fri, 28 Jan 2022 12:00:36 +0000 (13:00 +0100)
committerPablo Neira Ayuso <pablo@netfilter.org>
Fri, 4 Feb 2022 05:30:28 +0000 (06:30 +0100)
This allows to replace a tcp option with nop padding to selectively disable
a particular tcp option.

Optstrip mode is chosen when userspace passes the exthdr expression with
neither a source nor a destination register attribute.

This is identical to xtables TCPOPTSTRIP extension.
The only difference is that TCPOPTSTRIP allows to pass in a bitmap
of options to remove rather than a single number.

Unlike TCPOPTSTRIP this expression can be used multiple times
in the same rule to get the same effect.

We could add a new nested attribute later on in case there is a
use case for single-expression-multi-remove.

Signed-off-by: Florian Westphal <fw@strlen.de>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
net/netfilter/nft_exthdr.c

index dbe1f2e7dd9ed620b1ab0c99c214178e6dbc6a16..dfc06cdb8fa210b5b89c36598929f1ba26a6e939 100644 (file)
@@ -308,6 +308,63 @@ err:
        regs->verdict.code = NFT_BREAK;
 }
 
+static void nft_exthdr_tcp_strip_eval(const struct nft_expr *expr,
+                                     struct nft_regs *regs,
+                                     const struct nft_pktinfo *pkt)
+{
+       u8 buff[sizeof(struct tcphdr) + MAX_TCP_OPTION_SPACE];
+       struct nft_exthdr *priv = nft_expr_priv(expr);
+       unsigned int i, tcphdr_len, optl;
+       struct tcphdr *tcph;
+       u8 *opt;
+
+       tcph = nft_tcp_header_pointer(pkt, sizeof(buff), buff, &tcphdr_len);
+       if (!tcph)
+               goto err;
+
+       if (skb_ensure_writable(pkt->skb, nft_thoff(pkt) + tcphdr_len))
+               goto drop;
+
+       opt = (u8 *)nft_tcp_header_pointer(pkt, sizeof(buff), buff, &tcphdr_len);
+       if (!opt)
+               goto err;
+       for (i = sizeof(*tcph); i < tcphdr_len - 1; i += optl) {
+               unsigned int j;
+
+               optl = optlen(opt, i);
+               if (priv->type != opt[i])
+                       continue;
+
+               if (i + optl > tcphdr_len)
+                       goto drop;
+
+               for (j = 0; j < optl; ++j) {
+                       u16 n = TCPOPT_NOP;
+                       u16 o = opt[i+j];
+
+                       if ((i + j) % 2 == 0) {
+                               o <<= 8;
+                               n <<= 8;
+                       }
+                       inet_proto_csum_replace2(&tcph->check, pkt->skb, htons(o),
+                                                htons(n), false);
+               }
+               memset(opt + i, TCPOPT_NOP, optl);
+               return;
+       }
+
+       /* option not found, continue. This allows to do multiple
+        * option removals per rule.
+        */
+       return;
+err:
+       regs->verdict.code = NFT_BREAK;
+       return;
+drop:
+       /* can't remove, no choice but to drop */
+       regs->verdict.code = NF_DROP;
+}
+
 static void nft_exthdr_sctp_eval(const struct nft_expr *expr,
                                 struct nft_regs *regs,
                                 const struct nft_pktinfo *pkt)
@@ -457,6 +514,28 @@ static int nft_exthdr_tcp_set_init(const struct nft_ctx *ctx,
                                       priv->len);
 }
 
+static int nft_exthdr_tcp_strip_init(const struct nft_ctx *ctx,
+                                    const struct nft_expr *expr,
+                                    const struct nlattr * const tb[])
+{
+       struct nft_exthdr *priv = nft_expr_priv(expr);
+
+       if (tb[NFTA_EXTHDR_SREG] ||
+           tb[NFTA_EXTHDR_DREG] ||
+           tb[NFTA_EXTHDR_FLAGS] ||
+           tb[NFTA_EXTHDR_OFFSET] ||
+           tb[NFTA_EXTHDR_LEN])
+               return -EINVAL;
+
+       if (!tb[NFTA_EXTHDR_TYPE])
+               return -EINVAL;
+
+       priv->type = nla_get_u8(tb[NFTA_EXTHDR_TYPE]);
+       priv->op = NFT_EXTHDR_OP_TCPOPT;
+
+       return 0;
+}
+
 static int nft_exthdr_ipv4_init(const struct nft_ctx *ctx,
                                const struct nft_expr *expr,
                                const struct nlattr * const tb[])
@@ -517,6 +596,13 @@ static int nft_exthdr_dump_set(struct sk_buff *skb, const struct nft_expr *expr)
        return nft_exthdr_dump_common(skb, priv);
 }
 
+static int nft_exthdr_dump_strip(struct sk_buff *skb, const struct nft_expr *expr)
+{
+       const struct nft_exthdr *priv = nft_expr_priv(expr);
+
+       return nft_exthdr_dump_common(skb, priv);
+}
+
 static const struct nft_expr_ops nft_exthdr_ipv6_ops = {
        .type           = &nft_exthdr_type,
        .size           = NFT_EXPR_SIZE(sizeof(struct nft_exthdr)),
@@ -549,6 +635,14 @@ static const struct nft_expr_ops nft_exthdr_tcp_set_ops = {
        .dump           = nft_exthdr_dump_set,
 };
 
+static const struct nft_expr_ops nft_exthdr_tcp_strip_ops = {
+       .type           = &nft_exthdr_type,
+       .size           = NFT_EXPR_SIZE(sizeof(struct nft_exthdr)),
+       .eval           = nft_exthdr_tcp_strip_eval,
+       .init           = nft_exthdr_tcp_strip_init,
+       .dump           = nft_exthdr_dump_strip,
+};
+
 static const struct nft_expr_ops nft_exthdr_sctp_ops = {
        .type           = &nft_exthdr_type,
        .size           = NFT_EXPR_SIZE(sizeof(struct nft_exthdr)),
@@ -576,7 +670,7 @@ nft_exthdr_select_ops(const struct nft_ctx *ctx,
                        return &nft_exthdr_tcp_set_ops;
                if (tb[NFTA_EXTHDR_DREG])
                        return &nft_exthdr_tcp_ops;
-               break;
+               return &nft_exthdr_tcp_strip_ops;
        case NFT_EXTHDR_OP_IPV6:
                if (tb[NFTA_EXTHDR_DREG])
                        return &nft_exthdr_ipv6_ops;