/*
 *      Mobile-node functionality
 *
 *      Authors:
 *      Sami Kivisaari          <skivisaa@cc.hut.fi>
 *
 *      $Id: mn.c,v 1.30 2001/07/18 15:01:25 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/sched.h>
#include <linux/ipv6.h>
#include <linux/net.h>
#include <linux/init.h>

#include <net/ipv6.h>
#include <net/addrconf.h>
#include <net/ndisc.h>
#include <linux/ipsec.h>

#include "mipv6.h"
#include "mdetect.h"
#include "mipv6.h"
#include "bul.h"
#include "sendopts.h"
#include "ha.h"
#include "debug.h"
#include "mn.h"
#include "dhaad.h"

/*
 * Mobile Node information record
 */
struct mn_info {
/*	int ifindex;*/
	struct in6_addr home_addr;
	u8 home_plen;
	unsigned long home_addr_expires;
	rwlock_t lock;
};

static struct ha_info home_agent = {
	ADDRANY, 0, 0, 0, RW_LOCK_UNLOCKED
};

static struct mn_info mobile_node = {
	ADDRANY, 0, 0, RW_LOCK_UNLOCKED
};

static struct dhaad {
	int state;
	rwlock_t lock;
} dhaad = { -1, RW_LOCK_UNLOCKED };

/*  Defined in ndisc.c of IPv6 module */
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);

static int mipv6_cn_bu_send(struct mipv6_bul_entry *entry);
static int mipv6_cn_bu_send_dereg(struct mipv6_bul_entry *entry);

int mipv6_mn_is_home_addr(struct in6_addr *addr)
{
	int ret = 0;

	if (addr == NULL) {
		DEBUG((DBG_CRITICAL, "mipv6_mn_is_home_addr: Null argument"));
		return -1;
	}
	read_lock_bh(&mobile_node.lock);
	ret = ipv6_addr_cmp(addr, &mobile_node.home_addr);
	read_unlock_bh(&mobile_node.lock);

	return (!ret);
}

int mipv6_mn_get_homeaddr(struct in6_addr *home_addr)
{
	int plen = 0;

	read_lock_bh(&mobile_node.lock);
	if (ipv6_addr_any(&mobile_node.home_addr)) {
		read_unlock_bh(&mobile_node.lock);
		ipv6_addr_set(home_addr, 0, 0, 0, 0);
		return -1;
	}
	ipv6_addr_copy(home_addr, &mobile_node.home_addr);
	plen = mobile_node.home_plen;
	read_unlock_bh(&mobile_node.lock);

	return plen;
}

void mipv6_mn_set_info(struct in6_addr *home_addr, int plen, 
		       unsigned long lifetime)
{
	write_lock_bh(&mobile_node.lock);
	ipv6_addr_copy(&mobile_node.home_addr, home_addr);
	mobile_node.home_plen = plen;
	mobile_node.home_addr_expires = jiffies + lifetime * HZ;
	write_unlock_bh(&mobile_node.lock);
}

int mipv6_mn_get_homeagent(struct in6_addr *ha_addr)
{
	int plen = 0;

	read_lock_bh(&home_agent.lock);
	if (ipv6_addr_any(&home_agent.addr)) {
		read_unlock_bh(&home_agent.lock);
		ipv6_addr_set(ha_addr, 0, 0, 0, 0);
		DEBUG((DBG_WARNING, "Home Agent adddres is not set!"));
		return -1;
	}
	ipv6_addr_copy(ha_addr, &home_agent.addr);
	plen = home_agent.prefixlen;
	read_unlock_bh(&home_agent.lock);
	DEBUG((DBG_DATADUMP, "mn_get_homeagent: valid ha_addr: "
	       "%x:%x:%x:%x:%x:%x:%x:%x", NIPV6ADDR(ha_addr)));

	return plen;
}

int mipv6_mn_initiate_dhaad_conditional(struct in6_addr *ha_addr)
{
	int plen;

	if ((plen = mipv6_mn_get_homeagent(ha_addr)) < 0) {
		write_lock_bh(&dhaad.lock);
		if (!dhaad.state || mipv6_mn_subnet_foreign()) {
			dhaad.state = 1;
			mipv6_mn_dhaad_send_req();
		}
		write_unlock_bh(&dhaad.lock);
	}
	return plen;
}

int mipv6_mn_get_homeagent_info(struct in6_addr *ha_addr, int *plen,
				int *pref, unsigned long *lifetime)
{
	read_lock_bh(&home_agent.lock);
	if (ipv6_addr_type(&home_agent.addr) & IPV6_ADDR_ANY) {
		read_unlock_bh(&home_agent.lock);
		return -1;
	}
	ipv6_addr_copy(ha_addr, &home_agent.addr);
	*plen = home_agent.prefixlen;
	*pref = home_agent.preference;
	*lifetime = home_agent.lifetime;
	read_unlock_bh(&home_agent.lock);

	return 0;
}

void mipv6_mn_set_homeagent(struct in6_addr *ha_addr, int plen, 
			    int pref, unsigned long lifetime)
{
	write_lock_bh(&dhaad.lock);
	if (dhaad.state != -1)
		dhaad.state = 0;
	write_unlock_bh(&dhaad.lock);
	write_lock_bh(&home_agent.lock);
	ipv6_addr_copy(&home_agent.addr, ha_addr);
	home_agent.prefixlen = plen;
	home_agent.preference = pref;
	home_agent.lifetime = lifetime;
	write_unlock_bh(&home_agent.lock);
}

int mipv6_mn_init_homeagent(struct in6_addr *ha_addr, int plen, 
			    int pref, unsigned long lifetime)
{
	struct in6_addr anycast;

	/* If Home Agent address is Home Agents Anycast
	 * address, do DHAAD */
	ipv6_addr_set(&anycast, (int)0xffffffff, (int)0xffffffff,
		      (int)htonl(0xfdffffff), (int)htonl(0xfffffffe));
	if (mipv6_suffix_compare(ha_addr, &anycast, 128 - plen)) {
		DEBUG((DBG_INFO, "Home Agent anycast, doing DHAAD"));
		write_lock_bh(&dhaad.lock);
		if (dhaad.state != 1)
			dhaad.state = 0;
		write_unlock_bh(&dhaad.lock);
	} else {
		mipv6_mn_set_homeagent(ha_addr, plen, pref, 0);
		write_lock_bh(&dhaad.lock);
		if (dhaad.state != 1)
			dhaad.state = -1;
		write_unlock_bh(&dhaad.lock);
	}
	return 0;
}

void mipv6_tunneled_packet_received(struct sk_buff *skb,
				    struct ipv6hdr *inner, 
				    struct ipv6hdr *outer)
{
	struct mipv6_bul_entry *bulentry = mipv6_bul_get(&inner->saddr);
	struct in6_addr tmp, ha;
	int plen;
	
	DEBUG_FUNC();

	plen = mipv6_mn_get_homeaddr(&tmp);
	mipv6_mn_get_homeagent(&ha);
	/* We don't send bus in response to all tunneled packets */
	if (ipv6_addr_cmp(&tmp, &inner->daddr)) {
		DEBUG((DBG_WARNING, "Received a tunneld packet NOT sent "
		       "originally to home address"));
		return;
	}
        if (!ipv6_addr_cmp(&ha, &inner->saddr)) {
                DEBUG((DBG_ERROR, "HA BUG: Received a tunneled packet "
		       "originally sent by home agent, not sending BU"));
		return;
        }
	if (ipv6_addr_cmp(&ha, &outer->saddr)) {
		DEBUG((DBG_WARNING, "MIPV6 MN: Received a tunneled IPv6 packet"
		       " that was not tunneled by HA %x:%x:%x:%x:%x:%x:%x:%x,"
		       " but by %x:%x:%x:%x:%x:%x:%x:%x", 
		       NIPV6ADDR(&ha), NIPV6ADDR(&outer->saddr)));
		//return;
        }
	if (bulentry) {
		int state = bulentry->state;
		mipv6_bul_put(bulentry);
		if (state == SEND_NOMORE) {
			DEBUG((DBG_INFO, "Received a tunneled packet from CN"
			       " which does not understand BU"));
			return;
		}
	}
	DEBUG((DBG_INFO, "Sending BU to correspondent node"));

	if (inner->nexthdr != IPPROTO_DSTOPTS) {
		DEBUG((DBG_INFO, "Received tunneled packet without dst_opts"));
		/* max wait 1000 ms  before sending an empty packet with BU */
		mipv6_send_upd_option(&inner->daddr,&inner->saddr,
				      CN_BU_DELAY, 
				      INITIAL_BINDACK_TIMEOUT,
				      MAX_BINDACK_TIMEOUT , 1, 
#ifdef CN_REQ_ACK
				      MIPV6_BU_F_ACK, /* ack */
#else
				      0, /* no ack */
#endif
				      0, CN_BU_DEF_LIFETIME, NULL);
	}
	/* (Mis)use ipsec tunnel flag  */
	DEBUG((DBG_INFO, "setting rcv_tunnel flag in skb"));
	skb->security = skb->security | RCV_TUNNEL;

}

void mipv6_check_tunneled_packet(struct sk_buff *skb)
{

	DEBUG_FUNC();
	/* If tunnel flag was set */
	if (skb->security & RCV_TUNNEL) {
		DEBUG((DBG_WARNING, "skb has rcv_tunnel flag"));

		DEBUG((DBG_WARNING, "packet was tunneled. Sending BU to CN %x:%x:%x:%x:%x:%x:%x:%x", 
		       NIPV6ADDR(&skb->nh.ipv6h->saddr))); /* Should work also with home address option */


		mipv6_send_upd_option(
			&skb->nh.ipv6h->daddr,
			&skb->nh.ipv6h->saddr,
			CN_BU_DELAY, INITIAL_BINDACK_TIMEOUT,
			MAX_BINDACK_TIMEOUT, 1, 
#ifdef CN_REQ_ACK
			MIPV6_BU_F_ACK, /* ack */
#else
			0, /* no ack */
#endif
			64, CN_BU_DEF_LIFETIME, NULL);
	}
}



/*  We have moved to a foreign subnet, send home registrations
 *
 *  TODO!: If we have multiple interfaces then it is possible that only one
 *         of them moved.  This function should take as argument the home
 *         address that moved.
 */
int mipv6_mobile_node_moved(struct in6_addr *coa, struct in6_addr *prev_router, int plen)
{
	struct in6_addr homeaddr, ha;
	int plength;

	DEBUG_FUNC();
	plength = mipv6_mn_get_homeaddr(&homeaddr);
	DEBUG((DBG_INFO, "Sending a BU to HA with new Care of address : %x:%x:%x:%x:%x:%x:%x:%x",
	       NIPV6ADDR(coa)));
	DEBUG((DBG_INFO, "and with home address %x:%x:%x:%x:%x:%x:%x:%x",
	       NIPV6ADDR(&homeaddr)));

	/* DHAAD request will be sent if home_agent address is not set */
	mipv6_mn_initiate_dhaad_conditional(&ha);

	if (ipv6_addr_any(&ha)) {
		DEBUG((DBG_DATADUMP, "Home Agent Address not set or waiting for DHAAD"));
		return 0;
	}

        /* Binding update for HA */
	mipv6_send_upd_option(
		&homeaddr, &ha, HA_BU_DELAY, INITIAL_BINDACK_TIMEOUT, 
		MAX_BINDACK_TIMEOUT, 1,
		MIPV6_BU_F_HOME | MIPV6_BU_F_ACK, plen,
		HA_BU_DEF_LIFETIME, NULL);


	/* BU to previous router */
	mipv6_bu_to_prev_router(prev_router, plen);

        /*  A bu is sent to all CNs in the binding update list  */ 
	bul_iterate(mipv6_cn_bu_send);  
	return 0;
}

	
/*  We have returned home, send home deregistrations
 *
 *  TODO!: If we have multiple interfaces then it is possible that only one
 *         of them returned home.  This function should take take as argument
 *         the home address that returned home.
 */
int mipv6_mn_returned_home(struct in6_addr *prev_router, int plen)
{
	struct in6_addr homeaddr, ha;
	int plength;

	DEBUG_FUNC();
	DEBUG((DBG_INFO,
	       "Returned home. Sending a BU to HA with home address"));

	plength = mipv6_mn_get_homeaddr(&homeaddr);
	mipv6_mn_get_homeagent(&ha);

	if (ipv6_addr_any(&ha)) {
		DEBUG((DBG_DATADUMP, "Home Agent Address not set or waiting for DHAAD"));
		return 0;
	}
	mipv6_send_upd_option(
		&homeaddr, &ha, 
		HA_BU_DELAY, INITIAL_BINDACK_TIMEOUT, 
		MAX_BINDACK_TIMEOUT, 1,
		MIPV6_BU_F_HOME | MIPV6_BU_F_ACK, 
		plen, 0, NULL);

        /* BU to previous router */
	mipv6_bu_to_prev_router(prev_router, plen);
	
        /* A bu is sent to all CNs in the binding update list */ 
	bul_iterate(mipv6_cn_bu_send_dereg);

	return 0;
}

/*
 * After returning home, MN must advertise all its valid addresses in
 * home link to all nodes. 
 */
void mipv6_mn_send_home_na(int ifindex)
{
	struct net_device *dev = NULL;
	struct in6_addr mc_allnodes, addr;

	dev = dev_get_by_index(ifindex);

	if (dev == NULL) {
		DEBUG((DBG_ERROR, "Device %d not found.", ifindex));
		return;
	}

	ipv6_addr_all_nodes(&mc_allnodes);
	if (ipv6_get_lladdr(dev, &addr) == 0)
		ndisc_send_na(dev, NULL, &mc_allnodes, &addr, 0, 0, 1, 1);
	mipv6_mn_get_homeaddr(&addr);
	ndisc_send_na(dev, NULL, &mc_allnodes, &addr, 0, 0, 1, 1);
	dev_put(dev);
}

/* Sends BU to previous router */
void mipv6_bu_to_prev_router(struct in6_addr *prev_router,int plen)
{

	struct in6_addr homeaddr, prevcoa, ha;
	int plength;
 
	plength = mipv6_mn_get_homeaddr(&homeaddr);
	mipv6_mn_get_homeagent(&ha);
	
	if (prev_router && ipv6_addr_cmp(&ha, prev_router) && 
	    !ipv6_addr_any(prev_router)) {
		mipv6_get_old_care_of_address(NULL,&prevcoa);

		DEBUG((DBG_INFO, "Sending a BU to former router with "
		       "prevcoa: %x:%x:%x:%x:%x:%x:%x:%x",
		       NIPV6ADDR(&prevcoa)));
		DEBUG((DBG_INFO, "and with prev_router: "
		       "%x:%x:%x:%x:%x:%x:%x:%x",
		       NIPV6ADDR(prev_router)));
    
		mipv6_send_upd_option(
			&prevcoa, prev_router, HA_BU_DELAY, 
			INITIAL_BINDACK_TIMEOUT, MAX_BINDACK_TIMEOUT, 1,
			MIPV6_BU_F_HOME, plen, ROUTER_BU_DEF_LIFETIME, NULL);
		
	}
}  

static int mipv6_cn_bu_send(struct mipv6_bul_entry *entry)
{
	struct in6_addr homeaddr;
	int plength;

	if (entry->flags & MIPV6_BU_F_HOME) return ITERATOR_CONT;

	plength = mipv6_mn_get_homeaddr(&homeaddr);

	/*
	 * A lifetime of 30 seconds is used and a wait of 1 sec before using
	 * an empty packet.
	 */
	DEBUG((DBG_INFO,
	       "Sending REGISTRATION to CN: %x:%x:%x:%x:%x:%x:%x:%x",
	       NIPV6ADDR(&entry->cn_addr)));

	mipv6_send_upd_option(
		&homeaddr, &entry->cn_addr, CN_BU_DELAY, 
		INITIAL_BINDACK_TIMEOUT, MAX_BINDACK_TIMEOUT, 1,
		MIPV6_BU_F_ACK, plength, CN_BU_DEF_LIFETIME, NULL);

	return ITERATOR_CONT;
}

static int mipv6_cn_bu_send_dereg(struct mipv6_bul_entry *entry)
{
	struct in6_addr homeaddr;
	int plength;

	if(entry->flags & MIPV6_BU_F_HOME) return ITERATOR_CONT;

	plength = mipv6_mn_get_homeaddr(&homeaddr);

	DEBUG((DBG_INFO, 
	       "Sending DEREGISTRATION to CN: %x:%x:%x:%x:%x:%x:%x:%x",
	       NIPV6ADDR(&entry->cn_addr)));
	mipv6_send_upd_option(
		&homeaddr, &entry->cn_addr, CN_BU_DELAY, 
		INITIAL_BINDACK_TIMEOUT, MAX_BINDACK_TIMEOUT, 1, 
		MIPV6_BU_F_ACK, plength, 0, NULL);

	return ITERATOR_CONT;
}

#if 0
/* If there is nothing to initialize, we might as well get rid of
 * these. */
int __init mipv6_initialize_mn(void)
{
	DEBUG_FUNC();
	
	return 0;
}

void mipv6_shutdown_mn(void)
{
	DEBUG_FUNC();
}
#endif
