blob: 018b2a6fe13f2d94524378a49bbd80379ce7ed65 [file] [log] [blame]
/* Copyright (c) 2021, The Linux Foundation. All rights reserved.
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <linux/version.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/if.h>
#include <linux/list.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <net/xfrm.h>
#include <net/protocol.h>
#include <linux/inetdevice.h>
#include <net/addrconf.h>
#include <linux/netfilter.h>
#include <nss_api_if.h>
#include <nss_ipsec.h>
#include <nss_ipsecmgr.h>
#include "nss_ipsec_xfrm_tunnel.h"
#include "nss_ipsec_xfrm_sa.h"
#include "nss_ipsec_xfrm_flow.h"
#include "nss_ipsec_xfrm.h"
/*
* nss_ipsec_xfrm_tunnel_free_work()
* Tunnel Free work function.
*/
static void nss_ipsec_xfrm_tunnel_free_work(struct work_struct *work)
{
struct nss_ipsec_xfrm_tunnel *tun = container_of(work, struct nss_ipsec_xfrm_tunnel, free_work);
struct nss_ipsec_xfrm_drv *drv = tun->drv;
/*
* There should be no default_outer xfrm_state assoicated,
* with a tunnel that is getting freed.
*/
BUG_ON(atomic_read(&tun->default_spi));
BUG_ON(!tun->dev);
nss_ipsec_xfrm_info("%p: free tunnel: nss_dev %s", tun, tun->dev->name);
nss_ipsecmgr_tunnel_del(tun->dev);
atomic64_inc(&drv->stats.tun_freed);
if (atomic_dec_and_test(&drv->num_tunnels)) {
complete(&drv->complete);
}
kfree(tun);
}
/*
* nss_ipsec_xfrm_tunnel_final()
* Called when the last held reference to the tunnel is released.
*/
static void nss_ipsec_xfrm_tunnel_final(struct kref *kref)
{
struct nss_ipsec_xfrm_tunnel *tun = container_of(kref, struct nss_ipsec_xfrm_tunnel, ref);
nss_ipsec_xfrm_trace("%p:Tunnel destroyed (%s) \n", tun, tun->dev->name);
schedule_work(&tun->free_work);
}
/*
* nss_ipsec_xfrm_tunnel_rx_inner(()
* Inner exception callback
*/
static void nss_ipsec_xfrm_tunnel_rx_inner(void *app_data, struct sk_buff *skb)
{
struct nss_ipsec_xfrm_tunnel *tun = app_data;
struct nss_ipsec_xfrm_sa *default_sa;
struct nss_ipsec_xfrm_drv *drv = tun->drv;
nss_ipsec_xfrm_trace("%p: Packet(%px) exceptioned after decapsulation; ip_version %d\n", tun, skb, ip_hdr(skb)->version);
/*
* No secpath should be exisitng with the exception packet.
* XXX Should we add a BUG_ON() here?
*/
if (secpath_exists(skb)) {
nss_ipsec_xfrm_err("%p: Exceptioned packet(%px) already has a SP loaded; dropping\n", tun, skb);
goto drop;
}
/*
* sec_path is needed for IN/FWD IPSec policy check. sec_path needs the xfrm_state
* that was used to decrypt the packet.
* We feed the secpath with default_outer SA. Note that during rekey transition,
* this might not be the actual SA that was used to decrypt this packet.
* But it should be fine, as the old and new rekey SAs would share the same properties
* and mode.
*/
default_sa = nss_ipsec_xfrm_sa_ref_by_spi(atomic_read(&tun->default_spi), tun->family);
if (!default_sa) {
nss_ipsec_xfrm_err("%p: Exceptioned packet(%px) no default_outer SA assoicated; dropping\n", tun, skb);
goto drop;
}
/*
* Allocate and setup the SP in SKB
*/
if (!nss_ipsec_xfrm_sa_sp_set(default_sa, skb)) {
nss_ipsec_xfrm_err("%p: Failed to handle exception(%px) after decap; No SP(%p)\n", tun, skb, default_sa);
goto err;
}
atomic64_inc(&drv->stats.inner_except);
nss_ipsec_xfrm_sa_deref(default_sa);
netif_receive_skb(skb);
return;
err:
nss_ipsec_xfrm_sa_deref(default_sa);
drop:
atomic64_inc(&drv->stats.inner_drop);
dev_kfree_skb_any(skb);
return;
}
/*
* nss_ipsec_xfrm_tunnel_rx_outer()
* Outer exception callback
*/
static void nss_ipsec_xfrm_tunnel_rx_outer(void *app_data, struct sk_buff *skb)
{
int (*local_out)(struct net *, struct sock *, struct sk_buff *);
struct nss_ipsec_xfrm_tunnel *tun = app_data;
struct nss_ipsec_xfrm_drv *drv = tun->drv;
struct dst_entry *dst;
nss_ipsec_xfrm_trace("%p: Packet(%px) exceptioned after encapsulation; ip_version %d\n", tun, skb, ip_hdr(skb)->version);
if (ip_hdr(skb)->version == IPVERSION) {
struct iphdr *iph = ip_hdr(skb);
struct rtable *rt = ip_route_output(&init_net, iph->daddr, iph->saddr, 0, 0);
if (unlikely(IS_ERR(rt))) {
nss_ipsec_xfrm_warn("%px: Failed to handle ipv4 exception after encap; No route\n", skb);
goto drop;
}
dst = &rt->dst;
local_out = ip_local_out;
IPCB(skb)->flags |= IPSKB_XFRM_TRANSFORMED;
} else {
struct ipv6hdr *ip6h = ipv6_hdr(skb);
struct flowi6 fl6;
memset(&fl6, 0, sizeof(struct flowi6));
memcpy(&fl6.daddr, &ip6h->daddr, sizeof(fl6.daddr));
memcpy(&fl6.saddr, &ip6h->saddr, sizeof(fl6.saddr));
dst = ip6_route_output(&init_net, NULL, &fl6);
if (unlikely(IS_ERR(dst))) {
nss_ipsec_xfrm_warn("%px: Failed to handle ipv6 exception after encap; No route\n", skb);
goto drop;
}
local_out = ip6_local_out;
IP6CB(skb)->flags |= IP6SKB_XFRM_TRANSFORMED;
}
/*
* Sets the 'dst' entry for SKB and sends the packet out directly to the physical
* device associated with the IPsec tunnel interface.
*/
skb_dst_set(skb, dst);
skb->ip_summed = CHECKSUM_COMPLETE;
local_out(&init_net, NULL, skb);
atomic64_inc(&drv->stats.outer_except);
return;
drop:
dev_kfree_skb_any(skb);
atomic64_inc(&drv->stats.outer_drop);
return;
}
/*
* nss_ipsec_xfrm_tunnel_proc_event()
* NSS xfrm event cb registered with ipsecmgr
* Used for syncing per-SA stats.
*/
static void nss_ipsec_xfrm_tunnel_proc_event(void *app_data, struct nss_ipsecmgr_event *event)
{
struct nss_ipsec_xfrm_tunnel *tun = app_data;
/*
* We only handle stats event for now
*/
if (event->type != NSS_IPSECMGR_EVENT_SA_STATS) {
return;
}
nss_ipsec_xfrm_update_stats(tun->drv, &event->data.stats);
}
/*
* nss_ipsec_xfrm_tunnel_deref()
* Put tunnel ref. Schedules free if the last reference was released.
* Caller needs to hold the write lock, as this could be the last reference
* that is getting put.
*/
void nss_ipsec_xfrm_tunnel_deref(struct nss_ipsec_xfrm_tunnel *tun)
{
kref_put(&tun->ref, nss_ipsec_xfrm_tunnel_final);
}
/*
* nss_ipsec_xfrm_tunnel_ref
* Hold tunnel ref.
*/
struct nss_ipsec_xfrm_tunnel * nss_ipsec_xfrm_tunnel_ref(struct nss_ipsec_xfrm_tunnel *tun)
{
kref_get(&tun->ref);
return tun;
}
/*
* nss_ipsec_xfrm_tunnel_match()
* Returns true if the supplied IP addreses matches the tunnel's
* remote and local addresses
*/
bool nss_ipsec_xfrm_tunnel_match(struct nss_ipsec_xfrm_tunnel *tun, xfrm_address_t *l, xfrm_address_t *r, uint16_t family)
{
uint8_t status = 0;
if (family == AF_INET) {
nss_ipsec_xfrm_trace("%p: family %d remote %pI4 local %pI4", tun, family, &r->a4, &l->a4);
status += (tun->local.a4 ^ l->a4);
status += (tun->remote.a4 ^ r->a4);
return !status;
}
nss_ipsec_xfrm_trace( "%p: family %d remote %pI6 local %pI6", tun, family, r->a6, l->a6);
status += xfrm6_addr_equal(&tun->local, l);
status += xfrm6_addr_equal(&tun->remote, r);
return !status;
}
/*
* nss_ipsec_xfrm_tunnel_dealloc()
* Free a tunnel object.
*/
void nss_ipsec_xfrm_tunnel_dealloc(struct nss_ipsec_xfrm_tunnel *tun)
{
struct nss_ipsec_xfrm_drv *drv = tun->drv;
atomic64_inc(&drv->stats.tun_dealloced);
nss_ipsec_xfrm_tunnel_deref(tun);
}
/*
* nss_ipsec_xfrm_tunnel_alloc()
* Creates and initializes a new tunnel obj
*/
struct nss_ipsec_xfrm_tunnel *nss_ipsec_xfrm_tunnel_alloc(struct nss_ipsec_xfrm_drv *drv, xfrm_address_t *local,
xfrm_address_t *remote, uint16_t family)
{
struct nss_ipsecmgr_callback ipsec_cb = {0};
struct nss_ipsec_xfrm_tunnel *tun;
struct rtable *rt;
struct rt6_info *rt6;
struct dst_entry *dst;
uint8_t ttl_hop_limit;
uint16_t overhead;
uint32_t mtu;
switch (family) {
case AF_INET:
rt = ip_route_output(&init_net, remote->a4, 0, 0, 0);
if (IS_ERR(rt)) {
nss_ipsec_xfrm_err("%p:Failed to allocate tunnel; No IPv4 dst found\n", drv);
return NULL;
}
dst = &rt->dst;
ttl_hop_limit = ip4_dst_hoplimit(dst);
overhead = NSS_IPSEC_XFRM_TUNNEL_V4_MAX_OVERHEAD;
break;
case AF_INET6:
#if (LINUX_VERSION_CODE < KERNEL_VERSION(5, 4, 0))
rt6 = rt6_lookup(&init_net, &remote->in6, NULL, 0, 0);
#else
rt6 = rt6_lookup(&init_net, &remote->in6, NULL, 0, 0, 0);
#endif
if (!rt6) {
nss_ipsec_xfrm_err("Failed to allocate tunnel; No IPv6 dst found\n");
return NULL;
}
dst = &rt6->dst;
ttl_hop_limit = ip6_dst_hoplimit(dst);
overhead = NSS_IPSEC_XFRM_TUNNEL_V6_MAX_OVERHEAD;
break;
default:
nss_ipsec_xfrm_err("%p:Failed to allocate tunnel; bad family type (%u)\n", drv, family);
return NULL;
}
mtu = dst_mtu(dst) - overhead;
dst_release(dst);
tun = kzalloc(sizeof(struct nss_ipsec_xfrm_tunnel), GFP_KERNEL);
if (!tun) {
nss_ipsec_xfrm_err("Failed to create tunnel; Out of memory\n");
return NULL;
}
ipsec_cb.except_cb = nss_ipsec_xfrm_tunnel_rx_outer;
ipsec_cb.data_cb = nss_ipsec_xfrm_tunnel_rx_inner;
ipsec_cb.event_cb = nss_ipsec_xfrm_tunnel_proc_event;
ipsec_cb.app_data = tun;
/*
* Create IPsec manager tunnel netdevice
*/
tun->dev = nss_ipsecmgr_tunnel_add(&ipsec_cb);
if (!tun->dev) {
nss_ipsec_xfrm_err("%p: Failed to program tunnel(%p) in IPsec manager\n", drv, tun);
kfree(tun);
return NULL;
}
tun->ttl = ttl_hop_limit;
tun->family = family;
tun->drv = drv;
/*
* Copy the addresses for future comparision
*/
memcpy(&tun->remote, remote, sizeof(xfrm_address_t));
memcpy(&tun->local, local, sizeof(xfrm_address_t));
/*
* Tunnel reference held; will be released in free or deref
*/
kref_init(&tun->ref);
atomic64_inc(&drv->stats.tun_alloced);
atomic_inc(&drv->num_tunnels);
INIT_WORK(&tun->free_work, nss_ipsec_xfrm_tunnel_free_work);
/*
* Update the tunnel MTU for NSS netdevice
*/
rtnl_lock();
dev_set_mtu(tun->dev, mtu);
rtnl_unlock();
nss_ipsec_xfrm_trace("%p: XFRM offload tunnel created\n", tun);
return tun;
}