// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (c) 2008-2009 Patrick McHardy <kaber@trash.net>
* Copyright (c) 2016 Pablo Neira Ayuso <pablo@netfilter.org>
*
* Development of this code funded by Astaro AG (http://www.astaro.com/)
*/
#include <linux/kernel.h>
#include <linux/if_vlan.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/netlink.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nf_tables.h>
#include <net/netfilter/nf_tables_core.h>
#include <net/netfilter/nf_tables.h>
#include <net/netfilter/nf_tables_offload.h>
/* For layer 4 checksum field offset. */
#include <linux/tcp.h>
#include <linux/udp.h>
#include <net/gre.h>
#include <linux/icmpv6.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <net/sctp/checksum.h>
static bool nft_payload_rebuild_vlan_hdr(const struct sk_buff *skb, int mac_off,
struct vlan_ethhdr *veth)
{
if (skb_copy_bits(skb, mac_off, veth, ETH_HLEN))
return false;
veth->h_vlan_proto = skb->vlan_proto;
veth->h_vlan_TCI = htons(skb_vlan_tag_get(skb));
veth->h_vlan_encapsulated_proto = skb->protocol;
return true;
}
/* add vlan header into the user buffer for if tag was removed by offloads */
static bool
nft_payload_copy_vlan(u32 *d, const struct sk_buff *skb, u16 offset, u8 len)
{
int mac_off = skb_mac_header(skb) - skb->data;
u8 *vlanh, *dst_u8 = (u8 *) d;
struct vlan_ethhdr veth;
vlanh = (u8 *) &veth;
if (offset < VLAN_ETH_HLEN) {
u8 ethlen = len;
if (!nft_payload_rebuild_vlan_hdr(skb, mac_off, &veth))
return false;
if (offset + len > VLAN_ETH_HLEN)
ethlen -= offset + len - VLAN_ETH_HLEN;
memcpy(dst_u8, vlanh + offset, ethlen);
len -= ethlen;
if (len == 0)
return true;
dst_u8 += ethlen;
offset = ETH_HLEN;
} else {
offset -= VLAN_HLEN;
}
return skb_copy_bits(skb, offset + mac_off, dst_u8, len) == 0;
}
static int __nft_payload_inner_offset(struct nft_pktinfo *pkt)
{
unsigned int thoff = nft_thoff(pkt);
if (!(pkt->flags & NFT_PKTINFO_L4PROTO) || pkt->fragoff)
return -1;
switch (pkt->tprot) {
case IPPROTO_UDP:
pkt->inneroff = thoff + sizeof(struct udphdr);
break;
case IPPROTO_TCP: {
struct tcphdr *th, _tcph;
th = skb_header_pointer(pkt->skb, thoff, sizeof(_tcph), &_tcph);
if (!th)
return -1;
pkt->inneroff = thoff + __tcp_hdrlen(th);
}
break;
case IPPROTO_GRE: {
u32 offset = sizeof(struct gre_base_hdr);
struct gre_base_hdr *gre, _gre;
__be16 version;
gre = skb_header_pointer(pkt->skb, thoff, sizeof(_gre), &_gre);
if (!gre)
return -1;
version = gre->flags & GRE_VERSION;
switch (version) {
case GRE_VERSION_0:
if (gre->flags & GRE_ROUTING)
return -1;
if (gre->flags & GRE_CSUM) {
offset += sizeof_field(struct gre_full_hdr, csum) +
sizeof_field(struct gre_full_hdr, reserved1);
}
if (gre->flags & GRE_KEY)
offset += sizeof_field(struct gre_full_hdr, key);
if (gre->flags & GRE_SEQ)
offset += sizeof_field(struct gre_full_hdr, seq);
break;
default:
return -1;
}
pkt->inneroff = thoff + offset;
}
break;
case IPPROTO_IPIP:
pkt->inneroff = thoff;
break;
default:
return -1;
}
pkt->flags |= NFT_PKTINFO_INNER;
return 0;
}
int nft_payload_inner_offset(const struct nft_pktinfo *pkt)
{
if (!(pkt->flags & NFT_PKTINFO_INNER) &&
__nft_payload_inner_offset((struct nft_pktinfo *)pkt) < 0)
return -1;
return pkt->inneroff;
}
static bool nft_payload_need_vlan_adjust(u32 offset, u32 len)
{
unsigned int boundary = offset + len;
/* data past ether src/dst requested, copy needed */
if (boundary > offsetof(struct ethhdr, h_proto))
return true;
return false;
}
void nft_payload_eval(const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_pktinfo *pkt)
{
const struct nft_payload *priv = nft_expr_priv(expr);
const struct sk_buff *skb = pkt->skb;
u32 *dest = ®s->data[priv->dreg];
int offset;
if (priv->len % NFT_REG32_SIZE)
dest[priv->len / NFT_REG32_SIZE] = 0;
switch (priv->base) {
case NFT_PAYLOAD_LL_HEADER:
if (!skb_mac_header_was_set(skb) || skb_mac_header_len(skb) == 0)
goto err;
if (skb_vlan_tag_present(skb) &&
nft_payload_need_vlan_adjust(priv->offset, priv->len)) {
if (!nft_payload_copy_vlan(dest, skb,
priv->offset, priv->len))
goto err;
return;
}
offset = skb_mac_header(skb) - skb->data;
break;
case NFT_PAYLOAD_NETWORK_HEADER:
offset = skb_network_offset(skb) + pkt->nhoff;
break;
case NFT_PAYLOAD_TRANSPORT_HEADER:
if (!(pkt->flags & NFT_PKTINFO_L4PROTO) || pkt->fragoff)
goto err;
offset = nft_thoff(pkt);
break;
case NFT_PAYLOAD_INNER_HEADER:
offset = nft_payload_inner_offset(pkt);
if (offset < 0)
goto err;
break;
default:
DEBUG_NET_WARN_ON_ONCE(1);
goto err;
}
offset += priv->offset;
if (skb_copy_bits(skb, offset, dest, priv->len) < 0)
goto err;
return;
err:
regs->verdict.code = NFT_BREAK;
}
static const struct nla_policy nft_payload_policy[NFTA_PAYLOAD_MAX + 1] = {
[NFTA_PAYLOAD_SREG] = NLA_POLICY_MAX(NLA_BE32, NFT_REG32_MAX),
[NFTA_PAYLOAD_DREG] = NLA_POLICY_MAX(NLA_BE32, NFT_REG32_MAX),
[NFTA_PAYLOAD_BASE] = { .type = NLA_U32 },
[NFTA_PAYLOAD_OFFSET] = { .type = NLA_BE32 },
[NFTA_PAYLOAD_LEN] = NLA_POLICY_MAX(NLA_BE32, 255),
[NFTA_PAYLOAD_CSUM_TYPE] = { .type = NLA_U32 },
[NFTA_PAYLOAD_CSUM_OFFSET] = NLA_POLICY_MAX(NLA_BE32, 255),
[NFTA_PAYLOAD_CSUM_FLAGS] = NLA_POLICY_MASK(NLA_BE32, NFT_PAYLOAD_L4CSUM_PSEUDOHDR),
};
static int nft_payload_init(const struct nft_ctx *ctx,
const struct nft_expr *expr,
const struct nlattr * const tb[])
{
struct nft_payload *priv = nft_expr_priv(expr);
u32 offset;
int err;
priv->base = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_BASE]));
priv->len = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_LEN]));
err = nft_parse_u32_check(tb[NFTA_PAYLOAD_OFFSET], U16_MAX, &offset);
if (err < 0)
return err;
priv->offset = offset;
return nft_parse_register_store(ctx, tb[NFTA_PAYLOAD_DREG],
&priv->dreg, NULL, NFT_DATA_VALUE,
priv->len);
}
static int nft_payload_dump(struct sk_buff *skb,
const struct nft_expr *expr, bool reset)
{
const struct nft_payload *priv = nft_expr_priv(expr);
if (nft_dump_register(skb, NFTA_PAYLOAD_DREG, priv->dreg) ||
nla_put_be32(skb, NFTA_PAYLOAD_BASE, htonl(priv->base)) ||
nla_put_be32(skb, NFTA_PAYLOAD_OFFSET, htonl(priv->offset)) ||
nla_put_be32(skb, NFTA_PAYLOAD_LEN, htonl(priv->len)))
goto nla_put_failure;
return 0;
nla_put_failure:
return -1;
}
static bool nft_payload_offload_mask(struct nft_offload_reg *reg,
u32 priv_len, u32 field_len)
{
struct nft_data mask = {};
if (priv_len == field_len) {
memset(®->mask, 0xff, priv_len);
return true;
} else if (priv_len > field_len) {
return false;
}
memset(&mask, 0xff, priv_len);
memcpy(®->mask, &mask, field_len);
return true;
}
static int nft_payload_offload_ll(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
const struct nft_payload *priv)
{
struct nft_offload_reg *reg = &ctx->regs[priv->dreg];
switch (priv->offset) {
case offsetof(struct ethhdr, h_source):
if (!nft_payload_offload_mask(reg, priv->len, ETH_ALEN))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_ETH_ADDRS, eth_addrs,
src, ETH_ALEN, reg);
break;
case offsetof(struct ethhdr, h_dest):
if (!nft_payload_offload_mask(reg, priv->len, ETH_ALEN))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_ETH_ADDRS, eth_addrs,
dst, ETH_ALEN, reg);
break;
case offsetof(struct ethhdr, h_proto):
if (!nft_payload_offload_mask(reg, priv->len, sizeof(__be16)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_BASIC, basic,
n_proto, sizeof(__be16), reg);
nft_offload_set_dependency(ctx, NFT_OFFLOAD_DEP_NETWORK);
break;
case offsetof(struct vlan_ethhdr, h_vlan_TCI):
if (!nft_payload_offload_mask(reg, priv->len, sizeof(__be16)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH_FLAGS(FLOW_DISSECTOR_KEY_VLAN, vlan,
vlan_tci, sizeof(__be16), reg,
NFT_OFFLOAD_F_NETWORK2HOST);
break;
case offsetof(struct vlan_ethhdr, h_vlan_encapsulated_proto):
if (!nft_payload_offload_mask(reg, priv->len, sizeof(__be16)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_VLAN, vlan,
vlan_tpid, sizeof(__be16), reg);
nft_offload_set_dependency(ctx, NFT_OFFLOAD_DEP_NETWORK);
break;
case offsetof(struct vlan_ethhdr, h_vlan_TCI) + sizeof(struct vlan_hdr):
if (!nft_payload_offload_mask(reg, priv->len, sizeof(__be16)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH_FLAGS(FLOW_DISSECTOR_KEY_CVLAN, cvlan,
vlan_tci, sizeof(__be16), reg,
NFT_OFFLOAD_F_NETWORK2HOST);
break;
case offsetof(struct vlan_ethhdr, h_vlan_encapsulated_proto) +
sizeof(struct vlan_hdr):
if (!nft_payload_offload_mask(reg, priv->len, sizeof(__be16)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_CVLAN, cvlan,
vlan_tpid, sizeof(__be16), reg);
nft_offload_set_dependency(ctx, NFT_OFFLOAD_DEP_NETWORK);
break;
default:
return -EOPNOTSUPP;
}
return 0;
}
static int nft_payload_offload_ip(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
const struct nft_payload *priv)
{
struct nft_offload_reg *reg = &ctx->regs[priv->dreg];
switch (priv->offset) {
case offsetof(struct iphdr, saddr):
if (!nft_payload_offload_mask(reg, priv->len,
sizeof(struct in_addr)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_IPV4_ADDRS, ipv4, src,
sizeof(struct in_addr), reg);
nft_flow_rule_set_addr_type(flow, FLOW_DISSECTOR_KEY_IPV4_ADDRS);
break;
case offsetof(struct iphdr, daddr):
if (!nft_payload_offload_mask(reg, priv->len,
sizeof(struct in_addr)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_IPV4_ADDRS, ipv4, dst,
sizeof(struct in_addr), reg);
nft_flow_rule_set_addr_type(flow, FLOW_DISSECTOR_KEY_IPV4_ADDRS);
break;
case offsetof(struct iphdr, protocol):
if (!nft_payload_offload_mask(reg, priv->len, sizeof(__u8)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_BASIC, basic, ip_proto,
sizeof(__u8), reg);
nft_offload_set_dependency(ctx, NFT_OFFLOAD_DEP_TRANSPORT);
break;
default:
return -EOPNOTSUPP;
}
return 0;
}
static int nft_payload_offload_ip6(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
const struct nft_payload *priv)
{
struct nft_offload_reg *reg = &ctx->regs[priv->dreg];
switch (priv->offset) {
case offsetof(struct ipv6hdr, saddr):
if (!nft_payload_offload_mask(reg, priv->len,
sizeof(struct in6_addr)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_IPV6_ADDRS, ipv6, src,
sizeof(struct in6_addr), reg);
nft_flow_rule_set_addr_type(flow, FLOW_DISSECTOR_KEY_IPV6_ADDRS);
break;
case offsetof(struct ipv6hdr, daddr):
if (!nft_payload_offload_mask(reg, priv->len,
sizeof(struct in6_addr)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_IPV6_ADDRS, ipv6, dst,
sizeof(struct in6_addr), reg);
nft_flow_rule_set_addr_type(flow, FLOW_DISSECTOR_KEY_IPV6_ADDRS);
break;
case offsetof(struct ipv6hdr, nexthdr):
if (!nft_payload_offload_mask(reg, priv->len, sizeof(__u8)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_BASIC, basic, ip_proto,
sizeof(__u8), reg);
nft_offload_set_dependency(ctx, NFT_OFFLOAD_DEP_TRANSPORT);
break;
default:
return -EOPNOTSUPP;
}
return 0;
}
static int nft_payload_offload_nh(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
const struct nft_payload *priv)
{
int err;
switch (ctx->dep.l3num) {
case htons(ETH_P_IP):
err = nft_payload_offload_ip(ctx, flow, priv);
break;
case htons(ETH_P_IPV6):
err = nft_payload_offload_ip6(ctx, flow, priv);
break;
default:
return -EOPNOTSUPP;
}
return err;
}
static int nft_payload_offload_tcp(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
const struct nft_payload *priv)
{
struct nft_offload_reg *reg = &ctx->regs[priv->dreg];
switch (priv->offset) {
case offsetof(struct tcphdr, source):
if (!nft_payload_offload_mask(reg, priv->len, sizeof(__be16)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_PORTS, tp, src,
sizeof(__be16), reg);
break;
case offsetof(struct tcphdr, dest):
if (!nft_payload_offload_mask(reg, priv->len, sizeof(__be16)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_PORTS, tp, dst,
sizeof(__be16), reg);
break;
default:
return -EOPNOTSUPP;
}
return 0;
}
static int nft_payload_offload_udp(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
const struct nft_payload *priv)
{
struct nft_offload_reg *reg = &ctx->regs[priv->dreg];
switch (priv->offset) {
case offsetof(struct udphdr, source):
if (!nft_payload_offload_mask(reg, priv->len, sizeof(__be16)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_PORTS, tp, src,
sizeof(__be16), reg);
break;
case offsetof(struct udphdr, dest):
if (!nft_payload_offload_mask(reg, priv->len, sizeof(__be16)))
return -EOPNOTSUPP;
NFT_OFFLOAD_MATCH(FLOW_DISSECTOR_KEY_PORTS, tp, dst,
sizeof(__be16), reg);
break;
default:
return -EOPNOTSUPP;
}
return 0;
}
static int nft_payload_offload_th(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
const struct nft_payload *priv)
{
int err;
switch (ctx->dep.protonum) {
case IPPROTO_TCP:
err = nft_payload_offload_tcp(ctx, flow, priv);
break;
case IPPROTO_UDP:
err = nft_payload_offload_udp(ctx, flow, priv);
break;
default:
return -EOPNOTSUPP;
}
return err;
}
static int nft_payload_offload(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
const struct nft_expr *expr)
{
const struct nft_payload *priv = nft_expr_priv(expr);
int err;
switch (priv->base) {
case NFT_PAYLOAD_LL_HEADER:
err = nft_payload_offload_ll(ctx, flow, priv);
break;
case NFT_PAYLOAD_NETWORK_HEADER:
err = nft_payload_offload_nh(ctx, flow, priv);
break;
case NFT_PAYLOAD_TRANSPORT_HEADER:
err = nft_payload_offload_th(ctx, flow, priv);
break;
default:
err = -EOPNOTSUPP;
break;
}
return err;
}
static const struct nft_expr_ops nft_payload_ops = {
.type = &nft_payload_type,
.size = NFT_EXPR_SIZE(sizeof(struct nft_payload)),
.eval = nft_payload_eval,
.init = nft_payload_init,
.dump = nft_payload_dump,
.offload = nft_payload_offload,
};
const struct nft_expr_ops nft_payload_fast_ops = {
.type = &nft_payload_type,
.size = NFT_EXPR_SIZE(sizeof(struct nft_payload)),
.eval = nft_payload_eval,
.init = nft_payload_init,
.dump = nft_payload_dump,
.offload = nft_payload_offload,
};
void nft_payload_inner_eval(const struct nft_expr *expr, struct nft_regs *regs,
const struct nft_pktinfo *pkt,
struct nft_inner_tun_ctx *tun_ctx)
{
const struct nft_payload *priv = nft_expr_priv(expr);
const struct sk_buff *skb = pkt->skb;
u32 *dest = ®s->data[priv->dreg];
int offset;
if (priv->len % NFT_REG32_SIZE)
dest[priv->len / NFT_REG32_SIZE] = 0;
switch (priv->base) {
case NFT_PAYLOAD_TUN_HEADER:
if (!(tun_ctx->flags & NFT_PAYLOAD_CTX_INNER_TUN))
goto err;
offset = tun_ctx->inner_tunoff;
break;
case NFT_PAYLOAD_LL_HEADER:
if (!(tun_ctx->flags & NFT_PAYLOAD_CTX_INNER_LL))
goto err;
offset = tun_ctx->inner_lloff;
break;
case NFT_PAYLOAD_NETWORK_HEADER:
if (!(tun_ctx->flags & NFT_PAYLOAD_CTX_INNER_NH))
goto err;
offset = tun_ctx->inner_nhoff;
break;
case NFT_PAYLOAD_TRANSPORT_HEADER:
if (!(tun_ctx->flags & NFT_PAYLOAD_CTX_INNER_TH))
goto err;
offset = tun_ctx->inner_thoff;
break;
default:
DEBUG_NET_WARN_ON_ONCE(1);
goto err;
}
offset += priv->offset;
if (skb_copy_bits(skb, offset, dest, priv->len) < 0)
goto err;
return;
err:
regs->verdict.code = NFT_BREAK;
}
static int nft_payload_inner_init(const struct nft_ctx *ctx,
const struct nft_expr *expr,
const struct nlattr * const tb[])
{
struct nft_payload *priv = nft_expr_priv(expr);
u32 base, offset;
int err;
if (!tb[NFTA_PAYLOAD_BASE] || !tb[NFTA_PAYLOAD_OFFSET] ||
!tb[NFTA_PAYLOAD_LEN] || !tb[NFTA_PAYLOAD_DREG])
return -EINVAL;
base = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_BASE]));
switch (base) {
case NFT_PAYLOAD_TUN_HEADER:
case NFT_PAYLOAD_LL_HEADER:
case NFT_PAYLOAD_NETWORK_HEADER:
case NFT_PAYLOAD_TRANSPORT_HEADER:
break;
default:
return -EOPNOTSUPP;
}
priv->base = base;
priv->len = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_LEN]));
err = nft_parse_u32_check(tb[NFTA_PAYLOAD_OFFSET], U16_MAX, &offset);
if (err < 0)
return err;
priv->offset = offset;
return nft_parse_register_store(ctx, tb[NFTA_PAYLOAD_DREG],
&priv->dreg, NULL, NFT_DATA_VALUE,
priv->len);
}
static const struct nft_expr_ops nft_payload_inner_ops = {
.type = &nft_payload_type,
.size = NFT_EXPR_SIZE(sizeof(struct nft_payload)),
.init = nft_payload_inner_init,
.dump = nft_payload_dump,
/* direct call to nft_payload_inner_eval(). */
};
static inline void nft_csum_replace(__sum16 *sum, __wsum fsum, __wsum tsum)
{
csum_replace4(sum, (__force __be32)fsum, (__force __be32)tsum);
if (*sum == 0)
*sum = CSUM_MANGLED_0;
}
static bool nft_payload_udp_checksum(struct sk_buff *skb, unsigned int thoff)
{
struct udphdr *uh, _uh;
uh = skb_header_pointer(skb, thoff, sizeof(_uh), &_uh);
if (!uh)
return false;
return (__force bool)uh->check;
}
static int nft_payload_l4csum_offset(const struct nft_pktinfo *pkt,
struct sk_buff *skb,
unsigned int *l4csum_offset)
{
if (pkt->fragoff)
return -1;
switch (pkt->tprot) {
case IPPROTO_TCP:
*l4csum_offset = offsetof(struct tcphdr, check);
break;
case IPPROTO_UDP:
if (!nft_payload_udp_checksum(skb, nft_thoff(pkt)))
return -1;
fallthrough;
case IPPROTO_UDPLITE:
*l4csum_offset = offsetof(struct udphdr, check);
break;
case IPPROTO_ICMPV6:
*l4csum_offset = offsetof(struct icmp6hdr, icmp6_cksum);
break;
default:
return -1;
}
*l4csum_offset += nft_thoff(pkt);
return 0;
}
static int nft_payload_csum_sctp(struct sk_buff *skb, int offset)
{
struct sctphdr *sh;
if (skb_ensure_writable(skb, offset + sizeof(*sh)))
return -1;
sh = (struct sctphdr *)(skb->data + offset);
sh->checksum = sctp_compute_cksum(skb, offset);
skb->ip_summed = CHECKSUM_UNNECESSARY;
return 0;
}
static int nft_payload_l4csum_update(const struct nft_pktinfo *pkt,
struct sk_buff *skb,
__wsum fsum, __wsum tsum)
{
int l4csum_offset;
__sum16 sum;
/* If we cannot determine layer 4 checksum offset or this packet doesn't
* require layer 4 checksum recalculation, skip this packet.
*/
if (nft_payload_l4csum_offset(pkt, skb, &l4csum_offset) < 0)
return 0;
if (skb_copy_bits(skb, l4csum_offset, &sum, sizeof(sum)) < 0)
return -1;
/* Checksum mangling for an arbitrary amount of bytes, based on
* inet_proto_csum_replace*() functions.
*/
if (skb->ip_summed != CHECKSUM_PARTIAL) {
nft_csum_replace(&sum, fsum, tsum);
if (skb->ip_summed == CHECKSUM_COMPLETE) {
skb->csum = ~csum_add(csum_sub(~(skb->csum), fsum),
tsum);
}
} else {
sum = ~csum_fold(csum_add(csum_sub(csum_unfold(sum), fsum),
tsum));
}
if (skb_ensure_writable(skb, l4csum_offset + sizeof(sum)) ||
skb_store_bits(skb, l4csum_offset, &sum, sizeof(sum)) < 0)
return -1;
return 0;
}
static int nft_payload_csum_inet(struct sk_buff *skb, const u32 *src,
__wsum fsum, __wsum tsum, int csum_offset)
{
__sum16 sum;
if (skb_copy_bits(skb, csum_offset, &sum, sizeof(sum)) < 0)
return -1;
nft_csum_replace(&sum, fsum, tsum);
if (skb_ensure_writable(skb, csum_offset + sizeof(sum)) ||
skb_store_bits(skb, csum_offset, &sum, sizeof(sum)) < 0)
return -1;
return 0;
}
struct nft_payload_set {
enum nft_payload_bases base:8;
u16 offset;
u8 len;
u8 sreg;
u8 csum_type;
u8 csum_offset;
u8 csum_flags;
};
/* This is not struct vlan_hdr. */
struct nft_payload_vlan_hdr {
__be16 h_vlan_proto;
__be16 h_vlan_TCI;
};
static bool
nft_payload_set_vlan(const u32 *src, struct sk_buff *skb, u16 offset, u8 len,
int *vlan_hlen)
{
struct nft_payload_vlan_hdr *vlanh;
__be16 vlan_proto;
u16 vlan_tci;
if (offset >= offsetof(struct vlan_ethhdr, h_vlan_encapsulated_proto)) {
*vlan_hlen = VLAN_HLEN;
return true;
}
switch (offset) {
case offsetof(struct vlan_ethhdr, h_vlan_proto):
if (len == 2) {
vlan_proto = nft_reg_load_be16(src);
skb->vlan_proto = vlan_proto;
} else if (len == 4) {
vlanh = (struct nft_payload_vlan_hdr *)src;
__vlan_hwaccel_put_tag(skb, vlanh->h_vlan_proto,
ntohs(vlanh->h_vlan_TCI));
} else {
return false;
}
break;
case offsetof(struct vlan_ethhdr, h_vlan_TCI):
if (len != 2)
return false;
vlan_tci = ntohs(nft_reg_load_be16(src));
skb->vlan_tci = vlan_tci;
break;
default:
return false;
}
return true;
}
/* Ingress is very early, before l3 protocol handlers.
* There should be no in-tree code that trusts l3/l4 headers
* between ingress and NF_INET_PRE_ROUTING hooks.
*/
static bool nft_in_ingress(const struct nf_hook_state *s)
{
return s->pf == NFPROTO_NETDEV && s->hook == NF_NETDEV_INGRESS;
}
static bool nft_nh_write_ok_ip4(const struct nft_pktinfo *pkt,
const struct nft_payload_set *priv,
const u32 *src)
{
unsigned int offset = priv->offset + skb_network_offset(pkt->skb);
const u8 *new_octets = (const u8 *)src;
u8 old_octet;
switch (priv->offset) {
case 0: /* csum fixups does expand dscp/tos store to 2 bytes.
* make sure ihl/version remain unchanged.
*/
if (skb_copy_bits(pkt->skb, offset, &old_octet, sizeof(old_octet)))
return false;
return priv->len == 2 &&
*new_octets == old_octet;
case offsetof(struct iphdr, tos):
return priv->len == 1;
case offsetof(struct iphdr, id):
return priv->len == 2;
case offsetof(struct iphdr, ttl):
if (priv->len == 1)
return true;
if (priv->len != 2)
return false;
/* same, csum fixup does expand ttl store to two bytes.
* check protocol is not altered.
*/
if (skb_copy_bits(pkt->skb, offset + 1, &old_octet, sizeof(old_octet)))
return false;
return new_octets[1] == old_octet;
case offsetof(struct iphdr, check):
return priv->len <= 2 + 4 + 4;
case offsetof(struct iphdr, saddr):
return priv->len <= 4 + 4;
case offsetof(struct iphdr, daddr):
return priv->len <= 4;
}
return false;
}
static bool nft_nh_write_ok_ip6(const struct nft_pktinfo *pkt,
const struct nft_payload_set *priv,
const u32 *src)
{
const struct ipv6hdr *ih = (const void *)src;
switch (priv->offset) {
case 0: /* store to dscp must not alter ip6 version */
return priv->len <= 4 && ih->version == 6;
case 2:
return priv->len <= 2;
case offsetof(struct ipv6hdr, hop_limit):
return priv->len == 1;
case offsetof(struct ipv6hdr, saddr):
return priv->len <= 16 + 16;
case offsetof(struct ipv6hdr, daddr):
return priv->len <= 16;
}
return false;
}
static bool nft_nh_write_ok_arp(const struct nft_payload_set *priv)
{
/* Variable size for standard ethernet arp */
const unsigned int eth_ip = 2 * (ETH_ALEN + 4);
unsigned int offset = priv->offset;
switch (offset) {
case offsetof(struct arphdr, ar_op):
return priv->len == 2;
default:
break;
}
/* permit writes post fixed arp header size. offset + len are
* checked vs skb size via skb_ensure_writable.
*/
return offset >= sizeof(struct arphdr) && priv->len <= eth_ip;
}
static bool nft_nh_write_ok_netdev(const struct nft_pktinfo *pkt,
const struct nft_payload_set *priv,
const u32 *src)
{
#ifdef CONFIG_NF_TABLES_NETDEV
switch (pkt->skb->protocol) {
case htons(ETH_P_ARP):
return nft_nh_write_ok_arp(priv);
case htons(ETH_P_IP):
return nft_nh_write_ok_ip4(pkt, priv, src);
case htons(ETH_P_IPV6):
return nft_nh_write_ok_ip6(pkt, priv, src);
}
#endif
/* default to false for now, relax later in case we have
* use-cases that need inner header manipulation for
* encapsulated traffic like vlan or PPPoE.
*/
return false;
}
static bool nft_nh_write_ok_bridge(const struct nft_pktinfo *pkt,
const struct nft_payload_set *priv,
const u32 *src)
{
#if IS_ENABLED(CONFIG_NF_TABLES_BRIDGE)
switch (pkt->ethertype) {
case htons(ETH_P_ARP):
return nft_nh_write_ok_arp(priv);
case htons(ETH_P_IP):
return nft_nh_write_ok_ip4(pkt, priv, src);
case htons(ETH_P_IPV6):
return nft_nh_write_ok_ip6(pkt, priv, src);
}
#endif
/* see nft_nh_write_ok_netdev: default to false */
return false;
}
static bool nft_nh_write_ok(const struct nft_pktinfo *pkt,
const struct nft_payload_set *priv,
const u32 *src)
{
switch (pkt->state->pf) {
case NFPROTO_ARP:
return nft_nh_write_ok_arp(priv);
case NFPROTO_BRIDGE:
return nft_nh_write_ok_bridge(pkt, priv, src);
case NFPROTO_IPV4:
return nft_nh_write_ok_ip4(pkt, priv, src);
case NFPROTO_IPV6:
return nft_nh_write_ok_ip6(pkt, priv, src);
case NFPROTO_NETDEV:
if (pkt->state->hook == NF_NETDEV_INGRESS)
return true;
return nft_nh_write_ok_netdev(pkt, priv, src);
}
return false;
}
/* check linklayer modifications don't spill into network header. */
static bool nft_ll_write_ok(const struct nft_pktinfo *pkt, int offset)
{
if (nft_in_ingress(pkt->state))
return true;
return offset <= skb_network_offset(pkt->skb);
}
static bool nft_payload_validate_inet_csum_offset(const struct nft_ctx *ctx,
const struct nft_payload_set *priv)
{
switch (priv->base) {
case NFT_PAYLOAD_LL_HEADER:
break;
case NFT_PAYLOAD_NETWORK_HEADER:
if (ctx->family == NFPROTO_IPV4) {
if (offsetof(struct iphdr, check) == priv->csum_offset)
return true;
return false;
}
return true; /* run time validation required */
case NFT_PAYLOAD_TRANSPORT_HEADER:
if (priv->csum_flags) /* makes no sense, asks for "re-update" of L4 checksum */
return false;
/* no further check here; offset can't be negative so bogus
* offsets can corrupt L4 or payload but not l3 headers.
* We already allow arbitrary l4/inner payload writes.
*/
return true;
case NFT_PAYLOAD_INNER_HEADER:
return true;
case NFT_PAYLOAD_TUN_HEADER:
break;
}
return false;
}
/* do not allow arbitrary network header mangling via bogus csum_off.
* We only support ipv4. Only NFPROTO_IPV4 can be checked from control
* plane.
*/
static bool nft_payload_csum_nh_write_ok(const struct nft_payload_set *priv,
const struct nft_pktinfo *pkt)
{
switch (pkt->state->pf) {
case NFPROTO_IPV4:
/* Warning: NFPROTO_INET was not checked; we can't return true here. */
return priv->csum_offset == offsetof(struct iphdr, check);
case NFPROTO_IPV6:
return false;
case NFPROTO_BRIDGE:
return pkt->ethertype == htons(ETH_P_IP) &&
priv->csum_offset == offsetof(struct iphdr, check);
case NFPROTO_NETDEV:
return pkt->skb->protocol == htons(ETH_P_IP) &&
priv->csum_offset == offsetof(struct iphdr, check);
}
return false;
}
static bool nft_payload_csum_write_ok(const struct nft_pktinfo *pkt,
const struct nft_payload_set *priv)
{
switch (priv->base) {
case NFT_PAYLOAD_LL_HEADER:
break;
case NFT_PAYLOAD_NETWORK_HEADER:
return nft_payload_csum_nh_write_ok(priv, pkt);
case NFT_PAYLOAD_TRANSPORT_HEADER:
case NFT_PAYLOAD_INNER_HEADER:
/* neither offsets are validated, offsets cannot be
* negative so real l3 headers cannot be mangled.
*/
return true;
case NFT_PAYLOAD_TUN_HEADER:
break;
}
return false;
}
static void nft_payload_set_eval(const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_pktinfo *pkt)
{
const struct nft_payload_set *priv = nft_expr_priv(expr);
const u32 *src = ®s->data[priv->sreg];
int offset, csum_offset, vlan_hlen = 0;
struct sk_buff *skb = pkt->skb;
__wsum fsum, tsum;
switch (priv->base) {
case NFT_PAYLOAD_LL_HEADER:
if (!skb_mac_header_was_set(skb))
goto err;
if (skb_vlan_tag_present(skb) &&
nft_payload_need_vlan_adjust(priv->offset, priv->len)) {
if (!nft_payload_set_vlan(src, skb,
priv->offset, priv->len,
&vlan_hlen))
goto err;
if (!vlan_hlen)
return;
}
offset = skb_mac_header(skb) - skb->data - vlan_hlen;
if (!nft_ll_write_ok(pkt, priv->len + priv->offset + offset))
goto err;
break;
case NFT_PAYLOAD_NETWORK_HEADER:
if (!nft_nh_write_ok(pkt, priv, src))
goto err;
offset = skb_network_offset(skb);
break;
case NFT_PAYLOAD_TRANSPORT_HEADER:
if (!(pkt->flags & NFT_PKTINFO_L4PROTO) || pkt->fragoff)
goto err;
offset = nft_thoff(pkt);
break;
case NFT_PAYLOAD_INNER_HEADER:
offset = nft_payload_inner_offset(pkt);
if (offset < 0)
goto err;
break;
default:
DEBUG_NET_WARN_ON_ONCE(1);
goto err;
}
csum_offset = offset + priv->csum_offset;
offset += priv->offset;
if ((priv->csum_type == NFT_PAYLOAD_CSUM_INET || priv->csum_flags) &&
((priv->base != NFT_PAYLOAD_TRANSPORT_HEADER &&
priv->base != NFT_PAYLOAD_INNER_HEADER) ||
skb->ip_summed != CHECKSUM_PARTIAL)) {
if (offset + priv->len > skb->len)
goto err;
fsum = skb_checksum(skb, offset, priv->len, 0);
tsum = csum_partial(src, priv->len, 0);
if (priv->csum_type == NFT_PAYLOAD_CSUM_INET &&
nft_payload_csum_write_ok(pkt, priv) &&
nft_payload_csum_inet(skb, src, fsum, tsum, csum_offset))
goto err;
if (priv->csum_flags &&
nft_payload_l4csum_update(pkt, skb, fsum, tsum) < 0)
goto err;
}
if (skb_ensure_writable(skb, max(offset + priv->len, 0)) ||
skb_store_bits(skb, offset, src, priv->len) < 0)
goto err;
if (priv->csum_type == NFT_PAYLOAD_CSUM_SCTP &&
pkt->tprot == IPPROTO_SCTP &&
skb->ip_summed != CHECKSUM_PARTIAL) {
if (pkt->fragoff == 0 &&
nft_payload_csum_sctp(skb, nft_thoff(pkt)))
goto err;
}
return;
err:
regs->verdict.code = NFT_BREAK;
}
static int nft_payload_set_init(const struct nft_ctx *ctx,
const struct nft_expr *expr,
const struct nlattr * const tb[])
{
u32 csum_offset, offset, csum_type = NFT_PAYLOAD_CSUM_NONE;
struct nft_payload_set *priv = nft_expr_priv(expr);
int err;
if (ctx->net->user_ns != &init_user_ns)
return -EPERM;
priv->base = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_BASE]));
priv->len = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_LEN]));
err = nft_parse_u32_check(tb[NFTA_PAYLOAD_OFFSET], U16_MAX, &offset);
if (err < 0)
return err;
priv->offset = offset;
if (tb[NFTA_PAYLOAD_CSUM_TYPE])
csum_type = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_CSUM_TYPE]));
if (tb[NFTA_PAYLOAD_CSUM_OFFSET]) {
err = nft_parse_u32_check(tb[NFTA_PAYLOAD_CSUM_OFFSET], U8_MAX,
&csum_offset);
if (err < 0)
return err;
priv->csum_offset = csum_offset;
}
if (tb[NFTA_PAYLOAD_CSUM_FLAGS]) {
u32 flags;
flags = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_CSUM_FLAGS]));
if (flags & ~NFT_PAYLOAD_L4CSUM_PSEUDOHDR)
return -EINVAL;
priv->csum_flags = flags;
}
switch (csum_type) {
case NFT_PAYLOAD_CSUM_NONE:
if (priv->csum_offset) /* nonsensical */
return -EINVAL;
if (priv->csum_flags == 0)
break;
/* Userspace requests L4 checksum update, e.g.:
* - IPv6 stateless NAT (no l3 csum)
* - transport header mangling
* - inner data mangling
*/
if (priv->base == NFT_PAYLOAD_NETWORK_HEADER ||
priv->base == NFT_PAYLOAD_TRANSPORT_HEADER ||
priv->base == NFT_PAYLOAD_INNER_HEADER)
break;
return -EINVAL;
case NFT_PAYLOAD_CSUM_INET:
if (!nft_payload_validate_inet_csum_offset(ctx, priv))
return -EINVAL;
break;
case NFT_PAYLOAD_CSUM_SCTP:
if (priv->base != NFT_PAYLOAD_TRANSPORT_HEADER)
return -EINVAL;
if (priv->csum_offset != offsetof(struct sctphdr, checksum))
return -EINVAL;
if (priv->csum_flags)
return -EINVAL;
break;
default:
return -EOPNOTSUPP;
}
priv->csum_type = csum_type;
return nft_parse_register_load(ctx, tb[NFTA_PAYLOAD_SREG], &priv->sreg,
priv->len);
}
static int nft_payload_set_dump(struct sk_buff *skb,
const struct nft_expr *expr, bool reset)
{
const struct nft_payload_set *priv = nft_expr_priv(expr);
if (nft_dump_register(skb, NFTA_PAYLOAD_SREG, priv->sreg) ||
nla_put_be32(skb, NFTA_PAYLOAD_BASE, htonl(priv->base)) ||
nla_put_be32(skb, NFTA_PAYLOAD_OFFSET, htonl(priv->offset)) ||
nla_put_be32(skb, NFTA_PAYLOAD_LEN, htonl(priv->len)) ||
nla_put_be32(skb, NFTA_PAYLOAD_CSUM_TYPE, htonl(priv->csum_type)) ||
nla_put_be32(skb, NFTA_PAYLOAD_CSUM_OFFSET,
htonl(priv->csum_offset)) ||
nla_put_be32(skb, NFTA_PAYLOAD_CSUM_FLAGS, htonl(priv->csum_flags)))
goto nla_put_failure;
return 0;
nla_put_failure:
return -1;
}
static const struct nft_expr_ops nft_payload_set_ops = {
.type = &nft_payload_type,
.size = NFT_EXPR_SIZE(sizeof(struct nft_payload_set)),
.eval = nft_payload_set_eval,
.init = nft_payload_set_init,
.dump = nft_payload_set_dump,
};
static const struct nft_expr_ops *
nft_payload_select_ops(const struct nft_ctx *ctx,
const struct nlattr * const tb[])
{
enum nft_payload_bases base;
unsigned int offset, len;
int err;
if (tb[NFTA_PAYLOAD_BASE] == NULL ||
tb[NFTA_PAYLOAD_OFFSET] == NULL ||
tb[NFTA_PAYLOAD_LEN] == NULL)
return ERR_PTR(-EINVAL);
base = ntohl(nla_get_be32(tb[NFTA_PAYLOAD_BASE]));
switch (base) {
case NFT_PAYLOAD_LL_HEADER:
case NFT_PAYLOAD_NETWORK_HEADER:
case NFT_PAYLOAD_TRANSPORT_HEADER:
case NFT_PAYLOAD_INNER_HEADER:
break;
default:
return ERR_PTR(-EOPNOTSUPP);
}
if (tb[NFTA_PAYLOAD_SREG] != NULL) {
if (tb[NFTA_PAYLOAD_DREG] != NULL)
return ERR_PTR(-EINVAL);
return &nft_payload_set_ops;
}
if (tb[NFTA_PAYLOAD_DREG] == NULL)
return ERR_PTR(-EINVAL);
err = nft_parse_u32_check(tb[NFTA_PAYLOAD_OFFSET], U16_MAX, &offset);
if (err < 0)
return ERR_PTR(err);
err = nft_parse_u32_check(tb[NFTA_PAYLOAD_LEN], U8_MAX, &len);
if (err < 0)
return ERR_PTR(err);
if (len <= 4 && is_power_of_2(len) && IS_ALIGNED(offset, len) &&
base != NFT_PAYLOAD_LL_HEADER && base != NFT_PAYLOAD_INNER_HEADER)
return &nft_payload_fast_ops;
else
return &nft_payload_ops;
}
struct nft_expr_type nft_payload_type __read_mostly = {
.name = "payload",
.select_ops = nft_payload_select_ops,
.inner_ops = &nft_payload_inner_ops,
.policy = nft_payload_policy,
.maxattr = NFTA_PAYLOAD_MAX,
.owner = THIS_MODULE,
};