/*
 *	Simple IP implementation (missing option processing completely).
 *
 *	01/21/94, kay roemer.
 */

#include "config.h"
#include "kerbind.h"
#include "sockerr.h"
#include "atarierr.h"
#include "inet.h"
#include "in.h"
#include "ip.h"
#include "if.h"
#include "icmp.h"
#include "route.h"
#include "timer.h"
#include "util.h"

#define SIN(x)	((struct sockaddr_in *)x)

extern void	*memcpy		(void *, const void *, unsigned long);

static BUF	*ip_brdcst_copy	(BUF *, struct netif *, struct route *, short);
extern long	ip_forward	(BUF *, struct netif *, short);
static long	ip_do_opts	(struct ip_dgram *);

static long	ip_frag		(BUF *, struct netif *, unsigned long, short);
static BUF	*ip_defrag	(BUF *);
static long	frag_opts	(struct ip_dgram *, long);
static void	frag_timeout	(long);
static void	frag_delete	(struct fragment *);
static void	frag_insert	(struct fragment *, BUF *);
static BUF	*frag_pullup	(struct fragment *);

struct in_ip_ops *allipprotos = 0;

/* Return the local IP address which is 'nearest' to `dstaddr'. Note
 * that one machine can have several IP addresses on several networks,
 * so this routine must know `dstaddr'.
 */

unsigned long
ip_local_addr (dstaddr)
	unsigned long dstaddr;
{
	struct route *rt;
	struct ifaddr *ifa;

	if (dstaddr == INADDR_ANY) {
		struct netif *nif = if_primary ();

		if (!nif) return INADDR_ANY;
		ifa = if_af2ifaddr (nif, AF_INET);
	} else {
		rt = route_get (dstaddr);
		if (!rt) return INADDR_ANY;

		ifa = if_af2ifaddr (rt->nif, AF_INET);
		route_deref (rt);
	}
	return ifa ? SIN (&ifa->addr)->sin_addr.s_addr : INADDR_ANY;
}

short
ip_is_brdcst_addr (addr)
	unsigned long addr;
{
	struct netif *nif;
	struct ifaddr *ifa;
	
	if (addr == INADDR_ANY || addr == INADDR_BROADCAST)
		return 1;

	for (nif = allinterfaces; nif; nif = nif->next) {
		ifa = if_af2ifaddr (nif, AF_INET);
		if (!ifa) continue;
		
		if (addr == ifa->net ||
		    addr == ifa->subnet ||
		    addr == ifa->net_broadaddr)
			return 1;

		if (nif->flags & IFF_BROADCAST &&
		    addr == SIN (&ifa->ifu.broadaddr)->sin_addr.s_addr)
			return 1;
	}
	return 0;
}
 
short
ip_is_local_addr (addr)
	unsigned long addr;
{
	struct netif *nif;
	struct ifaddr *ifa;

	if (addr == INADDR_LOOPBACK)
		return 1;

	for (nif = allinterfaces; nif; nif = nif->next) {
		ifa = if_af2ifaddr (nif, AF_INET);
		if (ifa && addr == SIN (&ifa->addr)->sin_addr.s_addr)
			return 1;
	}
	return 0;
}

short
ip_chk_addr (addr, rt)
	unsigned long addr;
	struct route *rt;
{
	struct ifaddr *ifa;

	if (addr == INADDR_ANY || addr == INADDR_BROADCAST)
		return IPADDR_BRDCST;

	if ((addr & IN_CLASSA_NET) == 0x7f000000ul)
		return IPADDR_LOCAL;

	if (rt->flags & RTF_LOCAL)
		return IPADDR_LOCAL;
	
	ifa = if_af2ifaddr (rt->nif, AF_INET);
	if (ifa) {
		if (addr == SIN (&ifa->addr)->sin_addr.s_addr)
			return IPADDR_LOCAL;

		if (addr == ifa->net ||
		    addr == ifa->subnet ||
		    addr == ifa->net_broadaddr)
			return IPADDR_BRDCST;

		if (rt->nif->flags & IFF_BROADCAST &&
		    addr == SIN (&ifa->ifu.broadaddr)->sin_addr.s_addr)
			return IPADDR_BRDCST;
	}
	if (!IN_CLASSA (addr) && !IN_CLASSB (addr) && !IN_CLASSC (addr))
		return IPADDR_BADCLASS;

	return IPADDR_NONE;
}

/* Check whether foreign IP address `foreign' matches local IP address
 * `local'. Note that `foreign' might be a broadcast address that matches
 * our network and thus our machines address.
 * This routine requires the subnet portion to be a multiple of 8 bits to
 * recognice subnet broadcasts.
 */

short
ip_same_addr (local, foreign)
	unsigned long local, foreign;
{
	unsigned long mask = 0xffffffff;

	if (local == foreign) return 1;
	do {
		mask <<= 8;
		if ((local & mask) == (foreign & mask) &&
		    ((foreign & ~mask) == 0 || (foreign & ~mask) == ~mask))
			return 1;
	} while (mask);
	return 0;
}

unsigned long
ip_dst_addr (addr)
	unsigned long addr;
{
	struct netif *nif;
	struct ifaddr *ifa;
	
	if (addr == INADDR_ANY) {
		/* Connect to primary interface */
		nif = if_primary ();
		if (nif) {
			ifa = if_af2ifaddr (nif, AF_INET);
			if (ifa) return SIN (&ifa->addr)->sin_addr.s_addr;
		}
		DEBUG (("ip_dst_addr: no primary if with inet address"));
		return INADDR_LOOPBACK; /* sould never happen */
	} else if (addr == INADDR_BROADCAST) {
		/* Broadcast on local net */
		nif = if_primary ();
		if (nif && nif->flags & IFF_BROADCAST) {
			ifa = if_af2ifaddr (nif, AF_INET);
			if (ifa) return SIN (&ifa->ifu.broadaddr)->sin_addr.s_addr;
		}
		return INADDR_BROADCAST;
	}
	return addr;
}	

unsigned long
ip_netmask (addr)
	unsigned long addr;
{
	struct netif *nif;
	struct ifaddr *ifa;
	unsigned long netmask;

	if (IN_CLASSA (addr))
		netmask = IN_CLASSA_NET;
	else if (IN_CLASSB (addr))
		netmask = IN_CLASSB_NET;
	else if (IN_CLASSC (addr))
		netmask = IN_CLASSC_NET;
	else return 0;

	for (nif = allinterfaces; nif; nif = nif->next) {
		ifa = if_af2ifaddr (nif, AF_INET);
		if (ifa && ifa->net == (addr & netmask)) {
			return ifa->subnetmask;
		}
	}
	return netmask;
}

short
ip_priority (pri, tos)
	short pri;
	unsigned char tos;
{
	short newpri;

	newpri = (tos & IPTOS_LOWDELAY) ? 1 : 0;
	return MAX (pri, newpri);
}

void
ip_register (proto)
	struct in_ip_ops *proto;
{
	proto->next = allipprotos;
	allipprotos = proto;
}

static BUF *
ip_brdcst_copy (buf, nif, rt, addrtype)
	BUF *buf;
	struct netif *nif;
	struct route *rt;
	short addrtype;
{
	if (addrtype != IPADDR_BRDCST || nif == rt->nif)
		return 0;
	return buf_clone (buf, BUF_NORMAL);
}

/* Process the IP options in `iph'. For now, do nothing. */
static long
ip_do_opts (iph)
	struct ip_dgram *iph;
{
	return 0;
}

/* Forward IP datagram in `buf' to its next destination.
 * `local' -- 1 if locally generated datagram, 0 otherwise.
 * `nif'   -- network interface the datagram came from.
 */
long
ip_forward (buf, nif, local)
	BUF *buf;
	struct netif *nif;
	short local;
{
	struct ip_dgram *iph = (struct ip_dgram *)buf->dstart;
	struct route *rt;
	short addrtype;
	long r;

/* Validate incoming datagram */
	if (!local) {
		short pktlen = (long)buf->dend - (long)buf->dstart;
		
		if (pktlen < IP_MINLEN || pktlen != iph->length) {
			DEBUG (("ip_forward: invalid packet length"));
			buf_deref (buf, BUF_NORMAL);
			return 0;
		}
		if (chksum (iph, iph->hdrlen * sizeof (short))) {
			DEBUG (("ip_forward: bad checksum"));
			buf_deref (buf, BUF_NORMAL);
			return 0;
		}
		if (iph->version != IP_VERSION) {
			DEBUG (("ip_forward: %d: unsupp. IP version",
				iph->version));
			buf_deref (buf, BUF_NORMAL);
			return 0;
		}
	}

/* Process IP options */
	if (ip_do_opts (iph)) {
		DEBUG (("ip_forward: bad IP options"));
		buf_deref (buf, BUF_NORMAL);
		return EINVAL;
	}

/* Route datagram to next interface */
	rt = route_get (iph->daddr);
	if (!rt) {
		DEBUG (("ip_forward: not route to dst %lx", iph->daddr));
		icmp_send (ICMPT_DSTUR, ICMPC_NETUR, iph->saddr, buf, 0);
		return ENETUNREACH;
	}

	addrtype = ip_chk_addr (iph->daddr, rt);
	if (addrtype == IPADDR_BADCLASS) {
		DEBUG (("ip_forward: Dst addr not in class A/B/C"));
		buf_deref (buf, BUF_NORMAL);
		route_deref (rt);
		return EADDRNOTAVAIL;
	}

/* Check if the datagram is destined to this interface. If so send
 * the datagram to the local software. */
	if (!local && (addrtype == IPADDR_LOCAL || addrtype == IPADDR_BRDCST)) {
		struct in_ip_ops *p;
		BUF *buf2;

/* GATEWAY: If datagram is broadcast to a net we are directly connected to,
 * we must send the datagram to the local software AND forward it to the
 * network. buf2 will hold a copy if copying is necessary or zero if not. */
		buf2 = ip_brdcst_copy (buf, nif, rt, addrtype);

/* If the protocol input function returns 0, then it has taken over the packet
 * and we break out of the loop. If the function returns nonzero then go on.
 * Because the RAW handler is the last in the chain it will get all the packets
 * the other protocols don't want. */
		if ((buf = ip_defrag (buf))) {
		    for (p = allipprotos; p; p = p->next) {
			if ((p->proto == iph->proto || p->proto == IPPROTO_RAW)
			    && !(*p->input) (nif, buf, iph->saddr, iph->daddr))
				break;
		    }
		    if (!p) {
			DEBUG (("ip_forward: %d: No such proto", iph->proto));
			icmp_send (ICMPT_DSTUR,ICMPC_PROTOUR,iph->saddr,buf,0);
		    }
		}
		buf = buf2;
		if (!buf) {
			route_deref (rt);
			return 0;
		}
	}

/* See if the packets times out */
	if (!local && --iph->ttl <= 0) {
		DEBUG (("ip_forward: ttl exeeded"));
		icmp_send (ICMPT_TIMEX, ICMPC_TTLEX, iph->saddr, buf, 0);
		route_deref (rt);
		return 0;
	}

/* Fill in the source of the datagram (if not already done at higher level) */
	if (local && iph->saddr == INADDR_ANY) {
		struct ifaddr *ifa = if_af2ifaddr (rt->nif, AF_INET);

		if (!ifa) {
			DEBUG (("if_forward: chosen net if has no inet addr"));
			buf_deref (buf, BUF_NORMAL);
			route_deref (rt);
			return EADDRNOTAVAIL;
		}
		iph->saddr = SIN (&ifa->addr)->sin_addr.s_addr;
	}

/* Send ICMP redirects if necessary */
	if (!local && rt->nif == nif) {
		DEBUG (("ip_forward: sending redirect"));
		buf_deref (buf, BUF_NORMAL);
		route_deref (rt);
		return 0;
	}

/* Set output priority */
	if (!local)
		buf->info = ip_priority (0, iph->tos);

	r = ip_frag (buf, rt->nif,
		rt->flags & RTF_GATEWAY ? rt->gway : iph->daddr,
		addrtype == IPADDR_BRDCST);

	route_deref (rt);
	return r;
}

short ip_dgramid = 0;
static struct ip_options def_opts = { 0, IP_DEFAULT_TTL, IP_DEFAULT_TOS, 0 };

long
ip_send (saddr, daddr, buf, proto, flags, _opts)
	unsigned long saddr, daddr;
	BUF *buf;
	short proto, flags;
	struct ip_options *_opts;
{
	BUF *nbuf;
	struct ip_dgram *iph;
	struct ip_options *opts = _opts ? _opts : &def_opts;

	daddr = ip_dst_addr (daddr);
	if (!(flags & IP_BROADCAST) && ip_is_brdcst_addr (daddr)) {
		DEBUG (("ip_send: broadcasts not allowed"));
		buf_deref (buf, BUF_NORMAL);
		return EACCDN;
	}

/* Allocate and fill in IP header */
	nbuf = buf_reserve (buf, sizeof (*iph), BUF_RESERVE_START);
	if (!nbuf) {
		DEBUG (("ip_send: no space for IP header"));
		buf_deref (buf, BUF_NORMAL);
		return ENSMEM;
	}
	nbuf->dstart -= sizeof (*iph);
	iph = (struct ip_dgram *)nbuf->dstart;
	iph->version = IP_VERSION;
	iph->hdrlen  = sizeof (*iph) / sizeof (long);
	iph->tos     = opts->tos;
	iph->length  = (short)((long)nbuf->dend - (long)nbuf->dstart);
	iph->id      = ip_dgramid++;
	iph->fragoff = 0;
	iph->ttl     = opts->ttl;
	iph->proto   = proto;
	iph->saddr   = saddr;
	iph->daddr   = daddr;
	iph->chksum  = 0;

	nbuf->info = ip_priority (opts->pri, iph->tos);

	return ip_forward (nbuf, (struct netif *)0, 1);
}

void
ip_input (nif, buf)
	struct netif *nif;
	BUF *buf;
{
	ip_forward (buf, nif, 0);
}	

/* Strip off the options from the IP header `iph' which should only be
 * present in the first fragment and update the `hdrlen' field to
 * represent the new header length.
 */
static long
frag_opts (iph, optlen)
	struct ip_dgram *iph;
	long optlen;
{
	unsigned char *cp, len, type;
	long i;

	for (cp = iph->data, i = 0; i < optlen; ) {
		type = cp[i];
		switch (type & IPOPT_TYPE) {
		case IPOPT_EOL:
			++i;
			goto done;

		case IPOPT_NOP:
			++i;
			break;

		default:
			len = cp[i+1];
			if (type & IPOPT_COPY) i += len;
			else {
				memcpy (&cp[i], &cp[i+len], optlen-i-len);
				optlen -= len;
			}
			break;
		}
	}
done:
	while (i & 0x3) cp[i++] = IPOPT_NOP;
	i += IP_MINLEN;
	iph->hdrlen = i / sizeof (long);
	return i;
}

static long
ip_frag (buf, nif, nexthop, isbrcst)
	BUF *buf;
	struct netif *nif;
	unsigned long nexthop;
	short isbrcst;
{
	struct ip_dgram *fragiph, *iph = (struct ip_dgram *)buf->dstart;
	long fraglen, datalen, offset, fragoff, hdrlen, todo, r;
	BUF *fragbuf;
	char *data;

	if (iph->length <= nif->mtu) {
		iph->chksum = 0;
		iph->chksum = chksum (iph, iph->hdrlen * sizeof (short));
		return if_send (nif, buf, nexthop, isbrcst);
	}

	fragoff = iph->fragoff;
	hdrlen = iph->hdrlen * sizeof (long);
	fraglen = (nif->mtu - hdrlen) & ~7;
	datalen = iph->length - hdrlen;
	data = IP_DATA (buf);
	offset = 0;

	if (fragoff & IP_DF) {
		DEBUG (("ip_frag: need fragmentation, but DF set"));
		icmp_send (ICMPT_DSTUR, ICMPC_FNDF, iph->saddr, buf, 0);
		return EOPNOTSUPP;
	}
	if ((fragoff & IP_FRAGOFF) + datalen/8 > IP_FRAGOFF) {
		DEBUG (("ip_frag: datagram to long"));
		buf_deref (buf, BUF_NORMAL);
		return EINVAL;
	}
	while (datalen > 0) {
		if (datalen > fraglen) {
			todo = fraglen;
			fragbuf = buf_alloc (todo + hdrlen, 0, BUF_NORMAL);
			if (!fragbuf) {
				DEBUG (("ip_frag: out of bufs"));
				buf_deref (buf, BUF_NORMAL);
				return ENSMEM;
			}
			memcpy (fragbuf->dend, iph, hdrlen);
			fragbuf->dend += hdrlen;
		} else {
			todo = datalen;
			buf->dend = IP_DATA (buf);
			fragbuf = buf;
		}
		memcpy (fragbuf->dend, &data[offset], todo);
		fragbuf->dend += todo;
		fragbuf->info = buf->info;

		fragiph = (struct ip_dgram *)fragbuf->dstart;
		fragiph->length = hdrlen + todo;
		fragiph->fragoff += offset/8;

		datalen -= todo;
		if (datalen > 0) fragiph->fragoff |= IP_MF;

		fragiph->chksum = 0;
		fragiph->chksum = chksum (fragiph, hdrlen/2);
		r = if_send (nif, fragbuf, nexthop, isbrcst);
		if (r != 0) {
			DEBUG (("ip_frag: if_send failed with %ld", r));
			if (fragbuf != buf)
				buf_deref (buf, BUF_NORMAL);
			return r;
		}

		if (offset == 0 && hdrlen > IP_MINLEN) {
			/*
			 * after the first fragment has been sent,
			 * we strip off the options of the original
			 * IP header that should only be present in
			 * the first fragment.
			 */
			hdrlen = frag_opts (iph, hdrlen - IP_MINLEN);
		}
		offset += todo;
	}
	return 0;
}

static struct fragment allfrags[IPFRAG_HEADS];

static void
frag_delete (frag)
	struct fragment *frag;
{
	BUF *buf, *next;

	buf = frag->buf;
	frag->buf = 0;
	for (; buf; buf = next) {
		next = buf->link3;
		buf_deref (buf, BUF_NORMAL);
	}
	event_del (&frag->tmout);
}

static void
frag_timeout (arg)
	long arg;
{
	struct fragment *frag = (struct fragment *)arg;
	
	DEBUG (("frag_timeout: reassambly from id %x saddr 0x%lx timed out",
		frag->id, frag->saddr));

	icmp_send (ICMPT_TIMEX, ICMPC_FRAGEX, frag->saddr, frag->buf, 0);
	frag->buf = frag->buf->link3;
	frag_delete (frag);
}

static BUF *
frag_pullup (frag)
	struct fragment *frag;
{
	struct ip_dgram *iph;
	BUF *nbuf, *oldbuf;
	long length, offset;

	if (frag->totlen == 0 || frag->totlen != frag->curlen)
		return 0;

	TRACE (("frag_pullup: reassembling datagram from id %x saddr 0x%lx",
		frag->id, frag->saddr));

	iph = (struct ip_dgram *)frag->buf->dstart;
	length = iph->hdrlen * sizeof (long);

	nbuf = buf_alloc (length + frag->totlen, 0, BUF_NORMAL);
	if (!nbuf) {
		DEBUG (("frag_pullup: no space for new buf"));
		frag_delete (frag);
		return 0;
	}
	memcpy (nbuf->dend, iph, length);
	nbuf->dend += length;
	
	for (offset = 0; (oldbuf = frag->buf);) {
		iph = (struct ip_dgram *)oldbuf->dstart;
		if (offset != (iph->fragoff & IP_FRAGOFF)*8) {
			DEBUG (("frag_pullup: maleformed fragment list"));
			frag_delete (frag);
			return 0;
		}
		length = iph->length - iph->hdrlen * sizeof (long);
		memcpy (nbuf->dend, IP_DATA (oldbuf), length);
		nbuf->dend += length;
		offset += length;
		frag->buf = oldbuf->link3;
		buf_deref (oldbuf, BUF_NORMAL);
	}
	iph = (struct ip_dgram *)nbuf->dstart;
	iph->length = offset;
	iph->fragoff = 0;

	event_del (&frag->tmout);
	return nbuf;
}

static void
frag_insert (frag, buf)
	struct fragment *frag;
	BUF *buf;
{
#define OFFSET(x) (((struct ip_dgram *)(x)->dstart)->fragoff & IP_FRAGOFF)
	struct ip_dgram *iph = (struct ip_dgram *)buf->dstart;
	BUF **prev, *curr;
	long offset, datalen;

	prev = &frag->buf;
	curr = frag->buf;
	offset = iph->fragoff & IP_FRAGOFF;
	for (; curr; prev = &curr->link3, curr = curr->link3) {
		if (OFFSET (curr) > offset) break;
		else if (OFFSET (curr) == offset) {
			DEBUG (("frag_insert: dropping duplicate fragment"));
			buf_deref (buf, BUF_NORMAL);
			return;
		}
	}
	buf->link3 = curr;
	*prev = buf;

	datalen = iph->length - iph->hdrlen * sizeof (long);
	frag->curlen += datalen;
	if (!curr && !(iph->fragoff & IP_MF))
		frag->totlen = offset*8 + datalen;
#if 0
	if (event_delta (&frag->tmout) < iph->ttl*(1000/EVTGRAN))
		event_reset (&frag->tmout, iph->ttl*(1000/EVTGRAN));
#endif
}

static BUF *
ip_defrag (buf)
	BUF *buf;
{
	struct ip_dgram *iph = (struct ip_dgram *)buf->dstart;
	struct fragment *frag, *free;
	short i;

	if ((iph->fragoff & (IP_MF|IP_FRAGOFF)) == 0) return buf;

	free = 0;
	for (i = IPFRAG_HEADS, frag = allfrags; i > 0; --i, ++frag) {
		if (frag->buf) {
			if (iph->id == frag->id && iph->saddr == frag->saddr) {
				frag_insert (frag, buf);
				return frag_pullup (frag);
			}
		} else	free = frag;
	}
	if (free) {
		free->totlen = 0;
		free->curlen = 0;
		free->id = iph->id;
		free->saddr = iph->saddr;
		event_add (&free->tmout,IPFRAG_TMOUT,frag_timeout,(long)free);
		frag_insert (free, buf);
	} else {
		DEBUG (("ip_defrag: no more fragment slots, dropping dgram"));
		buf_deref (buf, BUF_NORMAL);
	}
	return 0;
}

long
ip_setsockopt (opts, level, optname, optval, optlen)
	struct ip_options *opts;
	short level, optname;
	char *optval;
	long optlen;
{
	if (level != IPPROTO_IP) return EOPNOTSUPP;

	switch (optname) {
	case IP_OPTIONS:
		break;

	case IP_HDRINCL:
		if (optlen != sizeof (long) || !optval)
			return EINVAL;
		opts->hdrincl = *(long *)optval ? 1 : 0;
		return 0;

	case IP_TOS:
		if (optlen != sizeof (long) || !optval)
			return EINVAL;
		opts->tos = (char)*(long *)optval;

	case IP_TTL:
		if (optlen != sizeof (long) || !optval)
			return EINVAL;
		opts->ttl = (char)*(long *)optval;
		return 0;

	case IP_RECVOPTS:
	case IP_RECVRETOPTS:
	case IP_RECVDSTADDR:
	case IP_RETOPTS:
		break;
	}
	return EOPNOTSUPP;
}

long
ip_getsockopt (opts, level, optname, optval, optlen)
	struct ip_options *opts;
	short level, optname;
	char *optval;
	long *optlen;
{
	if (level != IPPROTO_IP) return EOPNOTSUPP;

	switch (optname) {
	case IP_OPTIONS:
		break;

	case IP_HDRINCL:
		if (!optval || !optlen || *optlen < sizeof (long))
			return EINVAL;
		*(long *)optval = !!opts->hdrincl;
		*optlen = sizeof (long);
		return 0;

	case IP_TOS:
		if (!optval || !optlen || *optlen < sizeof (long))
			return EINVAL;
		*(long *)optval = (unsigned long)opts->tos;
		*optlen = sizeof (long);
		return 0;

	case IP_TTL:
		if (!optval || !optlen || *optlen < sizeof (long))
			return EINVAL;
		*(long *)optval = (unsigned long)opts->ttl;
		*optlen = sizeof (long);
		return 0;

	case IP_RECVOPTS:
	case IP_RECVRETOPTS:
	case IP_RECVDSTADDR:
	case IP_RETOPTS:
		break;
	}
	return EOPNOTSUPP;
}
