/*
 *	IPv6 Firewall
 *	Linux INET6 implementation
 *
 *	Authors:
 *	Pedro Roque		<roque@di.fc.ul.pt>	
 *
 *	$Id: ip6_fw.c,v 1.2 2005/06/16 13:46:46 cgoh Exp $
 *
 *	This program is free software; you can redistribute it and/or
 *      modify it under the terms of the GNU General Public License
 *      as published by the Free Software Foundation; either version
 *      2 of the License, or (at your option) any later version.
 */

#include <linux/config.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/socket.h>
#include <linux/sockios.h>
#include <linux/net.h>
#include <linux/route.h>
#include <linux/netdevice.h>
#include <linux/in6.h>
#include <linux/udp.h>
#include <linux/init.h>

#include <net/ipv6.h>
#include <net/ip6_route.h>
#include <net/ip6_fw.h>
#include <net/netlink.h>

static unsigned long ip6_fw_rule_cnt;
static struct ip6_fw_rule ip6_fw_rule_list = {
	{0},
	NULL, NULL,
	{0},
	IP6_FW_REJECT
};

static int ip6_fw_accept(struct dst_entry *dst, struct fl_acc_args *args);

struct flow_rule_ops ip6_fw_ops = {
	ip6_fw_accept
};


static struct rt6_info ip6_fw_null_entry = {
	{{NULL, 0, 0, NULL,
	  0, 0, 0, 0, 0, 0, 0, 0, -ENETUNREACH, NULL, NULL,
	  ip6_pkt_discard, ip6_pkt_discard, NULL}},
	NULL, {{{0}}}, 256, RTF_REJECT|RTF_NONEXTHOP, ~0UL,
	0, &ip6_fw_rule_list, {{{{0}}}, 128}, {{{{0}}}, 128}
};

static struct fib6_node ip6_fw_fib = {
	NULL, NULL, NULL, NULL,
	&ip6_fw_null_entry,
	0, RTN_ROOT|RTN_TL_ROOT, 0
};

static void ip6_rule_add(struct ip6_fw_rule *rl)
{
	struct ip6_fw_rule *next;

	start_bh_atomic();
	ip6_fw_rule_cnt++;
	next = &ip6_fw_rule_list;
	rl->next = next;
	rl->prev = next->prev;
	rl->prev->next = rl;
	next->prev = rl;
	end_bh_atomic();
}

static void ip6_rule_del(struct ip6_fw_rule *rl)
{
	struct ip6_fw_rule *next, *prev;

	start_bh_atomic();
	ip6_fw_rule_cnt--;
	next = rl->next;
	prev = rl->prev;
	next->prev = prev;
	prev->next = next;
	end_bh_atomic();
}

static __inline__ struct ip6_fw_rule * ip6_fwrule_alloc(void)
{
	struct ip6_fw_rule *rl;

	rl = kmalloc(sizeof(struct ip6_fw_rule), GFP_ATOMIC);
	if (rl)
	{
		memset(rl, 0, sizeof(struct ip6_fw_rule));
		rl->flowr.ops = &ip6_fw_ops;
	}
	return rl;
}

static __inline__ void ip6_fwrule_free(struct ip6_fw_rule * rl)
{
	kfree(rl);
}

static __inline__ int port_match(int rl_port, int fl_port)
{
	int res = 0;
	if (rl_port == 0 || (rl_port == fl_port))
		res = 1;
	return res;
}

static int ip6_fw_accept_trans(struct ip6_fw_rule *rl,
			       struct fl_acc_args *args)
{
	int res = FLOWR_NODECISION;
	int proto = 0;
	int sport = 0;
	int dport = 0;

	switch (args->type) {
	case FL_ARG_FORWARD:
	{
		struct sk_buff *skb = args->fl_u.skb;
		struct ipv6hdr *hdr = skb->nh.ipv6h;
		int len;

		len = skb->len - sizeof(struct ipv6hdr);

		proto = hdr->nexthdr;

		switch (proto) {
		case IPPROTO_TCP:
		{
			struct tcphdr *th;

			if (len < sizeof(struct tcphdr)) {
				res = FLOWR_ERROR;
				goto out;
			}
			th = (struct tcphdr *)(hdr + 1);
			sport = th->source;
			dport = th->dest;
			break;
		}
		case IPPROTO_UDP:
		{
			struct udphdr *uh;

			if (len < sizeof(struct udphdr)) {
				res = FLOWR_ERROR;
				goto out;
			}
			uh = (struct udphdr *)(hdr + 1);
			sport = uh->source;
			dport = uh->dest;
			break;
		}
		default:
			goto out;
		};
		break;
	}

	case FL_ARG_ORIGIN:
	{
		proto = args->fl_u.fl_o.flow->proto;

		if (proto == IPPROTO_ICMPV6) {
			goto out;
		} else {
			sport = args->fl_u.fl_o.flow->uli_u.ports.sport;
			dport = args->fl_u.fl_o.flow->uli_u.ports.dport;
		}
		break;
	}

	if (proto == rl->info.proto &&
	    port_match(args->fl_u.fl_o.flow->uli_u.ports.sport, sport) &&
	    port_match(args->fl_u.fl_o.flow->uli_u.ports.dport, dport)) {
		if (rl->policy & IP6_FW_REJECT)
			res = FLOWR_SELECT;
		else
			res = FLOWR_CLEAR;
	}

	default:
#if IP6_FW_DEBUG >= 1
		printk(KERN_DEBUG "ip6_fw_accept: unknown arg type\n");
#endif
		goto out;
	};

out:
	return res;
}

static int ip6_fw_accept(struct dst_entry *dst, struct fl_acc_args *args)
{
	struct rt6_info *rt;
	struct ip6_fw_rule *rl;
	int proto;
	int res = FLOWR_NODECISION;

	rt = (struct rt6_info *) dst;
	rl = (struct ip6_fw_rule *) rt->rt6i_flowr;

	proto = rl->info.proto;

	switch (proto) {
	case 0:
		if (rl->policy & IP6_FW_REJECT)
			res = FLOWR_SELECT;
		else
			res = FLOWR_CLEAR;
		break;
	case IPPROTO_TCP:
	case IPPROTO_UDP:
		res = ip6_fw_accept_trans(rl, args);
		break;
	case IPPROTO_ICMPV6:
	};

	return res;
}

static struct dst_entry * ip6_fw_dup(struct dst_entry *frule,
				     struct dst_entry *rt,
				     struct fl_acc_args *args)
{
	struct ip6_fw_rule *rl;
	struct rt6_info *nrt;
	struct rt6_info *frt;

	frt = (struct rt6_info *) frule;

	rl = (struct ip6_fw_rule *) frt->rt6i_flowr;

	nrt = ip6_rt_copy((struct rt6_info *) rt);

	if (nrt) {
		nrt->u.dst.input = frule->input;
		nrt->u.dst.output = frule->output;

		nrt->rt6i_flowr = flow_clone(frt->rt6i_flowr);

		nrt->rt6i_flags |= RTF_CACHE;
		nrt->rt6i_tstamp = jiffies;
	}

	return (struct dst_entry *) nrt;
}

int ip6_fw_reject(struct sk_buff *skb)
{
#if IP6_FW_DEBUG >= 1
	printk(KERN_DEBUG "packet rejected: \n");
#endif

	icmpv6_send(skb, ICMPV6_DEST_UNREACH, ICMPV6_ADM_PROHIBITED, 0,
		    skb->dev);
	/*
	 *	send it via netlink, as (rule, skb)
	 */

	kfree_skb(skb);
	return 0;
}

int ip6_fw_discard(struct sk_buff *skb)
{
	printk(KERN_DEBUG "ip6_fw: BUG fw_reject called\n");
	kfree_skb(skb);
	return 0;
}

int ip6_fw_msg_add(struct ip6_fw_msg *msg)
{
	struct in6_rtmsg rtmsg;
	struct ip6_fw_rule *rl;
	struct rt6_info *rt;
	int err;

	ipv6_addr_copy(&rtmsg.rtmsg_dst, &msg->dst);
	ipv6_addr_copy(&rtmsg.rtmsg_src, &msg->src);
	rtmsg.rtmsg_dst_len = msg->dst_len;
	rtmsg.rtmsg_src_len = msg->src_len;
	rtmsg.rtmsg_metric = IP6_RT_PRIO_FW;

	rl = ip6_fwrule_alloc();

	if (rl == NULL)
		return -ENOMEM;

	rl->policy = msg->policy;
	rl->info.proto = msg->proto;
	rl->info.uli_u.data = msg->u.data;

	rtmsg.rtmsg_flags = RTF_NONEXTHOP|RTF_POLICY;
	err = ip6_route_add(&rtmsg);

	if (err) {
		ip6_fwrule_free(rl);
		return err;
	}

	/* The rest will not work for now. --ABK (989725) */

#ifndef notdef
	ip6_fwrule_free(rl);
	return -EPERM;
#else
	rt->u.dst.error = -EPERM;

	if (msg->policy == IP6_FW_ACCEPT) {
		/*
		 *	Accept rules are never selected
		 *	(i.e. packets use normal forwarding)
		 */
		rt->u.dst.input = ip6_fw_discard;
		rt->u.dst.output = ip6_fw_discard;
	} else {
		rt->u.dst.input = ip6_fw_reject;
		rt->u.dst.output = ip6_fw_reject;
	}

	ip6_rule_add(rl);

	rt->rt6i_flowr = flow_clone((struct flow_rule *)rl);

	return 0;
#endif
}

static int ip6_fw_msgrcv(int unit, struct sk_buff *skb)
{
	int count = 0;

	while (skb->len) {
		struct ip6_fw_msg *msg;

		if (skb->len < sizeof(struct ip6_fw_msg)) {
			count = -EINVAL;
			break;
		}

		msg = (struct ip6_fw_msg *) skb->data;
		skb_pull(skb, sizeof(struct ip6_fw_msg));
		count += sizeof(struct ip6_fw_msg);

		switch (msg->action) {
		case IP6_FW_MSG_ADD:
			ip6_fw_msg_add(msg);
			break;
		case IP6_FW_MSG_DEL:
			break;
		default:
			return -EINVAL;
		};
	}

	return count;
}

static void ip6_fw_destroy(struct flow_rule *rl)
{
	ip6_fwrule_free((struct ip6_fw_rule *)rl);
}

#ifdef MODULE
#define ip6_fw_init module_init
#endif

__initfunc(void ip6_fw_init(void))
{
#ifdef CONFIG_NETLINK
	netlink_attach(NETLINK_IP6_FW, ip6_fw_msgrcv);
#endif
}

#ifdef MODULE
void module_cleanup(void)
{
#ifdef CONFIG_NETLINK
	netlink_detach(NETLINK_IP6_FW);
#endif
}
#endif
