/*
 *      Home-agent functionality
 *
 *      Authors:
 *      Sami Kivisaari           <skivisaa@cc.hut.fi>
 *      Henrik Petander          <lpetande@cc.hut.fi>
 *
 *      $Id: ha.c,v 1.21 2001/06/22 08:18:15 antti 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.
 *     
 *      TODO: Tunneling of router advertisements
 *      TODO: proxy neighbor discovery for all addresses on link
 */

#include <linux/autoconf.h>
#include <linux/net.h>
#include <linux/skbuff.h>
#include <linux/if_ether.h>
#include <linux/netdevice.h>
#include <linux/in6.h>
#include <linux/init.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv6.h>
#ifdef CONFIG_SYSCTL
#include <linux/sysctl.h>
#endif

#include <net/ipv6.h>
#include <net/ip6_fib.h>
#include <net/ip6_route.h>
#include <net/ndisc.h>
#include <net/addrconf.h>
#include <net/neighbour.h>

#include "bcache.h"
#include "tunnel.h"
#include "stats.h"
#include "ha.h"
#include "bcache.h"
#include "debug.h"
#include "access.h"
#include "dhaad.h"

static int mipv6_ha_tunnel_sitelocal = 1;

#ifdef CONFIG_SYSCTL
#include "sysctl.h"

static struct ctl_table_header *mipv6_ha_sysctl_header;

static struct mipv6_ha_sysctl_table
{
	struct ctl_table_header *sysctl_header;
	ctl_table mipv6_vars[3];
	ctl_table mipv6_mobility_table[2];
	ctl_table mipv6_proto_table[2];
	ctl_table mipv6_root_table[2];
} mipv6_ha_sysctl = {
	NULL,

        {{NET_IPV6_MOBILITY_TUNNEL_SITELOCAL, "tunnel_sitelocal",
	  &mipv6_ha_tunnel_sitelocal, sizeof(int), 0644, NULL, 
	  &proc_dointvec},
	 {0}},

	{{NET_IPV6_MOBILITY, "mobility", NULL, 0, 0555, 
	  mipv6_ha_sysctl.mipv6_vars}, {0}},
	{{NET_IPV6, "ipv6", NULL, 0, 0555, 
	  mipv6_ha_sysctl.mipv6_mobility_table}, {0}},
	{{CTL_NET, "net", NULL, 0, 0555, 
	  mipv6_ha_sysctl.mipv6_proto_table}, {0}}
};

#endif /* CONFIG_SYSCTL */

static int ha_nsol_rcv(
	struct sk_buff *skb, 
	struct in6_addr *saddr, 
	struct in6_addr *dest, 
	int dest_type);

/*  this should be in some header file but it isn't  */
extern void ndisc_send_na(
	struct net_device *dev, struct neighbour *neigh,
	struct in6_addr *daddr, struct in6_addr *solicited_addr,
	int router, int solicited, int override, int inc_opt);

/*  this is defined in kernel IPv6 module (sockglue.c)  */
extern struct packet_type ipv6_packet_type;

/*  address for this home agent  */
struct in6_addr home_agent_address;

int mipv6_lifetime_check(int lifetime)
{
	return (lifetime > MAX_LIFETIME) ? MAX_LIFETIME : lifetime;
}

int mipv6_proxy_nd_rem(
	struct in6_addr *home_address,
	int prefix_length,
	int router)
{
        /* When MN returns home HA leaves the solicited mcast groups
         * for MNs home addresses 
	 */
	struct in6_addr mcast;
	struct rt6_info *rt;
	struct net_device *dev;

	DEBUG_FUNC();

        /* The device is looked up from the routing table */
	rt = rt6_lookup(home_address, NULL, 0, 0);

        if(rt == NULL) {
                DEBUG((DBG_ERROR, "rt6_lookup failed for proxy nd remove"));
                return -1;
	}

	dev = rt->u.dst.dev;

        if(dev == NULL) {
		DEBUG((DBG_ERROR, "couldn't get dev"));
		if (rt)
			dst_release(&rt->u.dst);
		return -1;
	}


	addrconf_addr_solict_mult(home_address, &mcast);
	ipv6_dev_mc_dec(dev, &mcast);
	if (rt)
		dst_release(&rt->u.dst);

	return 0;
}


int mipv6_proxy_nd(
	struct in6_addr *home_address, 
	int prefix_length,
	int router)
{  
	/* The HA sends a proxy ndisc_na message to all hosts on MN's
	 * home subnet by sending a neighbor advertisement with the
	 * home address or all addresses of the mobile node if the
	 * prefix is not 0. The addresses are formed by combining the
	 * suffix or the host part of the address with each subnet
	 * prefix that exists in the home subnet 
	 */

        /* Since no previous entry for MN exists a proxy_nd advertisement
	 * is sent to all nodes link local multicast address
	 */	
	struct in6_addr mcdest, maddr;
	struct rt6_info *rt = NULL;
	int inc_opt = 1;
	int solicited = 0;
	int override = 1;
	struct net_device *dev;

	DEBUG_FUNC();

	ipv6_addr_all_nodes(&mcdest);  

	/* the correct device is looked up from the routing table 
	 * by the address of the MN, the source address is still hardcoded. 
	 */

	rt = rt6_lookup(home_address, NULL, 0, 0);

        if(rt == NULL) {
                DEBUG((DBG_ERROR, "rt6_lookup failed for mipv6_proxy_nd()"));
                return -1;
	}
		
	DEBUG((DBG_INFO, "Advertising address : %x:%x:%x:%x:%x:%x:%x:%x",
	       NIPV6ADDR(home_address)));

	dev = rt->u.dst.dev;
	if(dev != NULL) {
		DEBUG((DBG_INFO, "Sending proxy nd to device: %s"
		       " and to address: %x:%x:%x:%x:%x:%x:%x:%x", 
		       dev->name, NIPV6ADDR(&mcdest)));

		/* Proxy neighbor advertisement of MN's home address 
		 * to all nodes solicited multicast address 
		 */
		ndisc_send_na(dev, NULL, &mcdest, home_address, router, 
			      solicited, override, inc_opt);

		/* Join solicited mcast group for MN's home adress */
		addrconf_addr_solict_mult(home_address, &maddr);
		ipv6_dev_mc_inc(dev, &maddr);
		if (rt)
			dst_release(&rt->u.dst);
		return 0;
	} else {
		DEBUG((DBG_ERROR, "dev = NULL"));
		if (rt)
			dst_release(&rt->u.dst);
		return -1;
	}
}

/*
 * get pointer to protocol payload of ipv6 packet and the
 * protocol code
 */
static __u8 * get_protocol_data(struct sk_buff *skb, __u8 *nexthdr)
{
	int offset = (__u8 *)(skb->nh.ipv6h + 1) - skb->data;
	int len = skb->len - offset;

	*nexthdr = skb->nh.ipv6h->nexthdr;

	offset = ipv6_skip_exthdr(skb, offset, nexthdr, len);

	return skb->data + offset;
}

extern int mipv6_ra_rcv_ptr(struct sk_buff *skb, struct icmp6hdr *msg);

static unsigned int mipv6_intercept(
        unsigned int hooknum,
	struct sk_buff **p_skb,
	const struct net_device *in,
	const struct net_device *out,
	int (*okfn)(struct sk_buff *))
{
	int dest_type;	
	struct sk_buff *skb = (p_skb) ? *p_skb : NULL;
	struct sk_buff *buff;
	struct in6_addr *daddr, *saddr;
	struct mipv6_bcache_entry bc_entry;
	
	if(skb == NULL) return NF_ACCEPT;

	daddr = &skb->nh.ipv6h->daddr;
	saddr = &skb->nh.ipv6h->saddr;

	DEBUG((DBG_DATADUMP, "mipv6 intercept\n (source): %x:%x:%x:%x:%x:%x:%x:%x", 
	       NIPV6ADDR(saddr)));
	DEBUG((DBG_DATADUMP, " (destination): %x:%x:%x:%x:%x:%x:%x:%x",
		NIPV6ADDR(daddr)));

	/* check whether packet is destined to a mobile node's home
	 * address check also if the address is a directed multicast
	 * address and whether the packet could be destined to a
	 * mobile node. ICMP packets are checked to ensure that all
	 * neighbor solicitations to MNs home addresses are handled by
	 * the HA.  
	 */

	dest_type = ipv6_addr_type(daddr);

	DEBUG((DBG_DATADUMP, "binding exists=%d, of type=%d",
	       mipv6_bcache_exists(daddr), dest_type));
	
	if(mipv6_bcache_get(daddr, &bc_entry) >= 0 ||
	   (dest_type & IPV6_ADDR_MULTICAST )) {
		__u8 nexthdr;
		struct icmp6hdr *msg =
			(struct icmp6hdr *)get_protocol_data(skb, &nexthdr);

		if(nexthdr == IPPROTO_ICMPV6) {			
			switch(msg->icmp6_type) {
			case NDISC_NEIGHBOUR_SOLICITATION:
				if(ha_nsol_rcv(skb, saddr, daddr, dest_type)
				   == 0)
					return NF_DROP;
				else
					return NF_ACCEPT;
			case NDISC_ROUTER_ADVERTISEMENT:
				mipv6_ra_rcv_ptr(skb, msg);
			case NDISC_NEIGHBOUR_ADVERTISEMENT:
			case NDISC_ROUTER_SOLICITATION:
			case NDISC_REDIRECT:
				return NF_ACCEPT;
			default:
				/* Continue, we want to tunnel 
				 * other icmp messages  
				 */
			}
		}
		/* We don't tunnel datagrams to link local addresses
		 * nor site local if tunnel_sitelocal is not set.
		 * TODO[draft]: RAs should be tunneled in certain
		 * cases (see DRAFT9.7) */
		if (!(dest_type & IPV6_ADDR_LINKLOCAL) &&
		    (!(dest_type & IPV6_ADDR_SITELOCAL) 
		     || mipv6_ha_tunnel_sitelocal)) {
			int err;
			struct in6_addr ha_addr;
			struct rt6_info *rt = 	rt6_lookup(daddr, NULL, 0, 0);
			/* ipv6_get_saddr checks the route */
			if((err = ipv6_get_saddr((struct dst_entry *) rt, daddr, &ha_addr)) != 0) {
				DEBUG((DBG_ERROR, "Could not get home_agent address"));				
			}
			if (rt)
				dst_release(&rt->u.dst);

			if (dest_type & IPV6_ADDR_MULTICAST) {
				/* clone, encapsulate and pass */
				buff = skb_clone(skb, GFP_ATOMIC);
				if (buff) {
					mipv6_encapsulate_ipv6(
						buff, &ha_addr, &bc_entry.coa);
					MIPV6_INC_STATS(n_encapsulations);  
				}
				return NF_ACCEPT; 
			}
			mipv6_encapsulate_ipv6(skb, &ha_addr, &bc_entry.coa);

			MIPV6_INC_STATS(n_encapsulations);

			return NF_DROP;
		}
	}
	/* otherwise continue normally */	
	return NF_ACCEPT;

}

/*
 * Add Mobile IPv6 Home-Agents anycast (RFC2526) address to interface 
 */
static int ha_set_anycast_addr(char *name, struct in6_addr *pfix, int plen)
{
	struct net_device *dev;
	int ifindex;

	dev = dev_get_by_name(name);
	if (dev == NULL) {
		DEBUG((DBG_WARNING, "interface %s not found", name));
		return -1;
	}

	ifindex = dev->ifindex;
	dev_put(dev);
	dev = NULL;

	return mipv6_ha_set_anycast_addr(ifindex, pfix, plen);
}

static int ha_nsol_rcv(
	struct sk_buff *skb, 
	struct in6_addr *saddr, 
	struct in6_addr *daddr, 
	int dest_type)
{
	__u8 nexthdr;
	struct nd_msg *msg;
	struct neighbour *neigh;
	struct net_device *dev = skb->dev;
	struct mipv6_bcache_entry bc_entry;
	int inc, s_type = ipv6_addr_type(saddr);

	msg = (struct nd_msg *) get_protocol_data(skb, &nexthdr);
	DEBUG((DBG_INFO, "Neighbor solicitation received, to "
	       "%x:%x:%x:%x:%x:%x:%x:%x of type %d",
	       NIPV6ADDR(daddr), (dest_type & IPV6_ADDR_MULTICAST)));
	if (!msg)
	  return -1;

	if (msg->opt.opt_len == 0) return 1;
	if (msg->opt.opt_type == ND_OPT_SOURCE_LL_ADDR) {
		neigh = __neigh_lookup(&nd_tbl, saddr, skb->dev, 1);
		if (neigh) {
			neigh_update(neigh, msg->opt.link_addr, NUD_VALID, 1, 1);
			neigh_release(neigh);
			DEBUG((DBG_INFO, "Got SOURCE_LL_ADDR. nd_tbl updated."));
		}
	}
	else
		DEBUG((DBG_ERROR, " Unexpected option in neighbour solicitation: %x", msg->opt.opt_type));

	if (s_type & IPV6_ADDR_UNICAST) {
		neigh = ndisc_get_neigh(dev, saddr);
	        inc = dest_type & IPV6_ADDR_MULTICAST;

                /* TODO: Obviously, a check should be done right about here
		 * whether the address is an on-link address for mobile node
		 */ 
		if (inc) {
			DEBUG((DBG_INFO, "nsol was multicast to %x:%x:%x:%x:%x:%x:%x:%x",
			       NIPV6ADDR(&msg->target)));

			if (mipv6_bcache_get(&msg->target, &bc_entry) >= 0) {
				ndisc_send_na(dev, neigh, saddr,
					      &msg->target, bc_entry.router,
					      1, 1, inc);
				return 0;
			} else {
				DEBUG((DBG_INFO,
					"Multicast neighbor unknown"));
				return 1;
			}
		}
		else if (mipv6_bcache_get(daddr, &bc_entry) >= 0) {
			DEBUG((DBG_INFO, 
			       "unicast neighbour lookup succeeded"));
			ndisc_send_na(skb->dev, NULL, saddr, daddr,
				      bc_entry.router, 1, 1, 1);
			return 0;
		}
		
		DEBUG((DBG_INFO,"Unicast neighbor unknown"));
		return 1;
	}
		
	if (s_type & IPV6_ADDR_ANY) {
		DEBUG((DBG_INFO, 
		       "neighbor solicitation for MN with AC source"));
		ipv6_addr_all_nodes(saddr);
		if (mipv6_bcache_get(daddr, &bc_entry) >= 0)
			ndisc_send_na(dev, NULL, saddr, daddr,
				      bc_entry.router, 1, 0, 1);
		return 0;
	}
		
	return 1;
}

#define HUGE_NEGATIVE (~((~(unsigned int)0) & ((~(unsigned int)0)>>1)))

/*
 * Netfilter hook for packet interception
 */
struct nf_hook_ops intercept_hook_ops = {
        {NULL, NULL},     /* List head, no predecessor, no successor */
        mipv6_intercept,
        PF_INET6,
        NF_IP6_PRE_ROUTING,
        HUGE_NEGATIVE
};

int __init mipv6_initialize_ha(void)
{
	DEBUG_FUNC();

#ifdef CONFIG_SYSCTL
	mipv6_ha_sysctl_header = 
		register_sysctl_table(mipv6_ha_sysctl.mipv6_root_table, 0);
#endif
	mipv6_mobile_node_acl = mipv6_initialize_access();

	/*  register packet interception hook  */
	nf_register_hook(&intercept_hook_ops);

	return 0;
}

void __exit mipv6_shutdown_ha(void)
{
	DEBUG_FUNC();

#ifdef CONFIG_SYSCTL
	unregister_sysctl_table(mipv6_ha_sysctl_header);
#endif

	/*  remove packet interception hook  */
	nf_unregister_hook(&intercept_hook_ops);

	mipv6_destroy_access(mipv6_mobile_node_acl);
}

