blob: e3c46ee7c3b604000b6cc5a02c3f0a021ae759c5 [file] [log] [blame]
/*
**************************************************************************
* Copyright (c) 2016-2017, 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.
**************************************************************************
*/
/* nss_ipsecmgr_subnet.c
* NSS IPsec manager subnet rules
*/
#include <linux/list.h>
#include <linux/hashtable.h>
#include <linux/types.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <net/ipv6.h>
#include <linux/version.h>
#include <linux/debugfs.h>
#include <linux/vmalloc.h>
#include <nss_ipsecmgr.h>
#include "nss_ipsecmgr_priv.h"
extern struct nss_ipsecmgr_drv *ipsecmgr_ctx;
/*
* nss_ipsecmgr_subnet_key_data2idx()
* subnet specific api for converting word stream to index
*/
static uint32_t nss_ipsecmgr_subnet_key_data2idx(struct nss_ipsecmgr_key *key, const uint32_t table_sz)
{
struct nss_ipsecmgr_key tmp_key;
/*
* The subnet table is keyed without the protocol. This allows us to support
* "any" protocol configuration
*/
memcpy(&tmp_key, key, sizeof(struct nss_ipsecmgr_key));
nss_ipsecmgr_key_clear_8(&tmp_key, NSS_IPSECMGR_KEY_POS_IP_PROTO);
return nss_ipsecmgr_key_data2idx(&tmp_key, table_sz);
}
/*
* nss_ipsecmgr_netmask_is_default()
* confirm if key is for default netmask.
*/
static inline bool nss_ipsecmgr_netmask_is_default(struct nss_ipsecmgr_key *key)
{
uint8_t ip_ver;
uint64_t mask64_low;
uint64_t mask64_high;
ip_ver = nss_ipsecmgr_key_read_8(key, NSS_IPSECMGR_KEY_POS_IP_VER);
if (likely(ip_ver == 4)) {
return !nss_ipsecmgr_key_read_mask32(key, NSS_IPSECMGR_KEY_POS_IPV4_DST);
}
mask64_low = nss_ipsecmgr_key_read_mask64(key, NSS_IPSECMGR_KEY_POS_IPV6_DST);
mask64_high = nss_ipsecmgr_key_read_mask64(key, NSS_IPSECMGR_KEY_POS_IPV6_DST + 64);
return !(mask64_low || mask64_high);
}
/*
* nss_ipsecmgr_netmask2idx()
* Get the index of the netmask array from key.
*/
static inline uint32_t nss_ipsecmgr_netmask2idx(struct nss_ipsecmgr_key *key)
{
uint32_t mask32;
uint64_t mask64_low;
uint64_t mask64_high;
uint8_t ip_ver;
ip_ver = nss_ipsecmgr_key_read_8(key, NSS_IPSECMGR_KEY_POS_IP_VER);
if (likely(ip_ver == 4)) {
mask32 = nss_ipsecmgr_key_read_mask32(key, NSS_IPSECMGR_KEY_POS_IPV4_DST);
return ffs(mask32) - 1;
}
BUG_ON(ip_ver != 6);
mask64_low = nss_ipsecmgr_key_read_mask64(key, NSS_IPSECMGR_KEY_POS_IPV6_DST);
if (mask64_low) {
return __ffs64(mask64_low);
}
mask64_high = nss_ipsecmgr_key_read_mask64(key, NSS_IPSECMGR_KEY_POS_IPV6_DST + 64);
if (mask64_high) {
return __ffs64(mask64_high) + 64;
}
return NSS_IPSECMGR_MAX_NETMASK;
}
/*
* nss_ipsecmgr_netmask_free()
* deallocate a netmask entry
*/
static bool nss_ipsecmgr_netmask_free(struct nss_ipsecmgr_netmask_db *db, struct nss_ipsecmgr_key *key)
{
struct nss_ipsecmgr_netmask_entry *entry;
bool is_default;
uint32_t idx;
is_default = nss_ipsecmgr_netmask_is_default(key);
entry = db->default_entry;
if (is_default && !entry) { /* default with no entry */
return false;
} else if (is_default && --entry->count) { /* default but more than 1 entry */
return false;
} else if (is_default) { /* default but last entry */
db->default_entry = NULL;
goto free;
}
/*
* !default
*/
idx = nss_ipsecmgr_netmask2idx(key);
if (idx >= NSS_IPSECMGR_MAX_NETMASK) {
return false;
}
entry = db->entries[idx];
BUG_ON(!entry);
if (--entry->count) {
return false;
}
clear_bit(idx, db->bitmap);
db->entries[idx] = NULL;
free:
kfree(entry);
return true;
}
/*
* nss_ipsecmgr_subnet_dump()
* Dump single subnet stats
*/
static ssize_t nss_ipsecmgr_subnet_dump(struct nss_ipsecmgr_key *key, struct nss_ipsec_msg *nim, char *buf, ssize_t max_len)
{
uint32_t subnet[4] = {0}, mask[4] = {0};
ssize_t len = 0;
uint8_t ip_ver;
uint8_t proto;
proto = nss_ipsecmgr_key_read_8(key, NSS_IPSECMGR_KEY_POS_IP_PROTO);
ip_ver = nss_ipsecmgr_key_read_8(key, NSS_IPSECMGR_KEY_POS_IP_VER);
switch (ip_ver) {
case 4:
nss_ipsecmgr_key_read(key, subnet, mask, NSS_IPSECMGR_KEY_POS_IPV4_DST, 1);
len += snprintf(buf + len, max_len - len, "dst_ip: %pI4h\n", subnet);
len += snprintf(buf + len, max_len - len, "dst_mask: %pI4h\n", mask);
break;
case 6:
/*
* The subnet and mask bits from the key are read to the upper words of the array
* later converted to network order for display.
*/
nss_ipsecmgr_key_read(key, &subnet[0], &mask[0], NSS_IPSECMGR_KEY_POS_IPV6_DST, 4);
nss_ipsecmgr_v6addr_hton(subnet, subnet);
nss_ipsecmgr_v6addr_hton(mask, mask);
len += snprintf(buf + len, max_len - len, "dst_ip: %pI6c\n", subnet);
len += snprintf(buf + len, max_len - len, "dst_mask: %pI6c\n", mask);
break;
}
len += snprintf(buf + len, max_len - len, "proto: %d\n", proto);
return len;
}
/*
* nss_ipsecmgr_subnet_read_stats()
* retreive subnet stats
*/
static ssize_t nss_ipsecmgr_subnet_read_stats(struct nss_ipsecmgr_netmask_entry *netmask, char *buf, uint32_t max_len)
{
struct nss_ipsecmgr_subnet_entry *entry;
struct list_head *head;
ssize_t len = 0;
int idx;
/*
* Check if we have valid entries in the netmask db
*/
if (!netmask || (netmask->count == 0)) {
return 0;
}
head = &netmask->subnets[0];
for (idx = NSS_IPSECMGR_MAX_SUBNET; (max_len > 0) && idx--; head++) {
list_for_each_entry(entry, head, node) {
if (unlikely(max_len <= 0)) {
break;
}
len += nss_ipsecmgr_subnet_dump(&entry->key, &entry->nim, buf + len, max_len);
max_len = max_len - len;
}
}
return len;
}
/*
* nss_ipsecmgr_netmask_stats_read()
* read subnet statistics
*/
ssize_t nss_ipsecmgr_netmask_stats_read(struct file *fp, char __user *ubuf, size_t sz, loff_t *ppos)
{
struct nss_ipsecmgr_netmask_db *net_db;
ssize_t len, max_len;
uint32_t num_entries;
ssize_t ret;
char *buf;
int i;
net_db = &ipsecmgr_ctx->net_db;
num_entries = atomic_read(&net_db->num_entries);
if (!num_entries) {
return 0;
}
max_len = num_entries * NSS_IPSECMGR_SUBNET_STATS_SIZE;
buf = vzalloc(max_len);
if (!buf) {
nss_ipsecmgr_error("unable to allocate local buffer for subnet stats\n");
return 0;
}
/*
* Take the read lock.
*/
read_lock_bh(&ipsecmgr_ctx->lock);
/*
* retreive the default subnet entry stats
*/
len = nss_ipsecmgr_subnet_read_stats(net_db->default_entry, buf, max_len);
/*
* retreive the netmask entry db
*/
for (i = 0; i < NSS_IPSECMGR_MAX_NETMASK; i++) {
len += nss_ipsecmgr_subnet_read_stats(net_db->entries[i], buf + len, max_len - len);
}
read_unlock_bh(&ipsecmgr_ctx->lock);
ret = simple_read_from_buffer(ubuf, sz, ppos, buf, len);
vfree(buf);
return ret;
}
/*
* nss_ipsecmgr_subnet_update()
* update the subnet with its associated data
*/
static void nss_ipsecmgr_subnet_update(struct nss_ipsecmgr_priv *priv, struct nss_ipsecmgr_ref *ref, struct nss_ipsec_msg *nim)
{
struct nss_ipsecmgr_subnet_entry *subnet;
subnet = container_of(ref, struct nss_ipsecmgr_subnet_entry, ref);
memcpy(&subnet->nim, nim, sizeof(struct nss_ipsec_msg));
}
/*
* nss_ipsecmgr_subnet_free()
* free the associated subnet entry and notify NSS
*/
static void nss_ipsecmgr_subnet_free(struct nss_ipsecmgr_priv *priv, struct nss_ipsecmgr_ref *ref)
{
struct nss_ipsecmgr_subnet_entry *subnet;
struct nss_ipsecmgr_netmask_db *db = &ipsecmgr_ctx->net_db;
subnet = container_of(ref, struct nss_ipsecmgr_subnet_entry, ref);
BUG_ON(nss_ipsecmgr_ref_is_empty(ref) == false);
/*
* detach it from the netmask entry database and
* check if the netmask entry is empty. The netmask
* entry will get freed if there are no further entries
* available
*/
list_del(&subnet->node);
atomic_dec(&db->num_entries);
nss_ipsecmgr_netmask_free(&ipsecmgr_ctx->net_db, &subnet->key);
kfree(subnet);
}
/*
* nss_ipsecmgr_netmask_lookup()
* lookup a netmask entry
*/
static inline struct nss_ipsecmgr_netmask_entry *nss_ipsecmgr_netmask_lookup(struct nss_ipsecmgr_priv *priv, struct nss_ipsecmgr_key *key)
{
struct nss_ipsecmgr_netmask_db *db = &ipsecmgr_ctx->net_db;
uint32_t idx;
if (nss_ipsecmgr_netmask_is_default(key)) {
return db->default_entry;
}
idx = nss_ipsecmgr_netmask2idx(key);
return (idx >= NSS_IPSECMGR_MAX_NETMASK) ? NULL : db->entries[idx];
}
/*
* nss_ipsecmgr_netmask_alloc()
* allocate a netmask entry
*/
static struct nss_ipsecmgr_netmask_entry *nss_ipsecmgr_netmask_alloc(struct nss_ipsecmgr_priv *priv, struct nss_ipsecmgr_key *key)
{
struct nss_ipsecmgr_netmask_db *db = &ipsecmgr_ctx->net_db;
struct nss_ipsecmgr_netmask_entry *entry;
uint32_t idx;
entry = nss_ipsecmgr_netmask_lookup(priv, key);
if (entry) {
return entry;
}
entry = kzalloc(sizeof(struct nss_ipsecmgr_netmask_entry), GFP_ATOMIC);
if (!entry) {
return NULL;
}
nss_ipsecmgr_init_subnet_db(entry);
entry->count = 1;
if (nss_ipsecmgr_netmask_is_default(key)) {
db->default_entry = entry;
return entry;
}
idx = nss_ipsecmgr_netmask2idx(key);
if (idx >= NSS_IPSECMGR_MAX_NETMASK) {
kfree(entry);
return NULL;
}
entry->mask_bits = NSS_IPSECMGR_MAX_NETMASK - idx;
set_bit(idx, db->bitmap);
db->entries[idx] = entry;
return entry;
}
/*
* nss_ipsecmgr_copy_subnet()
* copy subnet nim
*/
void nss_ipsecmgr_copy_subnet(struct nss_ipsec_msg *nim, struct nss_ipsecmgr_ref *ref)
{
struct nss_ipsecmgr_subnet_entry *entry;
struct nss_ipsec_rule_data *data;
struct nss_ipsec_rule_oip *oip;
entry = container_of(ref, struct nss_ipsecmgr_subnet_entry, ref);
oip = &entry->nim.msg.rule.oip;
data = &entry->nim.msg.rule.data;
memcpy(&nim->msg.rule.oip, oip, sizeof(struct nss_ipsec_rule_oip));
memcpy(&nim->msg.rule.data, data, sizeof(struct nss_ipsec_rule_data));
}
/*
* nss_ipsecmgr_v4_subnet_sel2key()
* convert subnet selector to key
*/
void nss_ipsecmgr_v4_subnet_tuple2key(struct nss_ipsec_tuple *tuple, struct nss_ipsecmgr_key *key)
{
nss_ipsecmgr_key_reset(key);
nss_ipsecmgr_key_write_8(key, 4 /* ipv4 */, NSS_IPSECMGR_KEY_POS_IP_VER);
nss_ipsecmgr_key_write_8(key, tuple->proto_next_hdr, NSS_IPSECMGR_KEY_POS_IP_PROTO);
nss_ipsecmgr_key_write_32(key, nss_ipsecmgr_get_v4addr(tuple->dst_addr), NSS_IPSECMGR_KEY_POS_IPV4_DST);
key->len = NSS_IPSECMGR_KEY_LEN_IPV4_SUBNET;
}
/*
* nss_ipsecmgr_v6_subnet_sel2key()
* convert subnet selector to key
*/
void nss_ipsecmgr_v6_subnet_tuple2key(struct nss_ipsec_tuple *tuple, struct nss_ipsecmgr_key *key)
{
uint32_t pos;
uint32_t i;
nss_ipsecmgr_key_reset(key);
nss_ipsecmgr_key_write_8(key, 6 /* ipv6 */, NSS_IPSECMGR_KEY_POS_IP_VER);
nss_ipsecmgr_key_write_8(key, tuple->proto_next_hdr, NSS_IPSECMGR_KEY_POS_IP_PROTO);
for (i = 0; i < 4; i++) {
pos = NSS_IPSECMGR_KEY_POS_IPV6_DST + (i * NSS_IPSECMGR_BITS_PER_WORD);
nss_ipsecmgr_key_write_32(key, tuple->dst_addr[i], pos);
}
key->len = NSS_IPSECMGR_KEY_LEN_IPV6_SUBNET;
}
/*
* nss_ipsecmgr_v4_subnet2key()
* convert an v4 subnet into a key
*/
void nss_ipsecmgr_v4_subnet2key(struct nss_ipsecmgr_encap_v4_subnet *net, struct nss_ipsecmgr_key *key)
{
nss_ipsecmgr_key_reset(key);
nss_ipsecmgr_key_write_8(key, 4 /* ipv4 */, NSS_IPSECMGR_KEY_POS_IP_VER);
nss_ipsecmgr_key_write_8(key, (uint8_t)net->protocol, NSS_IPSECMGR_KEY_POS_IP_PROTO);
nss_ipsecmgr_key_write(key, &net->dst_subnet, &net->dst_mask, NSS_IPSECMGR_KEY_POS_IPV4_DST, 1);
/*
* clear mask if caller specify protocol as any (0xff).
* this will serve as default entry for any-protocol.
*/
if (net->protocol == NSS_IPSECMGR_PROTO_NEXT_HDR_ANY) {
nss_ipsecmgr_key_clear_8(key, NSS_IPSECMGR_KEY_POS_IP_PROTO);
}
key->len = NSS_IPSECMGR_KEY_LEN_IPV4_SUBNET;
}
/*
* nss_ipsecmgr_v6_subnet2key()
* convert an v6 subnet into a key
*/
void nss_ipsecmgr_v6_subnet2key(struct nss_ipsecmgr_encap_v6_subnet *net, struct nss_ipsecmgr_key *key)
{
nss_ipsecmgr_key_reset(key);
nss_ipsecmgr_key_write_8(key, 6 /* ipv6 */, NSS_IPSECMGR_KEY_POS_IP_VER);
nss_ipsecmgr_key_write_8(key, (uint8_t)net->next_hdr, NSS_IPSECMGR_KEY_POS_IP_PROTO);
nss_ipsecmgr_key_write(key, net->dst_subnet, net->dst_mask, NSS_IPSECMGR_KEY_POS_IPV6_DST, 4);
/*
* clear mask if caller specify protocol as any (0xff).
* this will serve as default entry for any-protocol.
*/
if (net->next_hdr == NSS_IPSECMGR_PROTO_NEXT_HDR_ANY) {
nss_ipsecmgr_key_clear_8(key, NSS_IPSECMGR_KEY_POS_IP_PROTO);
}
key->len = NSS_IPSECMGR_KEY_LEN_IPV6_SUBNET;
}
/*
* nss_ipsecmgr_v4_subnet_match()
* peform a v4 subnet based match in netmask database
*/
struct nss_ipsecmgr_ref *nss_ipsecmgr_v4_subnet_match(struct nss_ipsecmgr_priv *priv, struct nss_ipsecmgr_key *key)
{
struct nss_ipsecmgr_netmask_db *db = &ipsecmgr_ctx->net_db;
struct nss_ipsecmgr_key tmp_key;
struct nss_ipsecmgr_ref *ref;
int i;
/*
* cycle through the bitmap for each subnet
*/
for_each_set_bit(i, db->bitmap, 32) {
BUG_ON(db->entries[i] == NULL);
BUG_ON(db->entries[i]->count == 0);
memcpy(&tmp_key, key, sizeof(struct nss_ipsecmgr_key));
/*
* set the key with the right mask for hash index computation;
* each subnet index has its associated mask value
*/
nss_ipsecmgr_key_lshift_mask(&tmp_key, i, NSS_IPSECMGR_KEY_POS_IPV4_DST);
ref = nss_ipsecmgr_subnet_lookup(priv, &tmp_key);
if (ref) {
return ref;
}
}
memcpy(&tmp_key, key, sizeof(struct nss_ipsecmgr_key));
/*
* normal lookup failed; check default subnet entry
* - clear the destination netmask before lookup
*/
nss_ipsecmgr_key_clear_32(&tmp_key, NSS_IPSECMGR_KEY_POS_IPV4_DST);
ref = nss_ipsecmgr_subnet_lookup(priv, &tmp_key);
if (ref) {
return ref;
}
return NULL;
}
/*
* nss_ipsecmgr_v6_subnet_match()
* peform a v6 subnet based match in netmask database
*/
struct nss_ipsecmgr_ref *nss_ipsecmgr_v6_subnet_match(struct nss_ipsecmgr_priv *priv, struct nss_ipsecmgr_key *key)
{
struct nss_ipsecmgr_netmask_db *db = &ipsecmgr_ctx->net_db;
struct nss_ipsecmgr_key tmp_key;
struct nss_ipsecmgr_ref *ref;
int i;
/*
* cycle through the bitmap for each subnet
*/
for_each_set_bit(i, db->bitmap, NSS_IPSECMGR_MAX_NETMASK) {
BUG_ON(db->entries[i] == NULL);
BUG_ON(db->entries[i]->count == 0);
memcpy(&tmp_key, key, sizeof(struct nss_ipsecmgr_key));
nss_ipsecmgr_key_lshift_mask128(&tmp_key, i, NSS_IPSECMGR_KEY_POS_IPV6_DST);
ref = nss_ipsecmgr_subnet_lookup(priv, &tmp_key);
if (ref) {
return ref;
}
}
memcpy(&tmp_key, key, sizeof(struct nss_ipsecmgr_key));
/*
* normal lookup failed; check default subnet entry
* - clear the destination netmask before lookup
*/
nss_ipsecmgr_key_clear_128(&tmp_key, NSS_IPSECMGR_KEY_POS_IPV6_DST);
ref = nss_ipsecmgr_subnet_lookup(priv, &tmp_key);
if (ref) {
return ref;
}
return NULL;
}
/*
* nss_ipsecmgr_subnet_lookup()
* lookup a subnet entry
*/
struct nss_ipsecmgr_ref *nss_ipsecmgr_subnet_lookup(struct nss_ipsecmgr_priv *priv, struct nss_ipsecmgr_key *key)
{
struct nss_ipsecmgr_netmask_entry *netmask;
struct nss_ipsecmgr_subnet_entry *entry;
struct list_head *head;
int idx;
netmask = nss_ipsecmgr_netmask_lookup(priv, key);
if (!netmask) {
return NULL;
}
BUG_ON(netmask->count == 0);
idx = nss_ipsecmgr_subnet_key_data2idx(key, NSS_IPSECMGR_MAX_SUBNET);
head = &netmask->subnets[idx];
list_for_each_entry(entry, head, node) {
if (nss_ipsecmgr_key_cmp(&entry->key, key)) {
return &entry->ref;
}
}
return NULL;
}
/*
* nss_ipsecmgr_subnet_alloc()
* allocate a subnet entry
*/
struct nss_ipsecmgr_ref *nss_ipsecmgr_subnet_alloc(struct nss_ipsecmgr_priv *priv, struct nss_ipsecmgr_key *key)
{
struct nss_ipsecmgr_netmask_entry *netmask;
struct nss_ipsecmgr_subnet_entry *subnet;
struct nss_ipsecmgr_netmask_db *db;
struct nss_ipsecmgr_ref *ref;
uint32_t idx;
/*
* subnet lookup before allocating a new one
*/
ref = nss_ipsecmgr_subnet_lookup(priv, key);
if (ref) {
return ref;
}
/*
* allocate the netmask
*/
netmask = nss_ipsecmgr_netmask_alloc(priv, key);
if (!netmask) {
return NULL;
}
/*
* allocate the subnet entry
*/
subnet = kzalloc(sizeof(struct nss_ipsecmgr_subnet_entry), GFP_ATOMIC);
if (!subnet) {
nss_ipsecmgr_netmask_free(&ipsecmgr_ctx->net_db, key);
return NULL;
}
subnet->priv = priv;
ref = &subnet->ref;
/*
* add flow to the database
*/
INIT_LIST_HEAD(&subnet->node);
/*
* update key
*/
idx = nss_ipsecmgr_subnet_key_data2idx(key, NSS_IPSECMGR_MAX_SUBNET);
/*
* TODO
* For subnet entry with any protocol, just add it to the tail.
* This will force subnet with any-protcol to have least priority
* over other specific protocol entries during subnet lookup.
*/
memcpy(&subnet->key, key, sizeof(struct nss_ipsecmgr_key));
list_add(&subnet->node, &netmask->subnets[idx]);
netmask->count++;
db = &ipsecmgr_ctx->net_db;
atomic_inc(&db->num_entries);
/*
* initiallize the reference object
*/
nss_ipsecmgr_ref_init(&subnet->ref, nss_ipsecmgr_subnet_update, nss_ipsecmgr_subnet_free);
return ref;
}