/*
 * transport.c - handles transport protocol packets (UDP and TCP)
 */

#include <base/stddef.h>
#include <base/hash.h>
#include <runtime/rculist.h>
#include <runtime/sync.h>
#include <runtime/net.h>
#include <net/ip.h>

#include "defs.h"

#define TRANS_TBL_SIZE	16384

/* ephemeral port definitions (IANA suggested range) */
#define MIN_EPHEMERAL		49152
#define MAX_EPHEMERAL		65535

/* a seed value for transport handler table hashing calculations */
static uint32_t trans_seed;

/* a simple counter used to further randomize ephemeral ports */
static uint32_t ephemeral_offset;

static inline uint32_t trans_hash_3tuple(uint8_t proto, struct netaddr laddr)
{
	return hash_crc32c_one(trans_seed,
		(uint64_t)laddr.ip | ((uint64_t)laddr.port << 32) |
		((uint64_t)proto << 48));
}

static inline uint32_t trans_hash_5tuple(uint8_t proto, struct netaddr laddr,
				         struct netaddr raddr)
{
	return hash_crc32c_two(trans_seed,
		(uint64_t)laddr.ip | ((uint64_t)laddr.port << 32),
		(uint64_t)raddr.ip | ((uint64_t)raddr.port << 32) |
		((uint64_t)proto << 48));
}

static DEFINE_SPINLOCK(trans_lock);
static struct rcu_hlist_head trans_tbl[TRANS_TBL_SIZE];

/**
 * trans_table_add - adds an entry to the match table
 * @e: the entry to add
 *
 * Returns 0 if successful, or -EADDRINUSE if a conflicting entry is already in
 * the table, or -EINVAL if the local port is zero.
 */
int trans_table_add(struct trans_entry *e)
{
	struct trans_entry *pos;
	struct rcu_hlist_node *node;
	uint32_t idx;

	/* port zero is reserved for ephemeral port auto-assign */
	if (e->laddr.port == 0)
		return -EINVAL;

	assert(e->match == TRANS_MATCH_3TUPLE ||
	       e->match == TRANS_MATCH_5TUPLE);
	if (e->match == TRANS_MATCH_3TUPLE)
		idx = trans_hash_3tuple(e->proto, e->laddr);
	else
		idx = trans_hash_5tuple(e->proto, e->laddr, e->raddr);
	idx %= TRANS_TBL_SIZE;

	spin_lock_np(&trans_lock);
	rcu_hlist_for_each(&trans_tbl[idx], node, true) {
		pos = rcu_hlist_entry(node, struct trans_entry, link);
		if (pos->match != e->match)
			continue;
		if (e->match == TRANS_MATCH_3TUPLE &&
		    e->proto == pos->proto &&
		    e->laddr.ip == pos->laddr.ip &&
		    e->laddr.port == pos->laddr.port) {
			spin_unlock_np(&trans_lock);
			return -EADDRINUSE;
		} else if (e->proto == pos->proto &&
			   e->laddr.ip == pos->laddr.ip &&
			   e->laddr.port == pos->laddr.port &&
			   e->raddr.ip == pos->raddr.ip &&
			   e->raddr.port == pos->raddr.port) {
			spin_unlock_np(&trans_lock);
			return -EADDRINUSE;
		}
	}
	rcu_hlist_add_head(&trans_tbl[idx], &e->link);
	store_release(&ephemeral_offset, ephemeral_offset + 1);
	spin_unlock_np(&trans_lock);

	return 0;
}

/**
 * trans_table_add_with_ephemeral_port - adds an entry to the match table
 * while automatically selecting the local port number
 * @e: the entry to add
 *
 * We use algorithm 3 from RFC 6056.
 *
 * Returns 0 if successful or -EADDRNOTAVAIL if all ports are taken.
 */
int trans_table_add_with_ephemeral_port(struct trans_entry *e)
{
	uint16_t offset, next_ephemeral = 0;
	uint16_t num_ephemeral = MAX_EPHEMERAL - MIN_EPHEMERAL + 1;
	int ret;

	if (e->match != TRANS_MATCH_5TUPLE)
		return -EINVAL;

	e->laddr.port = 0;
	offset = trans_hash_5tuple(e->proto, e->laddr, e->raddr) +
							load_acquire(&ephemeral_offset);
	while (next_ephemeral < num_ephemeral) {
		uint32_t port = MIN_EPHEMERAL +
				(next_ephemeral++ + offset) % num_ephemeral;
		e->laddr.port = port;
		ret = trans_table_add(e);
		if (!ret)
			return 0;
	}

	return -EADDRNOTAVAIL;
}

/**
 * trans_table_remove - removes an entry from the match table
 * @e: the entry to remove
 *
 * The caller is responsible for eventually freeing the object with rcu_free().
 */
void trans_table_remove(struct trans_entry *e)
{
	spin_lock_np(&trans_lock);
	rcu_hlist_del(&e->link);
	spin_unlock_np(&trans_lock);
}

/* the first 4 bytes are identical for TCP and UDP */
struct l4_hdr {
	uint16_t sport, dport;
};

static struct trans_entry *trans_lookup(struct mbuf *m)
{
	const struct ip_hdr *iphdr;
	const struct l4_hdr *l4hdr;
	struct trans_entry *e;
	struct rcu_hlist_node *node;
	struct netaddr laddr, raddr;
	uint32_t hash;

	assert(rcu_read_lock_held());

	/* set up the network header pointers */
	mbuf_mark_transport_offset(m);
	iphdr = mbuf_network_hdr(m, *iphdr);
	if (unlikely(iphdr->proto != IPPROTO_UDP &&
		     iphdr->proto != IPPROTO_TCP))
		return NULL;
	l4hdr = (struct l4_hdr *)mbuf_data(m);
	if (unlikely(mbuf_length(m) < sizeof(*l4hdr)))
		return NULL;

	/* parse the source and destination network address */
	laddr.ip = ntoh32(iphdr->daddr);
	laddr.port = ntoh16(l4hdr->dport);
	raddr.ip = ntoh32(iphdr->saddr);
	raddr.port = ntoh16(l4hdr->sport);

	/* attempt to find a 5-tuple match */
	hash = trans_hash_5tuple(iphdr->proto, laddr, raddr);
	rcu_hlist_for_each(&trans_tbl[hash % TRANS_TBL_SIZE], node, false) {
		e = rcu_hlist_entry(node, struct trans_entry, link);
		if (e->match != TRANS_MATCH_5TUPLE)
			continue;
		if (e->proto == iphdr->proto &&
		    e->laddr.ip == laddr.ip && e->laddr.port == laddr.port &&
		    e->raddr.ip == raddr.ip && e->raddr.port == raddr.port) {
			return e;
		}
	}

	/* attempt to find a 3-tuple match */
	hash = trans_hash_3tuple(iphdr->proto, laddr);
	rcu_hlist_for_each(&trans_tbl[hash % TRANS_TBL_SIZE], node, false) {
		e = rcu_hlist_entry(node, struct trans_entry, link);
		if (e->match != TRANS_MATCH_3TUPLE)
			continue;
		if (e->proto == iphdr->proto &&
		    e->laddr.ip == laddr.ip && e->laddr.port == laddr.port) {
			return e;
		}
	}

	return NULL;
}

/**
 * net_rx_trans - receive L4 packets
 * @ms: an array of mbufs to process
 * @nr: the size of the @ms array
 */
void net_rx_trans(struct mbuf **ms, const unsigned int nr)
{
	int i;
	const struct ip_hdr *iphdr;

	/* deliver each packet to a L4 protocol handler */
	for (i = 0; i < nr; i++) {
		struct mbuf *m = ms[i];
		struct trans_entry *e;

		rcu_read_lock();
		e = trans_lookup(m);
		if (unlikely(!e)) {
			rcu_read_unlock();
			iphdr = mbuf_network_hdr(m, *iphdr);
			if (iphdr->proto == IPPROTO_TCP)
				tcp_rx_closed(m);
			mbuf_free(m);
			continue;
		}
		e->ops->recv(e, m);
		rcu_read_unlock();
	}
}

/**
 * trans_error - reports a network error to the L4 layer
 * @m: the mbuf that triggered the error
 * @err: the suggested ernno to report
 */
void trans_error(struct mbuf *m, int err)
{
	struct trans_entry *e;

        rcu_read_lock();
	e = trans_lookup(m);
	if (e && e->ops->err)
		e->ops->err(e, err);
	rcu_read_unlock();
}

/**
 * trans_init - initializes transport protocol infrastructure
 *
 * Returns 0 (always successful).
 */
int trans_init(void)
{
	int i;

	spin_lock_init(&trans_lock);

	for (i = 0; i < TRANS_TBL_SIZE; i++)
		rcu_hlist_init_head(&trans_tbl[i]);

	trans_seed = rand_crc32c(0x48FA8BC1 ^ iok.key);
	return 0;
}
