/* Library which manipulates firewall rules.  Version 0.1. */

/* (C)1998 Paul ``Rusty'' Russell - Placed under the GNU GPL (See
   COPYING for details). */

#include <assert.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>

#include "include/libipfwc.h"

#define IP_VERSION	4
#define IP_OFFSET	0x1FFF

static int sockfd = -1;
static void *ipfwc_fn = NULL;

static int init = 0;

static int ipfwc_init()
{
	ipfwc_fn = ipfwc_init;
	init = 1;
	return ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) != -1);
}

static int
do_setsockopt(int cmd, const void *data, int length)
{
	return setsockopt(sockfd, IPPROTO_IP, cmd, (char *)data, length) != -1;
}

/* Insert the entry `fw' in chain `chain' into position `rulenum'. */
int ipfwc_insert_entry(const ip_chainlabel chain, 
                       const struct ip_fwuser *fw, 
                       unsigned int rulenum)
{
	struct ip_fwnew new = { rulenum, *fw, "" };
	memcpy(new.fwn_label, chain, sizeof(new.fwn_label));

	if (!init && !ipfwc_init()) return 0;

	ipfwc_fn = ipfwc_insert_entry;
	return do_setsockopt(IP_FW_INSERT, &new, sizeof(new));
}

/* Atomically replace rule `rulenum' in `chain' with `fw'. */
int ipfwc_replace_entry(const ip_chainlabel chain, 
                        const struct ip_fwuser *fw, 
                        unsigned int rulenum)
{
	struct ip_fwnew new = { rulenum, *fw, "" };
	memcpy(new.fwn_label, chain, sizeof(new.fwn_label));

	if (!init && !ipfwc_init()) return 0;

	ipfwc_fn = ipfwc_replace_entry;
	return do_setsockopt(IP_FW_REPLACE, &new, sizeof(new));
}

/* Append entry `fw' to chain `chain'.  Equivalent to insert with
   rulenum = length of chain. */
int ipfwc_append_entry(const ip_chainlabel chain, const struct ip_fwuser *fw)
{
	struct ip_fwchange new = { *fw, "" };
	memcpy(new.fwc_label, chain, sizeof(new.fwc_label));

	if (!init && !ipfwc_init()) return 0;

	ipfwc_fn = ipfwc_append_entry;
	return do_setsockopt(IP_FW_APPEND, &new, sizeof(new));	
}

/* Delete the first rule in `chain' which matches `fw'. */
int ipfwc_delete_entry(const ip_chainlabel chain, const struct ip_fwuser *fw)
{
	struct ip_fwchange del = { *fw, "" };
	memcpy(del.fwc_label, chain, sizeof(del.fwc_label));

	if (!init && !ipfwc_init()) return 0;

	ipfwc_fn = ipfwc_delete_entry;
	return do_setsockopt(IP_FW_DELETE, &del, sizeof(del));	
}

/* Delete the rule in position `rulenum' in `chain'. */
int ipfwc_delete_num_entry(const ip_chainlabel chain, unsigned int rulenum)
{
	struct ip_fwdelnum del = { rulenum, "" };
	memcpy(del.fwd_label, chain, sizeof(del.fwd_label));

	if (!init && !ipfwc_init()) return 0;

	ipfwc_fn = ipfwc_delete_num_entry;
	return do_setsockopt(IP_FW_DELETE_NUM, &del, sizeof(del));
}

static struct ip_fwtest *
fw_to_fwtest(const struct ip_fw *fw, const ip_chainlabel chain)
{
	static struct ip_fwtest ipfwt;
	struct iphdr *iph;
	struct tcphdr *tcph;
	struct udphdr *udph;
	struct icmphdr *icmph;

	strcpy(ipfwt.fwt_label, chain);

	iph = &ipfwt.fwt_packet.fwp_iph;

	iph->version = IP_VERSION;
	iph->ihl = sizeof(struct iphdr) / 4;
	iph->tot_len = sizeof(struct ip_fwpkt);
	iph->frag_off &= htons(~IP_OFFSET);
	iph->protocol = fw->fw_proto;

	iph->saddr = fw->fw_src.s_addr;
	iph->daddr = fw->fw_dst.s_addr;

	strncpy(ipfwt.fwt_packet.fwp_vianame, fw->fw_vianame, IFNAMSIZ);

	if (fw->fw_flg & IP_FW_F_FRAG)
		iph->frag_off |= htons(2); /* = 64 bytes - why not? */

	/* The tcp and udp headers are ignored for fragments, anyway */
	switch (iph->protocol) {
	case IPPROTO_TCP:
		tcph = &ipfwt.fwt_packet.fwp_protoh.fwp_tcph;
		tcph->source = htons(fw->fw_spts[0]);
		tcph->dest = htons(fw->fw_dpts[0]);
		tcph->syn = (fw->fw_flg & IP_FW_F_TCPSYN) ? 1 : 0;
		break;
	case IPPROTO_UDP:
		udph = &ipfwt.fwt_packet.fwp_protoh.fwp_udph;
		udph->source = htons(fw->fw_spts[0]);
		udph->dest = htons(fw->fw_dpts[0]);
		break;
	case IPPROTO_ICMP:
		icmph = &ipfwt.fwt_packet.fwp_protoh.fwp_icmph;
		icmph->type = fw->fw_spts[0];
		icmph->code = fw->fw_dpts[0];
		break;
	}

	return &ipfwt;
}

/* Check the packet `fw' on chain `chain'.  Returns the verdict, or
   NULL and sets errno. */
const char *ipfwc_check_packet(const ip_chainlabel chain, 
                               struct ip_fw *fw)
{
	int old_errno = errno;

	if (!init && !ipfwc_init()) return NULL;

	ipfwc_fn = ipfwc_check_packet;
	if (do_setsockopt(IP_FW_CHECK, fw_to_fwtest(fw, chain), 
			  sizeof(struct ip_fwtest)))
		return "accepted";
	else switch (errno) {
	case ECONNRESET:
		errno = old_errno;
		return "masqueraded";
	case ETIMEDOUT:
		errno = old_errno;
		return "denied";
	case ECONNREFUSED:
		errno = old_errno;
		return "rejected";
	case ECONNABORTED:
		errno = old_errno;
		return "redirected";
	case ELOOP:
		errno = old_errno;
		return "caught in loop";
	case ENFILE:
		errno = old_errno;
		return "passed through chain";

	default:
		return NULL;
	}
}

/* Flushes the entries in the given chain (ie. empties chain). */
int ipfwc_flush_entries(const ip_chainlabel chain)
{
	if (!init && !ipfwc_init()) return 0;

	ipfwc_fn = ipfwc_flush_entries;
	return do_setsockopt(IP_FW_FLUSH, chain, sizeof(ip_chainlabel));
}

/* Zeroes the counters in a chain. */
int ipfwc_zero_entries(const ip_chainlabel chain)
{
	if (!init && !ipfwc_init()) return 0;

	ipfwc_fn = ipfwc_zero_entries;
	return do_setsockopt(IP_FW_ZERO, chain, sizeof(ip_chainlabel));
}

/* Creates a new chain. */
int ipfwc_create_chain(const ip_chainlabel chain)
{
	if (!init && !ipfwc_init()) return 0;

	ipfwc_fn = ipfwc_create_chain;
	return do_setsockopt(IP_FW_CREATECHAIN, chain, sizeof(ip_chainlabel));
}

/* Deletes a chain. */
int ipfwc_delete_chain(const ip_chainlabel chain)
{
	if (!init && !ipfwc_init()) return 0;

	ipfwc_fn = ipfwc_delete_chain;
	return do_setsockopt(IP_FW_DELETECHAIN, chain, sizeof(ip_chainlabel));
}

/* Sets the policy on a built-in chain. */
int ipfwc_set_policy(const ip_chainlabel chain, const ip_chainlabel policy)
{
	struct ip_fwpolicy fwp;

	if (!init && !ipfwc_init()) return 0;

	ipfwc_fn = ipfwc_set_policy;
	memcpy(fwp.fwp_policy, policy, sizeof(fwp.fwp_policy));
	memcpy(fwp.fwp_label, chain, sizeof(fwp.fwp_label));
	return do_setsockopt(IP_FW_POLICY, &fwp, sizeof(fwp));
}

/* Gets the names of all the chains.  Returns single malloc()d region;
   and array of ip_chainlabels.  Fills in num_chains.  Returns NULL on
   error.  */
struct ipfwc_fwchain *ipfwc_get_chainnames(unsigned int *num_chains)
{
	int nread;
	static unsigned int maxnum = 4;
	static struct ipfwc_fwchain *chains = NULL;
	FILE *fp;
	__u32 pkthi, pktlo, bytehi, bytelo;

	ipfwc_fn = ipfwc_get_chainnames;
	if (!chains) {
		chains = malloc(sizeof(struct ipfwc_fwchain) * maxnum);

		if (!chains) {
			errno = ENOMEM;
			return NULL;
		}
	}

	/* Read names from IP_FW_PROC_CHAIN_NAMES */
	fp = fopen("/proc/net/"IP_FW_PROC_CHAIN_NAMES, "r");
	if (!fp) {
		/* Bad kernel version? */
		if (errno == ENOENT) errno = 0;
		return NULL;
	}

	*num_chains = 0;
	while ((nread = fscanf(fp,"%s %s %u %u %u %u %u", 
			       chains[*num_chains].label, 
			       chains[*num_chains].policy, 
			       &chains[*num_chains].refcnt, 
			       &pkthi, &pktlo, &bytehi, &bytelo)) == 7) {
		chains[*num_chains].packets = ((__u64)pkthi)<<32 | pktlo;
		chains[*num_chains].bytes = ((__u64)bytehi)<<32 | bytelo;
		(*num_chains)++;
		if (*num_chains >= maxnum) {
			maxnum *= 2;
			chains = realloc(chains, 
					 sizeof(struct ipfwc_fwchain)*maxnum);
			if (!chains) {
				fclose(fp);
				errno = ENOMEM;
				return NULL;
			}
		}
	}

	/* Bad kernel version? */
	if (nread != -1) {
		fclose(fp);
		errno = 0;
		return NULL;
	}

	return chains;
}	

static const struct ipfwc_fwchain *
find_chain(const ip_chainlabel label,
	   const struct ipfwc_fwchain *chains,
	   unsigned int num_chains)
{
	unsigned int i;

	for (i = 0; i < num_chains; i++) {
		if (strcmp(label, chains[i].label) == 0)
			return chains+i;
	}
	return NULL;
}

#define _IPFWC_FMT2(i) "%" #i "s "
#define _IPFWC_FMT(i) _IPFWC_FMT2(i)
#define IPFWC_CHAIN_FMT _IPFWC_FMT(IP_FW_MAX_LABEL_LENGTH)

/* Take a snapshot of the rules.  Returns internal pointer to array of
   ipfwc_fwrules.  Fills in num_rules.  Returns NULL on error. */
struct ipfwc_fwrule *ipfwc_get_rules(unsigned int *num_rules, int zero)
{
	FILE *fp;
	__u32 pkthi, pktlo, bytehi, bytelo;
	unsigned int num_chains;
	struct ipfwc_fwchain *chains = ipfwc_get_chainnames(&num_chains);
	static unsigned int maxnum = 4;
	static struct ipfwc_fwrule *rules = NULL;
	ip_chainlabel label;
	int nread;
	unsigned short tosand, tosxor;

	ipfwc_fn = ipfwc_get_rules;
	if (!rules) {
		rules = malloc(sizeof(struct ipfwc_fwrule) * maxnum);
		if (!rules) {
			errno = ENOMEM;
			return NULL;
		}
	}

	fp = fopen("/proc/net/"IP_FW_PROC_CHAINS, zero ? "r+" : "r");
	if (!fp) {
		if (errno == ENOENT) errno = 0;
		return NULL;
	}

	*num_rules = 0;
	while ((nread = fscanf(fp,
			       IPFWC_CHAIN_FMT		/* Chain name */
			       "%X/%X->%X/%X "		/* IPs */
			       "%s "			/* Interface */
			       "%hX %hX "		/* flg & invflg */
			       "%hu "			/* Protocol */
			       "%u %u %u %u "		/* Counters */
			       "%hu-%hu %hu-%hu "	/* Ports */
			       "A%hX X%hX "		/* TOS masks */
			       "%hX "			/* fw_redir */
			       "%u "			/* fw_mark */
			       "%hu "			/* output size */
			       "%s",			/* Target */
			       label,
			       &rules[*num_rules].ipfw.ipfw.fw_src.s_addr,
			       &rules[*num_rules].ipfw.ipfw.fw_smsk.s_addr,
			       &rules[*num_rules].ipfw.ipfw.fw_dst.s_addr,
			       &rules[*num_rules].ipfw.ipfw.fw_dmsk.s_addr,
			       rules[*num_rules].ipfw.ipfw.fw_vianame,
			       &rules[*num_rules].ipfw.ipfw.fw_flg, 
			       &rules[*num_rules].ipfw.ipfw.fw_invflg,
			       &rules[*num_rules].ipfw.ipfw.fw_proto,
			       &pkthi, &pktlo, &bytehi, &bytelo,
			       &rules[*num_rules].ipfw.ipfw.fw_spts[0], 
			       &rules[*num_rules].ipfw.ipfw.fw_spts[1],
			       &rules[*num_rules].ipfw.ipfw.fw_dpts[0], 
			       &rules[*num_rules].ipfw.ipfw.fw_dpts[1],
			       &tosand, &tosxor,
			       &rules[*num_rules].ipfw.ipfw.fw_redirpt,
			       &rules[*num_rules].ipfw.ipfw.fw_mark,
			       &rules[*num_rules].ipfw.ipfw.fw_outputsize,
			       rules[*num_rules].ipfw.label)) == 23) {
		if (strcmp("-", rules[*num_rules].ipfw.ipfw.fw_vianame) == 0)
			(rules[*num_rules].ipfw.ipfw.fw_vianame)[0] = '\0';
		rules[*num_rules].ipfw.ipfw.fw_tosand = (unsigned char)tosand;
		rules[*num_rules].ipfw.ipfw.fw_tosxor = (unsigned char)tosxor;

		/* we always keep these addresses in network byte order */
		rules[*num_rules].ipfw.ipfw.fw_src.s_addr
			= htonl(rules[*num_rules].ipfw.ipfw.fw_src.s_addr);
		rules[*num_rules].ipfw.ipfw.fw_smsk.s_addr
			= htonl(rules[*num_rules].ipfw.ipfw.fw_smsk.s_addr);
		rules[*num_rules].ipfw.ipfw.fw_dst.s_addr
			= htonl(rules[*num_rules].ipfw.ipfw.fw_dst.s_addr);
		rules[*num_rules].ipfw.ipfw.fw_dmsk.s_addr
			= htonl(rules[*num_rules].ipfw.ipfw.fw_dmsk.s_addr);

		rules[*num_rules].packets = ((__u64)pkthi)<<32 | pktlo;
		rules[*num_rules].bytes = ((__u64)bytehi)<<32 | bytelo;

		rules[*num_rules].chain
			= find_chain(label, chains, num_chains);

		(*num_rules)++;
		if (*num_rules >= maxnum) {
			maxnum *= 2;
			rules = realloc(rules, 
					sizeof(struct ipfwc_fwrule)*maxnum);
			if (!rules) {
				fclose(fp);
				errno = ENOMEM;
				return NULL;
			}
		}
	}

	/* Bad kernel version? */
	if (nread != -1) {
		fclose(fp);
		errno = 0;
		return NULL;
	}

	return rules;
}

/* Translates errno numbers into more human-readable form than strerror.
 * `ipfwc_fn' is a pointer to the function which returned the error. */
const char *ipfwc_strerror(int err)
{
	unsigned int i;
	struct table_struct {
		void *fn;
		int err;
		const char *message;
	} table [] = 
	  { { NULL, ENOENT, "No chain by that name" },
	    { NULL, 0, "Incompatible with this kernel" },
	    { ipfwc_init, EPERM, "Permission denied (you must be root)" },
	    { ipfwc_delete_chain, ENOTEMPTY, "Chain is not empty" },
	    { ipfwc_create_chain, EEXIST, "Chain already exists" },
	    /* EINVAL for CHECK probably means bad interface. */
	    { ipfwc_check_packet, EINVAL, 
	      "bad arguments (does that interface exist?)" },
	    /* EINVAL for DELETE probably means no matching rule */
	    { ipfwc_delete_entry, EINVAL,
	      "bad rule (does a matching rule exist in that chain?)" },
	    { ipfwc_insert_entry, EINVAL,
	      "bad rule (does a matching rule exist in that chain?)" },
	    { ipfwc_delete_num_entry, EINVAL,
	      "bad rule (does a matching rule exist in that chain?)" },
	    { ipfwc_replace_entry, EINVAL,
	      "bad rule (does a matching rule exist in that chain?)" }
	  };

	for (i = 0; i < sizeof(table)/sizeof(struct table_struct); i++) {
		if ((!table[i].fn || table[i].fn == ipfwc_fn)
		    && table[i].err == err)
			return table[i].message;
	}
	return strerror(err);
}

