// SPDX-License-Identifier: GPL-2.0 OR Linux-OpenIB
/* -
* net/sched/act_ct.c Connection Tracking action
*
* Authors: Paul Blakey <paulb@mellanox.com>
* Yossi Kuperman <yossiku@mellanox.com>
* Marcelo Ricardo Leitner <marcelo.leitner@gmail.com>
*/
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/skbuff.h>
#include <linux/rtnetlink.h>
#include <linux/pkt_cls.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <net/netlink.h>
#include <net/pkt_sched.h>
#include <net/pkt_cls.h>
#include <net/act_api.h>
#include <net/ip.h>
#include <net/ipv6_frag.h>
#include <uapi/linux/tc_act/tc_ct.h>
#include <net/tc_act/tc_ct.h>
#include <net/netfilter/nf_conntrack.h>
#include <net/netfilter/nf_conntrack_core.h>
#include <net/netfilter/nf_conntrack_zones.h>
#include <net/netfilter/nf_conntrack_helper.h>
#include <net/netfilter/ipv6/nf_defrag_ipv6.h>
#include <uapi/linux/netfilter/nf_nat.h>
static struct tc_action_ops act_ct_ops;
static unsigned int ct_net_id;
struct tc_ct_action_net {
struct tc_action_net tn; /* Must be first */
bool labels;
};
/* Determine whether skb->_nfct is equal to the result of conntrack lookup. */
static bool tcf_ct_skb_nfct_cached(struct net *net, struct sk_buff *skb,
u16 zone_id, bool force)
{
enum ip_conntrack_info ctinfo;
struct nf_conn *ct;
ct = nf_ct_get(skb, &ctinfo);
if (!ct)
return false;
if (!net_eq(net, read_pnet(&ct->ct_net)))
return false;
if (nf_ct_zone(ct)->id != zone_id)
return false;
/* Force conntrack entry direction. */
if (force && CTINFO2DIR(ctinfo) != IP_CT_DIR_ORIGINAL) {
if (nf_ct_is_confirmed(ct))
nf_ct_kill(ct);
nf_conntrack_put(&ct->ct_general);
nf_ct_set(skb, NULL, IP_CT_UNTRACKED);
return false;
}
return true;
}
/* Trim the skb to the length specified by the IP/IPv6 header,
* removing any trailing lower-layer padding. This prepares the skb
* for higher-layer processing that assumes skb->len excludes padding
* (such as nf_ip_checksum). The caller needs to pull the skb to the
* network header, and ensure ip_hdr/ipv6_hdr points to valid data.
*/
static int tcf_ct_skb_network_trim(struct sk_buff *skb, int family)
{
unsigned int len;
int err;
switch (family) {
case NFPROTO_IPV4:
len = ntohs(ip_hdr(skb)->tot_len);
break;
case NFPROTO_IPV6:
len = sizeof(struct ipv6hdr)
+ ntohs(ipv6_hdr(skb)->payload_len);
break;
default:
len = skb->len;
}
err = pskb_trim_rcsum(skb, len);
return err;
}
static u8 tcf_ct_skb_nf_family(struct sk_buff *skb)
{
u8 family = NFPROTO_UNSPEC;
switch (skb->protocol) {
case htons(ETH_P_IP):
family = NFPROTO_IPV4;
break;
case htons(ETH_P_IPV6):
family = NFPROTO_IPV6;
break;
default:
break;
}
return family;
}
static int tcf_ct_ipv4_is_fragment(struct sk_buff *skb, bool *frag)
{
unsigned int len;
len = skb_network_offset(skb) + sizeof(struct iphdr);
if (unlikely(skb->len < len))
return -EINVAL;
if (unlikely(!pskb_may_pull(skb, len)))
return -ENOMEM;
*frag = ip_is_fragment(ip_hdr(skb));
return 0;
}
static int tcf_ct_ipv6_is_fragment(struct sk_buff *skb, bool *frag)
{
unsigned int flags = 0, len, payload_ofs = 0;
unsigned short frag_off;
int nexthdr;
len = skb_network_offset(skb) + sizeof(struct ipv6hdr);
if (unlikely(skb->len < len))
return -EINVAL;
if (unlikely(!pskb_may_pull(skb, len)))
return -ENOMEM;
nexthdr = ipv6_find_hdr(skb, &payload_ofs, -1, &frag_off, &flags);
if (unlikely(nexthdr < 0))
return -EPROTO;
*frag = flags & IP6_FH_F_FRAG;
return 0;
}
static int tcf_ct_handle_fragments(struct net *net, struct sk_buff *skb,
u8 family, u16 zone)
{
enum ip_conntrack_info ctinfo;
struct nf_conn *ct;
int err = 0;
bool frag;
/* Previously seen (loopback)? Ignore. */
ct = nf_ct_get(skb, &ctinfo);
if ((ct && !nf_ct_is_template(ct)) || ctinfo == IP_CT_UNTRACKED)
return 0;
if (family == NFPROTO_IPV4)
err = tcf_ct_ipv4_is_fragment(skb, &frag);
else
err = tcf_ct_ipv6_is_fragment(skb, &frag);
if (err || !frag)
return err;
skb_get(skb);
if (family == NFPROTO_IPV4) {
enum ip_defrag_users user = IP_DEFRAG_CONNTRACK_IN + zone;
memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
local_bh_disable();
err = ip_defrag(net, skb, user);
local_bh_enable();
if (err && err != -EINPROGRESS)
goto out_free;
} else { /* NFPROTO_IPV6 */
#if IS_ENABLED(CONFIG_NF_DEFRAG_IPV6)
enum ip6_defrag_users user = IP6_DEFRAG_CONNTRACK_IN + zone;
memset(IP6CB(skb), 0, sizeof(struct inet6_skb_parm));
err = nf_ct_frag6_gather(net, skb, user);
if (err && err != -EINPROGRESS)
goto out_free;
#else
err = -EOPNOTSUPP;
goto out_free;
#endif
}
skb_clear_hash(skb);
skb->ignore_df = 1;
return err;