/*
 *      Mobile IPv6 main module
 *
 *      Authors:
 *      Sami Kivisaari          <skivisaa@cc.hut.fi>
 *      Antti Tuominen          <ajtuomin@tml.hut.fi>
 *
 *      $Id: mipv6.c,v 1.46 2001/07/23 06:44:56 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.
 *
 */

#include <linux/autoconf.h>
#include <linux/module.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv6.h>
#include <linux/ipsec.h>
#include <net/mipglue.h>
#include <net/addrconf.h>
#include <net/ip6_route.h>
#include <net/checksum.h>

#ifdef CONFIG_SYSCTL
#include <linux/sysctl.h>
#endif /* CONFIG_SYSCTL */
int mipv6_is_ha = 0;
int mipv6_is_mn = 0;
int mipv6_debug = 1;

extern int home_preferred;

#if defined(MODULE) && LINUX_VERSION_CODE > 0x20115
MODULE_AUTHOR("MIPL Team");
MODULE_DESCRIPTION("Mobile IPv6");
MODULE_PARM(mipv6_is_ha, "i");
MODULE_PARM(mipv6_is_mn, "i");
MODULE_PARM(mipv6_debug, "i");
#endif

#include "mipv6.h"
#include "tunnel.h"
#include "ha.h"
#include "mn.h"
#include "mdetect.h"
#include "procrcv.h"
#include "stats.h"
#include "sendopts.h"
#include "bul.h"
#include "debug.h"
#include "bcache.h"
#include "dstopts.h"
#include "ah.h"
#include "halist.h"
#include "dhaad.h"
#ifdef CONFIG_SYSCTL
#include "sysctl.h"
#endif /* CONFIG_SYSCTL */
#include "mipv6_ioctl.h"

#define MIPV6_BUL_SIZE 128
#define MIPV6_BCACHE_SIZE 128
#define MIPV6_HALIST_SIZE 128

#define SUCC 1
#define TRUE 1
#define FALSE 0

/*
 * Perform prefix comparison bitwise
 */
int mipv6_prefix_compare(struct in6_addr *addr,
			 struct in6_addr *prefix, unsigned int nprefix)
{
	int i;

	if (nprefix > 128)
		return FALSE;

	for (i = 0; nprefix > 0; nprefix -= 32, i++) {
		if (nprefix >= 32) {
			if (addr->s6_addr32[i] != prefix->s6_addr32[i])
				return FALSE;
		} else {
			if (((addr->s6_addr32[i] ^ prefix->s6_addr32[i]) &
			     ((~0) << (32 - nprefix))) != 0)
				return FALSE;
			return TRUE;
		}
	}

	return TRUE;
}

/*
 * Perform suffix comparison bitwise
 */
int mipv6_suffix_compare(struct in6_addr *addr,
			 struct in6_addr *suffix, unsigned int nsuffix)
{
	int i;

	if (nsuffix > 128)
		return FALSE;

	for (i = 3; nsuffix > 0; nsuffix -= 32, i--) {
		if (nsuffix >= 32) {
			if (addr->s6_addr32[i] != suffix->s6_addr32[i])
				return FALSE;
		} else {
			if (((addr->s6_addr32[i] ^ suffix->s6_addr32[i]) &
			     ((~0) << nsuffix)) != 0)
				return FALSE;
			return TRUE;
		}
	}

	return TRUE;
}

/* could make mods atomic, don't know if its worth it */
void mipv6_finalize_modify_xmit(void **alloclist)
{
	int i;

	DEBUG_FUNC();
	for (i = 0; alloclist[i] != NULL; i++)
		kfree(alloclist[i]);
}


#ifndef NO_AH
int mipv6_create_ah(struct ipv6_txoptions *opt)
{

	struct mipv6_ah *ah = kmalloc(sizeof(struct mipv6_ah), GFP_ATOMIC);
	if (ah == NULL) {
		DEBUG((DBG_CRITICAL, "Couldn't allocate memory for AH"));
		return -ENOMEM;
	}
	DEBUG_FUNC();

	/* fill in the AH */
	ah->ah_hl = sizeof(struct mipv6_ah) / 4 - 2;
	DEBUG((DBG_DATADUMP, "ah length: %d", ah->ah_hl));
	ah->ah_rv = 0;
	ah->ah_spi = 0;
	ah->ah_rpl = 0;
	memset(ah->ah_data, 0, AHHMAC_HASHLEN);
	opt->auth = (struct ipv6_opt_hdr *) ah;
	return 0;
}
#endif

struct ipv6_txoptions *mipv6_modify_xmit_packets(struct sock *sk,
						 struct sk_buff *skb,
						 struct ipv6_txoptions
						 *opts, struct flowi *fl,
						 struct dst_entry **dst,
						 void *allocptrs[])
{
	struct ipv6_rt_hdr *oldrthdr = NULL, *newrthdr = NULL;
	struct ipv6_opt_hdr *newdo0hdr = NULL, *olddo0hdr = NULL,
		*newdo1hdr = NULL, *olddo1hdr = NULL;
	struct ipv6_txoptions *newopts = NULL;
	struct mipv6_bcache_entry bc_entry;
	struct in6_addr tmpaddr, *saddr, *daddr;
	int nalloc = 0, changed_rt_hdr = FALSE, changed_dstopts0_hdr =
		FALSE, changed_dstopts1_hdr = FALSE, add_ha = 0;

	DEBUG_FUNC();

	/*
	 * we have to be prepared to the fact that saddr might not be present,
	 * if that is the case, we acquire saddr just as kernel does.
	 */
	saddr = fl ? fl->fl6_src : NULL;
	daddr = fl ? fl->fl6_dst : NULL;

	if (daddr == NULL)
		return opts;
	if (saddr == NULL) {
		/*
		 * TODO!: we might end up having wrong saddr here.
		 * Kernel does this bit differently.
		 */
		int err = ipv6_get_saddr(NULL, daddr, &tmpaddr);
		if (err)
			return opts;
		else
			saddr = &tmpaddr;
	}

	DEBUG((DBG_DATADUMP, " modify_xmit: dest. address of packet: %x:%x:%x:%x:%x:%x:%x:%x", NIPV6ADDR(daddr)));
 	DEBUG((DBG_DATADUMP, "and src. address: %x:%x:%x:%x:%x:%x:%x:%x", NIPV6ADDR(saddr)));

	/* Check old routing header and destination options headers */
	oldrthdr = (opts != NULL) ? opts->srcrt : NULL;
	olddo0hdr = (opts != NULL) ? opts->dst0opt : NULL;
	olddo1hdr = (opts != NULL) ? opts->dst1opt : NULL;
	if (mipv6_bcache_get(daddr, &bc_entry) == 0) {
		DEBUG((DBG_INFO, "Binding exists. Adding routing header"));

		/*
		 * Append care-of-address to routing header (original
		 * destination address is home address, the first
		 * source route segment gets put to the destination
		 * address and the home address gets to the last
		 * segment of source route (just as it should)) 
		 */
		newrthdr = mipv6_append_rt_header(oldrthdr, &bc_entry.coa);

		/*
		 * reroute output (we have to do this in case of TCP
                 * segment) 
		 */
		if (dst) {
			struct in6_addr *tmp = fl->fl6_dst;
			fl->fl6_dst = &bc_entry.coa;

			dst_release(*dst);
			*dst = ip6_route_output(sk, fl);
			if (skb)
				skb->dst = *dst;
			fl->fl6_dst = tmp;

			DEBUG((DBG_INFO, "Rerouted outgoing packet"));
		}

		changed_rt_hdr = (newrthdr != NULL)
			&& (newrthdr != oldrthdr);
		if (changed_rt_hdr)
			allocptrs[nalloc++] = newrthdr;
		else {
			DEBUG((DBG_ERROR,
			       "Could not add routing header (hdr=0x%p)",
			       newrthdr));
			/* TODO: Should drop the packet */
		}
	}

	/* Add ha_option if  */
	add_ha = mipv6_is_mn && mipv6_mn_subnet_foreign() && mipv6_mn_is_home_addr(saddr);
	newdo1hdr = mipv6_add_dst1opts(saddr, daddr, olddo1hdr, &add_ha);

	/* Only home address option is inserted to first dst opt header */
	if (add_ha)
		newdo0hdr = mipv6_add_dst0opts(saddr, olddo0hdr, add_ha);

	changed_dstopts0_hdr = ((newdo0hdr != NULL)
				&& (newdo0hdr != olddo0hdr));
	changed_dstopts1_hdr = ((newdo1hdr != NULL)
				&& (newdo1hdr != olddo1hdr));

	if (changed_dstopts0_hdr)
		allocptrs[nalloc++] = newdo0hdr;

	if (changed_dstopts1_hdr)
		allocptrs[nalloc++] = newdo1hdr;
	else {
		DEBUG((DBG_DATADUMP,
		       "Destination options not piggybacked. (ohdr=0x%p, nhdr=0x%p)",
		       olddo1hdr, newdo1hdr));
	}

	/*
	 * If any headers were changed, reallocate new ipv6_txoptions and
	 * update it to match new headers
	 */
	if (changed_rt_hdr || changed_dstopts0_hdr || changed_dstopts1_hdr) {
		DEBUG((DBG_INFO, "Rebuilding ipv6_txoptions"));

		newopts = (struct ipv6_txoptions *)
			kmalloc(sizeof(struct ipv6_txoptions), GFP_ATOMIC);

		allocptrs[nalloc++] = newopts;

		if (newopts == NULL) {
			DEBUG((DBG_ERROR,
			       "Error: failed to allocate ipv6_txoptions!"));
			return opts;
		}

		if (opts == NULL) {
			memset(newopts, 0, sizeof(struct ipv6_txoptions));
			newopts->tot_len = sizeof(struct ipv6_txoptions);
		} else
			memcpy(newopts, opts,
			       sizeof(struct ipv6_txoptions));

		if (changed_rt_hdr) {
			newopts->srcrt = newrthdr;
			newopts->opt_nflen += ipv6_optlen(newopts->srcrt) -
				((oldrthdr != NULL) ? 
				 ipv6_optlen(oldrthdr) : 0);
		}
		if (changed_dstopts0_hdr) {
			newopts->dst0opt = newdo0hdr;
			newopts->opt_nflen += ipv6_optlen(newopts->dst0opt) -
				((olddo0hdr != NULL) ? 
				 ipv6_optlen(olddo0hdr) : 0);
		}
		if (changed_dstopts1_hdr) {
			newopts->dst1opt = newdo1hdr;
			newopts->opt_flen += ipv6_optlen(newopts->dst1opt) -
				((olddo1hdr != NULL) ? 
				 ipv6_optlen(olddo0hdr) : 0);
			/* TODO: Add ah only when sending a ba or bu, 
			 *  now it is added also for BR 
			 */
#ifndef NO_AH
			if (mipv6_create_ah(newopts) >= 0) {
			 	allocptrs[nalloc++] = newopts->auth;  
			 	newopts->opt_flen += sizeof(struct mipv6_ah);
 			}
#endif
	
		}


		opts = newopts;
	}

	allocptrs[nalloc] = NULL;

	return opts;
}


/*
 * Required because we can only modify addresses after the packet is
 * constructed.  We otherwise mess with higher level protocol
 * pseudoheaders. With strict protocol layering life would be SO much
 * easier!  
 */
static unsigned int modify_xmit_addrs(unsigned int hooknum,
				      struct sk_buff **skb,
				      const struct net_device *in,
				      const struct net_device *out,
				      int (*okfn) (struct sk_buff *))
{
	struct ipv6hdr *hdr = (*skb) ? (*skb)->nh.ipv6h : NULL;
	int add_ha;

	/*
	 *	In this implementation, whether or not home address options
	 *	is attached or not, srcaddr is changed to the care-of address.
	 */
	if (hdr != NULL) {
		add_ha = mipv6_is_mn && mipv6_mn_subnet_foreign() && mipv6_mn_is_home_addr(&hdr->saddr);
	} else {
		add_ha = 1;
	}
	if (hdr == NULL)
		return NF_ACCEPT;

	DEBUG((DBG_INFO,
	       "Replace source address (home-address => care-of-address)"));
	DEBUG((DBG_INFO, "%x:%x:%x:%x:%x:%x:%x:%x", NIPV6ADDR(&hdr->saddr)));
	if (add_ha)
		mipv6_get_care_of_address(&hdr->saddr, &hdr->saddr);
	DEBUG((DBG_INFO, "%x:%x:%x:%x:%x:%x:%x:%x", NIPV6ADDR(&hdr->saddr)));

	return NF_ACCEPT;
}

/* Destination Unreachable ICMP error message handler */

static int mipv6_icmpv6_dest_unreach(struct sk_buff *skb)
{
	struct icmp6hdr *phdr = (struct icmp6hdr *) skb->h.raw;
	int left = (skb->tail - (unsigned char *) (phdr + 1))
	    - sizeof(struct ipv6hdr);
	struct ipv6hdr *hdr = (struct ipv6hdr *) (phdr + 1);
	struct ipv6_opt_hdr *ehdr;
	struct ipv6_rt_hdr *rthdr = NULL;
	struct in6_addr *addr;
	int hdrlen, nexthdr = hdr->nexthdr;

	DEBUG_FUNC();

	ehdr = (struct ipv6_opt_hdr *) (hdr + 1);

	while (nexthdr != NEXTHDR_ROUTING) {
		hdrlen = ipv6_optlen(ehdr);
		if (hdrlen > left)
			return 0;
		if (!(nexthdr == NEXTHDR_HOP || nexthdr == NEXTHDR_DEST))
			return 0;
		nexthdr = ehdr->nexthdr;
		ehdr = (struct ipv6_opt_hdr *) ((u8 *) ehdr + hdrlen);
		left -= hdrlen;
	}

	if (nexthdr == NEXTHDR_ROUTING) {
		if (ipv6_optlen(ehdr) > left)
			return 0;
		rthdr = (struct ipv6_rt_hdr *) ehdr;
		if (rthdr->segments_left != 1)
			return 0;
	}

	if (rthdr == NULL) {
		DEBUG((DBG_ERROR, "null pointer in rthdr"));
		return 0;
	}

	addr = (struct in6_addr *) ((u32 *) rthdr + 2);
	if (mipv6_bcache_exists(addr) >= 0) {
		if (!mipv6_bcache_delete(addr, CACHE_ENTRY)) {
			DEBUG((DBG_INFO, "Deleted bcache entry "
			       "%x:%x:%x:%x:%x:%x:%x:%x (reason: "
			       "dest unreachable) ", NIPV6ADDR(addr)));
		}
	}
	return 0;
}

/* BUL callback */

static int bul_entry_expired(struct mipv6_bul_entry *bulentry)
{
	DEBUG((DBG_INFO, "bul entry 0x%x lifetime expired, deleting entry",
	       (int) bulentry));
	return 1;
}

/* Parameter Problem ICMP error message handler */

static int mipv6_icmpv6_paramprob(struct sk_buff *skb)
{
	struct icmp6hdr *phdr = (struct icmp6hdr *) skb->h.raw;
	struct in6_addr *saddr = skb ? &skb->nh.ipv6h->saddr : NULL;
	struct ipv6hdr *hdr = (struct ipv6hdr *) (phdr + 1);
	int ulen = (skb->tail - (unsigned char *) (phdr + 1));

	int errptr;
	__u8 *off_octet;

	DEBUG_FUNC();

	/* We only handle code 2 messages. */
	if (phdr->icmp6_code != ICMPV6_UNK_OPTION)
		return 0;

	/* Find offending octet in the original packet. */
	errptr = ntohl(phdr->icmp6_pointer);

	/* There is not enough of the original packet left to figure
	 * out what went wrong. Bail out. */
	if (ulen <= errptr)
		return 0;

	off_octet = ((__u8 *) hdr + errptr);
	DEBUG((DBG_INFO, "Parameter problem: offending octet %d [0x%2x]",
	       errptr, *off_octet));

	/* If CN did not understand Binding Update, set BUL entry to
	 * SEND_NOMORE so no further BUs are sumbitted to this CN. */
	if (*off_octet == MIPV6_TLV_BINDUPDATE) {
		struct mipv6_bul_entry *bulentry = mipv6_bul_get(saddr);

		if (bulentry) {
			bulentry->state = SEND_NOMORE;
			bulentry->callback = bul_entry_expired;
			bulentry->callback_time = jiffies +
				DUMB_CN_BU_LIFETIME * HZ;
			mipv6_bul_put(bulentry);
			DEBUG((DBG_INFO, "BUL entry set to SEND_NOMORE"));
		}
	}
	/* If CN did not understand Home Address Option, we log an
	 * error and discard the error message. */
	else if (*off_octet == MIPV6_TLV_HOMEADDR) {
		DEBUG((DBG_WARNING, "Correspondent node does not "
		       "implement Home Address Option receipt."));
		return 1;
	}

	return 0;
}

/* Home Agent Address Discovery Request/Reply ICMP message handler */

static int mipv6_ha_addr_disc(struct sk_buff *skb)
{
	struct icmp6hdr *phdr = (struct icmp6hdr *) skb->h.raw;
	struct in6_addr *address;
	struct in6_addr *saddr = &skb->nh.ipv6h->saddr;
	struct in6_addr *daddr = &skb->nh.ipv6h->daddr;
	__u8 type = phdr->icmp6_type;
	__u16 identifier;
	int ulen = (skb->tail - (unsigned char *) ((__u32 *) phdr + 4));
	int ifindex = ((struct inet6_skb_parm *)skb->cb)->iif;
	int i;

	DEBUG_FUNC();

	/* Invalid packet checks. */
	if (phdr->icmp6_type == MIPV6_HA_ADDR_DISC_REQUEST &&
	    ulen < sizeof(struct in6_addr)) return 0;

	if (phdr->icmp6_type == MIPV6_HA_ADDR_DISC_REPLY &&
	    ulen % sizeof(struct in6_addr) != 0)
		return 0;

	if (phdr->icmp6_code != 0)
		return 0;

	identifier = ntohs(phdr->icmp6_identifier);
	address = (struct in6_addr *) ((__u32 *) phdr + 4);

	if (type == MIPV6_HA_ADDR_DISC_REQUEST) {
		if (mipv6_is_ha) {
			/* 
			 * Make sure we have the right ifindex (if the
			 * req came through another interface. 
			 */
			struct rt6_info *rt = rt6_lookup(daddr, NULL, 0, 0);
			if (rt) {
				ifindex = rt->rt6i_dev->ifindex;
				dst_release(&rt->u.dst);
			}
			/*
			 * send reply with list
			 */
			mipv6_ha_dhaad_send_rep(ifindex, identifier,
						saddr);
		}
	} else if (type == MIPV6_HA_ADDR_DISC_REPLY) {
		if (mipv6_is_mn) {
			/* receive list of home agent addresses
			 * add to home agents list
			 */
			struct in6_addr curr_ha, *new_ha = NULL;
			int sender_on_list = 0;
			int n_addr = ulen / sizeof(struct in6_addr);
			if (ulen % sizeof(struct in6_addr)) return 1;
			DEBUG((DBG_DATADUMP, 
			       "DHAAD: got %d home agents", n_addr));
			for (i = 0; i < n_addr; i++) {
				DEBUG((DBG_DATADUMP, 
				       "HA[%d] %x:%x:%x:%x:%x:%x:%x:%x",
				       i, NIPV6ADDR((address))));
				mipv6_halist_add(ifindex, address, NULL, 
						 i + 1, 60);
				if (ipv6_addr_cmp(saddr, address) == 0)
					sender_on_list = 1;
				address++;
						 
			}
			if (!sender_on_list)
				mipv6_halist_add(ifindex, saddr, NULL, 0, 60);
			mipv6_mn_get_homeagent(&curr_ha);
			new_ha = mipv6_mn_get_prefha(&curr_ha);
			if (new_ha) {
				if (ipv6_addr_cmp(&curr_ha, new_ha) == 0) {
					DEBUG((DBG_INFO, "Home Agent not changed by DHAAD"));
				} else {
					int plen;
					struct in6_addr tmp;
					plen = mipv6_mn_get_homeaddr(&tmp);
					if (plen < 0) /* should never happen */
						DEBUG((DBG_ERROR, "Home Address not set"));
					mipv6_mn_set_homeagent(new_ha, plen, 0, 0);
					if (mipv6_mn_subnet_foreign()) {
						struct in6_addr coa;
						mipv6_get_care_of_address(NULL, &coa);
						mipv6_mobile_node_moved(&coa, NULL, plen);
					}
				}
				kfree(new_ha);
			} else {
				DEBUG((DBG_WARNING, "No suitable HA found!"));
			}
		}
	}

	return 1;
}

/* Receives ICMPv6 messages and passes to corresponding handlers */

static int mipv6_icmpv6_rcv(struct sk_buff *skb)
{
	int type, ret = 0;
	struct in6_addr *saddr = &skb->nh.ipv6h->saddr;
	struct in6_addr *daddr = &skb->nh.ipv6h->daddr;
	struct icmp6hdr *hdr =
	    (struct icmp6hdr *) (skb ? skb->h.raw : NULL);

	DEBUG_FUNC();

	switch (skb->ip_summed) {
	case CHECKSUM_NONE:
		skb->csum = csum_partial((char *)hdr, skb->len, 0);
	case CHECKSUM_HW:
		if (csum_ipv6_magic(saddr, daddr, skb->len,
				    IPPROTO_ICMPV6, skb->csum)) {
			DEBUG((DBG_WARNING, "icmp cksum error"));
			kfree_skb(skb);
			return 0;
		}
	}

	if (!pskb_pull(skb, sizeof(struct icmp6hdr)))
		goto discard;

	type = hdr->icmp6_type;

	switch (type) {
	case ICMPV6_DEST_UNREACH:
		ret = mipv6_icmpv6_dest_unreach(skb);
		break;

	case ICMPV6_PARAMPROB:
		if (mipv6_is_mn)
			ret = mipv6_icmpv6_paramprob(skb);
		break;

	case NDISC_ROUTER_ADVERTISEMENT:
		/* We can't intercept RAs here since some of them
		 * won't make it this far.  We use NF_IP6_PRE_ROUTING
		 * nf_hook instead.  ra_rcv_ptr processes them. */
		break;

	case MIPV6_HA_ADDR_DISC_REQUEST:
	case MIPV6_HA_ADDR_DISC_REPLY:
		ret = mipv6_ha_addr_disc(skb);
		break;

	default:
	}
 discard:
	kfree_skb(skb);
	return 0;
}

/* Called from ndisc.c's router_discovery to determine whether to
 * change the current default router and pass the ra to the default
 * router adding mechanism of ndisc.c. return value 1 passes ra and 0
 * doesn't. 
 */

int mipv6_ra_rcv_ptr(struct sk_buff *skb, struct icmp6hdr *msg)
{
	int optlen, ha_info_pref = 0, ha_info_lifetime = 0;
	int ifi = ((struct inet6_skb_parm *)skb->cb)->iif;
	struct ra_msg *ra = (msg) ? (struct ra_msg *)msg :
		(struct ra_msg *) skb->h.raw;
	struct in6_addr *saddr = &skb->nh.ipv6h->saddr;
	struct in6_addr home_prefix;
	int home_plen = 0;
	struct router nrt;
	struct hal {
		struct in6_addr prefix;
		struct hal *next;
	};
	struct hal *ha_queue = NULL;

	__u8 * opt = (__u8 *)(ra + 1);

	if (!mipv6_is_ha && !mipv6_is_mn)
		return 1;

	memset(&nrt, 0, sizeof(struct router));

	if (ra->icmph.icmp6_home_agent) {
		nrt.flags |= ND_RA_FLAG_HA;
		DEBUG((DBG_DATADUMP, "RA has ND_RA_FLAG_HA up"));
	} else if (mipv6_is_ha) return 1;

	if (ra->icmph.icmp6_addrconf_managed) {
		nrt.flags |= ND_RA_FLAG_MANAGED;
		DEBUG((DBG_DATADUMP, "RA has ND_RA_FLAG_MANAGED up"));
	}

	if (ra->icmph.icmp6_addrconf_other) {
		nrt.flags |= ND_RA_FLAG_OTHER;
		DEBUG((DBG_DATADUMP, "RA has ND_RA_FLAG_OTHER up"));
	}

	nrt.lifetime = ntohs(ra->icmph.icmp6_rt_lifetime) * HZ;
	ipv6_addr_copy(&nrt.ll_addr, saddr);
	nrt.ifindex = ifi;

	optlen = (skb->tail - (unsigned char *)ra) - sizeof(struct ra_msg);

	while (optlen > 0) {
		int len = (opt[1] << 3);
		if (len == 0) return 1;
		
		if (opt[0] == ND_OPT_PREFIX_INFO) {
			struct prefix_info *pinfo = (struct prefix_info *) opt;
			if (len < sizeof(struct prefix_info)) return 1;
			/* if RA has H bit set and Prefix Info Option
			 * has R bit set, queue this address to be
			 * added to Home Agents List
			 */
			if ((nrt.flags & ND_RA_FLAG_HA) && pinfo->router_address) {
				struct hal *tmp = kmalloc(sizeof(struct hal), GFP_ATOMIC);
				if (tmp == NULL)
					return -ENOMEM;
				ipv6_addr_copy(&tmp->prefix, &pinfo->prefix);
				tmp->next = ha_queue;
				ha_queue = tmp;
			}				
			/* if second prefix info and not from homenet address,
			 *  then use the first one 
			 */ 
			if (mipv6_is_ha) goto nextopt;
			home_plen = mipv6_mn_get_homeaddr(&home_prefix);
			if (!ipv6_addr_any(&nrt.raddr) && 
			    !(pinfo->prefix_len == home_plen && 
			      mipv6_prefix_compare(&pinfo->prefix, &home_prefix, 
						   home_plen))) {
				DEBUG((DBG_INFO, "Second prefix info "
				       "option and not from homenet"));
			} else {
				ipv6_addr_copy(&nrt.raddr, &pinfo->prefix);
				nrt.pfix_len = pinfo->prefix_len;
				if (pinfo->router_address)
					nrt.glob_addr = 1;
				else
					nrt.glob_addr = 0;
				DEBUG((DBG_DATADUMP, "Address of the received "
				       "prefix info option: %x:%x:%x:%x:%x:%x:%x:%x", 
				       NIPV6ADDR(&nrt.raddr)));
				DEBUG((DBG_DATADUMP, "the length of the prefix is %d", 
				       nrt.pfix_len));
			}
		}
		if (opt[0] == ND_OPT_SOURCE_LL_ADDR) {
			nrt.link_addr_len = skb->dev->addr_len;
			memcpy(nrt.link_addr, opt + 2, nrt.link_addr_len);
		}
		if (opt[0] == ND_OPT_RTR_ADV_INTERVAL) {			
			nrt.interval = ntohl(*(__u32 *)(opt+4)) * HZ / 1000;
			DEBUG((DBG_DATADUMP, 
			       "received router interval option with interval : %d ",
			       nrt.interval / HZ));
			
			if (nrt.interval / HZ > MAX_RADV_INTERVAL) {
				nrt.interval = 0;
				DEBUG((DBG_DATADUMP, "but we are using: %d, "
				       "because interval>MAX_RADV_INTERVAL",
				       nrt.interval / HZ));
			}
		}
		if (opt[0] == ND_OPT_HOME_AGENT_INFO) {
			__u16 tmp;
			tmp = ntohs(*(__u16 *)(opt + 4));
			ha_info_pref = (tmp & 0x8000) ? -(int)((u16)(~0))^(tmp + 1) : tmp;
			ha_info_lifetime = ntohs(*(__u16 *)(opt + 6));
			DEBUG((DBG_DATADUMP,
			       "received home agent info with preference : %d and lifetime : %d",
			       ha_info_pref, ha_info_lifetime));
		}
	nextopt:
		optlen -= len;
		opt += len;
	}
	while (ha_queue) {
		struct hal *tmp = ha_queue->next;
		int lifetime = ha_info_lifetime > 0 ? ha_info_lifetime : nrt.lifetime;
		if (ha_info_lifetime) {
			mipv6_halist_add(ifi, &ha_queue->prefix, &nrt.ll_addr,
					ha_info_pref, ha_info_lifetime);
		} else {
			if (mipv6_halist_delete(&ha_queue->prefix) < 0) {
				DEBUG((DBG_INFO, "mipv6_ra_rcv_ptr: Not able "
					"to delete %x:%x:%x:%x:%x:%x:%x:%x",
					NIPV6ADDR(&ha_queue->prefix)));
			}
		}
		kfree(ha_queue);
		ha_queue = tmp;
	}

	if (mipv6_is_mn)
		return mipv6_router_event(&nrt);

	return 1;
}

int mipv6_ra_rcv(struct sk_buff *skb)
{
	return mipv6_ra_rcv_ptr(skb, NULL);
}

/* We set a netfilter hook so that we can modify outgoing packet's
 * source addresses 
 */
#define HUGE_NEGATIVE (~((~(unsigned int)0) >> 1))

struct nf_hook_ops addr_modify_hook_ops = {
	{NULL, NULL},		/* List head, no predecessor, no successor */
	modify_xmit_addrs,
	PF_INET6,
	NF_IP6_LOCAL_OUT,
	HUGE_NEGATIVE		/* Should be of EXTREMELY high priority since we
				 * do not want to mess with IPSec (possibly
				 * implemented as packet filter)
				 */
};


void mipv6_get_saddr_hook(struct inet6_ifaddr *ifp,
			  struct in6_addr *homeaddr)
{
	int dummy;
	/*  TODO!: conditional stuff if the address scope is e.g. link-local  */
	dummy = mipv6_mn_get_homeaddr(homeaddr);
	/* TODO: use ifp->idev->dev->ifindex to select interface */
}


#ifdef CONFIG_SYSCTL
/* Sysctl table */
ctl_table mipv6_mobility_table[] = {
	{NET_IPV6_MOBILITY_DEBUG, "debuglevel",
	 &mipv6_debug, sizeof(int), 0644, NULL,
	 &proc_dointvec},
	{0}
};
#endif				/* CONFIG_SYSCTL */

static struct inet6_protocol mipv6_icmpv6_protocol = {
	mipv6_icmpv6_rcv,	/* handler              */
	NULL,			/* error control        */
	NULL,			/* next                 */
	IPPROTO_ICMPV6,		/* protocol ID          */
	0,			/* copy                 */
	NULL,			/* data                 */
	"MIPv6 ICMPv6"		/* name                 */
};

/*  Initialize the module  */
int __init init_module(void)
{
	printk(KERN_INFO "Initializing MIPL mobile IPv6\n");
	printk(KERN_INFO "Nodeconfig:  ha=%d mn=%d\n", !!mipv6_is_ha,
	       !!mipv6_is_mn);
	printk(KERN_INFO "Debug-level: %d\n", mipv6_debug);

	if (mipv6_is_ha && mipv6_is_mn) {
		printk(KERN_ERR "** Fatal error: Invalid nodeconfig! **\n");
		return 1;
	}

	/* For Source Address Selection */
	if (mipv6_is_mn)
		home_preferred = mipv6_is_mn;

	/*  Initialize data structures  */
	mipv6_initialize_bcache(MIPV6_BCACHE_SIZE);

	mipv6_initialize_stats();

	if (mipv6_is_ha || mipv6_is_mn) {
		mipv6_initialize_tunnel();
		mipv6_initialize_halist(MIPV6_HALIST_SIZE);
		mipv6_initialize_dhaad();
	}

	if (mipv6_is_ha)
		mipv6_initialize_ha();
	if (mipv6_is_mn) {
		mipv6_initialize_bul(MIPV6_BUL_SIZE);
#if 0
		mipv6_initialize_mn();
#endif
		mipv6_initialize_mdetect();

		/* COA to home transformation hook */
		MIPV6_SETCALL(mipv6_get_home_address, mipv6_get_saddr_hook);

		/* router advertisement processing and clearing of neighbor table */
		MIPV6_SETCALL(mipv6_ra_rcv, mipv6_ra_rcv);
		/* Actual HO, deletes also old routes after the addition of new ones in ndisc */
		MIPV6_SETCALL(mipv6_change_router, mipv6_change_router);
               
		/* Set packet modification hook (source addresses) */
		nf_register_hook(&addr_modify_hook_ops);
	}

	mipv6_initialize_sendopts();
	mipv6_initialize_procrcv();
#ifndef NO_AH
	mipv6_initialize_ah();
	/* Authentication header processing hook */
	MIPV6_SETCALL(mipv6_handle_auth, mipv6_handle_auth);
#endif

	/* Register our ICMPv6 handler */
	inet6_add_protocol(&mipv6_icmpv6_protocol);

	/* Set hook on outgoing packets */
	MIPV6_SETCALL(mipv6_finalize_modify_xmit, mipv6_finalize_modify_xmit);
	MIPV6_SETCALL(mipv6_modify_xmit_packets, mipv6_modify_xmit_packets);


#if defined(CONFIG_SYSCTL) && defined(CONFIG_IPV6_MOBILITY_DEBUG)
	/* register sysctl table */
	mipv6_sysctl_register();
#endif
	mipv6_initialize_ioctl();
	return 0;
}

/*  Cleanup module  */
void __exit cleanup_module(void)
{
	DEBUG_FUNC();

	mipv6_shutdown_ioctl();
#if defined(CONFIG_SYSCTL) && defined(CONFIG_IPV6_MOBILITY_DEBUG)
	/* unregister sysctl table */
	mipv6_sysctl_unregister();
#endif

	/* Unregister our ICMPv6 handler */
	inet6_del_protocol(&mipv6_icmpv6_protocol);

	/*  Invalidate all custom kernel hooks  */
	mipv6_invalidate_calls();

#ifndef NO_AH
	mipv6_shutdown_ah();
#endif
	mipv6_shutdown_sendopts();

	if (mipv6_is_ha || mipv6_is_mn) {
		mipv6_shutdown_dhaad();
		mipv6_shutdown_halist();
		mipv6_shutdown_tunnel();
	}
	
	if (mipv6_is_mn) {
		nf_unregister_hook(&addr_modify_hook_ops);
		mipv6_shutdown_mdetect();
#if 0
		mipv6_shutdown_mn();
#endif
		mipv6_shutdown_bul();
		home_preferred = 0;
	}

	if (mipv6_is_ha)
		mipv6_shutdown_ha();

	mipv6_shutdown_stats();
	mipv6_shutdown_bcache();
}


