/* 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 <setjmp.h>				/* setjmp, longjmp */
#include <stdio.h>				/* snprintf */
#include <stdarg.h>				/* va_* */
#include <stdlib.h>				/* free */
#include <string.h>				/* str* */
#include <unistd.h>				/* getpagesize */
#include <net/ethernet.h>			/* ETH_ALEN */

#include <libipset/debug.h>			/* D() */
#include <libipset/data.h>			/* IPSET_OPT_* */
#include <libipset/errcode.h>			/* ipset_errcode */
#include <libipset/print.h>			/* ipset_print_* */
#include <libipset/types.h>			/* struct ipset_type */
#include <libipset/transport.h>			/* transport */
#include <libipset/mnl.h>			/* default backend */
#include <libipset/utils.h>			/* STREQ */
#include <libipset/ui.h>			/* IPSET_ENV_* */
#include <libipset/session.h>			/* prototypes */

#include <linux/genetlink.h>

#define IPSET_NEST_MAX	4

/* The session structure */
struct ipset_session {
	const struct ipset_transport *transport;/* Transport protocol */
	struct ipset_handle *handle;		/* Transport handler */
	struct ipset_data *data;		/* Input/output data */
	/* Command state */
	enum ipset_cmd cmd;			/* Current command */
	uint32_t lineno;			/* Current lineno in restore mode */
	char saved_setname[IPSET_MAXNAMELEN];	/* Saved setname */
	const struct ipset_type *saved_type;	/* Saved type */
	struct nlattr *nested[IPSET_NEST_MAX];	/* Pointer to nest levels */
	uint8_t nestid;				/* Current nest level */
	bool version_checked;			/* Version checked */
	/* Output buffer */
	char outbuf[IPSET_OUTBUFLEN];		/* Output buffer */
	enum ipset_output_mode mode;		/* Output mode */
	ipset_outfn outfn;			/* Output function */
	/* Error/warning reporting */
	char report[IPSET_ERRORBUFLEN];		/* Error/report buffer */
	char *errmsg;
	char *warnmsg;
	uint8_t envopts;			/* Session env opts */
	/* Kernel message buffer */
	size_t bufsize;
	void *buffer;
};

/*
 * Glue functions
 */

/**
 * ipset_session_data - return pointer to the data
 * @session: session structure
 *
 * Returns the pointer to the data structure of the session.
 */
struct ipset_data *
ipset_session_data(const struct ipset_session *session)
{
	assert(session);
	return session->data;
}

/**
 * ipset_session_handle - return pointer to the handle
 * @session: session structure
 *
 * Returns the pointer to the transport handle structure of the session.
 */
struct ipset_handle *
ipset_session_handle(const struct ipset_session *session)
{
	assert(session);
	return session->handle;
}

/**
 * ipset_saved_type - return pointer to the saved type
 * @session: session structure
 *
 * Returns the pointer to the saved type from the last ipset_cmd
 * It is required to decode type-specific error codes in restore mode.
 */
const struct ipset_type *
ipset_saved_type(const struct ipset_session *session)
{
	assert(session);
	return session->saved_type;
}

/**
 * ipset_session_lineno - set session lineno
 * @session: session structure
 *
 * Set session lineno to report parser errors correctly.
 */
void
ipset_session_lineno(struct ipset_session *session, uint32_t lineno)
{
	assert(session);
	session->lineno = lineno;
}

/*
 * Environment options
 */

/**
 * ipset_envopt_parse - parse/set environment option
 * @session: session structure
 * @opt: environment option
 * @arg: option argument (unused)
 *
 * Parse and set an environment option.
 *
 * Returns 0 on success or a negative error code.
 */
int
ipset_envopt_parse(struct ipset_session *session, int opt,
		   const char *arg UNUSED)
{
	assert(session);

	switch (opt) {
	case IPSET_ENV_SORTED:
	case IPSET_ENV_QUIET:
	case IPSET_ENV_RESOLVE:
	case IPSET_ENV_EXIST:
		session->envopts |= opt;
		return 0;
	default:
		break;
	}
	return -1;
}

/**
 * ipset_envopt_test - test environment option
 * @session: session structure
 * @opt: environment option
 *
 * Test whether the environment option is set in the session.
 *
 * Returns true or false.
 */
bool
ipset_envopt_test(struct ipset_session *session, enum ipset_envopt opt)
{
	assert(session);
	return session->envopts & opt;
}

/**
 * ipset_session_output - set the session output mode
 * @session: session structure
 * @mode: output mode
 *
 * Set the output mode for the session.
 *
 * Returns 0 on success or a negative error code.
 */
int
ipset_session_output(struct ipset_session *session,
		     enum ipset_output_mode mode)
{
	assert(session);
	session->mode = mode;
	return 0;
}

/*
 * Error and warning reporting
 */

/**
 * ipset_session_report - fill the report buffer
 * @session: session structure
 * @type: report type
 * @fmt: message format
 *
 * Fill the report buffer with an error or warning message.
 * Depending on the report type, set the error or warning
 * message pointer.
 *
 * Returns -1.
 */
int __attribute__((format(printf,3,4)))
ipset_session_report(struct ipset_session *session,
		     enum ipset_err_type type, 
		     const char *fmt, ...)
{
	int len, offset = 0;
	va_list args;
	
	assert(session);
	assert(fmt);

	if (session->lineno != 0 && type == IPSET_ERROR) {
		sprintf(session->report, "Error in line %u: ",
			session->lineno);
	}
	offset = strlen(session->report);
	
	va_start(args, fmt);
	len = vsnprintf(session->report + offset,
		        IPSET_ERRORBUFLEN - 1 - offset,
		        fmt, args);
	va_end(args);
	
	if (len >= IPSET_ERRORBUFLEN - 1 - offset)
		session->report[IPSET_ERRORBUFLEN - 1] = '\0';
	if (strlen(session->report) < IPSET_ERRORBUFLEN - 1)
		strcat(session->report, "\n");

	if (type == IPSET_ERROR) {
		session->errmsg = session->report;
		session->warnmsg = NULL;
	} else {
		session->errmsg = NULL;
		session->warnmsg = session->report;
	}
	return -1;
}

/**
 * ipset_session_reset - reset the report buffer
 * @session: session structure
 *
 * Reset the report buffer, the error and warning pointers.
 */
void
ipset_session_report_reset(struct ipset_session *session)
{
	assert(session);
	session->report[0] = '\0';
	session->errmsg = session->warnmsg = NULL;
}

/**
 * ipset_session_error - return the report buffer as error
 * @session: session structure
 *
 * Return the pointer to the report buffer as an error report.
 * If there is no error message in the buffer, NULL returned.
 */
const char *
ipset_session_error(const struct ipset_session *session)
{
	assert(session);

	return session->errmsg;
}

/**
 * ipset_session_warning - return the report buffer as warning
 * @session: session structure
 *
 * Return the pointer to the report buffer as a warning report.
 * If there is no warning message in the buffer, NULL returned.
 */
const char *
ipset_session_warning(const struct ipset_session *session)
{
	assert(session);

	return session->warnmsg;
}

/*
 * Receive data from the kernel
 */

struct ipset_attr_policy {
	uint16_t type;
	uint16_t len;
	enum ipset_opt opt;
};

/* Attribute policies and mapping to options */
static const struct ipset_attr_policy cmd_attrs[] = {
	[IPSET_ATTR_PROTOCOL] = {
		.type = MNL_TYPE_U8,
	},
	[IPSET_ATTR_SETNAME] = {
		.type = MNL_TYPE_NUL_STRING,
		.opt  = IPSET_SETNAME,
		.len  = IPSET_MAXNAMELEN,
	},
	[IPSET_ATTR_TYPENAME] = {
		.type = MNL_TYPE_NUL_STRING,
		.opt = IPSET_OPT_TYPENAME,
		.len  = IPSET_MAXNAMELEN,
	},
	/* IPSET_ATTR_SETNAME2 is an alias for IPSET_ATTR_TYPENAME */
	[IPSET_ATTR_REVISION] = {
		.type = MNL_TYPE_U8,
		.opt = IPSET_OPT_REVISION,
	},
	[IPSET_ATTR_FAMILY] = {
		.type = MNL_TYPE_U8,
		.opt = IPSET_OPT_FAMILY,
	},
	[IPSET_ATTR_FLAGS] = {
		.type = MNL_TYPE_U32,
		.opt = IPSET_OPT_FLAGS,
	},
	[IPSET_ATTR_DATA] = {
		.type = MNL_TYPE_NESTED,
	},
	[IPSET_ATTR_ADT] = {
		.type = MNL_TYPE_NESTED,
	},
	[IPSET_ATTR_REVISION_MIN] = {
		.type = MNL_TYPE_U8,
		.opt = IPSET_OPT_REVISION_MIN,
	},
	/* IPSET_ATTR_PROTOCOL_MIN is an alias for IPSET_ATTR_REVISION_MIN */
	[IPSET_ATTR_LINENO] = {
		.type = MNL_TYPE_U32,
		.opt = IPSET_OPT_LINENO,
	},
};

static const struct ipset_attr_policy create_attrs[] = {
	[IPSET_ATTR_IP]	= {
		.type = MNL_TYPE_NESTED,
		.opt = IPSET_OPT_IP,
	},
	[IPSET_ATTR_IP_TO] = {
		.type = MNL_TYPE_NESTED,
		.opt = IPSET_OPT_IP_TO,
	},
	[IPSET_ATTR_CIDR] = {
		.type = MNL_TYPE_U8,
		.opt = IPSET_OPT_CIDR,
	},
	[IPSET_ATTR_PORT] = {
		.type = MNL_TYPE_U16,
		.opt = IPSET_OPT_PORT,
	},
	[IPSET_ATTR_PORT_TO] = {
		.type = MNL_TYPE_U16,
		.opt = IPSET_OPT_PORT_TO,
	},
	[IPSET_ATTR_TIMEOUT] = {
		.type = MNL_TYPE_U32,
		.opt = IPSET_OPT_TIMEOUT,
	},
	[IPSET_ATTR_PROTO] = {
		.type = MNL_TYPE_U8,
		.opt = IPSET_OPT_PROTO,
	},
	[IPSET_ATTR_CADT_FLAGS] = {
		.type = MNL_TYPE_U32,
		.opt = IPSET_OPT_CADT_FLAGS,
	},
	[IPSET_ATTR_GC] = {
		.type = MNL_TYPE_U32,
		.opt = IPSET_OPT_GC,
	},
	[IPSET_ATTR_HASHSIZE] = {
		.type = MNL_TYPE_U32,
		.opt = IPSET_OPT_HASHSIZE,
	},
	[IPSET_ATTR_MAXELEM] = {
		.type = MNL_TYPE_U32,
		.opt = IPSET_OPT_MAXELEM,
	},
	[IPSET_ATTR_NETMASK] = {
		.type = MNL_TYPE_U8,
		.opt = IPSET_OPT_NETMASK,
	},
	[IPSET_ATTR_PROBES] = {
		.type = MNL_TYPE_U8,
		.opt = IPSET_OPT_PROBES,
	},
	[IPSET_ATTR_RESIZE] = {
		.type = MNL_TYPE_U8,
		.opt = IPSET_OPT_RESIZE,
	},
	[IPSET_ATTR_SIZE] = {
		.type = MNL_TYPE_U32,
		.opt = IPSET_OPT_SIZE,
	},
	[IPSET_ATTR_ELEMENTS] = {
		.type = MNL_TYPE_U32,
		.opt = IPSET_OPT_ELEMENTS,
	},
	[IPSET_ATTR_REFERENCES] = {
		.type = MNL_TYPE_U32,
		.opt = IPSET_OPT_REFERENCES,
	},
	[IPSET_ATTR_MEMSIZE] = {
		.type = MNL_TYPE_U32,
		.opt = IPSET_OPT_MEMSIZE,
	},
};

static const struct ipset_attr_policy adt_attrs[] = {
	[IPSET_ATTR_IP]	= {
		.type = MNL_TYPE_NESTED,
		.opt = IPSET_OPT_IP,
	},
	[IPSET_ATTR_IP_TO] = {
		.type = MNL_TYPE_NESTED,
		.opt = IPSET_OPT_IP_TO,
	},
	[IPSET_ATTR_CIDR] = {
		.type = MNL_TYPE_U8,
		.opt = IPSET_OPT_CIDR,
	},
	[IPSET_ATTR_PORT] = {
		.type = MNL_TYPE_U16,
		.opt = IPSET_OPT_PORT,
	},
	[IPSET_ATTR_PORT_TO] = {
		.type = MNL_TYPE_U16,
		.opt = IPSET_OPT_PORT_TO,
	},
	[IPSET_ATTR_PROTO] = {
		.type = MNL_TYPE_U8,
		.opt = IPSET_OPT_PROTO,
	},
	[IPSET_ATTR_TIMEOUT] = {
		.type = MNL_TYPE_U32,
		.opt = IPSET_OPT_TIMEOUT,
	},
	[IPSET_ATTR_CADT_FLAGS] = {
		.type = MNL_TYPE_U32,
		.opt = IPSET_OPT_CADT_FLAGS,
	},
	[IPSET_ATTR_LINENO] = {
		.type = MNL_TYPE_U32,
		.opt = IPSET_OPT_LINENO,
	},
	[IPSET_ATTR_ETHER] = {
		.type = MNL_TYPE_BINARY,
		.opt = IPSET_OPT_ETHER,
		.len  = ETH_ALEN,
	},
	[IPSET_ATTR_NAME] = {
		.type = MNL_TYPE_NUL_STRING,
		.opt = IPSET_OPT_NAME,
		.len  = IPSET_MAXNAMELEN,
	},
	[IPSET_ATTR_NAMEREF] = {
		.type = MNL_TYPE_NUL_STRING,
		.opt = IPSET_OPT_NAMEREF,
		.len  = IPSET_MAXNAMELEN,
	},
	[IPSET_ATTR_IP2] = {
		.type = MNL_TYPE_NESTED,
		.opt = IPSET_OPT_IP2,
	},
	[IPSET_ATTR_CIDR2] = {
		.type = MNL_TYPE_U8,
		.opt = IPSET_OPT_CIDR2,
	},
};

static const struct ipset_attr_policy ipaddr_attrs[] = {
	[IPSET_ATTR_IPADDR_IPV4] = {
		.type = MNL_TYPE_U32,
	},
	[IPSET_ATTR_IPADDR_IPV6] = {
		.type = MNL_TYPE_BINARY,
		.len = sizeof(union nf_inet_addr),
	},
};

static int
generic_data_attr_cb(const struct nlattr *attr, void *data,
		     int attr_max, const struct ipset_attr_policy *policy)
{
	const struct nlattr **tb = data;
	int type = mnl_attr_get_type(attr);
	
	D("attr type: %u, len %u", type, attr->nla_len);
	if (mnl_attr_type_valid(attr, attr_max) < 0) {
		D("attr type: %u INVALID", type);
		return MNL_CB_ERROR;
	}
	if (mnl_attr_validate(attr, policy[type].type) < 0) {
		D("attr type: %u POLICY, attrlen %u", type,
		  mnl_attr_get_payload_len(attr));
		return MNL_CB_ERROR;
	}
	if (policy[type].type == MNL_TYPE_NUL_STRING
	    && mnl_attr_get_payload_len(attr) > IPSET_MAXNAMELEN)
	    	return MNL_CB_ERROR;
	tb[type] = attr;
	return MNL_CB_OK;
}

static int
create_attr_cb(const struct nlattr *attr, void *data)
{
	return generic_data_attr_cb(attr, data,
				    IPSET_ATTR_CREATE_MAX, create_attrs);
}

static int
adt_attr_cb(const struct nlattr *attr, void *data)
{
	return generic_data_attr_cb(attr, data,
				    IPSET_ATTR_ADT_MAX, adt_attrs);
}

static int
ipaddr_attr_cb(const struct nlattr *attr, void *data)
{
	return generic_data_attr_cb(attr, data,
				    IPSET_ATTR_IPADDR_MAX, ipaddr_attrs);
}

#define FAILURE(format, args...) \
	{ ipset_err(session, format  , ## args); return MNL_CB_ERROR; }

static int
attr2data(struct ipset_session *session, struct nlattr *nla[],
	  int type, const struct ipset_attr_policy attrs[])
{
	struct ipset_data *data = session->data;
	const struct ipset_attr_policy *attr;
	const void *d;
	int ret;

	attr = &attrs[type];
	d = mnl_attr_get_payload(nla[type]);

	if (attr->type == MNL_TYPE_NESTED && attr->opt) {
		/* IP addresses */
		struct nlattr *ipattr[IPSET_ATTR_IPADDR_MAX+1] = {};
		uint8_t family = ipset_data_family(data);
		int atype;
		D("attr type %u", type);
		if (mnl_attr_parse_nested(nla[type],
					  ipaddr_attr_cb, ipattr) < 0)
			FAILURE("Broken kernel message, cannot validate "
				"IP address attribute!");

		/* Validate by hand */
		switch (family) {
		case AF_INET:
			atype = IPSET_ATTR_IPADDR_IPV4;
			if (!ipattr[atype])
				FAILURE("Broken kernel message: IPv4 address "
					"expected but not received!");
			if (ipattr[atype]->nla_len < sizeof(uint32_t))
				FAILURE("Broken kernel message: "
					"cannot validate IPv4 "
					"address attribute!");
			break;
		case AF_INET6:
			atype = IPSET_ATTR_IPADDR_IPV6;
			if (!ipattr[atype])
				FAILURE("Broken kernel message: IPv6 address "
					"expected but not received!");
			if (ipattr[atype]->nla_len < sizeof(struct in6_addr))
				FAILURE("Broken kernel message: "
					"cannot validate IPv6 "
					"address attribute!");
			break;
		default:
			FAILURE("Broken kernel message: "
				"IP address attribute but "
				"family is unspecified!");
		}
		d = mnl_attr_get_payload(ipattr[atype]);
	} else if (nla[type]->nla_type & NLA_F_NET_BYTEORDER) {
		switch (attr->type) {
		case MNL_TYPE_U32: {
			uint32_t value;
		
			value  = ntohl(*(const uint32_t *)d);

			d = &value;
			break;
		}
		case MNL_TYPE_U16: {
			uint16_t value;
		
			value = ntohs(*(const uint16_t *)d);

			d = &value;
			break;
		}
		default:
			break;
		}
	}
#ifdef IPSET_DEBUG
	if (type == IPSET_ATTR_TYPENAME) 
		D("nla typename %s", (const char *) d);
#endif
	ret = ipset_data_set(data, attr->opt, d);
#ifdef IPSET_DEBUG
	if (type == IPSET_ATTR_TYPENAME) 
		D("nla typename %s",
		  (const char *) ipset_data_get(data, IPSET_OPT_TYPENAME));
#endif	
	return ret;
}

#define ATTR2DATA(session, nla, type, attrs)		\
	if (attr2data(session, nla, type, attrs) < 0)	\
		return MNL_CB_ERROR

static const char cmd2name[][9] = {
	[IPSET_CMD_NONE]	= "NONE",
	[IPSET_CMD_CREATE]	= "CREATE",
	[IPSET_CMD_DESTROY]	= "DESTROY",
	[IPSET_CMD_FLUSH]	= "FLUSH",
	[IPSET_CMD_RENAME]	= "RENAME",
	[IPSET_CMD_SWAP]	= "SWAP",
	[IPSET_CMD_LIST]	= "LIST",
	[IPSET_CMD_SAVE]	= "SAVE",
	[IPSET_CMD_ADD]		= "ADD",
	[IPSET_CMD_DEL]		= "DEL",
	[IPSET_CMD_TEST]	= "TEST",
	[IPSET_CMD_HEADER]	= "HEADER",
	[IPSET_CMD_TYPE]	= "TYPE",
	[IPSET_CMD_PROTOCOL]	= "PROTOCOL",
};

static inline int
call_outfn(struct ipset_session *session)
{
	int ret = session->outfn("%s", session->outbuf);
	
	session->outbuf[0] = '\0';
	
	return ret < 0 ? ret : 0;
}

/* Handle printing failures */
static jmp_buf printf_failure;

static int __attribute__((format(printf,2,3)))
safe_snprintf(struct ipset_session *session, const char *fmt, ...)
{
	va_list args;
	int len, ret, loop = 0;

retry:
	len = strlen(session->outbuf);
	D("len: %u, retry %u", len, loop);
	va_start(args, fmt);
	ret = vsnprintf(session->outbuf + len, IPSET_OUTBUFLEN - len,
			fmt, args);
	va_end(args);
	
	if (ret < 0) {
		ipset_err(session,
			 "Internal error at printing to output buffer");
		longjmp(printf_failure, 1);
	}

	if (ret >= IPSET_OUTBUFLEN - len) {
		/* Buffer was too small, push it out and retry */
		D("print buffer and try again: %u", len);
		if (loop++) {
			ipset_err(session,
				"Internal error at printing, loop detected!");
			longjmp(printf_failure, 1);
		}

		session->outbuf[len] = '\0';
		if (!call_outfn(session))
			goto retry;
	}
	return ret;
}

static int
safe_dprintf(struct ipset_session *session, ipset_printfn fn,
	     enum ipset_opt opt)
{
	int len, ret, loop = 0;

retry:
	len = strlen(session->outbuf);
	D("len: %u, retry %u", len, loop);
	ret = fn(session->outbuf + len, IPSET_OUTBUFLEN - len,
		 session->data, opt, session->envopts);
	
	if (ret < 0) {
		ipset_err(session,
			"Internal error at printing to output buffer");
		longjmp(printf_failure, 1);
	}

	if (ret >= IPSET_OUTBUFLEN - len) {
		/* Buffer was too small, push it out and retry */
		D("print buffer and try again: %u", len);
		if (loop++) {
			ipset_err(session,
				"Internal error at printing, loop detected!");
			longjmp(printf_failure, 1);
		}

		session->outbuf[len] = '\0';
		if (!call_outfn(session))
			goto retry;
	}
	return ret;
}
 
static int
list_adt(struct ipset_session *session, struct nlattr *nla[])
{
	const struct ipset_data *data = session->data;
	const struct ipset_type *type;
	const struct ipset_arg *arg;
	uint8_t family;
	int i, found = 0;

	D("enter");	
	/* Check and load type, family */
	if (!ipset_data_test(data, IPSET_OPT_TYPE))
		type = ipset_type_get(session, IPSET_CMD_ADD);
	else
		type = ipset_data_get(data, IPSET_OPT_TYPE);

	if (type == NULL)
		return MNL_CB_ERROR;
	family = ipset_data_family(data);

	for (i = IPSET_ATTR_UNSPEC + 1; i <= IPSET_ATTR_ADT_MAX; i++)
		if (nla[i]) {
			found++;
			ATTR2DATA(session, nla, i, adt_attrs);
	}
	D("attr found %u", found);
	if (!found)
		return MNL_CB_OK;

	switch (session->mode) {
	case IPSET_LIST_SAVE:
		safe_snprintf(session, "add %s ", ipset_data_setname(data));
		break;
	case IPSET_LIST_XML:
		safe_snprintf(session, "    <member>");
		break;
	case IPSET_LIST_PLAIN:
	default:
		break;
	}
	
	safe_dprintf(session, ipset_print_elem, IPSET_OPT_ELEM);

	for (arg = type->args[IPSET_ADD]; arg != NULL && arg->print; arg++) {
		if (!ipset_data_test(data, arg->opt))
			continue;
		switch (session->mode) {
		case IPSET_LIST_SAVE:
		case IPSET_LIST_PLAIN:
			safe_snprintf(session, " %s ", arg->name[0]);
			if (arg->has_arg == IPSET_NO_ARG)
				break;
			safe_dprintf(session, arg->print, arg->opt);
			break;
		case IPSET_LIST_XML:
			if (arg->has_arg == IPSET_NO_ARG) {
				safe_snprintf(session,
					      "    <%s/>\n",
					      arg->name[0]);
				break;
			}
			safe_snprintf(session, "    <%s>",
				      arg->name[0]);
			safe_dprintf(session, arg->print, arg->opt);
			safe_snprintf(session, "</%s>\n",
				      arg->name[0]);
			break;
		default:
			break;
		}
	}
	
	if (session->mode == IPSET_LIST_XML)
		safe_snprintf(session, "</member>\n");
	else
		safe_snprintf(session, "\n");

	return MNL_CB_OK;
}

#define FAMILY_TO_STR(f)		\
	((f) == AF_INET ? "inet" :	\
	 (f) == AF_INET6 ? "inet6" : "any")

static int
list_create(struct ipset_session *session, struct nlattr *nla[])
{
	const struct ipset_data *data = session->data;
	const struct ipset_type *type;
	const struct ipset_arg *arg;
	uint8_t family;
	int i;

	for (i = IPSET_ATTR_UNSPEC + 1; i <= IPSET_ATTR_CREATE_MAX; i++)
		if (nla[i]) {
			D("add attr %u, opt %u", i, create_attrs[i].opt);
			ATTR2DATA(session, nla, i, create_attrs);
		}

	type = ipset_type_check(session);
	if (type == NULL)
		return MNL_CB_ERROR;
	family = ipset_data_family(data);

	switch (session->mode) {
	case IPSET_LIST_SAVE:
		safe_snprintf(session, "create %s %s ",
			      ipset_data_setname(data),
			      type->name);
		break;
	case IPSET_LIST_PLAIN:
		safe_snprintf(session, "Name: %s\n"
			      "Type: %s\nHeader: ",
			      ipset_data_setname(data),
			      type->name);
		break;
	case IPSET_LIST_XML:
		safe_snprintf(session,
			      "<ipset name=\"%s\">\n"
			      "  <type>%s</type>\n"
			      "  <header>\n",
			      ipset_data_setname(data),
			      type->name);
		break;
	default:
		break;
	}

	for (arg = type->args[IPSET_CREATE]; arg != NULL && arg->opt; arg++) {
		if (!arg->print
		    || !ipset_data_test(data, arg->opt)
		    || (arg->opt == IPSET_OPT_FAMILY
		        && family == type->family))
			continue;
		switch (session->mode) {
		case IPSET_LIST_SAVE:
		case IPSET_LIST_PLAIN:
			safe_snprintf(session, "%s ", arg->name[0]);
			if (arg->has_arg == IPSET_NO_ARG)
				break;
			safe_dprintf(session, arg->print, arg->opt);
			safe_snprintf(session, " ");
			break;
		case IPSET_LIST_XML:
			if (arg->has_arg == IPSET_NO_ARG) {
				safe_snprintf(session,
					      "    <%s/>\n",
					      arg->name[0]);
				break;
			}
			safe_snprintf(session, "    <%s>",
				      arg->name[0]);
			safe_dprintf(session, arg->print, arg->opt);
			safe_snprintf(session, "</%s>\n",
				      arg->name[0]);
			break;
		default:
			break;
		}
	}
	switch (session->mode) {
	case IPSET_LIST_SAVE:
		safe_snprintf(session, "\n");
		break;
	case IPSET_LIST_PLAIN:
		safe_snprintf(session, "\nSize in memory: ");
		safe_dprintf(session, ipset_print_number, IPSET_OPT_MEMSIZE);
		safe_snprintf(session, "\nReferences: ");
		safe_dprintf(session, ipset_print_number, IPSET_OPT_REFERENCES);
		safe_snprintf(session, "\nMembers:\n");
		break;
	case IPSET_LIST_XML:
		safe_snprintf(session, "</elements>\n    <memsize>");
		safe_dprintf(session, ipset_print_number, IPSET_OPT_MEMSIZE);
		safe_snprintf(session, "</memsize>\n    <references>");
		safe_dprintf(session, ipset_print_number, IPSET_OPT_REFERENCES);
		safe_snprintf(session, "</references>\n  </header>\n  <members>\n");
		break;
	default:
		break;
	}

	return MNL_CB_OK;
}

static int
print_set_done(struct ipset_session *session)
{
	D("called for %s", session->saved_setname[0] == '\0'
		? "NONE" : session->saved_setname);
	switch (session->mode) {
	case IPSET_LIST_XML:
		if (session->saved_setname[0] == '\0')
			safe_snprintf(session, "\n");
		else
			safe_snprintf(session, "  </members>\n</ipset>\n");
		break;
	case IPSET_LIST_SAVE:
		/* No empty lines between the sets */
		break;
	default:
		safe_snprintf(session, "\n");
		break;
	}
	return call_outfn(session) ? MNL_CB_ERROR : MNL_CB_STOP;
}

static int
callback_list(struct ipset_session *session, struct nlattr *nla[],
	      enum ipset_cmd cmd)
{
	struct ipset_data *data = session->data;

	if (setjmp(printf_failure))
		return MNL_CB_ERROR;

	if (!nla[IPSET_ATTR_SETNAME])
		FAILURE("Broken %s kernel message: missing setname!",
			cmd2name[cmd]);

	ATTR2DATA(session, nla, IPSET_ATTR_SETNAME, cmd_attrs);
	D("setname %s", ipset_data_setname(data));
	if (STREQ(ipset_data_setname(data), session->saved_setname)) {
		/* Header part already seen */
		if (ipset_data_test(data, IPSET_OPT_TYPE)
		    && nla[IPSET_ATTR_DATA] != NULL)
			FAILURE("Broken %s kernel message: "
				"extra DATA received!", cmd2name[cmd]);
	} else {
		if (nla[IPSET_ATTR_DATA] == NULL)
			FAILURE("Broken %s kernel message: "
				"missing DATA part!", cmd2name[cmd]);
					 
		/* Close previous set printing */
		if (session->saved_setname[0] != '\0')
			print_set_done(session);
	}

	if (nla[IPSET_ATTR_DATA] != NULL) {
		struct nlattr *cattr[IPSET_ATTR_CREATE_MAX+1] = {};

		if (!(nla[IPSET_ATTR_TYPENAME]
		      && nla[IPSET_ATTR_FAMILY]
		      && nla[IPSET_ATTR_REVISION]))
			FAILURE("Broken %s kernel message: missing %s!",
				cmd2name[cmd],
				!nla[IPSET_ATTR_TYPENAME] ? "typename" : 
				!nla[IPSET_ATTR_FAMILY] ? "family" : "revision");

		/* Reset CREATE specific flags */
		ipset_data_flags_unset(data, IPSET_CREATE_FLAGS);
		D("nla typename %s",
		  (char *) mnl_attr_get_payload(nla[IPSET_ATTR_TYPENAME]));
		D("nla typename %s",
		  (char *) mnl_attr_get_payload(nla[IPSET_ATTR_TYPENAME]));
		ATTR2DATA(session, nla, IPSET_ATTR_FAMILY, cmd_attrs);
		ATTR2DATA(session, nla, IPSET_ATTR_TYPENAME, cmd_attrs);
		ATTR2DATA(session, nla, IPSET_ATTR_REVISION, cmd_attrs);
		D("head: family %u, typename %s",
		  ipset_data_family(data),
		  (const char *) ipset_data_get(data, IPSET_OPT_TYPENAME));
		if (mnl_attr_parse_nested(nla[IPSET_ATTR_DATA],
					  create_attr_cb, cattr) < 0)
			FAILURE("Broken %s kernel message: "
				"cannot validate DATA attributes!",
				cmd2name[cmd]);
		if (list_create(session, cattr) != MNL_CB_OK)
			return MNL_CB_ERROR;
		strcpy(session->saved_setname, ipset_data_setname(data));
	}
	
	if (nla[IPSET_ATTR_ADT] != NULL) {
		struct nlattr *tb, *adt[IPSET_ATTR_ADT_MAX+1];

		mnl_attr_for_each_nested(tb, nla[IPSET_ATTR_ADT]) {
			D("ADT attributes for %s", ipset_data_setname(data));
			memset(adt, 0, sizeof(adt));
			/* Reset ADT specific flags */
			ipset_data_flags_unset(data, IPSET_ADT_FLAGS);
			if (mnl_attr_parse_nested(tb, adt_attr_cb, adt) < 0)
				FAILURE("Broken %s kernel message: "
					"cannot validate ADT attributes!",
					cmd2name[cmd]);
			if (list_adt(session, adt) != MNL_CB_OK)
				return MNL_CB_ERROR;
		}
	}
	return call_outfn(session) ? MNL_CB_ERROR : MNL_CB_OK;
}

#ifndef IPSET_PROTOCOL_MIN
#define IPSET_PROTOCOL_MIN	IPSET_PROTOCOL
#endif

#ifndef IPSET_PROTOCOL_MAX
#define IPSET_PROTOCOL_MAX	IPSET_PROTOCOL
#endif

static int
callback_version(struct ipset_session *session, struct nlattr *nla[])
{
	uint8_t min, max;
	
	min = max = mnl_attr_get_u8(nla[IPSET_ATTR_PROTOCOL]);

	if (nla[IPSET_ATTR_PROTOCOL_MIN]) {
		min = mnl_attr_get_u8(nla[IPSET_ATTR_PROTOCOL_MIN]);
		D("min: %u", min);
	}

	if (min > IPSET_PROTOCOL_MAX || max < IPSET_PROTOCOL_MIN)
		FAILURE("Cannot communicate with kernel: "
			"Kernel support protocol versions %u-%u "
			"while userspace supports protocol versions %u-%u",
			min, max, IPSET_PROTOCOL_MIN, IPSET_PROTOCOL_MAX);

	if (!(session->envopts & IPSET_ENV_QUIET)
	    && max != IPSET_PROTOCOL_MAX)
		ipset_warn(session,
			   "Kernel support protocol versions %u-%u "
			   "while userspace supports protocol versions %u-%u",
			   min, max, IPSET_PROTOCOL_MIN, IPSET_PROTOCOL_MAX);

	session->version_checked = true;

	return MNL_CB_STOP;
}

static int
callback_header(struct ipset_session *session, struct nlattr *nla[])
{
	const char *setname;
	const struct ipset_data *data = session->data;
	
	if (!nla[IPSET_ATTR_SETNAME])
		FAILURE("Broken HEADER kernel message: missing setname!");

	setname = mnl_attr_get_str(nla[IPSET_ATTR_SETNAME]);
	if (!STREQ(setname, ipset_data_setname(data)))
		FAILURE("Broken HEADER kernel message: sent setname `%s' "
			"does not match with received one `%s'!",
			ipset_data_setname(data), setname);
	
	if (!(nla[IPSET_ATTR_TYPENAME]
	      && nla[IPSET_ATTR_REVISION]
	      && nla[IPSET_ATTR_FAMILY]))
		FAILURE("Broken HEADER kernel message: "
			"missing attribute '%s'!",
			!nla[IPSET_ATTR_TYPENAME] ? "typename" :
			!nla[IPSET_ATTR_REVISION] ? "revision" :
			"family");

	ATTR2DATA(session, nla, IPSET_ATTR_TYPENAME, cmd_attrs);
	ATTR2DATA(session, nla, IPSET_ATTR_REVISION, cmd_attrs);
	ATTR2DATA(session, nla, IPSET_ATTR_FAMILY, cmd_attrs);
	D("got family: %u", ipset_data_family(session->data));

	return MNL_CB_STOP;
}

static int
callback_type(struct ipset_session *session, struct nlattr *nla[])
{
	const struct ipset_data *data = session->data;
	const char *typename, *orig;
	
	if (!(nla[IPSET_ATTR_TYPENAME]
	      && nla[IPSET_ATTR_REVISION]
	      && nla[IPSET_ATTR_FAMILY]))
		FAILURE("Broken TYPE kernel message: "
			"missing attribute '%s'!",
			!nla[IPSET_ATTR_TYPENAME] ? "typename" :
			!nla[IPSET_ATTR_REVISION] ? "revision" :
			"family");

	typename = mnl_attr_get_str(nla[IPSET_ATTR_TYPENAME]);
	orig = ipset_data_get(data, IPSET_OPT_TYPENAME);
	if (!STREQ(typename, orig))
		FAILURE("Broken TYPE kernel message: sent typename `%s' "
			"does not match with received one `%s'!",
			orig, typename);
	
	ATTR2DATA(session, nla, IPSET_ATTR_TYPENAME, cmd_attrs);
	ATTR2DATA(session, nla, IPSET_ATTR_REVISION, cmd_attrs);
	ATTR2DATA(session, nla, IPSET_ATTR_FAMILY, cmd_attrs);
	if (nla[IPSET_ATTR_REVISION_MIN])
		ATTR2DATA(session, nla, IPSET_ATTR_REVISION_MIN, cmd_attrs);

	return MNL_CB_STOP;
}

static int
cmd_attr_cb(const struct nlattr *attr, void *data)
{
	return generic_data_attr_cb(attr, data, IPSET_ATTR_CMD_MAX, cmd_attrs);
}

#if 0
static int
mnl_attr_parse_dbg(const struct nlmsghdr *nlh, int offset,
		   mnl_attr_cb_t cb, void *data)
{
	int ret = MNL_CB_OK;
	struct nlattr *attr = mnl_nlmsg_get_payload_offset(nlh, offset);
	int len = nlh->nlmsg_len - MNL_NLMSG_HDRLEN - MNL_ALIGN(offset);
	
	while (mnl_attr_ok(attr, len)) {
		D("attr: type %u, attrlen %u, len %u",
		  mnl_attr_get_type(attr), attr->nla_len, len);
		if (cb && (ret = cb(attr, data)) <= MNL_CB_STOP)
			return ret;
		attr = mnl_attr_next(attr, &len);
	}
	return ret;
}
#endif

static int
callback_data(const struct nlmsghdr *nlh, void *data)
{
	struct ipset_session *session = data;
	struct nlattr *nla[IPSET_ATTR_CMD_MAX+1] = {};
	uint8_t proto, cmd;
	int ret = MNL_CB_OK, nfmsglen = MNL_ALIGN(sizeof(struct genlmsghdr));
	
	D("called, nlmsg_len %u", nlh->nlmsg_len);
	cmd = ipset_get_nlmsg_type(nlh);
	if (cmd == IPSET_CMD_LIST && session->cmd == IPSET_CMD_SAVE)
		/* Kernel always send IPSET_CMD_LIST */
		cmd = IPSET_CMD_SAVE;

	if (cmd != session->cmd)
		FAILURE("Protocol error, we sent command %s "
			"and received %s[%u]",
			cmd2name[session->cmd],
			cmd < IPSET_MSG_MAX ? cmd2name[cmd] : "unknown", cmd);

	if (mnl_attr_parse(nlh, nfmsglen, cmd_attr_cb, nla) < MNL_CB_STOP)
		FAILURE("Broken %s kernel message: "
			"cannot validate and parse attributes",
			cmd2name[cmd]);

	if (!nla[IPSET_ATTR_PROTOCOL])
		FAILURE("Sad, sad day: kernel message %s "
		        "does not carry the protocol version.",
		        cmd2name[cmd]);

	proto = mnl_attr_get_u8(nla[IPSET_ATTR_PROTOCOL]);

	/* Check protocol */
	if (cmd != IPSET_CMD_PROTOCOL && proto != IPSET_PROTOCOL)
		FAILURE("Giving up: kernel protocol version %u "
			"does not match our protocol version %u",
			proto, IPSET_PROTOCOL);

	D("Message: %s", cmd2name[cmd]);
	switch (cmd) {
	case IPSET_CMD_LIST:
	case IPSET_CMD_SAVE:
		ret = callback_list(session, nla, cmd);
		D("flag multi: %u", nlh->nlmsg_flags & NLM_F_MULTI);
		if (ret >= MNL_CB_STOP && !(nlh->nlmsg_flags & NLM_F_MULTI))
			ret = print_set_done(session);
		break;
	case IPSET_CMD_PROTOCOL:
		if (!session->version_checked)
			ret = callback_version(session, nla);
		break;
	case IPSET_CMD_HEADER:
		ret = callback_header(session, nla);
		break;
	case IPSET_CMD_TYPE:
		ret = callback_type(session, nla);
		break;
	default:
		FAILURE("Data message received when not expected at %s",
			cmd2name[session->cmd]);
	}
	D("return code: %s", ret == MNL_CB_STOP ? "stop" :
			     ret == MNL_CB_OK ? "ok" : "error");
	return ret;
}

static int
callback_done(const struct nlmsghdr *nlh UNUSED, void *data)
{
	struct ipset_session *session = data;

	D(" called");
	if (session->cmd == IPSET_CMD_LIST || session->cmd == IPSET_CMD_SAVE)
	    	return print_set_done(session);
	
	FAILURE("Invalid message received in non LIST or SAVE state.");
}

static int
decode_errmsg(struct ipset_session *session, const struct nlmsghdr *nlh)
{
	const struct nlmsgerr *err = mnl_nlmsg_get_payload(nlh);
	const struct nlmsghdr *msg = &err->msg;
	struct nlattr *nla[IPSET_ATTR_CMD_MAX+1] = {};
	enum ipset_cmd cmd;
	int nfmsglen = MNL_ALIGN(sizeof(struct genlmsghdr));
	
	if (nlh->nlmsg_len < (uint32_t) MNL_ALIGN(sizeof(struct nlmsgerr))
	    || nlh->nlmsg_len < MNL_ALIGN(sizeof(struct nlmsgerr))
	    			+ msg->nlmsg_len)
		FAILURE("Broken error report message received.");

	cmd = ipset_get_nlmsg_type(msg);
	D("nlsmg_len: %u", msg->nlmsg_len);
	if (cmd != session->cmd)
		FAILURE("Protocol error, we sent command %s "
			"and received error report for %s[%u]",
			cmd2name[session->cmd],
			cmd < IPSET_MSG_MAX ? cmd2name[cmd] : "unknown", cmd);

	if (mnl_attr_parse(msg, nfmsglen, cmd_attr_cb, nla) < MNL_CB_STOP)
		FAILURE("Broken %s error report message: "
			"cannot validate attributes",
			cmd2name[cmd]);

	if (!nla[IPSET_ATTR_PROTOCOL])
		FAILURE("Broken %s error report message: "
			"missing protocol attribute",
			cmd2name[cmd]);
	
	if (nla[IPSET_ATTR_LINENO]) {
		session->lineno = mnl_attr_get_u32(nla[IPSET_ATTR_LINENO]);
		if (nla[IPSET_ATTR_LINENO]->nla_type & NLA_F_NET_BYTEORDER)
			session->lineno = ntohl(session->lineno);
	}
	
	return ipset_errcode(session, cmd, -err->error);
}	

static int
callback_error(const struct nlmsghdr *nlh, void *cbdata)
{
	struct ipset_session *session = cbdata;
	struct ipset_data *data = session->data;
	const struct nlmsgerr *err = mnl_nlmsg_get_payload(nlh);
	int ret = MNL_CB_ERROR;

	D(" called, cmd %s", cmd2name[session->cmd]);
	if (nlh->nlmsg_len < mnl_nlmsg_size(sizeof(struct nlmsgerr)))
		FAILURE("Broken error message received.");

	if (err->error == 0) {
		/* ACK */
		ret = MNL_CB_STOP;

		switch (session->cmd) {
		case IPSET_CMD_CREATE:
			/* Add successfully created set to the cache */
			ipset_cache_add(ipset_data_setname(data),
					ipset_data_get(data, IPSET_OPT_TYPE),
					ipset_data_family(data));
			break;
		case IPSET_CMD_DESTROY:
			/* Delete destroyed sets from the cache */
			ipset_cache_del(ipset_data_setname(data));
			/* Fall through */
		case IPSET_CMD_FLUSH:
			break;
		case IPSET_CMD_RENAME:
			ipset_cache_rename(ipset_data_setname(data),
					   ipset_data_get(data, IPSET_OPT_SETNAME2));
			break;
		case IPSET_CMD_SWAP:
			ipset_cache_swap(ipset_data_setname(data),
					 ipset_data_get(data, IPSET_OPT_SETNAME2));
			break;
		case IPSET_CMD_TEST:
			if (!(session->envopts & IPSET_ENV_QUIET)) {
				ipset_print_elem(session->report, IPSET_ERRORBUFLEN,
						 session->data, IPSET_OPT_NONE, 0);
				ipset_warn(session, " is in set %s.",
					   ipset_data_setname(data));
			}
			/* Fall through */
		case IPSET_CMD_ADD:
		case IPSET_CMD_DEL:
			break;
		case IPSET_CMD_LIST:
		case IPSET_CMD_SAVE:
			/* No set in kernel */
			print_set_done(session);
			break;
		default:
			FAILURE("ACK message received to command %s[%u], which is not expected",
				session->cmd < IPSET_MSG_MAX
				? cmd2name[session->cmd] : "unknown",
				session->cmd);
		}
		return ret;
	}
	D("nlmsgerr error: %u", -err->error);

	/* Error messages */
	
	/* Special case for IPSET_CMD_TEST */
	if (session->cmd == IPSET_CMD_TEST
	    && err->error == -IPSET_ERR_EXIST) {
		if (!(session->envopts & IPSET_ENV_QUIET)) {
			ipset_print_elem(session->report, IPSET_ERRORBUFLEN,
					 session->data, IPSET_OPT_NONE, 0);
			ipset_warn(session, " is NOT in set %s.",
				   ipset_data_setname(data));
		}
		return ret;
	}

	decode_errmsg(session, nlh);
	
	return ret;
}

static int
callback_noop(const struct nlmsghdr *nlh UNUSED, void *data UNUSED)
{
	return MNL_CB_OK;
}
/*
 * Build and send messages
 */

static inline int
open_nested(struct ipset_session *session, struct nlmsghdr *nlh, int attr)
{
	if (nlh->nlmsg_len + MNL_ATTR_HDRLEN > session->bufsize)
		return 1;
	session->nested[session->nestid++] = mnl_attr_nest_start(nlh, attr);
	return 0;
}

static inline void
close_nested(struct ipset_session *session, struct nlmsghdr *nlh)
{
	mnl_attr_nest_end(nlh, session->nested[session->nestid-1]);
	session->nested[--session->nestid] = NULL;
}

static size_t
attr_len(const struct ipset_attr_policy *attr, uint8_t family, uint16_t *flags)
{
	switch (attr->type) {
	case MNL_TYPE_NESTED:
		if (attr->len)
			return attr->len;

		*flags = NLA_F_NET_BYTEORDER;
		return family == AF_INET ? sizeof(uint32_t)
					 : sizeof(struct in6_addr);
	case MNL_TYPE_U32:
		*flags = NLA_F_NET_BYTEORDER;
		return sizeof(uint32_t);
	case MNL_TYPE_U16:
		*flags = NLA_F_NET_BYTEORDER;
		return sizeof(uint16_t);
	case MNL_TYPE_U8:
		return sizeof(uint8_t);
	default:
		return attr->len;
	}
}

#define BUFFER_FULL(bufsize, nlmsg_len, nestlen, attrlen)	\
(nlmsg_len + nestlen + MNL_ATTR_HDRLEN + MNL_ALIGN(alen) + MNL_ALIGN(sizeof(struct nlmsgerr)) > bufsize)

static int
rawdata2attr(struct ipset_session *session, struct nlmsghdr *nlh,
	     const void *d, int type, uint8_t family,
	     const struct ipset_attr_policy attrs[])
{
	const struct ipset_attr_policy *attr;
	int alen;
	uint16_t flags = 0;


	attr = &attrs[type];
	if (attr->type == MNL_TYPE_NESTED) {
		/* IP addresses */
		struct nlattr *nested;
		int atype = family == AF_INET ? IPSET_ATTR_IPADDR_IPV4
					      : IPSET_ATTR_IPADDR_IPV6;

		alen = attr_len(attr, family, &flags);
		if (BUFFER_FULL(session->bufsize, nlh->nlmsg_len, MNL_ATTR_HDRLEN, alen))
			return 1;
		nested = mnl_attr_nest_start(nlh, type);
		D("family: %s", family == AF_INET ? "INET" :
				family == AF_INET6 ? "INET6" : "UNSPEC");
		mnl_attr_put(nlh, atype | flags, alen, d);
		mnl_attr_nest_end(nlh, nested);
		
		return 0;
	}

	alen = attr_len(attr, family, &flags);
	if (BUFFER_FULL(session->bufsize, nlh->nlmsg_len, 0, alen))
		return 1;

	switch (attr->type) {
	case MNL_TYPE_U32: {
		uint32_t value = htonl(*(const uint32_t *)d);
		
		d = &value;
		break;
	}
	case MNL_TYPE_U16: {
		uint16_t value = htons(*(const uint16_t *)d);
		
		d = &value;
		break;
	}
	default:
		break;
	}
	
	mnl_attr_put(nlh, type | flags, alen, d);

	return 0;
}

static int
data2attr(struct ipset_session *session, struct nlmsghdr *nlh,
	  struct ipset_data *data, int type, uint8_t family,
	  const struct ipset_attr_policy attrs[])
{
	const struct ipset_attr_policy *attr = &attrs[type];

	return rawdata2attr(session, nlh, ipset_data_get(data, attr->opt),
			    type, family, attrs);
}

#define ADDATTR_PROTOCOL(nlh)						\
	mnl_attr_put_u8(nlh, IPSET_ATTR_PROTOCOL, IPSET_PROTOCOL)

#define ADDATTR(session, nlh, data, type, family, attrs)		\
	data2attr(session, nlh, data, type, family, attrs)

#define ADDATTR_SETNAME(session, nlh, data)				\
	data2attr(session, nlh, data, IPSET_ATTR_SETNAME, AF_INET, cmd_attrs)

#define ADDATTR_IF(session, nlh, data, type, family, attrs)		\
	ipset_data_test(data, attrs[type].opt) ? 			\
		data2attr(session, nlh, data, type, family, attrs) : 0

#define ADDATTR_RAW(session, nlh, data, type, attrs)			\
	rawdata2attr(session, nlh, data, type, AF_INET, attrs)

static void
addattr_create(struct ipset_session *session,
	       struct nlmsghdr *nlh, struct ipset_data *data, uint8_t family)
{
	int i;

	for (i = IPSET_ATTR_UNSPEC + 1; i <= IPSET_ATTR_CREATE_MAX; i++)
		ADDATTR_IF(session, nlh, data, i, family, create_attrs);
}

static int
addattr_adt(struct ipset_session *session,
	    struct nlmsghdr *nlh, struct ipset_data *data, uint8_t family)
{
	int i;
	
	for (i = IPSET_ATTR_UNSPEC + 1; i <= IPSET_ATTR_ADT_MAX; i++)
		if (ADDATTR_IF(session, nlh, data, i, family, adt_attrs))
			return 1;
	return 0;
}

#define PRIVATE_MSG_BUFLEN	256

static int
build_send_private_msg(struct ipset_session *session, enum ipset_cmd cmd)
{
	char buffer[PRIVATE_MSG_BUFLEN] __attribute__ ((aligned));
	struct nlmsghdr *nlh = (void *)buffer;
	struct ipset_data *data = session->data;
	int len = PRIVATE_MSG_BUFLEN, ret;
	enum ipset_cmd saved = session->cmd;

	/* Initialize header */
	session->transport->fill_hdr(session->handle, cmd, buffer, len, 0);
		
	ADDATTR_PROTOCOL(nlh);

	switch (cmd) {
	case IPSET_CMD_PROTOCOL:
		break;
	case IPSET_CMD_HEADER:
		if (!ipset_data_test(data, IPSET_SETNAME))
			return ipset_err(session,
				"Invalid internal HEADER command: "
				"missing setname");
		ADDATTR_SETNAME(session, nlh, data);
		break;
	case IPSET_CMD_TYPE:
		if (!ipset_data_test(data, IPSET_OPT_TYPENAME))
			return ipset_err(session,
				"Invalid internal TYPE command: "
				"missing settype");
		ADDATTR(session, nlh, data, IPSET_ATTR_TYPENAME, AF_INET, cmd_attrs);
		if (ipset_data_test(data, IPSET_OPT_FAMILY))
			ADDATTR(session, nlh, data, IPSET_ATTR_FAMILY, AF_INET, cmd_attrs);
		else
			/* bitmap:port and list:set types */
			mnl_attr_put_u8(nlh, IPSET_ATTR_FAMILY, AF_UNSPEC);
		break;
	default:
		return ipset_err(session, "Internal error: "
				 "unknown private command %u", cmd);
	}

	/* Backup, then restore real command */
	session->cmd = cmd;
	ret = session->transport->query(session->handle, buffer, len);
	session->cmd = saved;

	return ret;
}

static inline bool
may_aggregate_ad(struct ipset_session *session, enum ipset_cmd cmd)
{
	return session->lineno != 0
	       && (cmd == IPSET_CMD_ADD || cmd == IPSET_CMD_DEL)
	       && cmd == session->cmd
	       && STREQ(ipset_data_setname(session->data), session->saved_setname);
}

static int
build_msg(struct ipset_session *session, bool aggregate)
{
	struct nlmsghdr *nlh = session->buffer;
	struct ipset_data *data = session->data;

	/* Public commands */
	D("cmd %s, nlmsg_len: %u", cmd2name[session->cmd], nlh->nlmsg_len);
	if (nlh->nlmsg_len == 0) {
		/* Initialize header */
		aggregate = false;
		session->transport->fill_hdr(session->handle,
					     session->cmd,
					     session->buffer,
					     session->bufsize,
					     session->envopts);
		ADDATTR_PROTOCOL(nlh);
	}
	D("Protocol added, aggregate %s", aggregate ? "yes" : "no");
	switch (session->cmd) {
	case IPSET_CMD_CREATE: {
		const struct ipset_type *type;

		/* Sanity checkings */
		if (!ipset_data_test(data, IPSET_SETNAME))
			return ipset_err(session,
				"Invalid create command: missing setname");
		if (!ipset_data_test(data, IPSET_OPT_TYPE))
			return ipset_err(session,
				"Invalid create command: missing settype");
		
		type = ipset_data_get(data, IPSET_OPT_TYPE);
		/* Core attributes:
		 * setname, typename, revision, family, flags (optional) */
		ADDATTR_SETNAME(session, nlh, data);
		ADDATTR(session, nlh, data, IPSET_ATTR_TYPENAME, AF_INET, cmd_attrs);
		ADDATTR_RAW(session, nlh, &type->revision,
			    IPSET_ATTR_REVISION, cmd_attrs);
		D("family: %u, type family %u",
		  ipset_data_family(data), type->family);
		ADDATTR(session, nlh, data, IPSET_ATTR_FAMILY, AF_INET, cmd_attrs);

		/* Type-specific create attributes */
		D("call open_nested");
		open_nested(session, nlh, IPSET_ATTR_DATA);
		addattr_create(session, nlh, data, type->family);
		D("call close_nested");
		close_nested(session, nlh);
		break;
	}
	case IPSET_CMD_DESTROY:
	case IPSET_CMD_FLUSH:
	case IPSET_CMD_LIST:
	case IPSET_CMD_SAVE:
		if (ipset_data_test(data, IPSET_SETNAME))
			ADDATTR_SETNAME(session, nlh, data);
		break;
	case IPSET_CMD_RENAME:
	case IPSET_CMD_SWAP:
		if (!ipset_data_test(data, IPSET_SETNAME))
			return ipset_err(session,
				"Invalid %s command: missing from-setname",
				session->cmd == IPSET_CMD_SWAP ? "swap" : "rename");
		if (!ipset_data_test(data, IPSET_OPT_SETNAME2))
			return ipset_err(session,
				"Invalid %s command: missing to-setname",
				session->cmd == IPSET_CMD_SWAP ? "swap" : "rename");
		ADDATTR_SETNAME(session, nlh, data);
		ADDATTR_RAW(session, nlh, ipset_data_get(data, IPSET_OPT_SETNAME2),
			    IPSET_ATTR_SETNAME2, cmd_attrs);
		break;
	case IPSET_CMD_ADD:
	case IPSET_CMD_DEL: {
		const struct ipset_type *type;

		if (!aggregate) {
			/* Setname, type not checked/added yet */
			if (!ipset_data_test(data, IPSET_SETNAME))
				return ipset_err(session,
					"Invalid %s command: missing setname",
					session->cmd == IPSET_CMD_ADD ? "add" : "del");

			if (!ipset_data_test(data, IPSET_OPT_TYPE))
				return ipset_err(session,
					"Invalid %s command: missing settype",
					session->cmd == IPSET_CMD_ADD ? "add" : "del");

			/* Core options: setname */
			ADDATTR_SETNAME(session, nlh, data);
			if (session->lineno != 0) {
				/* Restore mode */
				ADDATTR_RAW(session, nlh, &session->lineno,
					    IPSET_ATTR_LINENO, cmd_attrs);
				open_nested(session, nlh, IPSET_ATTR_ADT);
			}
		}
		type = ipset_data_get(data, IPSET_OPT_TYPE);
		D("family: %u, type family %u",
		  ipset_data_family(data), type->family);
		if (open_nested(session, nlh, IPSET_ATTR_DATA)) {
			D("open_nested failed");
			return 1;
		}
		if (addattr_adt(session, nlh, data, ipset_data_family(data))
		    || ADDATTR_RAW(session, nlh, &session->lineno,
				   IPSET_ATTR_LINENO, cmd_attrs)) {
			/* Cancel last, unfinished nested attribute */
			mnl_attr_nest_cancel(nlh, session->nested[session->nestid-1]);
			session->nested[--session->nestid] = NULL;
			return 1;
		}
		close_nested(session, nlh);
		break;
	}
	case IPSET_CMD_TEST: {
		const struct ipset_type *type;
		/* Return codes are not aggregated, so tests cannot be either */

		/* Setname, type not checked/added yet */
		
		if (!ipset_data_test(data, IPSET_SETNAME))
			return ipset_err(session,
				"Invalid test command: missing setname");

		if (!ipset_data_test(data, IPSET_OPT_TYPE))
			return ipset_err(session,
				"Invalid test command: missing settype");
		
		type = ipset_data_get(data, IPSET_OPT_TYPE);
		D("family: %u, type family %u",
		  ipset_data_family(data), type->family);
		ADDATTR_SETNAME(session, nlh, data);
		open_nested(session, nlh, IPSET_ATTR_DATA);
		addattr_adt(session, nlh, data, ipset_data_family(data));
		close_nested(session, nlh);
		break;
	}
	default:
		return ipset_err(session, "Internal error: unknown command %u",
				 session->cmd);
	}
	return 0;
}

/**
 * ipset_commit - commit buffered commands
 * @session: session structure
 *
 * Commit buffered commands, if there are any.
 *
 * Returns 0 on success or a negative error code.
 */
int
ipset_commit(struct ipset_session *session)
{
	struct nlmsghdr *nlh;
	int ret = 0, i;

	assert(session);

	nlh = session->buffer;
	D("send buffer: len %u, cmd %s", nlh->nlmsg_len, cmd2name[session->cmd]);
	if (nlh->nlmsg_len == 0)
		/* Nothing to do */
		return 0;

	/* Close nested data blocks */
	for (i = session->nestid - 1; i >= 0; i--)
		close_nested(session, nlh);

	/* Send buffer */
	ret = session->transport->query(session->handle,
					session->buffer,
					session->bufsize);

	/* Reset saved data and nested state */
	session->saved_setname[0] = '\0';
	for (i = session->nestid - 1; i >= 0; i--)
		session->nested[i] = NULL;
	session->nestid = 0;
	nlh->nlmsg_len = 0;

	D("ret: %d", ret);

	if (ret < 0) {
		if (session->report[0] != '\0')
			return -1;
		else
			return ipset_err(session,
				 	 "Internal protocol error");
	}
	return 0;
}

static mnl_cb_t cb_ctl[] = {
	[NLMSG_NOOP] = callback_noop,
	[NLMSG_ERROR] = callback_error,
	[NLMSG_DONE]  = callback_done,
	[NLMSG_OVERRUN] = callback_noop,
	[NLMSG_MIN_TYPE] = callback_data,
};
	
static inline struct ipset_handle *
init_transport(struct ipset_session *session)
{
	session->handle = session->transport->init(cb_ctl, session);

	return session->handle;
}

/**
 * ipset_cmd - execute a command
 * @session: session structure
 * @cmd: command to execute
 * @lineno: command line number in restore mode
 *
 * Execute - or prepare/buffer in restore mode - a command.
 * It is the caller responsibility that the data field be filled out
 * with all required parameters for a successful execution.
 * The data field is cleared after this function call for the public
 * commands.
 *
 * Returns 0 on success or a negative error code.
 */
int
ipset_cmd(struct ipset_session *session, enum ipset_cmd cmd, uint32_t lineno)
{
	struct ipset_data *data;
	bool aggregate = false;
	int ret = -1;
	
	assert(session);

	if (cmd <= IPSET_CMD_NONE || cmd >= IPSET_MSG_MAX)
		return 0;

	/* Initialize transport method if not done yet */
	if (session->handle == NULL && init_transport(session) == NULL)
		return ipset_err(session,
				 "Cannot open session to kernel.");

	data = session->data;

	/* Check protocol version once */
	if (!session->version_checked) {
		if (build_send_private_msg(session, IPSET_CMD_PROTOCOL) < 0)
			return -1;
	}

	/* Private commands */
	if (cmd == IPSET_CMD_TYPE || cmd == IPSET_CMD_HEADER) 
		return build_send_private_msg(session, cmd);
	
	/* Check aggregatable commands */
	aggregate = may_aggregate_ad(session, cmd);
	if (!aggregate) {
		/* Flush possible aggregated commands */
		ret = ipset_commit(session);
		if (ret < 0)
			return ret;
	}

	/* Real command: update lineno too */
	session->cmd = cmd;
	session->lineno = lineno;
	
	/* Set default output mode */
	if (cmd == IPSET_CMD_LIST) {
		if (session->mode == IPSET_LIST_NONE)
			session->mode = IPSET_LIST_PLAIN;
	} else if (cmd == IPSET_CMD_SAVE) {
		if (session->mode == IPSET_LIST_NONE)
			session->mode = IPSET_LIST_SAVE;
	}

	D("next: build_msg");
	/* Build new message or append buffered commands */
	ret = build_msg(session, aggregate);
	D("build_msg returned %u", ret);
	if (ret > 0) {
		/* Buffer is full, send buffered commands */
		ret = ipset_commit(session);
		if (ret < 0)
			goto cleanup;
		ret = build_msg(session, false);
		D("build_msg 2 returned %u", ret);
	}
	if (ret < 0)
		goto cleanup;
	D("past: build_msg");

	/* We have to save the type for error handling */
	session->saved_type = ipset_data_get(data, IPSET_OPT_TYPE);
	if (session->lineno != 0
	    && (cmd == IPSET_CMD_ADD || cmd == IPSET_CMD_DEL)) {
		/* Save setname for the next possible aggregated restore line */
		strcpy(session->saved_setname, ipset_data_setname(data));
		ipset_data_reset(data);
		/* Don't commit: we may aggregate next command */
		ret = 0;
		goto cleanup;
	}

	D("call commit");	
	ret = ipset_commit(session);

cleanup:
	D("reset data");
	ipset_data_reset(data);
	return ret;
}

/**
 * ipset_session_init - initialize an ipset session
 *
 * Initialize an ipset session by allocating a session structure
 * and filling out with the initialization data.
 *
 * Returns the created session sctructure on success or NULL.
 */
struct ipset_session *
ipset_session_init(ipset_outfn outfn)
{
	struct ipset_session *session;
	size_t bufsize = getpagesize();

	/* Create session object */
	session = calloc(1, sizeof(struct ipset_session) + bufsize);
	if (session == NULL)
		return NULL;
	session->bufsize = bufsize;
	session->buffer = session + 1;

	/* The single transport method yet */
	session->transport = &ipset_mnl_transport;
	
	/* Output function */
	session->outfn = outfn;
	
	/* Initialize data structures */
	session->data = ipset_data_init();
	if (session->data == NULL)
		goto free_session;

	ipset_cache_init();
	return session;

free_session:
   	free(session);
   	return NULL;
}

/**
 * ipset_session_fini - destroy an ipset session
 * @session: session structure
 *
 * Destroy an ipset session: release the created structures.
 *
 * Returns 0 on success or a negative error code.
 */
int
ipset_session_fini(struct ipset_session *session)
{
	assert(session);

	if (session->handle)
		session->transport->fini(session->handle);
	if (session->data)
		ipset_data_fini(session->data);

	ipset_cache_fini();
	free(session);
	return 0;
}
