/* NHRP packet handling functions
 * Copyright (c) 2014-2015 Timo Teräs
 *
 * This file is free software: you may copy, redistribute 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.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <netinet/if_ether.h>
#include "nhrpd.h"
#include "zbuf.h"
#include "thread.h"
#include "hash.h"

#include "nhrp_protocol.h"
#include "os.h"

struct nhrp_reqid_pool nhrp_packet_reqid;

static uint16_t family2proto(int family)
{
	switch (family) {
	case AF_INET:
		return ETH_P_IP;
	case AF_INET6:
		return ETH_P_IPV6;
	}
	return 0;
}

static int proto2family(uint16_t proto)
{
	switch (proto) {
	case ETH_P_IP:
		return AF_INET;
	case ETH_P_IPV6:
		return AF_INET6;
	}
	return AF_UNSPEC;
}

struct nhrp_packet_header *nhrp_packet_push(struct zbuf *zb, uint8_t type,
					    const union sockunion *src_nbma,
					    const union sockunion *src_proto,
					    const union sockunion *dst_proto)
{
	struct nhrp_packet_header *hdr;

	hdr = zbuf_push(zb, struct nhrp_packet_header);
	if (!hdr)
		return NULL;

	*hdr = (struct nhrp_packet_header){
		.afnum = htons(family2afi(sockunion_family(src_nbma))),
		.protocol_type =
			htons(family2proto(sockunion_family(src_proto))),
		.version = NHRP_VERSION_RFC2332,
		.type = type,
		.hop_count = 64,
		.src_nbma_address_len = sockunion_get_addrlen(src_nbma),
		.src_protocol_address_len = sockunion_get_addrlen(src_proto),
		.dst_protocol_address_len = sockunion_get_addrlen(dst_proto),
	};

	zbuf_put(zb, sockunion_get_addr(src_nbma), hdr->src_nbma_address_len);
	zbuf_put(zb, sockunion_get_addr(src_proto),
		 hdr->src_protocol_address_len);
	zbuf_put(zb, sockunion_get_addr(dst_proto),
		 hdr->dst_protocol_address_len);

	return hdr;
}

struct nhrp_packet_header *nhrp_packet_pull(struct zbuf *zb,
					    union sockunion *src_nbma,
					    union sockunion *src_proto,
					    union sockunion *dst_proto)
{
	struct nhrp_packet_header *hdr;

	hdr = zbuf_pull(zb, struct nhrp_packet_header);
	if (!hdr)
		return NULL;

	sockunion_set(src_nbma, afi2family(htons(hdr->afnum)),
		      zbuf_pulln(zb,
				 hdr->src_nbma_address_len
					 + hdr->src_nbma_subaddress_len),
		      hdr->src_nbma_address_len + hdr->src_nbma_subaddress_len);
	sockunion_set(src_proto, proto2family(htons(hdr->protocol_type)),
		      zbuf_pulln(zb, hdr->src_protocol_address_len),
		      hdr->src_protocol_address_len);
	sockunion_set(dst_proto, proto2family(htons(hdr->protocol_type)),
		      zbuf_pulln(zb, hdr->dst_protocol_address_len),
		      hdr->dst_protocol_address_len);

	return hdr;
}

uint16_t nhrp_packet_calculate_checksum(const uint8_t *pdu, uint16_t len)
{
	const uint16_t *pdu16 = (const uint16_t *)pdu;
	uint32_t csum = 0;
	int i;

	for (i = 0; i < len / 2; i++)
		csum += pdu16[i];
	if (len & 1)
		csum += htons(pdu[len - 1]);

	while (csum & 0xffff0000)
		csum = (csum & 0xffff) + (csum >> 16);

	return (~csum) & 0xffff;
}

void nhrp_packet_complete(struct zbuf *zb, struct nhrp_packet_header *hdr)
{
	unsigned short size;

	if (hdr->extension_offset)
		nhrp_ext_push(zb, hdr,
			      NHRP_EXTENSION_END
				      | NHRP_EXTENSION_FLAG_COMPULSORY);

	size = zb->tail - (uint8_t *)hdr;
	hdr->packet_size = htons(size);
	hdr->checksum = 0;
	hdr->checksum = nhrp_packet_calculate_checksum((uint8_t *)hdr, size);
}

struct nhrp_cie_header *nhrp_cie_push(struct zbuf *zb, uint8_t code,
				      const union sockunion *nbma,
				      const union sockunion *proto)
{
	struct nhrp_cie_header *cie;

	cie = zbuf_push(zb, struct nhrp_cie_header);
	*cie = (struct nhrp_cie_header){
		.code = code,
	};
	if (nbma) {
		cie->nbma_address_len = sockunion_get_addrlen(nbma);
		zbuf_put(zb, sockunion_get_addr(nbma), cie->nbma_address_len);
	}
	if (proto) {
		cie->protocol_address_len = sockunion_get_addrlen(proto);
		zbuf_put(zb, sockunion_get_addr(proto),
			 cie->protocol_address_len);
	}

	return cie;
}

struct nhrp_cie_header *nhrp_cie_pull(struct zbuf *zb,
				      struct nhrp_packet_header *hdr,
				      union sockunion *nbma,
				      union sockunion *proto)
{
	struct nhrp_cie_header *cie;

	cie = zbuf_pull(zb, struct nhrp_cie_header);
	if (!cie)
		return NULL;

	if (cie->nbma_address_len + cie->nbma_subaddress_len > 0) {
		sockunion_set(nbma, afi2family(htons(hdr->afnum)),
			      zbuf_pulln(zb,
					 cie->nbma_address_len
						 + cie->nbma_subaddress_len),
			      cie->nbma_address_len + cie->nbma_subaddress_len);
	} else {
		sockunion_family(nbma) = AF_UNSPEC;
	}

	if (cie->protocol_address_len) {
		sockunion_set(proto, proto2family(htons(hdr->protocol_type)),
			      zbuf_pulln(zb, cie->protocol_address_len),
			      cie->protocol_address_len);
	} else {
		sockunion_family(proto) = AF_UNSPEC;
	}

	return cie;
}

struct nhrp_extension_header *
nhrp_ext_push(struct zbuf *zb, struct nhrp_packet_header *hdr, uint16_t type)
{
	struct nhrp_extension_header *ext;
	ext = zbuf_push(zb, struct nhrp_extension_header);
	if (!ext)
		return NULL;

	if (!hdr->extension_offset)
		hdr->extension_offset =
			htons(zb->tail - (uint8_t *)hdr
			      - sizeof(struct nhrp_extension_header));

	*ext = (struct nhrp_extension_header){
		.type = htons(type), .length = 0,
	};
	return ext;
}

void nhrp_ext_complete(struct zbuf *zb, struct nhrp_extension_header *ext)
{
	ext->length = htons(zb->tail - (uint8_t *)ext
			    - sizeof(struct nhrp_extension_header));
}

struct nhrp_extension_header *nhrp_ext_pull(struct zbuf *zb,
					    struct zbuf *payload)
{
	struct nhrp_extension_header *ext;
	uint16_t plen;

	ext = zbuf_pull(zb, struct nhrp_extension_header);
	if (!ext)
		return NULL;

	plen = htons(ext->length);
	zbuf_init(payload, zbuf_pulln(zb, plen), plen, plen);
	return ext;
}

void nhrp_ext_request(struct zbuf *zb, struct nhrp_packet_header *hdr,
		      struct interface *ifp)
{
	/* Place holders for standard extensions */
	nhrp_ext_push(zb, hdr,
		      NHRP_EXTENSION_FORWARD_TRANSIT_NHS
			      | NHRP_EXTENSION_FLAG_COMPULSORY);
	nhrp_ext_push(zb, hdr,
		      NHRP_EXTENSION_REVERSE_TRANSIT_NHS
			      | NHRP_EXTENSION_FLAG_COMPULSORY);
	nhrp_ext_push(zb, hdr,
		      NHRP_EXTENSION_RESPONDER_ADDRESS
			      | NHRP_EXTENSION_FLAG_COMPULSORY);
}

int nhrp_ext_reply(struct zbuf *zb, struct nhrp_packet_header *hdr,
		   struct interface *ifp, struct nhrp_extension_header *ext,
		   struct zbuf *extpayload)
{
	struct nhrp_interface *nifp = ifp->info;
	struct nhrp_afi_data *ad = &nifp->afi[htons(hdr->afnum)];
	struct nhrp_extension_header *dst;
	struct nhrp_cie_header *cie;
	uint16_t type;

	type = htons(ext->type) & ~NHRP_EXTENSION_FLAG_COMPULSORY;
	if (type == NHRP_EXTENSION_END)
		return 0;

	dst = nhrp_ext_push(zb, hdr, htons(ext->type));
	if (!dst)
		goto err;

	switch (type) {
	case NHRP_EXTENSION_RESPONDER_ADDRESS:
		cie = nhrp_cie_push(zb, NHRP_CODE_SUCCESS, &nifp->nbma,
				    &ad->addr);
		if (!cie)
			goto err;
		cie->mtu = htons(ad->mtu);
		cie->holding_time = htons(ad->holdtime);
		break;
	default:
		if (type & NHRP_EXTENSION_FLAG_COMPULSORY)
			goto err;
	/* fallthru */
	case NHRP_EXTENSION_FORWARD_TRANSIT_NHS:
	case NHRP_EXTENSION_REVERSE_TRANSIT_NHS:
		/* Supported compulsory extensions, and any
		 * non-compulsory that is not explicitly handled,
		 * should be just copied. */
		zbuf_copy(zb, extpayload, zbuf_used(extpayload));
		break;
	}
	nhrp_ext_complete(zb, dst);
	return 0;
err:
	zbuf_set_werror(zb);
	return -1;
}

static void nhrp_packet_recvraw(struct thread *t)
{
	int fd = THREAD_FD(t), ifindex;
	struct zbuf *zb;
	struct interface *ifp;
	struct nhrp_peer *p;
	union sockunion remote_nbma;
	uint8_t addr[64];
	size_t len, addrlen;

	thread_add_read(master, nhrp_packet_recvraw, 0, fd, NULL);

	zb = zbuf_alloc(1500);
	if (!zb)
		return;

	len = zbuf_size(zb);
	addrlen = sizeof(addr);
	if (os_recvmsg(zb->buf, &len, &ifindex, addr, &addrlen) < 0)
		goto err;

	zb->head = zb->buf;
	zb->tail = zb->buf + len;

	switch (addrlen) {
	case 4:
		sockunion_set(&remote_nbma, AF_INET, addr, addrlen);
		break;
	default:
		goto err;
	}

	ifp = if_lookup_by_index(ifindex, VRF_DEFAULT);
	if (!ifp)
		goto err;

	p = nhrp_peer_get(ifp, &remote_nbma);
	if (!p)
		goto err;

	nhrp_peer_recv(p, zb);
	nhrp_peer_unref(p);
	return;

err:
	zbuf_free(zb);
}

int nhrp_packet_init(void)
{
	thread_add_read(master, nhrp_packet_recvraw, 0, os_socket(), NULL);
	return 0;
}
