/*-
* Copyright (c) 2009-2019 The NetBSD Foundation, Inc.
* All rights reserved.
*
* This material is based upon work partially supported by The
* NetBSD Foundation under a contract with Mindaugas Rasiukevicius.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* NPF tableset module.
*
* Notes
*
* The tableset is an array of tables. After the creation, the array
* is immutable. The caller is responsible to synchronise the access
* to the tableset.
*
* Warning (not applicable for the userspace npfkern):
*
* The thmap_put()/thmap_del() are not called from the interrupt
* context and are protected by an IPL_NET mutex(9), therefore they
* do not need SPL wrappers -- see the comment at the top of the
* npf_conndb.c source file.
*/
struct npf_table {
/*
* The storage type can be: a) hashmap b) LPM c) cdb.
* There are separate trees for IPv4 and IPv6.
*/
union {
struct {
thmap_t * t_map;
LIST_HEAD(, npf_tblent) t_gc;
};
lpm_t * t_lpm;
struct {
void * t_blob;
size_t t_bsize;
struct cdbr * t_cdb;
};
struct {
npf_tblent_t ** t_elements[NPF_ADDR_SLOTS];
unsigned t_allocated[NPF_ADDR_SLOTS];
unsigned t_used[NPF_ADDR_SLOTS];
};
} /* C11 */;
LIST_HEAD(, npf_tblent) t_list;
unsigned t_nitems;
/*
* Table ID, type and lock. The ID may change during the
* config reload, it is protected by the npf_t::config_lock.
*/
int t_type;
unsigned t_id;
kmutex_t t_lock;
void
npf_tableset_destroy(npf_tableset_t *ts)
{
/*
* Destroy all tables (no references should be held, since the
* ruleset should be destroyed before).
*/
for (u_int tid = 0; tid < ts->ts_nitems; tid++) {
npf_table_t *t = ts->ts_map[tid];
if (t == NULL)
continue;
membar_release();
if (atomic_dec_uint_nv(&t->t_refcnt) > 0)
continue;
membar_acquire();
npf_table_destroy(t);
}
kmem_free(ts, NPF_TABLESET_SIZE(ts->ts_nitems));
}
/*
* npf_tableset_insert: insert the table into the specified tableset.
*
* => Returns 0 on success. Fails and returns error if ID is already used.
*/
int
npf_tableset_insert(npf_tableset_t *ts, npf_table_t *t)
{
const u_int tid = t->t_id;
int error;
/*
* npf_tableset_getbyname: look for a table in the set given the name.
*/
npf_table_t *
npf_tableset_getbyname(npf_tableset_t *ts, const char *name)
{
npf_table_t *t;
for (u_int tid = 0; tid < ts->ts_nitems; tid++) {
if ((t = ts->ts_map[tid]) == NULL)
continue;
if (strcmp(name, t->t_name) == 0)
return t;
}
return NULL;
}
/*
* npf_tableset_reload: iterate all tables and if the new table is of the
* same type and has no items, then we preserve the old one and its entries.
*
* => The caller is responsible for providing synchronisation.
*/
void
npf_tableset_reload(npf_t *npf, npf_tableset_t *nts, npf_tableset_t *ots)
{
for (u_int tid = 0; tid < nts->ts_nitems; tid++) {
npf_table_t *t, *ot;
if ((t = nts->ts_map[tid]) == NULL) {
continue;
}
/* If our table has entries, just load it. */
if (t->t_nitems) {
continue;
}
/* Look for a currently existing table with such name. */
ot = npf_tableset_getbyname(ots, t->t_name);
if (ot == NULL) {
/* Not found: we have a new table. */
continue;
}
/* Found. Did the type change? */
if (t->t_type != ot->t_type) {
/* Yes, load the new. */
continue;
}
/*
* Preserve the current table. Acquire a reference since
* we are keeping it in the old table set. Update its ID.
*/
atomic_inc_uint(&ot->t_refcnt);
nts->ts_map[tid] = ot;
/*
* npf_table_check: validate the name, ID and type.
*/
int
npf_table_check(npf_tableset_t *ts, const char *name, uint64_t tid,
uint64_t type, bool replacing)
{
const npf_table_t *t;
if (tid >= ts->ts_nitems) {
return EINVAL;
}
if (!replacing && ts->ts_map[tid] != NULL) {
return EEXIST;
}
switch (type) {
case NPF_TABLE_LPM:
case NPF_TABLE_IPSET:
case NPF_TABLE_CONST:
case NPF_TABLE_IFADDR:
break;
default:
return EINVAL;
}
if (strlen(name) >= NPF_TABLE_MAXNAMELEN) {
return ENAMETOOLONG;
}
if ((t = npf_tableset_getbyname(ts, name)) != NULL) {
if (!replacing || t->t_id != tid) {
return EEXIST;
}
}
return 0;
}
static int
table_ifaddr_insert(npf_table_t *t, const int alen, npf_tblent_t *ent)
{
const unsigned aidx = NPF_ADDRLEN2IDX(alen);
const unsigned allocated = t->t_allocated[aidx];
const unsigned used = t->t_used[aidx];
/*
* No need to check for duplicates.
*/
if (allocated <= used) {
npf_tblent_t **old_elements = t->t_elements[aidx];
npf_tblent_t **elements;
size_t toalloc, newsize;
elements = kmem_zalloc(newsize, KM_NOSLEEP);
if (elements == NULL) {
return ENOMEM;
}
for (unsigned i = 0; i < used; i++) {
elements[i] = old_elements[i];
}
if (allocated) {
const size_t len = allocated * sizeof(npf_tblent_t *);
KASSERT(old_elements != NULL);
kmem_free(old_elements, len);
}
t->t_elements[aidx] = elements;
t->t_allocated[aidx] = toalloc;
}
t->t_elements[aidx][used] = ent;
t->t_used[aidx]++;
return 0;
}
/*
* npf_table_insert: add an IP CIDR entry into the table.
*/
int
npf_table_insert(npf_table_t *t, const int alen,
const npf_addr_t *addr, const npf_netmask_t mask)
{
npf_tblent_t *ent;
int error;
if (error) {
pool_cache_put(tblent_cache, ent);
}
return error;
}
/*
* npf_table_remove: remove the IP CIDR entry from the table.
*/
int
npf_table_remove(npf_table_t *t, const int alen,
const npf_addr_t *addr, const npf_netmask_t mask)
{
npf_tblent_t *ent = NULL;
int error;
error = npf_netmask_check(alen, mask);
if (error) {
return error;
}
mutex_enter(&t->t_lock);
switch (t->t_type) {
case NPF_TABLE_IPSET:
ent = thmap_del(t->t_map, addr, alen);
if (__predict_true(ent != NULL)) {
LIST_REMOVE(ent, te_listent);
LIST_INSERT_HEAD(&t->t_gc, ent, te_listent);
ent = NULL; // to be G/C'ed
t->t_nitems--;
} else {
error = ENOENT;
}
break;
case NPF_TABLE_LPM:
ent = lpm_lookup(t->t_lpm, addr, alen);
if (__predict_true(ent != NULL)) {
LIST_REMOVE(ent, te_listent);
lpm_remove(t->t_lpm, &ent->te_addr,
ent->te_alen, ent->te_preflen);
t->t_nitems--;
} else {
error = ENOENT;
}
break;
case NPF_TABLE_CONST:
case NPF_TABLE_IFADDR:
error = EINVAL;
break;
default:
KASSERT(false);
ent = NULL;
}
mutex_exit(&t->t_lock);
if (ent) {
pool_cache_put(tblent_cache, ent);
}
return error;
}
/*
* npf_table_lookup: find the table according to ID, lookup and match
* the contents with the specified IP address.
*/
int
npf_table_lookup(npf_table_t *t, const int alen, const npf_addr_t *addr)
{
const void *data;
size_t dlen;
bool found;
int error;
error = npf_netmask_check(alen, NPF_NO_NETMASK);
if (error) {
return error;
}
switch (t->t_type) {
case NPF_TABLE_IPSET:
/* Note: the caller is in the npf_config_read_enter(). */
found = thmap_get(t->t_map, addr, alen) != NULL;
break;
case NPF_TABLE_LPM:
mutex_enter(&t->t_lock);
found = lpm_lookup(t->t_lpm, addr, alen) != NULL;
mutex_exit(&t->t_lock);
break;
case NPF_TABLE_CONST:
if (cdbr_find(t->t_cdb, addr, alen, &data, &dlen) == 0) {
found = dlen == (unsigned)alen &&
memcmp(addr, data, dlen) == 0;
} else {
found = false;
}
break;
case NPF_TABLE_IFADDR: {
const unsigned aidx = NPF_ADDRLEN2IDX(alen);
found = false;
for (unsigned i = 0; i < t->t_used[aidx]; i++) {
const npf_tblent_t *elm = t->t_elements[aidx][i];
KASSERT(elm->te_alen == alen);
if (memcmp(&elm->te_addr, addr, alen) == 0) {
found = true;
break;
}
}
break;
}
default:
KASSERT(false);
found = false;
}
static int
table_cdb_list(npf_table_t *t, void *ubuf, size_t len)
{
size_t off = 0, dlen;
const void *data;
int error = 0;
for (size_t i = 0; i < t->t_nitems; i++) {
if (cdbr_get(t->t_cdb, i, &data, &dlen) != 0) {
return EINVAL;
}
error = table_ent_copyout(data, dlen, 0, ubuf, len, &off);
if (error)
break;
}
return error;
}
/*
* npf_table_list: copy a list of all table entries into a userspace buffer.
*/
int
npf_table_list(npf_table_t *t, void *ubuf, size_t len)
{
int error = 0;
mutex_enter(&t->t_lock);
switch (t->t_type) {
case NPF_TABLE_IPSET:
error = table_generic_list(t, ubuf, len);
break;
case NPF_TABLE_LPM:
error = table_generic_list(t, ubuf, len);
break;
case NPF_TABLE_CONST:
error = table_cdb_list(t, ubuf, len);
break;
case NPF_TABLE_IFADDR:
error = table_generic_list(t, ubuf, len);
break;
default:
KASSERT(false);
}
mutex_exit(&t->t_lock);
return error;
}
/*
* npf_table_flush: remove all table entries.
*/
int
npf_table_flush(npf_table_t *t)
{
int error = 0;
mutex_enter(&t->t_lock);
switch (t->t_type) {
case NPF_TABLE_IPSET:
table_ipset_flush(t);
break;
case NPF_TABLE_LPM:
table_tree_flush(t);
break;
case NPF_TABLE_CONST:
case NPF_TABLE_IFADDR:
error = EINVAL;
break;
default:
KASSERT(false);
}
mutex_exit(&t->t_lock);
return error;
}