#include	"u.h"
#include	"../port/lib.h"
#include	"mem.h"
#include	"dat.h"
#include	"fns.h"
#include	"../port/error.h"

#include	"ip.h"
#include	"ipv6.h"

static Block*		ip6reassemble(IP*, int, Block*);
static Fragment6*	ipfragallo6(IP*);
static void		ipfragfree6(IP*, Fragment6*);
static Block*		procopts(Block *bp);
static Block*		procxtns(IP *ip, Block *bp, int doreasm);
static int		unfraglen(Block *bp, uchar *nexthdr, int setfh, int popfh);

void
ip_init_6(Fs *f)
{
	v6params *v6p;

	v6p = smalloc(sizeof(v6params));

	v6p->rp.mflag		= 0;		/* default not managed */
	v6p->rp.oflag		= 0;
	v6p->rp.maxraint	= 600000;	/* millisecs */
	v6p->rp.minraint	= 200000;
	v6p->rp.linkmtu		= 0;		/* no mtu sent */
	v6p->rp.reachtime	= 0;
	v6p->rp.rxmitra		= 0;
	v6p->rp.ttl		= MAXTTL;
	v6p->rp.routerlt	= (3 * v6p->rp.maxraint) / 1000;

	v6p->hp.rxmithost	= RETRANS_TIMER;

	f->v6p			= v6p;
}

int
ipoput6(Fs *f, Block *bp, Ipifc *gating, int ttl, int tos, Routehint *rh)
{
	int medialen, len, chunk, uflen, flen, seglen, lid, offset, fragoff;
	int morefrags, blklen, rv = 0;
	uchar *gate, nexthdr;
	Block *xp, *nb;
	Fraghdr6 fraghdr;
	IP *ip;
	Ip6hdr *eh;
	Ipifc *ifc;
	Route *r;

	ip = f->ip;
	ip->stats[OutRequests]++;

	/* Fill out the ip header */
	eh = (Ip6hdr*)bp->rp;
	assert(BLEN(bp) >= IP6HDR);
	len = blocklen(bp);
	if(len >= IP_MAX){
		ip->stats[OutDiscards]++;
		netlog(f, Logip, "%I -> %I: exceeded ip max size: %d\n", eh->src, eh->dst, len);
		goto free;
	}

	r = v6lookup(f, eh->dst, eh->src, rh);
	if(r == nil || (r->type & Rv4) != 0 || (ifc = r->ifc) == nil){
		ip->stats[OutNoRoutes]++;
		netlog(f, Logip, "%I -> %I: no interface\n", eh->src, eh->dst);
		rv = -1;
		goto free;
	}

	if(r->type & (Rifc|Runi|Rbcast|Rmulti))
		gate = eh->dst;
	else
		gate = r->v6.gate;

	if(!canrlock(ifc)){
		ip->stats[OutDiscards]++;
		goto free;
	}
	if(waserror()){
		runlock(ifc);
		/* bp is freed by m->bwrite() called from ipifcoput() */
		return -1;
	}

	if(ifc->m == nil || r->ifcid != ifc->ifcid){
		rv = -1;
		goto raise;
	}

	medialen = ifc->maxtu - ifc->m->hsize;
	if(gating != nil){
		int mtu = gating->maxtu - gating->m->hsize;
		if(medialen < mtu)
			mtu = medialen;
		tcpmssclamp(bp->rp, BLEN(bp), mtu);
	} else {
		eh->vcf[0] = IP_VER6;
		eh->vcf[0] |= tos >> 4;
		eh->vcf[1]  = tos << 4;
	}
	eh->ttl = ttl;

	/* If we dont need to fragment just send it */
	if(len <= medialen) {
		hnputs(eh->ploadlen, len - IP6HDR);
		ipifcoput(ifc, bp, V6, gate, rh);
		runlock(ifc);
		poperror();
		return 0;
	}

	if(gating != nil && !ifc->reassemble) {
		/*
		 * v6 intermediate nodes are not supposed to fragment pkts;
		 * we fragment if ifc->reassemble is turned on; an exception
		 * needed for nat.
		 */
		ip->stats[OutDiscards]++;
		icmppkttoobig6(f, gating, bp, medialen);
		netlog(f, Logip, "%I -> %I: gated pkts not fragmented\n", eh->src, eh->dst);
		goto raise;
	}

	/* start v6 fragmentation */
	uflen = unfraglen(bp, &nexthdr, 1, 0);
	if(uflen < IP6HDR || nexthdr == FH) {
		ip->stats[FragFails]++;
		ip->stats[OutDiscards]++;
		netlog(f, Logip, "%I -> %I: fragment header botch\n", eh->src, eh->dst);
		goto raise;
	}
	if(uflen > medialen) {
		ip->stats[FragFails]++;
		ip->stats[OutDiscards]++;
		netlog(f, Logip, "%I -> %I: unfragmentable part too big: %d\n", eh->src, eh->dst, uflen);
		goto raise;
	}

	flen = len - uflen;
	seglen = (medialen - (uflen + IP6FHDR)) & ~7;
	if(seglen < 8) {
		ip->stats[FragFails]++;
		ip->stats[OutDiscards]++;
		netlog(f, Logip, "%I -> %I: seglen < 8\n", eh->src, eh->dst);
		goto raise;
	}

	lid = incref(&ip->id6);
	fraghdr.nexthdr = nexthdr;
	fraghdr.res = 0;
	hnputl(fraghdr.id, lid);

	xp = bp;
	offset = uflen;
	while (offset && offset >= BLEN(xp)) {
		offset -= BLEN(xp);
		xp = xp->next;
	}
	xp->rp += offset;

	fragoff = 0;
	morefrags = 1;

	for(; fragoff < flen; fragoff += seglen) {
		nb = allocb(uflen + IP6FHDR + seglen);

		if(fragoff + seglen >= flen) {
			seglen = flen - fragoff;
			morefrags = 0;
		}

		hnputs(eh->ploadlen, seglen+IP6FHDR);
		memmove(nb->wp, eh, uflen);
		nb->wp += uflen;

		hnputs(fraghdr.offsetRM, fragoff); /* last 3 bits must be 0 */
		fraghdr.offsetRM[1] |= morefrags;
		memmove(nb->wp, &fraghdr, IP6FHDR);
		nb->wp += IP6FHDR;

		/* Copy data */
		chunk = seglen;
		while (chunk) {
			if(xp == nil) {
				ip->stats[OutDiscards]++;
				ip->stats[FragFails]++;
				freeblist(nb);
				netlog(f, Logip, "xp == nil: chunk in v6%d\n", chunk);
				goto raise;
			}
			blklen = chunk;
			if(BLEN(xp) < chunk)
				blklen = BLEN(xp);
			memmove(nb->wp, xp->rp, blklen);

			nb->wp += blklen;
			xp->rp += blklen;
			chunk -= blklen;
			if(xp->rp == xp->wp)
				xp = xp->next;
		}
		ipifcoput(ifc, nb, V6, gate, rh);
		ip->stats[FragCreates]++;
	}
	ip->stats[FragOKs]++;

raise:
	runlock(ifc);
	poperror();
free:
	freeblist(bp);
	return rv;
}

void
ipiput6(Fs *f, Ipifc *ifc, Block *bp)
{
	int hl, len, hop, tos;
	IP *ip;
	Ip6hdr *h;
	Proto *p;

	ip = f->ip;
	ip->stats[InReceives]++;

	/*
	 *  Ensure we have all the header info in the first
	 *  block.  Make life easier for other protocols by
	 *  collecting up to the first 64 bytes in the first block.
	 */
	if(BLEN(bp) < 64) {
		hl = blocklen(bp);
		if(hl < IP6HDR)
			hl = IP6HDR;
		if(hl > 64)
			hl = 64;
		bp = pullupblock(bp, hl);
		if(bp == nil)
			return;
	}

	/* Check header version */
	h = (Ip6hdr*)bp->rp;
	if((h->vcf[0] & 0xF0) != IP_VER6) {
		ip->stats[InHdrErrors]++;
		netlog(f, Logip, "ip: bad version %ux\n", (h->vcf[0]&0xF0)>>2);
		goto drop;
	}
	len = IP6HDR + nhgets(h->ploadlen);
	if((bp = trimblock(bp, 0, len)) == nil){
		ip->stats[InHdrErrors]++;
		netlog(f, Logip, "%I -> %I: bogus packet length: %d\n", h->src, h->dst, len);
		return;
	}
	h = (Ip6hdr*)bp->rp;

	/* route */
	if(!ipforme(f, h->dst)) {
		Routehint rh;
		Route *r;
		Ipifc *nifc;

		if(!ip->iprouting)
			goto drop;

		/* don't forward to link-local destinations */
		if(islinklocal(h->dst) ||
		   (isv6mcast(h->dst) && (h->dst[1]&0xF) <= Link_local_scop)){
			ip->stats[OutDiscards]++;
			goto drop;
		}
			
		/* don't forward to source's network */
		rh.r = nil;
		rh.a = nil;
		r  = v6lookup(f, h->dst, h->src, &rh);
		if(r == nil || (nifc = r->ifc) == nil || (r->type & Rv4) != 0
		|| (nifc == ifc && !ifc->reflect)){
			ip->stats[OutDiscards]++;
			goto drop;
		}

		/* don't forward if packet has timed out */
		hop = h->ttl;
		if(hop <= 1) {
			ip->stats[InHdrErrors]++;
			icmpttlexceeded6(f, ifc, bp);
			goto drop;
		}

		/* process headers & reassemble if the interface expects it */
		bp = procxtns(ip, bp, nifc->reassemble);
		if(bp == nil)
			return;

		ip->stats[ForwDatagrams]++;
		h = (Ip6hdr*)bp->rp;
		tos = (h->vcf[0]&0x0F)<<2 | (h->vcf[1]&0xF0)>>2;
		ipoput6(f, bp, ifc, hop - 1, tos, &rh);
		return;
	}

	/* reassemble & process headers if needed */
	bp = procxtns(ip, bp, 1);
	if(bp == nil)
		return;

	h = (Ip6hdr*)bp->rp;
	p = Fsrcvpcol(f, h->proto);
	if(p != nil && p->rcv != nil) {
		ip->stats[InDelivers]++;
		(*p->rcv)(p, ifc, bp);
		return;
	}

	ip->stats[InDiscards]++;
	ip->stats[InUnknownProtos]++;
drop:
	freeblist(bp);
}

/*
 * ipfragfree6 - copied from ipfragfree4 - assume hold fraglock6
 */
static void
ipfragfree6(IP *ip, Fragment6 *frag)
{
	Fragment6 *fl, **l;

	if(frag->blist != nil)
		freeblist(frag->blist);
	frag->blist = nil;
	frag->id = 0;
	memset(frag->src, 0, IPaddrlen);
	memset(frag->dst, 0, IPaddrlen);

	l = &ip->flisthead6;
	for(fl = *l; fl != nil; fl = fl->next) {
		if(fl == frag) {
			*l = frag->next;
			break;
		}
		l = &fl->next;
	}

	frag->next = ip->fragfree6;
	ip->fragfree6 = frag;
}

/*
 * ipfragallo6 - copied from ipfragalloc4
 */
static Fragment6*
ipfragallo6(IP *ip)
{
	Fragment6 *f;

	while(ip->fragfree6 == nil) {
		/* free last entry on fraglist */
		for(f = ip->flisthead6; f->next != nil; f = f->next)
			;
		ipfragfree6(ip, f);
	}
	f = ip->fragfree6;
	ip->fragfree6 = f->next;
	f->next = ip->flisthead6;
	ip->flisthead6 = f;
	f->age = NOW + 30000;

	return f;
}

static Block*
procxtns(IP *ip, Block *bp, int doreasm)
{
	uchar proto;
	int offset;

	offset = unfraglen(bp, &proto, 0, doreasm);
	if(offset >= IP6HDR && proto == FH && doreasm) {
		bp = ip6reassemble(ip, offset, bp);
		if(bp == nil)
			return nil;
		offset = unfraglen(bp, &proto, 0, 0);
		if(proto == FH)
			offset = -1;
	}
	if(offset < IP6HDR){
		ip->stats[InHdrErrors]++;
		ip->stats[InDiscards]++;
		freeblist(bp);
		return nil;
	}
	if(proto == DOH || offset > IP6HDR)
		bp = procopts(bp);
	return bp;
}

/*
 * returns length of "Unfragmentable part", i.e., sum of lengths of ipv6 hdr,
 * hop-by-hop & routing headers if present; *nexthdr is set to nexthdr value
 * of the last header in the "Unfragmentable part"; if setfh != 0, nexthdr
 * field of the last header in the "Unfragmentable part" is set to FH.
 * When the last header is a fragment header and popfh != 0 then set
 * the nexthdr value of the previous header to the nexthdr value of the
 * fragment header. returns -1 on error.
 */
static int
unfraglen(Block *bp, uchar *nexthdr, int setfh, int popfh)
{
	uchar *e, *p, *q;

	e = bp->wp;
	p = bp->rp;
	q = p+6;   /* proto, = p+sizeof(Ip6hdr.vcf)+sizeof(Ip6hdr.ploadlen) */
	*nexthdr = *q;
	p += IP6HDR;
	while(*nexthdr == HBH || *nexthdr == RH){
		if(p+2 > e)
			return -1;
		q = p;
		*nexthdr = *q;
		p += ((int)p[1] + 1) * 8;
	}
	if(p > e)
		return -1;
	if(*nexthdr == FH){
		if(p+IP6FHDR > e || *p == FH)
			return -1;
		if(popfh)
			*q = *p;
	} else if(setfh)
		*q = FH;
	return p - bp->rp;
}

static Block*
procopts(Block *bp)
{
	return bp;
}

static Block*
ip6reassemble(IP* ip, int uflen, Block* bp)
{
	int offset, ovlap, fragsize, len;
	uchar src[IPaddrlen], dst[IPaddrlen];
	uint id;
	Block *bl, **l, *prev;
	Fraghdr6 *fraghdr;
	Fragment6 *f, *fnext;
	Ipfrag *fp, *fq;
	Ip6hdr* ih;

	/*
	 *  block lists are too hard, concatblock into a single block
	 */
	bp = concatblock(bp);

	ih = (Ip6hdr*)bp->rp;
	fraghdr = (Fraghdr6*)(bp->rp + uflen);
	id = nhgetl(fraghdr->id);
	offset = nhgets(fraghdr->offsetRM);
	fragsize = BLEN(bp) - uflen - IP6FHDR;

	memmove(src, ih->src, IPaddrlen);
	memmove(dst, ih->dst, IPaddrlen);

	qlock(&ip->fraglock6);

	/*
	 *  find a reassembly queue for this fragment
	 */
	for(f = ip->flisthead6; f != nil; f = fnext){
		fnext = f->next;
		if(f->id == id && ipcmp(f->src, src) == 0 && ipcmp(f->dst, dst) == 0)
			break;
		if(f->age < NOW){
			ip->stats[ReasmTimeout]++;
			ipfragfree6(ip, f);
		}
	}

	/*
	 *  if this isn't a fragmented packet, accept it
	 *  and get rid of any fragments that might go
	 *  with it.
	 */
	if((offset & ~6) == 0) {	/* 1st frag is also last */
		if(f != nil) {
			ip->stats[ReasmFails]++;
			ipfragfree6(ip, f);
		}
		qunlock(&ip->fraglock6);

		/* get rid of frag header */
		memmove(bp->rp + IP6FHDR, bp->rp, uflen);
		bp->rp += IP6FHDR;
		ih = (Ip6hdr*)bp->rp;
		hnputs(ih->ploadlen, BLEN(bp)-IP6HDR);

		return bp;
	}

	if(bp->base+IPFRAGSZ > bp->rp){
		bp = padblock(bp, IPFRAGSZ);
		bp->rp += IPFRAGSZ;
	}

	fp = (Ipfrag*)bp->base;
	fp->foff = offset & ~7;
	fp->flen = fragsize;

	/* First fragment allocates a reassembly queue */
	if(f == nil) {
		f = ipfragallo6(ip);
		f->id = id;
		memmove(f->src, src, IPaddrlen);
		memmove(f->dst, dst, IPaddrlen);

		f->blist = bp;

		ip->stats[ReasmReqds]++;
		qunlock(&ip->fraglock6);

		return nil;
	}

	/*
	 *  find the new fragment's position in the queue
	 */
	prev = nil;
	l = &f->blist;
	bl = f->blist;
	while(bl != nil && fp->foff > ((Ipfrag*)bl->base)->foff) {
		prev = bl;
		l = &bl->next;
		bl = bl->next;
	}

	/* Check overlap of a previous fragment - trim away as necessary */
	if(prev != nil) {
		fq = (Ipfrag*)prev->base;
		ovlap = fq->foff + fq->flen - fp->foff;
		if(ovlap > 0) {
			if(ovlap >= fp->flen) {
				qunlock(&ip->fraglock6);
				freeb(bp);
				return nil;
			}
			fq->flen -= ovlap;
		}
	}

	/* Link onto assembly queue */
	bp->next = *l;
	*l = bp;

	/* Check to see if succeeding segments overlap */
	if(bp->next != nil) {
		l = &bp->next;
		offset = fp->foff + fp->flen;

		/* Take completely covered segments out */
		while((bl = *l) != nil) {
			fq = (Ipfrag*)bl->base;
			ovlap = offset - fq->foff;
			if(ovlap <= 0)
				break;
			if(ovlap < fq->flen) {
				/* move up ip and frag header */
				memmove(bl->rp + ovlap, bl->rp, BLEN(bl) - fq->flen);
				bl->rp += ovlap;
				fq->flen -= ovlap;
				fq->foff += ovlap;
				break;
			}
			*l = bl->next;
			bl->next = nil;
			freeb(bl);
		}
	}

	/*
	 *  look for a complete packet.  if we get to a fragment
	 *  with the trailing bit of fraghdr->offsetRM[1] set, we're done.
	 */
	offset = 0;
	for(bl = f->blist; bl != nil; bl = bl->next, offset += fp->flen) {
		fp = (Ipfrag*)bl->base;
		if(fp->foff != offset)
			break;

		fraghdr = (Fraghdr6*)(bl->wp - fp->flen - IP6FHDR);
		if(fraghdr->offsetRM[1] & 1)
			continue;

		bl = f->blist;
		fq = (Ipfrag*)bl->base;

		/* get rid of frag header in first fragment */
		memmove(bl->rp + IP6FHDR, bl->rp, BLEN(bl) - fq->flen - IP6FHDR);
		bl->rp += IP6FHDR;
		len = BLEN(bl);

		/*
		 * Pullup all the fragment headers and
		 * return a complete packet
		 */
		for(bl = bl->next; bl != nil && len < IP_MAX; bl = bl->next) {
			fq = (Ipfrag*)bl->base;
			fragsize = fq->flen;
			bl->rp = bl->wp - fragsize;
			len += fragsize;
		}

		if(len >= IP_MAX){
			ipfragfree6(ip, f);
			ip->stats[ReasmFails]++;
			qunlock(&ip->fraglock6);

			return nil;
		}

		bl = f->blist;
		f->blist = nil;
		ipfragfree6(ip, f);

		ih = (Ip6hdr*)bl->rp;
		hnputs(ih->ploadlen, len-IP6HDR);

		ip->stats[ReasmOKs]++;
		qunlock(&ip->fraglock6);

		return bl;
	}
	qunlock(&ip->fraglock6);
	return nil;
}
