/* Copyright 2007-2010 Jozsef Kadlecsik (kadlec@blackhole.kfki.hu)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */
#include <assert.h>				/* assert */
#include <errno.h>				/* errno */
#include <stdlib.h>				/* calloc, free */
#include <time.h>				/* time */
#include <arpa/inet.h>				/* hto* */

#include <libipset/linux_ip_set.h>		/* enum ipset_cmd */
#include <libipset/debug.h>			/* D() */
#include <libipset/session.h>			/* ipset_session_handle */
#include <libipset/ui.h>			/* IPSET_ENV_EXIST */
#include <libipset/utils.h>			/* UNUSED */
#include <libipset/mnl.h>			/* prototypes */

#ifndef NFNL_SUBSYS_IPSET
#define NFNL_SUBSYS_IPSET	6
#endif

/* Internal data structure for the kernel-userspace communication parameters */
struct ipset_handle {
	struct mnl_socket *h;		/* the mnl socket */
	unsigned int seq;		/* netlink message sequence number */
	unsigned int portid;		/* the socket port identifier */
	mnl_cb_t *cb_ctl;		/* control block callbacks */
	void *data;			/* data pointer */
};

/* Netlink flags of the commands */
static const uint16_t cmdflags[] = {
	[IPSET_CMD_CREATE-1]	= NLM_F_REQUEST|NLM_F_ACK|
					NLM_F_CREATE|NLM_F_EXCL,
	[IPSET_CMD_DESTROY-1]	= NLM_F_REQUEST|NLM_F_ACK,
	[IPSET_CMD_FLUSH-1]	= NLM_F_REQUEST|NLM_F_ACK,
	[IPSET_CMD_RENAME-1]	= NLM_F_REQUEST|NLM_F_ACK,
	[IPSET_CMD_SWAP-1]	= NLM_F_REQUEST|NLM_F_ACK,
	[IPSET_CMD_LIST-1]	= NLM_F_REQUEST|NLM_F_ACK|
					NLM_F_ROOT|NLM_F_MATCH|NLM_F_DUMP,
	[IPSET_CMD_SAVE-1]	= NLM_F_REQUEST|NLM_F_ACK|
					NLM_F_ROOT|NLM_F_MATCH|NLM_F_DUMP,
	[IPSET_CMD_ADD-1]	= NLM_F_REQUEST|NLM_F_ACK|NLM_F_EXCL,
	[IPSET_CMD_DEL-1]	= NLM_F_REQUEST|NLM_F_ACK|NLM_F_EXCL,
	[IPSET_CMD_TEST-1]	= NLM_F_REQUEST|NLM_F_ACK,
	[IPSET_CMD_HEADER-1]	= NLM_F_REQUEST,
	[IPSET_CMD_TYPE-1]	= NLM_F_REQUEST,
	[IPSET_CMD_PROTOCOL-1]	= NLM_F_REQUEST,
};

/**
 * ipset_get_nlmsg_type - get ipset netlink message type
 * @nlh: pointer to the netlink message header
 *
 * Returns the ipset netlink message type, i.e. the ipset command.
 */
int
ipset_get_nlmsg_type(const struct nlmsghdr *nlh)
{
	return nlh->nlmsg_type & ~(NFNL_SUBSYS_IPSET << 8);
}

static void
ipset_mnl_fill_hdr(struct ipset_handle *handle, enum ipset_cmd cmd,
		   void *buffer, size_t len UNUSED, uint8_t envflags)
{
	struct nlmsghdr *nlh;
	struct nfgenmsg *nfg;

	assert(handle);
	assert(buffer);
	assert(cmd > IPSET_CMD_NONE && cmd < IPSET_MSG_MAX);

	nlh = mnl_nlmsg_put_header(buffer);
	nlh->nlmsg_type = cmd | (NFNL_SUBSYS_IPSET << 8);
	nlh->nlmsg_flags = cmdflags[cmd - 1];
	if (envflags & IPSET_ENV_EXIST)
		nlh->nlmsg_flags &=  ~NLM_F_EXCL;

	nfg = mnl_nlmsg_put_extra_header(nlh, sizeof(struct nfgenmsg));
	nfg->nfgen_family = AF_INET;
	nfg->version = NFNETLINK_V0;
	nfg->res_id = htons(0);
}

static int
ipset_mnl_query(struct ipset_handle *handle, void *buffer, size_t len)
{
	struct nlmsghdr *nlh = buffer;
	int ret;

	assert(handle);
	assert(buffer);

	nlh->nlmsg_seq = ++handle->seq;
#ifdef IPSET_DEBUG
	ipset_debug_msg("sent", nlh, nlh->nlmsg_len);
#endif
	if (mnl_socket_sendto(handle->h, nlh, nlh->nlmsg_len) < 0)
		return -ECOMM;

	ret = mnl_socket_recvfrom(handle->h, buffer, len);
#ifdef IPSET_DEBUG
	ipset_debug_msg("received", buffer, ret);
#endif
	while (ret > 0) {
		ret = mnl_cb_run2(buffer, ret,
				  handle->seq, handle->portid,
				  handle->cb_ctl[NLMSG_MIN_TYPE],
				  handle->data,
				  handle->cb_ctl, NLMSG_MIN_TYPE);
		D("nfln_cb_run2, ret: %d, errno %d", ret, errno);
		if (ret <= 0)
			break;
		ret = mnl_socket_recvfrom(handle->h, buffer, len);
		D("message received, ret: %d", ret);
	}
	return ret > 0 ? 0 : ret;
}

static struct ipset_handle *
ipset_mnl_init(mnl_cb_t *cb_ctl, void *data)
{
	struct ipset_handle *handle;

	assert(cb_ctl);
	assert(data);

	handle = calloc(1, sizeof(*handle));
	if (!handle)
		return NULL;

	handle->h = mnl_socket_open(NETLINK_NETFILTER);
	if (!handle->h)
		goto free_handle;

	if (mnl_socket_bind(handle->h, 0, MNL_SOCKET_AUTOPID) < 0)
		goto close_nl;

	handle->portid = mnl_socket_get_portid(handle->h);
	handle->cb_ctl = cb_ctl;
	handle->data = data;
	handle->seq = time(NULL);

	return handle;

close_nl:
	mnl_socket_close(handle->h);
free_handle:
	free(handle);

	return NULL;
}

static int
ipset_mnl_fini(struct ipset_handle *handle)
{
	assert(handle);

	if (handle->h)
		mnl_socket_close(handle->h);

	free(handle);
	return 0;
}

const struct ipset_transport ipset_mnl_transport = {
	.init	= ipset_mnl_init,
	.fini	= ipset_mnl_fini,
	.fill_hdr = ipset_mnl_fill_hdr,
	.query	= ipset_mnl_query,
};
