/*-
 * Copyright (c) 2010-2025 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.
 */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: npf.c,v 1.56 2025/07/01 19:55:15 joe Exp $");

#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#if !defined(_NPF_STANDALONE)
#include <sys/ioctl.h>
#endif
#include <netinet/in_systm.h>
#include <netinet/in.h>
#include <net/if.h>

#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <errno.h>
#include <err.h>

#include <nv.h>
#include <dnv.h>

#include <cdbw.h>

#define	_NPF_PRIVATE
#include "npf.h"

struct nl_rule {
	nvlist_t *	rule_dict;
};

struct nl_rproc {
	nvlist_t *	rproc_dict;
};

struct nl_table {
	nvlist_t *	table_dict;
};

struct nl_alg {
	nvlist_t *	alg_dict;
};

struct nl_ext {
	nvlist_t *	ext_dict;
};

struct nl_config {
	nvlist_t *	ncf_dict;

	/* Temporary rule list. */
	nvlist_t **	ncf_rule_list;
	unsigned	ncf_rule_count;

	/* Iterators. */
	unsigned	ncf_reduce[16];
	unsigned	ncf_nlevel;

	nl_rule_t	ncf_cur_rule;
	nl_table_t	ncf_cur_table;
	nl_rproc_t	ncf_cur_rproc;
};

/*
 * Various helper routines.
 */

static bool
_npf_add_addr(nvlist_t *nvl, const char *name, int af, const npf_addr_t *addr)
{
	size_t sz;

	if (af == AF_INET) {
		sz = sizeof(struct in_addr);
	} else if (af == AF_INET6) {
		sz = sizeof(struct in6_addr);
	} else {
		return false;
	}
	nvlist_add_binary(nvl, name, addr, sz);
	return nvlist_error(nvl) == 0;
}

static unsigned
_npf_get_addr(const nvlist_t *nvl, const char *name, npf_addr_t *addr)
{
	const void *d;
	size_t sz = 0;

	d = nvlist_get_binary(nvl, name, &sz);
	switch (sz) {
	case sizeof(struct in_addr):
	case sizeof(struct in6_addr):
		memcpy(addr, d, sz);
		return (unsigned)sz;
	}
	return 0;
}

static bool
_npf_dataset_lookup(const nvlist_t *dict, const char *dataset,
    const char *key, const char *name)
{
	const nvlist_t * const *items;
	size_t nitems;

	if (!nvlist_exists_nvlist_array(dict, dataset)) {
		return false;
	}
	items = nvlist_get_nvlist_array(dict, dataset, &nitems);
	for (unsigned i = 0; i < nitems; i++) {
		const char *item_name;

		item_name = dnvlist_get_string(items[i], key, NULL);
		if (item_name && strcmp(item_name, name) == 0) {
			return true;
		}
	}
	return false;
}

static const nvlist_t *
_npf_dataset_getelement(nvlist_t *dict, const char *dataset, unsigned i)
{
	const nvlist_t * const *items;
	size_t nitems;

	if (!nvlist_exists_nvlist_array(dict, dataset)) {
		return NULL;
	}
	items = nvlist_get_nvlist_array(dict, dataset, &nitems);
	if (i < nitems) {
		return items[i];
	}
	return NULL;
}

/*
 * _npf_rules_process: transform the ruleset representing nested rules
 * with sublists into a single array with skip-to marks.
 */
static void
_npf_rules_process(nl_config_t *ncf, nvlist_t *dict, const char *key)
{
	nvlist_t **items;
	size_t nitems;

	if (!nvlist_exists_nvlist_array(dict, key)) {
		return;
	}
	items = nvlist_take_nvlist_array(dict, key, &nitems);
	for (unsigned i = 0; i < nitems; i++) {
		nvlist_t *rule_dict = items[i];
		size_t len = (ncf->ncf_rule_count + 1) * sizeof(nvlist_t *);
		void *p = realloc(ncf->ncf_rule_list, len);

		/*
		 * - Add rule to the transformed array.
		 * - Process subrules recursively.
		 * - Add the skip-to position.
		 */
		ncf->ncf_rule_list = p;
		ncf->ncf_rule_list[ncf->ncf_rule_count] = rule_dict;
		ncf->ncf_rule_count++;

		if (nvlist_exists_nvlist_array(rule_dict, "subrules")) {
			unsigned idx;

			_npf_rules_process(ncf, rule_dict, "subrules");
			idx = ncf->ncf_rule_count; // post-recursion index
			nvlist_add_number(rule_dict, "skip-to", idx);
		}
		assert(nvlist_error(rule_dict) == 0);
	}
	free(items);
}

/*
 * _npf_init_error: initialize the error structure with the message
 * from the current error number
 */
static int
_npf_init_error(int error, npf_error_t *errinfo)
{
	if (error && errinfo) {
		memset(errinfo, 0, sizeof(*errinfo));
		errinfo->error_msg = strerror(error);
	}
	return error;
}

/*
 * _npf_extract_error: check the error number field and extract the
 * error details into the npf_error_t structure.
 */
static int
_npf_extract_error(nvlist_t *resp, npf_error_t *errinfo)
{
	int error;

	error = dnvlist_get_number(resp, "errno", 0);
	if (error && errinfo) {
		memset(errinfo, 0, sizeof(npf_error_t));

		errinfo->id = dnvlist_get_number(resp, "id", 0);
		errinfo->error_msg =
		    dnvlist_take_string(resp, "error-msg", NULL);
		errinfo->source_file =
		    dnvlist_take_string(resp, "source-file", NULL);
		errinfo->source_line =
		    dnvlist_take_number(resp, "source-line", 0);
	}
	return error;
}

/*
 * npf_xfer_fd: transfer the given request and receive a response.
 *
 * => Sets the 'operation' key on the 'req' dictionary.
 * => On success: returns 0 and valid nvlist in 'resp'.
 * => On failure: returns an error number.
 */
static int
_npf_xfer_fd(int fd, unsigned long cmd, nvlist_t *req, nvlist_t **resp)
{
	struct stat st;
	int kernver;

	/*
	 * Set the NPF version and operation.
	 */
	if (!nvlist_exists(req, "version")) {
		nvlist_add_number(req, "version", NPF_VERSION);
	}
	nvlist_add_number(req, "operation", cmd);

	/*
	 * Determine the type of file descriptor:
	 * - If socket, then perform nvlist_send()/nvlist_recv().
	 * - If a character device, then use ioctl.
	 */
	if (fstat(fd, &st) == -1) {
		goto err;
	}
	switch (st.st_mode & S_IFMT) {
#if !defined(__NetBSD__)
	case S_IFSOCK:
		if (nvlist_send(fd, req) == -1) {
			goto err;
		}
		if (resp && (*resp = nvlist_recv(fd, 0)) == NULL) {
			goto err;
		}
		break;
#endif
#if !defined(_NPF_STANDALONE)
	case S_IFBLK:
	case S_IFCHR:
		if (ioctl(fd, IOC_NPF_VERSION, &kernver) == -1) {
			goto err;
		}
		if (kernver != NPF_VERSION) {
			errno = EPROGMISMATCH;
			goto err;
		}
		if (nvlist_xfer_ioctl(fd, cmd, req, resp) == -1) {
			goto err;
		}
		break;
#else
		(void)kernver;
#endif
	default:
		errno = ENOTSUP;
		goto err;
	}
	return 0;
err:
	return errno ? errno : EIO;
}

/*
 * npf_xfer_fd_errno: same as npf_xfer_fd(), but:
 *
 * => After successful retrieval of the response, inspects it, extracts
 *    the 'errno' value (if any) and returns it.
 * => Destroys the response.
 */
static int
_npf_xfer_fd_errno(int fd, unsigned long cmd, nvlist_t *req)
{
	nvlist_t *resp;
	int error;

	error = _npf_xfer_fd(fd, cmd, req, &resp);
	if (error) {
		return error;
	}
	error = _npf_extract_error(resp, NULL);
	nvlist_destroy(resp);
	return error;
}

/*
 * CONFIGURATION INTERFACE.
 */

nl_config_t *
npf_config_create(void)
{
	nl_config_t *ncf;

	ncf = calloc(1, sizeof(nl_config_t));
	if (!ncf) {
		return NULL;
	}
	ncf->ncf_dict = nvlist_create(0);
	nvlist_add_number(ncf->ncf_dict, "version", NPF_VERSION);
	return ncf;
}

int
npf_config_submit(nl_config_t *ncf, int fd, npf_error_t *errinfo)
{
	nvlist_t *resp = NULL;
	int error;

	/* Ensure the config is built. */
	(void)npf_config_build(ncf);

	error = _npf_xfer_fd(fd, IOC_NPF_LOAD, ncf->ncf_dict, &resp);
	if (error) {
		return _npf_init_error(errno, errinfo);
	}
	error = _npf_extract_error(resp, errinfo);
	nvlist_destroy(resp);
	return error;
}

nl_config_t *
npf_config_retrieve(int fd)
{
	nl_config_t *ncf;
	nvlist_t *req, *resp = NULL;
	int error;

	ncf = calloc(1, sizeof(nl_config_t));
	if (!ncf) {
		return NULL;
	}

	req = nvlist_create(0);
	error = _npf_xfer_fd(fd, IOC_NPF_SAVE, req, &resp);
	nvlist_destroy(req);

	if (error || _npf_extract_error(resp, NULL) != 0) {
		nvlist_destroy(resp);
		free(ncf);
		return NULL;
	}
	ncf->ncf_dict = resp;
	return ncf;
}

void *
npf_config_export(nl_config_t *ncf, size_t *length)
{
	/* Ensure the config is built. */
	(void)npf_config_build(ncf);
	return nvlist_pack(ncf->ncf_dict, length);
}

nl_config_t *
npf_config_import(const void *blob, size_t len)
{
	nl_config_t *ncf;

	ncf = calloc(1, sizeof(nl_config_t));
	if (!ncf) {
		return NULL;
	}
	ncf->ncf_dict = nvlist_unpack(blob, len, 0);
	if (!ncf->ncf_dict) {
		free(ncf);
		return NULL;
	}
	return ncf;
}

int
npf_config_flush(int fd)
{
	nl_config_t *ncf;
	npf_error_t errinfo;
	int error;

	ncf = npf_config_create();
	if (!ncf) {
		return ENOMEM;
	}
	nvlist_add_bool(ncf->ncf_dict, "flush", true);
	error = npf_config_submit(ncf, fd, &errinfo);
	npf_config_destroy(ncf);
	return error;
}

bool
npf_config_active_p(nl_config_t *ncf)
{
	return dnvlist_get_bool(ncf->ncf_dict, "active", false);
}

bool
npf_config_loaded_p(nl_config_t *ncf)
{
	return nvlist_exists_nvlist_array(ncf->ncf_dict, "rules");
}

const void *
npf_config_build(nl_config_t *ncf)
{
	_npf_rules_process(ncf, ncf->ncf_dict, "__rules");
	if (ncf->ncf_rule_list) {
		/* Set the transformed ruleset. */
		nvlist_move_nvlist_array(ncf->ncf_dict, "rules",
		    ncf->ncf_rule_list, ncf->ncf_rule_count);

		/* Clear the temporary list. */
		ncf->ncf_rule_list = NULL;
		ncf->ncf_rule_count = 0;
	}
	assert(nvlist_error(ncf->ncf_dict) == 0);
	return (void *)ncf->ncf_dict;
}

void
npf_config_destroy(nl_config_t *ncf)
{
	nvlist_destroy(ncf->ncf_dict);
	free(ncf);
}

/*
 * PARAMETERS.
 */

int
npf_param_get(nl_config_t *ncf, const char *name, int *valp)
{
	const nvlist_t *params;

	params = dnvlist_get_nvlist(ncf->ncf_dict, "params", NULL);
	if (params == NULL || !nvlist_exists(params, name)) {
		return ENOENT;
	}
	*valp = (int)dnvlist_get_number(params, name, 0);
	return 0;
}

int
npf_param_set(nl_config_t *ncf, const char *name, int val)
{
	nvlist_t *params;

	/* Ensure params dictionary. */
	if (nvlist_exists(ncf->ncf_dict, "params")) {
		params = nvlist_take_nvlist(ncf->ncf_dict, "params");
	} else {
		params = nvlist_create(0);
	}

	/*
	 * If the parameter is already set, then free it first.
	 * Set the parameter.  Note: values can be negative.
	 */
	if (nvlist_exists(params, name)) {
		nvlist_free_number(params, name);
	}
	nvlist_add_number(params, name, (uint64_t)val);
	nvlist_add_nvlist(ncf->ncf_dict, "params", params);
	return 0;
}

const char *
npf_param_iterate(nl_config_t *ncf, nl_iter_t *iter, int *val, int *defval)
{
	void *cookie = (void *)(intptr_t)*iter;
	const nvlist_t *params, *dparams;
	const char *name;
	int type;

	assert(sizeof(nl_iter_t) >= sizeof(void *));

	params = dnvlist_get_nvlist(ncf->ncf_dict, "params", NULL);
	if (params == NULL) {
		return NULL;
	}
skip:
	if ((name = nvlist_next(params, &type, &cookie)) == NULL) {
		*iter = NPF_ITER_BEGIN;
		return NULL;
	}
	if (type != NV_TYPE_NUMBER) {
		goto skip; // should never happen, though
	}
	if (defval) {
		dparams = dnvlist_get_nvlist(ncf->ncf_dict,
		    "params-defaults", NULL);
		if (dparams == NULL) {
			errno = EINVAL;
			return NULL;
		}
		*defval = (int)nvlist_get_number(dparams, name);
	}

	*val = (int)nvlist_get_number(params, name);
	*iter = (intptr_t)cookie;
	return name;
}

/*
 * DYNAMIC RULESET INTERFACE.
 */

static inline bool
_npf_nat_ruleset_p(const char *name)
{
	return strncmp(name, NPF_RULESET_MAP_PREF,
	    sizeof(NPF_RULESET_MAP_PREF) - 1) == 0;
}

int
npf_ruleset_add(int fd, const char *rname, nl_rule_t *rl, uint64_t *id)
{
	const bool natset = _npf_nat_ruleset_p(rname);
	nvlist_t *rule_nvl = rl->rule_dict, *resp;
	int error;

	nvlist_add_number(rule_nvl, "attr",
	    NPF_RULE_DYNAMIC | nvlist_take_number(rule_nvl, "attr"));

	if (natset && !dnvlist_get_bool(rule_nvl, "nat-rule", false)) {
		errno = EINVAL;
		return errno;
	}
	nvlist_add_string(rule_nvl, "ruleset-name", rname);
	nvlist_add_bool(rule_nvl, "nat-ruleset", natset);
	nvlist_add_number(rule_nvl, "command", NPF_CMD_RULE_ADD);

	error = _npf_xfer_fd(fd, IOC_NPF_RULE, rule_nvl, &resp);
	if (error) {
		return error;
	}
	*id = nvlist_get_number(resp, "id");
	nvlist_destroy(resp);
	return 0;
}

int
npf_ruleset_remove(int fd, const char *rname, uint64_t id)
{
	const bool natset = _npf_nat_ruleset_p(rname);
	nvlist_t *rule_nvl = nvlist_create(0);
	int error;

	nvlist_add_string(rule_nvl, "ruleset-name", rname);
	nvlist_add_bool(rule_nvl, "nat-ruleset", natset);
	nvlist_add_number(rule_nvl, "command", NPF_CMD_RULE_REMOVE);
	nvlist_add_number(rule_nvl, "id", id);

	error = _npf_xfer_fd_errno(fd, IOC_NPF_RULE, rule_nvl);
	nvlist_destroy(rule_nvl);
	return error;
}

int
npf_ruleset_remkey(int fd, const char *rname, const void *key, size_t len)
{
	const bool natset = _npf_nat_ruleset_p(rname);
	nvlist_t *rule_nvl = nvlist_create(0);
	int error;

	nvlist_add_string(rule_nvl, "ruleset-name", rname);
	nvlist_add_bool(rule_nvl, "nat-ruleset", natset);
	nvlist_add_number(rule_nvl, "command", NPF_CMD_RULE_REMKEY);
	nvlist_add_binary(rule_nvl, "key", key, len);

	error = _npf_xfer_fd_errno(fd, IOC_NPF_RULE, rule_nvl);
	nvlist_destroy(rule_nvl);
	return error;
}

int
npf_ruleset_flush(int fd, const char *rname)
{
	const bool natset = _npf_nat_ruleset_p(rname);
	nvlist_t *rule_nvl = nvlist_create(0);
	int error;

	nvlist_add_string(rule_nvl, "ruleset-name", rname);
	nvlist_add_bool(rule_nvl, "nat-ruleset", natset);
	nvlist_add_number(rule_nvl, "command", NPF_CMD_RULE_FLUSH);

	error = _npf_xfer_fd_errno(fd, IOC_NPF_RULE, rule_nvl);
	nvlist_destroy(rule_nvl);
	return error;
}

/*
 * NPF EXTENSION INTERFACE.
 */

nl_ext_t *
npf_ext_construct(const char *name)
{
	nl_ext_t *ext;

	ext = malloc(sizeof(*ext));
	if (!ext) {
		return NULL;
	}
	ext->ext_dict = nvlist_create(0);
	nvlist_add_string(ext->ext_dict, "name", name);
	return ext;
}

void
npf_ext_param_u32(nl_ext_t *ext, const char *key, uint32_t val)
{
	nvlist_add_number(ext->ext_dict, key, val);
}

void
npf_ext_param_bool(nl_ext_t *ext, const char *key, bool val)
{
	nvlist_add_bool(ext->ext_dict, key, val);
}

void
npf_ext_param_string(nl_ext_t *ext, const char *key, const char *val)
{
	nvlist_add_string(ext->ext_dict, key, val);
}

/*
 * RULE INTERFACE.
 */

nl_rule_t *
npf_rule_create(const char *name, uint32_t attr, const char *ifname)
{
	nl_rule_t *rl;

	rl = malloc(sizeof(nl_rule_t));
	if (!rl) {
		return NULL;
	}
	rl->rule_dict = nvlist_create(0);
	nvlist_add_number(rl->rule_dict, "attr", attr);
	if (name) {
		nvlist_add_string(rl->rule_dict, "name", name);
	}
	if (ifname) {
		nvlist_add_string(rl->rule_dict, "ifname", ifname);
	}
	return rl;
}

int
npf_rule_setcode(nl_rule_t *rl, int type, const void *code, size_t len)
{
	if (type != NPF_CODE_BPF) {
		return ENOTSUP;
	}
	nvlist_add_number(rl->rule_dict, "code-type", (unsigned)type);
	nvlist_add_binary(rl->rule_dict, "code", code, len);
	return nvlist_error(rl->rule_dict);
}

int
npf_rule_setkey(nl_rule_t *rl, const void *key, size_t len)
{
	nvlist_add_binary(rl->rule_dict, "key", key, len);
	return nvlist_error(rl->rule_dict);
}

int
npf_rule_setinfo(nl_rule_t *rl, const void *info, size_t len)
{
	nvlist_add_binary(rl->rule_dict, "info", info, len);
	return nvlist_error(rl->rule_dict);
}

int
npf_rule_setprio(nl_rule_t *rl, int pri)
{
	nvlist_add_number(rl->rule_dict, "prio", (uint64_t)pri);
	return nvlist_error(rl->rule_dict);
}

int
npf_rule_setproc(nl_rule_t *rl, const char *name)
{
	nvlist_add_string(rl->rule_dict, "rproc", name);
	return nvlist_error(rl->rule_dict);
}

/* both user and group */
int
npf_rule_setrid(nl_rule_t *rl, struct r_id rid, const char *name)
{
	uint64_t uid_element[3] = { rid.id[0], rid.id[1], rid.op };
	nvlist_add_number_array(rl->rule_dict, name, uid_element, 3);
	return nvlist_error(rl->rule_dict);
}

void *
npf_rule_export(nl_rule_t *rl, size_t *length)
{
	return nvlist_pack(rl->rule_dict, length);
}

bool
npf_rule_exists_p(nl_config_t *ncf, const char *name)
{
	const char *key = nvlist_exists_nvlist_array(ncf->ncf_dict,
	    "rules") ? "rules" : "__rules"; // config may not be built yet
	return _npf_dataset_lookup(ncf->ncf_dict, key, "name", name);
}

int
npf_rule_insert(nl_config_t *ncf, nl_rule_t *parent, nl_rule_t *rl)
{
	nvlist_t *rule_dict = rl->rule_dict;
	nvlist_t *target;
	const char *key;

	if (parent) {
		/* Subrule of the parent. */
		target = parent->rule_dict;
		key = "subrules";
	} else {
		/* Global ruleset. */
		target = ncf->ncf_dict;
		key = "__rules";
	}
	nvlist_append_nvlist_array(target, key, rule_dict);
	nvlist_destroy(rule_dict);
	free(rl);
	return 0;
}

static nl_rule_t *
_npf_rule_iterate1(nl_config_t *ncf, const char *key,
    nl_iter_t *iter, unsigned *level)
{
	unsigned i = *iter;
	const nvlist_t *rule_dict;
	uint32_t skipto;

	if (i == 0) {
		/* Initialise the iterator. */
		ncf->ncf_nlevel = 0;
		ncf->ncf_reduce[0] = 0;
	}

	rule_dict = _npf_dataset_getelement(ncf->ncf_dict, key, i);
	if (!rule_dict) {
		*iter = NPF_ITER_BEGIN;
		return NULL;
	}
	*iter = i + 1; // next
	*level = ncf->ncf_nlevel;

	skipto = dnvlist_get_number(rule_dict, "skip-to", 0);
	if (skipto) {
		ncf->ncf_nlevel++;
		ncf->ncf_reduce[ncf->ncf_nlevel] = skipto;
	}
	if (ncf->ncf_reduce[ncf->ncf_nlevel] == (i + 1)) {
		assert(ncf->ncf_nlevel > 0);
		ncf->ncf_nlevel--;
	}

	ncf->ncf_cur_rule.rule_dict = __UNCONST(rule_dict); // XXX
	return &ncf->ncf_cur_rule;
}

nl_rule_t *
npf_rule_iterate(nl_config_t *ncf, nl_iter_t *iter, unsigned *level)
{
	return _npf_rule_iterate1(ncf, "rules", iter, level);
}

const char *
npf_rule_getname(nl_rule_t *rl)
{
	return dnvlist_get_string(rl->rule_dict, "name", NULL);
}

uint32_t
npf_rule_getattr(nl_rule_t *rl)
{
	return dnvlist_get_number(rl->rule_dict, "attr", 0);
}

const char *
npf_rule_getinterface(nl_rule_t *rl)
{
	return dnvlist_get_string(rl->rule_dict, "ifname", NULL);
}

const void *
npf_rule_getinfo(nl_rule_t *rl, size_t *len)
{
	return dnvlist_get_binary(rl->rule_dict, "info", len, NULL, 0);
}

const char *
npf_rule_getproc(nl_rule_t *rl)
{
	return dnvlist_get_string(rl->rule_dict, "rproc", NULL);
}

int
npf_rule_getrid(struct r_id *r_id, nl_rule_t *rl, const char *key)
{
	if (nvlist_exists_number_array(rl->rule_dict, key)) {
		size_t nitems;
		const uint64_t *rid = nvlist_get_number_array(rl->rule_dict, key, &nitems);
		assert(nitems == 3);

		r_id->id[0] = (uint32_t)rid[0];
		r_id->id[1] = (uint32_t)rid[1];
		r_id->op = (uint8_t)rid[2];
		return 0;
	}
	return -1;
}

uint64_t
npf_rule_getid(nl_rule_t *rl)
{
	return dnvlist_get_number(rl->rule_dict, "id", 0);
}

const void *
npf_rule_getcode(nl_rule_t *rl, int *type, size_t *len)
{
	*type = (int)dnvlist_get_number(rl->rule_dict, "code-type", 0);
	return dnvlist_get_binary(rl->rule_dict, "code", len, NULL, 0);
}

int
_npf_ruleset_list(int fd, const char *rname, nl_config_t *ncf)
{
	const bool natset = _npf_nat_ruleset_p(rname);
	nvlist_t *req, *resp;
	int error;

	req = nvlist_create(0);
	nvlist_add_string(req, "ruleset-name", rname);
	nvlist_add_bool(req, "nat-ruleset", natset);
	nvlist_add_number(req, "command", NPF_CMD_RULE_LIST);

	error = _npf_xfer_fd(fd, IOC_NPF_RULE, req, &resp);
	nvlist_destroy(req);
	if (error) {
		return error;
	}

	if (nvlist_exists_nvlist_array(resp, "rules")) {
		nvlist_t **rules;
		size_t n;

		rules = nvlist_take_nvlist_array(resp, "rules", &n);
		nvlist_move_nvlist_array(ncf->ncf_dict, "rules", rules, n);
	}
	nvlist_destroy(resp);
	return 0;
}

void
npf_rule_destroy(nl_rule_t *rl)
{
	nvlist_destroy(rl->rule_dict);
	free(rl);
}

/*
 * RULE PROCEDURE INTERFACE.
 */

nl_rproc_t *
npf_rproc_create(const char *name)
{
	nl_rproc_t *rp;

	rp = malloc(sizeof(nl_rproc_t));
	if (!rp) {
		return NULL;
	}
	rp->rproc_dict = nvlist_create(0);
	nvlist_add_string(rp->rproc_dict, "name", name);
	return rp;
}

int
npf_rproc_extcall(nl_rproc_t *rp, nl_ext_t *ext)
{
	nvlist_t *rproc_dict = rp->rproc_dict;
	const char *name = dnvlist_get_string(ext->ext_dict, "name", NULL);

	if (_npf_dataset_lookup(rproc_dict, "extcalls", "name", name)) {
		return EEXIST;
	}
	nvlist_append_nvlist_array(rproc_dict, "extcalls", ext->ext_dict);
	nvlist_destroy(ext->ext_dict);
	free(ext);
	return 0;
}

bool
npf_rproc_exists_p(nl_config_t *ncf, const char *name)
{
	return _npf_dataset_lookup(ncf->ncf_dict, "rprocs", "name", name);
}

int
npf_rproc_insert(nl_config_t *ncf, nl_rproc_t *rp)
{
	const char *name;

	name = dnvlist_get_string(rp->rproc_dict, "name", NULL);
	if (!name) {
		return EINVAL;
	}
	if (npf_rproc_exists_p(ncf, name)) {
		return EEXIST;
	}
	nvlist_append_nvlist_array(ncf->ncf_dict, "rprocs", rp->rproc_dict);
	nvlist_destroy(rp->rproc_dict);
	free(rp);
	return 0;
}

nl_rproc_t *
npf_rproc_iterate(nl_config_t *ncf, nl_iter_t *iter)
{
	const nvlist_t *rproc_dict;
	unsigned i = *iter;

	rproc_dict = _npf_dataset_getelement(ncf->ncf_dict, "rprocs", i);
	if (!rproc_dict) {
		*iter = NPF_ITER_BEGIN;
		return NULL;
	}
	*iter = i + 1; // next
	ncf->ncf_cur_rproc.rproc_dict = __UNCONST(rproc_dict); // XXX
	return &ncf->ncf_cur_rproc;
}

const char *
npf_rproc_getname(nl_rproc_t *rp)
{
	return dnvlist_get_string(rp->rproc_dict, "name", NULL);
}

/*
 * NAT INTERFACE.
 */

nl_nat_t *
npf_nat_create(int type, unsigned flags, const char *ifname)
{
	nl_rule_t *rl;
	nvlist_t *rule_dict;
	uint32_t attr;

	attr = NPF_RULE_PASS | NPF_RULE_FINAL | NPF_RULE_LAYER_3 |
	    (type == NPF_NATOUT ? NPF_RULE_OUT : NPF_RULE_IN);

	/* Create a rule for NAT policy.  Next, will add NAT data. */
	rl = npf_rule_create(NULL, attr, ifname);
	if (!rl) {
		return NULL;
	}
	rule_dict = rl->rule_dict;

	/* Translation type and flags. */
	nvlist_add_number(rule_dict, "type", type);
	nvlist_add_number(rule_dict, "flags", flags);
	nvlist_add_bool(rule_dict, "nat-rule", true);
	return (nl_nat_t *)rl;
}

int
npf_nat_insert(nl_config_t *ncf, nl_nat_t *nt)
{
	nvlist_append_nvlist_array(ncf->ncf_dict, "nat", nt->rule_dict);
	nvlist_destroy(nt->rule_dict);
	free(nt);
	return 0;
}

nl_nat_t *
npf_nat_iterate(nl_config_t *ncf, nl_iter_t *iter)
{
	unsigned level;
	return _npf_rule_iterate1(ncf, "nat", iter, &level);
}

int
npf_nat_setaddr(nl_nat_t *nt, int af, npf_addr_t *addr, npf_netmask_t mask)
{
	/* Translation IP and mask. */
	if (!_npf_add_addr(nt->rule_dict, "nat-addr", af, addr)) {
		return nvlist_error(nt->rule_dict);
	}
	nvlist_add_number(nt->rule_dict, "nat-mask", (uint32_t)mask);
	return nvlist_error(nt->rule_dict);
}

int
npf_nat_setport(nl_nat_t *nt, in_port_t port)
{
	/* Translation port (for redirect case). */
	nvlist_add_number(nt->rule_dict, "nat-port", port);
	return nvlist_error(nt->rule_dict);
}

int
npf_nat_settable(nl_nat_t *nt, unsigned tid)
{
	/*
	 * Translation table ID; the address/mask will then serve as a filter.
	 */
	nvlist_add_number(nt->rule_dict, "nat-table-id", tid);
	return nvlist_error(nt->rule_dict);
}

int
npf_nat_setalgo(nl_nat_t *nt, unsigned algo)
{
	nvlist_add_number(nt->rule_dict, "nat-algo", algo);
	return nvlist_error(nt->rule_dict);
}

int
npf_nat_setnpt66(nl_nat_t *nt, uint16_t adj)
{
	int error;

	if ((error = npf_nat_setalgo(nt, NPF_ALGO_NPT66)) != 0) {
		return error;
	}
	nvlist_add_number(nt->rule_dict, "npt66-adj", adj);
	return nvlist_error(nt->rule_dict);
}

int
npf_nat_gettype(nl_nat_t *nt)
{
	return dnvlist_get_number(nt->rule_dict, "type", 0);
}

unsigned
npf_nat_getflags(nl_nat_t *nt)
{
	return dnvlist_get_number(nt->rule_dict, "flags", 0);
}

unsigned
npf_nat_getalgo(nl_nat_t *nt)
{
	return dnvlist_get_number(nt->rule_dict, "nat-algo", 0);
}

const npf_addr_t *
npf_nat_getaddr(nl_nat_t *nt, size_t *alen, npf_netmask_t *mask)
{
	const void *data;

	if (nvlist_exists(nt->rule_dict, "nat-addr")) {
		data = nvlist_get_binary(nt->rule_dict, "nat-addr", alen);
		*mask = nvlist_get_number(nt->rule_dict, "nat-mask");
	} else {
		data = NULL;
		*alen = 0;
		*mask = NPF_NO_NETMASK;
	}
	return data;
}

in_port_t
npf_nat_getport(nl_nat_t *nt)
{
	return (uint16_t)dnvlist_get_number(nt->rule_dict, "nat-port", 0);
}

unsigned
npf_nat_gettable(nl_nat_t *nt)
{
	return dnvlist_get_number(nt->rule_dict, "nat-table-id", 0);
}

/*
 * TABLE INTERFACE.
 */

nl_table_t *
npf_table_create(const char *name, unsigned id, int type)
{
	nl_table_t *tl;

	tl = malloc(sizeof(*tl));
	if (!tl) {
		return NULL;
	}
	tl->table_dict = nvlist_create(0);
	nvlist_add_string(tl->table_dict, "name", name);
	nvlist_add_number(tl->table_dict, "id", id);
	nvlist_add_number(tl->table_dict, "type", type);
	return tl;
}

int
npf_table_add_entry(nl_table_t *tl, int af, const npf_addr_t *addr,
    const npf_netmask_t mask)
{
	nvlist_t *entry;

	entry = nvlist_create(0);
	if (!entry) {
		return ENOMEM;
	}
	if (!_npf_add_addr(entry, "addr", af, addr)) {
		nvlist_destroy(entry);
		return EINVAL;
	}
	nvlist_add_number(entry, "mask", mask);
	nvlist_append_nvlist_array(tl->table_dict, "entries", entry);
	nvlist_destroy(entry);
	return 0;
}

static inline int
_npf_table_build_const(nl_table_t *tl)
{
	struct cdbw *cdbw;
	const nvlist_t * const *entries;
	int error = 0, fd = -1;
	size_t nitems, len;
	void *cdb, *buf;
	struct stat sb;
	char sfn[32];

	if (dnvlist_get_number(tl->table_dict, "type", 0) != NPF_TABLE_CONST) {
		return 0;
	}

	if (!nvlist_exists_nvlist_array(tl->table_dict, "entries")) {
		return 0;
	}

	/*
	 * Create a constant database and put all the entries.
	 */
	if ((cdbw = cdbw_open()) == NULL) {
		return errno;
	}
	entries = nvlist_get_nvlist_array(tl->table_dict, "entries", &nitems);
	for (unsigned i = 0; i < nitems; i++) {
		const nvlist_t *entry = entries[i];
		const npf_addr_t *addr;
		size_t alen;

		addr = dnvlist_get_binary(entry, "addr", &alen, NULL, 0);
		if (addr == NULL || alen == 0 || alen > sizeof(npf_addr_t)) {
			error = EINVAL;
			goto out;
		}
		if (cdbw_put(cdbw, addr, alen, addr, alen) == -1) {
			error = errno;
			goto out;
		}
	}

	/*
	 * Write the constant database into a temporary file.
	 */
	strncpy(sfn, "/tmp/npfcdb.XXXXXX", sizeof(sfn));
	sfn[sizeof(sfn) - 1] = '\0';

	if ((fd = mkstemp(sfn)) == -1) {
		error = errno;
		goto out;
	}
	unlink(sfn);

	if (cdbw_output(cdbw, fd, "npf-table-cdb", NULL) == -1) {
		error = errno;
		goto out;
	}
	if (fstat(fd, &sb) == -1) {
		error = errno;
		goto out;
	}
	len = sb.st_size;

	/*
	 * Memory-map the database and copy it into a buffer.
	 */
	buf = malloc(len);
	if (!buf) {
		error = ENOMEM;
		goto out;
	}
	cdb = mmap(NULL, len, PROT_READ, MAP_FILE | MAP_PRIVATE, fd, 0);
	if (cdb == MAP_FAILED) {
		error = errno;
		free(buf);
		goto out;
	}
	munmap(cdb, len);

	/*
	 * Move the data buffer to the nvlist.
	 */
	nvlist_move_binary(tl->table_dict, "data", buf, len);
	error = nvlist_error(tl->table_dict);
out:
	if (fd != -1) {
		close(fd);
	}
	cdbw_close(cdbw);
	return error;
}

int
npf_table_insert(nl_config_t *ncf, nl_table_t *tl)
{
	const char *name;
	int error;

	name = dnvlist_get_string(tl->table_dict, "name", NULL);
	if (!name) {
		return EINVAL;
	}
	if (_npf_dataset_lookup(ncf->ncf_dict, "tables", "name", name)) {
		return EEXIST;
	}
	if ((error = _npf_table_build_const(tl)) != 0) {
		return error;
	}
	nvlist_append_nvlist_array(ncf->ncf_dict, "tables", tl->table_dict);
	nvlist_destroy(tl->table_dict);
	free(tl);
	return 0;
}

int
npf_table_replace(int fd, nl_table_t *tl, npf_error_t *errinfo)
{
	nvlist_t *resp = NULL;
	int error;

	/* Ensure const tables are built. */
	if ((error = _npf_table_build_const(tl)) != 0) {
		return _npf_init_error(errno, errinfo);
	}
	error = _npf_xfer_fd(fd, IOC_NPF_TABLE_REPLACE, tl->table_dict, &resp);
	if (error) {
		assert(resp == NULL);
		return _npf_init_error(errno, errinfo);
	}
	error = _npf_extract_error(resp, errinfo);
	nvlist_destroy(resp);
	return error;
}

nl_table_t *
npf_table_iterate(nl_config_t *ncf, nl_iter_t *iter)
{
	const nvlist_t *table_dict;
	unsigned i = *iter;

	table_dict = _npf_dataset_getelement(ncf->ncf_dict, "tables", i);
	if (!table_dict) {
		*iter = NPF_ITER_BEGIN;
		return NULL;
	}
	*iter = i + 1; // next
	ncf->ncf_cur_table.table_dict = __UNCONST(table_dict); // XXX
	return &ncf->ncf_cur_table;
}

unsigned
npf_table_getid(nl_table_t *tl)
{
	return dnvlist_get_number(tl->table_dict, "id", (unsigned)-1);
}

const char *
npf_table_getname(nl_table_t *tl)
{
	return dnvlist_get_string(tl->table_dict, "name", NULL);
}

int
npf_table_gettype(nl_table_t *tl)
{
	return dnvlist_get_number(tl->table_dict, "type", 0);
}

void
npf_table_destroy(nl_table_t *tl)
{
	nvlist_destroy(tl->table_dict);
	free(tl);
}

/*
 * ALG INTERFACE.
 */

int
npf_alg_load(nl_config_t *ncf, const char *name)
{
	nvlist_t *alg_dict;

	if (_npf_dataset_lookup(ncf->ncf_dict, "algs", "name", name)) {
		return EEXIST;
	}
	alg_dict = nvlist_create(0);
	nvlist_add_string(alg_dict, "name", name);
	nvlist_append_nvlist_array(ncf->ncf_dict, "algs", alg_dict);
	nvlist_destroy(alg_dict);
	return 0;
}

/*
 * CONNECTION / NAT ENTRY INTERFACE.
 */

typedef struct {
	unsigned	alen;
	unsigned	proto;
	npf_addr_t	addr[3];
	in_port_t	port[3];
} npf_connpoint_t;

static int
_npf_conn_lookup(int fd, const int af, npf_addr_t *addr[2], in_port_t port[2],
    unsigned proto, const char *ifname, unsigned di)
{
	nvlist_t *req = NULL, *resp = NULL, *key_nv;
	const nvlist_t *nat;
	int error = EINVAL;

	/*
	 * Setup the connection lookup key.
	 */
	if ((key_nv = nvlist_create(0)) == NULL) {
		return ENOMEM;
	}
	if (!_npf_add_addr(key_nv, "saddr", af, addr[0])) {
		nvlist_destroy(key_nv);
		goto out;
	}
	if (!_npf_add_addr(key_nv, "daddr", af, addr[1])) {
		nvlist_destroy(key_nv);
		goto out;
	}
	nvlist_add_number(key_nv, "sport", htons(port[0]));
	nvlist_add_number(key_nv, "dport", htons(port[1]));
	nvlist_add_number(key_nv, "proto", proto);
	if (ifname) {
		nvlist_add_string(key_nv, "ifname", ifname);
	}
	if (di) {
		nvlist_add_number(key_nv, "di", di);
	}

	/*
	 * Setup the request.
	 */
	if ((req = nvlist_create(0)) == NULL) {
		error = ENOMEM;
		goto out;
	}
	nvlist_move_nvlist(req, "key", key_nv);

	/* Lookup: retrieve the connection entry. */
	error = _npf_xfer_fd(fd, IOC_NPF_CONN_LOOKUP, req, &resp);
	if (error) {
		goto out;
	}

	/*
	 * Get the NAT entry and extract the translated pair.
	 */
	if ((nat = dnvlist_get_nvlist(resp, "nat", NULL)) == NULL) {
		error = ENOENT;
		goto out;
	}
	if (_npf_get_addr(nat, "oaddr", addr[0]) == 0 ||
	    _npf_get_addr(nat, "taddr", addr[1]) == 0) {
		error = EINVAL;
		goto out;
	}
	port[0] = ntohs(nvlist_get_number(nat, "oport"));
	port[1] = ntohs(nvlist_get_number(nat, "tport"));
out:
	if (resp) {
		nvlist_destroy(resp);
	}
	if (req) {
		nvlist_destroy(req);
	}
	return error;
}

int
npf_nat_lookup(int fd, int af, npf_addr_t *addr[2], in_port_t port[2],
    int proto, int di __unused)
{
	int error;

	port[0] = ntohs(port[0]); port[1] = ntohs(port[1]);
	error = _npf_conn_lookup(fd, af, addr, port, proto, NULL, 0);
	port[0] = htons(port[0]); port[1] = htons(port[1]);
	return error;
}

static bool
npf_connkey_handle(const nvlist_t *key_nv, npf_connpoint_t *ep)
{
	unsigned alen1, alen2;

	alen1 = _npf_get_addr(key_nv, "saddr", &ep->addr[0]);
	alen2 = _npf_get_addr(key_nv, "daddr", &ep->addr[1]);
	if (alen1 == 0 || alen1 != alen2) {
		return false;
	}
	ep->alen = alen1;
	ep->port[0] = ntohs(nvlist_get_number(key_nv, "sport"));
	ep->port[1] = ntohs(nvlist_get_number(key_nv, "dport"));
	ep->proto = nvlist_get_number(key_nv, "proto");
	return true;
}

static void
npf_conn_handle(const nvlist_t *conn, npf_conn_func_t func, void *arg)
{
	const nvlist_t *key_nv, *nat_nv;
	const char *ifname;
	npf_connpoint_t ep;

	memset(&ep, 0, sizeof(npf_connpoint_t));

	ifname = dnvlist_get_string(conn, "ifname", NULL);
	key_nv = dnvlist_get_nvlist(conn, "forw-key", NULL);
	if (!npf_connkey_handle(key_nv, &ep)) {
		goto err;
	}
	if ((nat_nv = dnvlist_get_nvlist(conn, "nat", NULL)) != NULL) {
		if (_npf_get_addr(nat_nv, "taddr", &ep.addr[2]) != ep.alen) {
			goto err;
		}
		ep.port[2] = ntohs(nvlist_get_number(nat_nv, "tport"));
	}
	/*
	 * XXX: add 'proto' and 'flow'; perhaps expand and pass the
	 * whole to npf_connpoint_t?
	 */
	(*func)((unsigned)ep.alen, ep.addr, ep.port, ifname, arg);
err:
	return;
}

int
npf_conn_list(int fd, npf_conn_func_t func, void *arg)
{
	nl_config_t *ncf;
	const nvlist_t * const *conns;
	size_t nitems;

	ncf = npf_config_retrieve(fd);
	if (!ncf) {
		return errno;
	}
	if (!nvlist_exists_nvlist_array(ncf->ncf_dict, "conn-list")) {
		return 0;
	}
	conns = nvlist_get_nvlist_array(ncf->ncf_dict, "conn-list", &nitems);
	for (unsigned i = 0; i < nitems; i++) {
		const nvlist_t *conn = conns[i];
		npf_conn_handle(conn, func, arg);
	}
	npf_config_destroy(ncf);
	return 0;
}

/*
 * MISC.
 */

void
_npf_debug_addif(nl_config_t *ncf, const char *ifname)
{
	nvlist_t *debug;

	/*
	 * Initialise the debug dictionary on the first call.
	 */
	debug = dnvlist_take_nvlist(ncf->ncf_dict, "debug", NULL);
	if (debug == NULL) {
		debug = nvlist_create(0);
	}
	if (!_npf_dataset_lookup(debug, "interfaces", "name", ifname)) {
		nvlist_t *ifdict = nvlist_create(0);
		nvlist_add_string(ifdict, "name", ifname);
		nvlist_add_number(ifdict, "index", if_nametoindex(ifname));
		nvlist_append_nvlist_array(debug, "interfaces", ifdict);
		nvlist_destroy(ifdict);
	}
	nvlist_move_nvlist(ncf->ncf_dict, "debug", debug);
}

void
_npf_config_dump(nl_config_t *ncf, int fd)
{
	(void)npf_config_build(ncf);
	nvlist_dump(ncf->ncf_dict, fd);
}
