/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only */
/* Copyright (c) 2017-2025 Brett Sheffield <bacs@librecast.net> */

#define _GNU_SOURCE
#include <librecast_pvt.h>
#include <librecast/net.h>
#include <librecast/router.h>
#ifdef HAVE_LIBLCRQ
#include <lcrq.h>
#endif
#ifdef HAVE_MLD
#include <mld.h>
#endif
#include "hash.h"
#include <librecast/sync.h>
#include <librecast/mdex.h>
#include <librecast/q.h>
#include <arpa/inet.h>
#include <assert.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <poll.h>
#ifdef HAVE_LINUX_NETLINK_H
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#endif

#ifdef HAVE_LIBLCRQ
# define ESI(row, col) chan->ESI[row * nesi + col]
# define ESIr(row) &chan->ESI[row * nesi]
# define BLK(row, col) &chan->rq_blk[(row * nesi + col) * T]
#endif
#if defined(HAVE_LIBLCRQ) && (HAVE_RQ_OTI)
# define OTI_OH sizeof(rq_oti_t) + sizeof(rq_scheme_t)
#else
# define OTI_OH 0
#endif

/* define macro for lcrq < 0.1.0 */
#ifndef  rq_pidset
# define rq_pidset(sbn, esi) ((sbn << 24) | (esi & 0x00ffffff));
#endif

#ifndef IPV6_MIN_MTU
#define IPV6_MIN_MTU 1280
#endif

#define SEND_RETRIES 254
#define SEND_RETRY_DELAY 100

lc_ctx_t *ctx_list = NULL;

static void lc_op_data_handler(lc_socket_call_t *sc, lc_message_t *msg);
static void lc_op_ping_handler(lc_socket_call_t *sc, lc_message_t *msg);
static void lc_op_pong_handler(lc_socket_call_t *sc, lc_message_t *msg);

int (*lc_msg_logger)(lc_channel_t *, lc_message_t *, void *logdb) = NULL;

void (*lc_op_handler[LC_OP_MAX])(lc_socket_call_t *, lc_message_t *) = {
	lc_op_data_handler,
	lc_op_ping_handler,
	lc_op_pong_handler,
};

int lc_getrandom(void *buf, size_t buflen)
{
	int err, fd;

	if ((fd = open("/dev/urandom", O_RDONLY)) == -1) return -1;
	err = read(fd, buf, buflen);
	close(fd);

	return err;
}

lc_ctx_t *lc_socket_ctx(lc_socket_t *sock)
{
	return sock->ctx;
}

lc_ctx_t *lc_channel_ctx(lc_channel_t *chan)
{
	return chan->ctx;
}

rq_t *lc_channel_rq(lc_channel_t *chan)
{
#ifdef HAVE_LIBLCRQ
	return chan->rq;
#else
	(void)chan;
	return (errno = ENOTSUP), NULL;
#endif
}

int lc_ctx_rq_overhead(lc_ctx_t *ctx, int overhead)
{
#ifdef HAVE_LIBLCRQ
	return (ctx->rq_oh = overhead);
#else
	(void)ctx, (void)overhead;
	return (errno = ENOTSUP), -1;
#endif
}

int lc_channel_rq_overhead(lc_channel_t *chan, int overhead)
{
#ifdef HAVE_LIBLCRQ
	return (chan->rq_oh = overhead);
#else
	(void)chan, (void)overhead;
	return (errno = ENOTSUP), -1;
#endif
}

lc_socket_t *lc_channel_socket(lc_channel_t *chan)
{
	return chan->sock;
}

unsigned char *lc_channel_get_hash(lc_channel_t *chan)
{
	return chan->hash;
}

struct in6_addr *lc_channel_in6addr(lc_channel_t *chan)
{
	if (!chan) return (errno = EINVAL), NULL;
	if (!chan->sa.sin6_family) {
		chan->sa.sin6_family = AF_INET6;
		lc_hashtoaddr(&chan->sa.sin6_addr, chan->hash, HASHSIZE, LC_DEFAULT_FLAGS);
	}
	if (!chan->sa.sin6_port) chan->sa.sin6_port = htons(LC_DEFAULT_PORT);
	return &(chan->sa.sin6_addr);
}

struct sockaddr_in6 *lc_channel_sockaddr(lc_channel_t *chan)
{
	return &chan->sa;
}

int lc_channel_socket_raw(lc_channel_t *chan)
{
	return chan->sock->sock;
}

lc_socket_t * lc_socket_by_fd(lc_ctx_t *ctx, int fd)
{
	lc_socket_t *sock = NULL;
	pthread_mutex_lock(&ctx->mtx);
	for (lc_socket_t *s = ctx->sock_list; s; s = s->next) {
		if (s->sock == fd) {
			sock = s;
			break;
		}
	}
	pthread_mutex_unlock(&ctx->mtx);
	return sock;
}

void lc_ctx_ifx(lc_ctx_t *ctx, unsigned int ifx)
{
	ctx->ifx = ifx;
}

void lc_ctx_ratelimit(lc_ctx_t *ctx, size_t bps_out, size_t bps_in)
{
	ctx->bps_out = bps_out;
	ctx->bps_in = bps_in;
}

void lc_channel_ratelimit(lc_channel_t *chan, size_t bps_out, size_t bps_in)
{
	chan->bps_out = bps_out;
	chan->bps_in = bps_in;
}

int lc_socket_raw(lc_socket_t *sock)
{
	return sock->sock;
}

void *lc_msg_data(lc_message_t *msg)
{
	return (msg) ? msg->data: NULL;
}

int lc_msg_get(lc_message_t *msg, lc_msg_attr_t attr, void **value)
{
	if (!msg || !value) return LC_ERROR_INVALID_PARAMS;
	switch (attr) {
	case LC_ATTR_DATA:
		*value = msg->data;
		break;
	case LC_ATTR_LEN:
		*value = (void *)&msg->len;
		break;
	case LC_ATTR_OPCODE:
		*value = (void *)&msg->op;
		break;
	default:
		return LC_ERROR_MSG_ATTR_UNKNOWN;
	}
	return 0;
}

int lc_msg_set(lc_message_t *msg, lc_msg_attr_t attr, void *value)
{
	if (!msg) return LC_ERROR_INVALID_PARAMS;
	switch (attr) {
	case LC_ATTR_DATA:
		msg->data = value;
		break;
	case LC_ATTR_LEN:
		msg->len = *(lc_len_t *)value;
		break;
	case LC_ATTR_OPCODE:
		msg->op = *(lc_opcode_t *)value;
		break;
	default:
		return LC_ERROR_MSG_ATTR_UNKNOWN;
	}
	return 0;
}

int lc_msg_id(lc_message_t *msg, unsigned char *id, size_t len)
{
#ifndef HASH_TYPE
	return (errno = ENOTSUP), -1;
#else
	hash_state state;
	hash_init(&state, NULL, 0, len);
	hash_update(&state, (unsigned char *)msg->data, msg->len);
	hash_update(&state, (unsigned char *)msg->srcaddr, sizeof(struct in6_addr));
	hash_final(&state, id, len);
	return 0;
#endif
}

int lc_socket_getopt(lc_socket_t *sock, int optname, void *optval, socklen_t *optlen)
{
	if (sock->type != LC_SOCK_IN6) return (errno = ENOTSUP), -1;
	return getsockopt(sock->sock, IPPROTO_IPV6, optname, optval, optlen);
}

int lc_socket_setopt(lc_socket_t *sock, int optname, const void *optval, socklen_t optlen)
{
	if (sock->type != LC_SOCK_IN6) return (errno = ENOTSUP), -1;
	return setsockopt(sock->sock, IPPROTO_IPV6, optname, optval, optlen);
}

int lc_socket_loop(lc_socket_t *sock, int val)
{
	if (sock->type != LC_SOCK_IN6) return (errno = ENOTSUP), -1;
	return setsockopt(sock->sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &val, sizeof val);
}

int lc_socket_ttl(lc_socket_t *sock, int val)
{
	sock->ttl = val;
	if (sock->type != LC_SOCK_IN6) return 0;
	return setsockopt(sock->sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &val, sizeof val);
}

lc_router_t *lc_socket_router(lc_socket_t *sock)
{
	return sock->router;
}

static void *_free(void *msg, void *hint)
{
	free(msg);
	return hint;
}

void lc_msg_free(void *arg)
{
	lc_message_t *msg = (lc_message_t *)arg;
	if (msg->free) {
		msg->free(msg->data, msg->hint);
		msg->data = NULL;
	}
}

void *lc_msg_init(lc_message_t *msg)
{
	return memset(msg, 0, sizeof(lc_message_t));
}

int lc_msg_init_data(lc_message_t *msg, void *data, size_t len, lc_free_fn_t *f, void *hint)
{
	lc_msg_init(msg);
	msg->len = len;
	msg->data = data;
	msg->free = f;
	msg->hint = hint;
	return 0;
}

int lc_msg_init_size(lc_message_t *msg, size_t len)
{
	lc_msg_init(msg);
	msg->data = malloc(len);
	if (!msg->data) return -1;
	msg->len = len;
	msg->free = &_free;
	return 0;
}

int lc_ctx_coding_get(lc_ctx_t *ctx)
{
	return ctx->coding;
}

int lc_ctx_coding_set(lc_ctx_t *ctx, int coding)
{
	return (ctx->coding = coding);
}

int lc_socket_coding_get(lc_socket_t *sock)
{
	return sock->ccoding;
}

int lc_socket_coding_set(lc_socket_t *sock, int coding)
{
	return (sock->ccoding = coding);
}

int lc_channel_coding_get(lc_channel_t *chan)
{
	return chan->coding;
}

int lc_channel_coding_set(lc_channel_t *chan, int coding)
{
	if (chan->sock) chan->sock->ccoding |= coding;
	return (chan->coding = coding);
}

static int lc_ctx_keygetorset(lc_ctx_t *ctx, lc_key_t *key, int type, int set)
{
	lc_key_t *k;
	switch (type) {
		case LC_CODE_SYMM:
			k = &ctx->key;
			break;
		case LC_CODE_PUBK:
			k = &ctx->pek;
			break;
		default:
			return (errno = EINVAL), -1;
	}
	if (set) {
		k->key = key->key;
		k->keylen = key->keylen;
	}
	else {
		key->key = k->key;
		key->keylen = k->keylen;
	}
	return 0;
}

static int lc_channel_keygetorset(lc_channel_t *chan, lc_key_t *key, int type, int set)
{
	lc_key_t *k;
	switch (type) {
		case LC_CODE_SYMM:
			k = &chan->key;
			break;
		case LC_CODE_PUBK:
			k = &chan->pek;
			break;
		case LC_CODE_SIGN:
			k = &chan->ssk;
			break;
		default:
			return (errno = EINVAL), -1;
	}
	if (set) {
		k->key = key->key;
		k->keylen = key->keylen;
	}
	else {
		key->key = k->key;
		key->keylen = k->keylen;
	}
	return 0;
}

int lc_ctx_getkey(lc_ctx_t *ctx, lc_key_t *key, int type)
{
	return lc_ctx_keygetorset(ctx, key, type, 0);
}

int lc_ctx_setkey(lc_ctx_t *ctx, lc_key_t *key, int type)
{
	return lc_ctx_keygetorset(ctx, key, type, 1);
}

int lc_ctx_set_sym_key(lc_ctx_t *ctx, unsigned char *key, size_t len)
{
	lc_key_t k = { .key = key, .keylen = len };
	return lc_ctx_setkey(ctx, &k, LC_CODE_SYMM);
}

int lc_ctx_set_pub_key(lc_ctx_t *ctx, unsigned char *key, size_t len)
{
	lc_key_t k = { .key = key, .keylen = len };
	return lc_ctx_setkey(ctx, &k, LC_CODE_PUBK);
}

int lc_channel_getkey(lc_channel_t *chan, lc_key_t *key, int type)
{
	return lc_channel_keygetorset(chan, key, type, 0);
}

int lc_channel_setkey(lc_channel_t *chan, lc_key_t *key, int type)
{
	return lc_channel_keygetorset(chan, key, type, 1);
}

int lc_channel_set_sym_key(lc_channel_t *chan, unsigned char *key, size_t len)
{
	lc_key_t k = { .key = key, .keylen = len };
	return lc_channel_setkey(chan, &k, LC_CODE_SYMM);
}

int lc_channel_set_pub_key(lc_channel_t *chan, unsigned char *key, size_t len)
{
	lc_key_t k = { .key = key, .keylen = len };
	return lc_channel_setkey(chan, &k, LC_CODE_PUBK);
}

static void lc_close_nack_logger(lc_channel_logging_t *logging)
{
	pthread_cancel(logging->nack_thread);
	pthread_join(logging->nack_thread, NULL);
	pthread_mutex_destroy(&logging->mutex);
	while (logging->oldest) {
		lc_packet_log_t * go = logging->oldest;
		logging->oldest = go->next;
		free(go);
	}
	free(logging);
}

static void free_gap_detect(lc_channel_gap_detect_t * gap_detect) {
	int saveerrno = errno;
	lc_socket_close(gap_detect->sock_nack);
	lc_channel_free(gap_detect->chan_nack);
	free(gap_detect);
	errno = saveerrno;
}

int lc_channel_detect_gaps(lc_channel_t *chan)
{
	lc_channel_gap_detect_t * gap_detect = calloc(1, sizeof(lc_channel_gap_detect_t));
	socklen_t looplen;
	int isloop;
	if (! gap_detect) return -1;
	gap_detect->next = 0;
	gap_detect->chan_nack = lc_channel_sidehash(chan, (void *)SIDE_CHANNEL_NACK, strlen(SIDE_CHANNEL_NACK));
	if (! gap_detect->chan_nack) goto error;
	gap_detect->sock_nack = lc_socket_new(chan->ctx);
	if (! gap_detect->sock_nack) goto error;
	/* if the original channel had loopback we'll need that too */
	looplen = sizeof(isloop);
	if (getsockopt(chan->sock->sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &isloop, &looplen) == 0)
		if (isloop)
			if (lc_socket_loop(gap_detect->sock_nack, 1)) goto error;
	lc_channel_bind(gap_detect->sock_nack, gap_detect->chan_nack);
	if (chan->gap_detect) free_gap_detect(chan->gap_detect);
	chan->gap_detect = gap_detect;
	return 0;
error:
	free_gap_detect(gap_detect);
	return -1;
}

static int do_gap_detection(lc_channel_gap_detect_t *gapd, lc_seq_t seq) {
	/* if we've never received a packet since setting this up, we'll assume packets
	 * before this one have all arrived (no way to know) */
	int duplicate = 0;
	if (! gapd->next) {
		gapd->next = seq + 1;
		gapd->bitmap = ~(uint64_t)0;
		return 0;
	}
	/* see if this is a retrasmission / arrived out of order */
	if (seq < gapd->next) {
		/* arrived out of order, update the bitmap if this is one of the previous
		 * 64 packets;
		 * don't do "if (seq >= gapd->next - 64)" in case the subtraction overflows */
		if (seq + 64 >= gapd->next) {
			if (gapd->bitmap & (uint64_t)1 << (seq + 64 - gapd->next))
				duplicate = 1;
			gapd->bitmap |= (uint64_t)1 << (seq + 64 - gapd->next);
		}
	}
	else {
		/* we'll likely need to shift the bitmap so that it covers seq - 63 to seq;
		 * if any bit we shift out is 0 that is the last chance to send a NACK for it */
		while (gapd->next <= seq) {
			if (! (gapd->bitmap & ((uint64_t)1 << 63))) {
				uint64_t miss = htobe64(gapd->next - 64);
				lc_socket_send(gapd->sock_nack, &miss, sizeof(miss), 0);
			}
			gapd->bitmap <<= 1;
			gapd->next++;
		}
		/* now gapd->bitmap == seq + 1 so bit 0 needs to be set: we've seen seq! */
		gapd->bitmap |= (uint64_t)1;
	}
	/* either way, check if there are any other NACKs we could do with sending; to avoid
	 * sending too many NACKs for the same packet we only check packets older than seq */
	if (~gapd->bitmap) {
		int bit;
		uint64_t mask = (uint64_t)1;
		lc_seq_t mseq = gapd->next - 1;
		for (bit = 0; bit < 64; bit++, mask <<= 1, mseq--) {
			if (mseq < seq && ! (gapd->bitmap & mask)) {
				uint64_t miss = htobe64(mseq);
				if (lc_socket_send(gapd->sock_nack, &miss, sizeof(miss), 0) == -1)
					duplicate = -1;
			}
		}
	}
	return duplicate;
}

int lc_channel_check_seqno(lc_channel_t *chan, lc_seq_t seq)
{
	if (chan && chan->gap_detect)
		return do_gap_detection(chan->gap_detect, seq);
	else return 0;
}

/* remove channel from ctx list */
static void channel_unlink(lc_channel_t *chan)
{
	pthread_mutex_lock(&chan->ctx->mtx);
	lc_channel_t *prev = NULL;
	lc_channel_t *next = aload(&chan->next);
	for (lc_channel_t *p = aload(&chan->ctx->chan_list); p; p = aload(&p->next)) {
		if (p == chan) {
			lc_channel_t **ptr = (prev) ? &prev->next : &chan->ctx->chan_list;
			CAE_loop(ptr, &p, next);
			break;
		}
		prev = p;
	}
	pthread_mutex_unlock(&chan->ctx->mtx);
}

void lc_channel_free(lc_channel_t * chan)
{
	int err = errno;
	if (!chan) return;
#ifdef HAVE_LIBLCRQ
	if (chan->rq) rq_free(chan->rq);
	if (chan->rq_blk) free (chan->rq_blk);
	if (chan->ESI) free(chan->ESI);
#endif
	if (chan->logging) lc_close_nack_logger(chan->logging);
	if (chan->gap_detect) free_gap_detect(chan->gap_detect);
	channel_unlink(chan);
	int deleted = 0;
	if (CAE(&chan->deleted, &deleted, DELETE_LATER))
		lc_channel_release(chan);
	errno = err;
}

static void chan_ratelimit(lc_channel_t *chan)
{
	uint64_t nss, nse;
	double s, bps;
	struct timespec now = {0};
	nss = chan->byt_reset.tv_nsec + chan->byt_reset.tv_sec * NANO;
	while (1) {
		if (clock_gettime(CLOCK_TAI, &now) == -1) break;
		nse = now.tv_nsec + now.tv_sec * NANO;
		s = (double)(nse - nss) / NANO;
		bps = chan->byt_out * 8 / s;
		if (bps < chan->bps_out) break;
		usleep(100);
	}
}

static void chan_byt_out(lc_channel_t *chan, size_t byt)
{
	fetch_add(&chan->byt_out, byt);
	fetch_add(&chan->sock->byt_out, byt);
}

static ssize_t chan_sendmsg(lc_channel_t *chan, struct msghdr *msg, int flags, ssize_t ohead)
{
	ssize_t rc = 0;
	if (flags & MSG_DROP) {
		for (int i = 0; i < (int)msg->msg_iovlen; i++) rc += msg->msg_iov[i].iov_len;
	}
	else {
		int retries = 0;
send_again:
		rc = sendmsg(chan->sock->sock, msg, flags);
		if (rc == -1 && errno == ENOBUFS && retries++ < SEND_RETRIES) {
			usleep(SEND_RETRY_DELAY);
			goto send_again;
		}
	}
	if (rc > 0) chan_byt_out(chan, rc);
	if (rc > ohead) rc -= ohead; /* return information bytes */
	return rc;
}

static void cleanup_freeifaddrs(void *arg)
{
	if (arg) freeifaddrs(arg);
}

/* socket is not bound to an interface. Send on ALL multicast capable interfaces */
static ssize_t chan_sendmsg_unbound(lc_channel_t *chan, struct msghdr *msg, int flags)
{
	int sock = chan->sock->sock;
	struct ifaddrs *ifap = NULL;
	ssize_t rc = 0;
	int oldstate;
	/* set cancelstate until we have cleanup macro ready */
	pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
	rc = getifaddrs(&ifap);
	if (rc == -1) return -1;
	pthread_cleanup_push(cleanup_freeifaddrs, ifap);
	pthread_setcancelstate(oldstate, NULL); /* and relax */
	for (struct ifaddrs *ifa = ifap; ifa; ifa = ifa->ifa_next) {
		if (ifa->ifa_addr == NULL) continue;
		if (ifa->ifa_addr->sa_family != AF_INET6) continue; /* IPv6 */
		if (!(ifa->ifa_flags & IFF_MULTICAST)) continue;    /* multicast */
		unsigned int ifx = if_nametoindex(ifa->ifa_name);
		rc = setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifx, sizeof ifx);
		if (rc == -1) break;
		rc = chan_sendmsg(chan, msg, flags, 0);
		if (rc == -1) break;
		/* process each interface only once */
		while (ifa->ifa_next && ifa->ifa_name == ifa->ifa_next->ifa_name) ifa = ifa->ifa_next;
	}
	pthread_cleanup_pop(1); /* freeifaddrs(ifap); */
	return rc;
}

static ssize_t chan_sendmsg_tlv(lc_channel_t *chan, struct msghdr *msg, int flags, ssize_t overhead)
{
	lc_tlv_t tlv_hash = { .type = LC_TLV_HASH, .len = HASHSIZE };
	const int hdrs = 2;
	struct iovec iov[msg->msg_iovlen + hdrs];
	const size_t iovlen = sizeof iov / sizeof iov[0];
	iov[0].iov_base = &tlv_hash;
	iov[0].iov_len = sizeof tlv_hash;
	iov[1].iov_base = chan->hash;
	iov[1].iov_len = HASHSIZE;
	for (int i = 0; i < hdrs; i++) overhead += iov[i].iov_len;
	for (int i = 0; i < (int)msg->msg_iovlen; i++) {
		iov[i + hdrs].iov_base = msg->msg_iov[i].iov_base;
		iov[i + hdrs].iov_len = msg->msg_iov[i].iov_len;
	}
	msg->msg_iov = iov;
	msg->msg_iovlen = iovlen;
	return chan_sendmsg(chan, msg, flags, overhead);
}

static ssize_t chan_prepare_sendmsg(lc_channel_t *chan, struct msghdr *msg, int flags, ssize_t overhead)
{
	const int hdrs = 1;
	struct iovec iov[msg->msg_iovlen + hdrs + 1];
	const size_t iovlen = sizeof iov / sizeof iov[0];
	unsigned char sig[crypto_sign_BYTES];
	if (chan->bps_out) chan_ratelimit(chan);
	if (chan->token) {
		if (!chan->ssk.key) return (errno = ENOKEY), -1;
		/* prepend token */
		iov[0].iov_base = chan->token;
		iov[0].iov_len = sizeof(lc_token_t);
		/* append buffer for msg signature */
		iov[iovlen - 1].iov_base = sig;
		iov[iovlen - 1].iov_len = sizeof sig;
		/* sign message (including token) */
		crypto_sign_state state;
		crypto_sign_init(&state);
		crypto_sign_update(&state, iov[0].iov_base, iov[0].iov_len);
		for (int i = 0; i < (int)msg->msg_iovlen; i++) {
			iov[i + hdrs].iov_base = msg->msg_iov[i].iov_base;
			iov[i + hdrs].iov_len = msg->msg_iov[i].iov_len;
			crypto_sign_update(&state, iov[i + hdrs].iov_base, iov[i + hdrs].iov_len);
		}
		if (crypto_sign_final_create(&state, sig, NULL, chan->ssk.key) == -1) {
			return (errno = ENOTRECOVERABLE), -1;
		}
		msg->msg_iov = iov;
		msg->msg_iovlen = iovlen;
		overhead += sizeof(lc_token_t) + sizeof sig;
	}
	if (chan->sock->type == LC_SOCK_IN6) {
		msg->msg_name = (struct sockaddr *)&chan->sa;
		msg->msg_namelen = sizeof(struct sockaddr_in6);
	}
	else if (chan->sock->type == LC_SOCK_PAIR) {
		if (lc_socket_oil_cmp(chan->sock, chan->hash)) return 0;
		return chan_sendmsg_tlv(chan, msg, flags, overhead);
	}
	if (!chan->sock->ifx) return chan_sendmsg_unbound(chan, msg, flags);
	return chan_sendmsg(chan, msg, flags, overhead);
}

static ssize_t chan_send(lc_channel_t *chan, const void *buf, size_t len, int flags, ssize_t ohead)
{
	struct iovec iov = { .iov_base = (void * const)buf, .iov_len = len};
	struct msghdr msg = { .msg_iov = &iov, .msg_iovlen = 1 };
	return chan_prepare_sendmsg(chan, &msg, flags, ohead);
}

#ifdef HAVE_LIBLCRQ
#ifdef HAVE_LIBSODIUM
#define OH_SYMM crypto_secretbox_MACBYTES + crypto_secretbox_NONCEBYTES
#if HAVE_RQ_OTI
#define OH_OTI sizeof(rq_oti_t) + sizeof(rq_scheme_t)
#endif
static unsigned char *encrypt_buffer(lc_channel_t *chan, const void *buf, size_t len)
{
	size_t elen = len + OH_SYMM;
	unsigned char *ebuf;
	unsigned char *nonce;
	ebuf = malloc(elen);
	if (!ebuf) return NULL;
	nonce = ebuf + len + crypto_secretbox_MACBYTES;
	randombytes_buf(nonce, crypto_secretbox_NONCEBYTES);
	crypto_secretbox_easy(ebuf, buf, len, nonce, chan->key.key);
	return ebuf;
}
#endif
static ssize_t channel_send_coded_rq(lc_channel_t *chan, const void *buf, size_t len, int flags)
{
	static ssize_t ohead;
	uint32_t esi = 0;
	uint8_t sbn = 0;
	rq_pid_t pid = 0;
	if (buf || !chan->rq) {
		/* new data, encode */
		unsigned char *data = (unsigned char *)buf;
		unsigned char *ebuf = NULL;
		if (chan->rq) rq_free(chan->rq); /* new data, free old rq ctx */
		hash_generic(chan->oid[0].hash, HASHSIZE, (void *)buf, len);
		ohead = HASHSIZE; /* OID */
#if HAVE_RQ_OTI
		if (chan->coding & LC_CODE_FEC_OTI) ohead += OH_OTI;
#endif
		chan->pid = pid;
#ifdef HAVE_LIBSODIUM
		if (chan->coding & LC_CODE_SYMM) {
			ebuf = encrypt_buffer(chan, buf, len);
			if (!ebuf) return -1;
			data = ebuf;
			len += OH_SYMM;
			ohead += OH_SYMM;
		}
#endif
		chan->rq = rq_init(len, chan->T);
		if (!chan->rq) return -1;
		if (rq_encode(chan->rq, data, len)) return -1;
		free(ebuf);
	}
	else {
		pid = chan->pid;
		sbn = rq_pid2sbn(pid);
		esi = rq_pid2esi(pid);
		if (!chan->rq) return -1; /* no rq object */
		if ((uint32_t)(rq_KP(chan->rq) + RQ_OVERHEAD * 2) == ++esi) {
			esi = 0;
			if (rq_Z(chan->rq) == ++sbn) sbn = 0;
		}
		pid = rq_pidset(sbn, esi);
		chan->pid = pid;
	}
	/* send next symbol */
	struct iovec iov[7];
	struct msghdr msg = { .msg_iov = iov, };
	uint8_t sym[chan->T];
	const int rqf = ((chan->coding & LC_CODE_FEC_RAND) || esi >= rq_K(chan->rq)) ? RQ_RAND : 0;
	int v = 0;
	/* prepend OID */
	iov[v].iov_base = chan->oid[0].hash;
	iov[v++].iov_len = HASHSIZE;
#if HAVE_RQ_OTI
	rq_oti_t oti = 0;
	rq_scheme_t scheme = 0;
	if (chan->coding & LC_CODE_FEC_OTI) {
		rq_oti(chan->rq, &oti, &scheme);
		iov[v].iov_base = &oti;
		iov[v++].iov_len = sizeof oti;
		iov[v].iov_base = &scheme;
		iov[v++].iov_len = sizeof scheme;
	}
#endif
	iov[v].iov_base = &pid;
	iov[v++].iov_len = sizeof pid;
	iov[v].iov_base = sym;
	iov[v++].iov_len = sizeof sym;
	msg.msg_iovlen = v;
	rq_symbol(chan->rq, &pid, sym, rqf);
	return chan_prepare_sendmsg(chan, &msg, flags, ohead);
}
#endif

#ifdef HAVE_LIBSODIUM
static ssize_t channel_send_symm(lc_channel_t *chan, const void *buf, size_t len, int flags)
{
	const size_t elen = len + crypto_secretbox_MACBYTES + crypto_secretbox_NONCEBYTES;
	unsigned char ebuf[elen];
	unsigned char *sbuf = (unsigned char *)buf;
	ssize_t ohead = 0;

	memset(ebuf, 0, elen);
	if (buf && (chan->coding & LC_CODE_SYMM)) {
		unsigned char *nonce = ebuf + len + crypto_secretbox_MACBYTES;
		randombytes_buf(nonce, crypto_secretbox_NONCEBYTES);
		crypto_secretbox_easy(ebuf, sbuf, len, nonce, chan->key.key);
		sbuf = ebuf;
		ohead += crypto_secretbox_MACBYTES + crypto_secretbox_NONCEBYTES;
	}
	return chan_send(chan, sbuf, elen, flags, ohead);
}
#endif

#ifdef HAVE_LIBLCRQ
static void channel_free_rq(lc_channel_t *chan)
{
	chan->orig = 0; chan->syms = 0;
	if (chan->rq_blk) { free (chan->rq_blk); chan->rq_blk = NULL; }
	if (chan->ESI) { free(chan->ESI); chan->ESI = NULL; }
	rq_free(chan->rq); chan->rq = NULL;
}
#endif

ssize_t lc_channel_send(lc_channel_t *chan, const void *buf, size_t len, int flags)
{
	if (chan->coding & LC_CODE_FEC_RQ) {
#ifdef HAVE_LIBLCRQ
		ssize_t rc = channel_send_coded_rq(chan, buf, len, flags);
		if (rc >= 0 && chan->rq_oh >= 0) {
			const int pkts = rq_KP(chan->rq) + chan->rq_oh;
			ssize_t byt = rc;
			for (int i = 1; i < pkts; i++) {
				rc = channel_send_coded_rq(chan, NULL, 0, flags);
				if (rc < 0) return rc;
				byt += rc;
			}
			rc = rq_F(chan->rq); /* return nett bytes */
			if (chan->coding & LC_CODE_SYMM) rc -= OH_SYMM;
		}
		return rc;
#else
		return (errno = ENOTSUP), -1;
#endif
	}
	if (chan->coding & LC_CODE_SYMM) {
#ifdef HAVE_LIBSODIUM
		return channel_send_symm(chan, buf, len, flags);
#else
		return (errno = ENOTSUP), -1;
#endif
	}
	return chan_send(chan, buf, len, flags, 0);
}

/* gather up msg parts into a single buffer for encoding */
static ssize_t channel_send_gather(lc_channel_t *chan, struct msghdr *msg, const size_t len, int flags)
{
	char buf[len];
	char *ptr = buf;
	for (int i = 0; i < (int)msg->msg_iovlen; i++) {
		memcpy(ptr, msg->msg_iov[i].iov_base, msg->msg_iov[i].iov_len);
		ptr += msg->msg_iov[i].iov_len;
	}
	return lc_channel_send(chan, buf, len, flags);
}

static ssize_t channel_send_encoded(lc_channel_t *chan, struct msghdr *msg, int flags)
{
	size_t len = 0;
	if (!msg) return lc_channel_send(chan, NULL, len, flags);
	for (int i = 0; i < (int)msg->msg_iovlen; i++) {
		len += msg->msg_iov[i].iov_len;
	}
	return channel_send_gather(chan, msg, len, flags);
}

ssize_t lc_channel_sendmsg(lc_channel_t *chan, struct msghdr *msg, int flags)
{
	if (chan->coding) return channel_send_encoded(chan, msg, flags);
	return chan_prepare_sendmsg(chan, msg, flags, 0);
}

ssize_t lc_socket_sendmsg(lc_socket_t *sock, struct msghdr *msg, int flags)
{
	ssize_t bytes = 0, rc = -1;
	if (!pthread_mutex_lock(&sock->ctx->mtx)) {
		for (lc_channel_t *chan = sock->ctx->chan_list; chan; chan = chan->next) {
			pthread_mutex_unlock(&sock->ctx->mtx);
			if (chan->sock == sock) {
				if ((rc = lc_channel_sendmsg(chan, msg, flags)) >= 0) {
					bytes += rc;
				}
				else return -1;
			}
			if (pthread_mutex_lock(&sock->ctx->mtx)) return -1;
		}
		pthread_mutex_unlock(&sock->ctx->mtx);
	}
	return (rc >= 0) ? bytes : -1;
}

ssize_t lc_socket_send(lc_socket_t *sock, const void *buf, size_t len, int flags)
{
	struct iovec iov = { .iov_base = (void * const)buf, .iov_len = len};
	struct msghdr msg = { .msg_iov = &iov, .msg_iovlen = 1 };
	return lc_socket_sendmsg(sock, &msg, flags);
}

int lc_channel_nack_handler_thr(lc_channel_t *chan, int n_seconds, void *(*nack_thread)(void *))
{
	pthread_attr_t attr;
	pthread_mutexattr_t mutexattr;
	int saveerrno;
	lc_channel_logging_t *logging = calloc(1, sizeof(lc_channel_logging_t));
	if (! logging) return -1;
	logging->chan_nack = lc_channel_sidehash(chan, (void *)SIDE_CHANNEL_NACK, strlen(SIDE_CHANNEL_NACK));
	if (! logging->chan_nack) goto error;
	logging->sock_nack = lc_socket_new(chan->ctx);
	if (! logging->sock_nack) goto error;
	lc_channel_bind(logging->sock_nack, logging->chan_nack);
	if (lc_channel_join(logging->chan_nack)) goto error;
	logging->chan_data = chan;
	logging->n_seconds = n_seconds;
	if (chan->logging) lc_close_nack_logger(chan->logging);
	chan->logging = logging;
	pthread_mutexattr_init(&mutexattr);
	pthread_mutex_init(&logging->mutex, &mutexattr);
	pthread_mutexattr_destroy(&mutexattr);
	logging->oldest = NULL;
	logging->newest = NULL;
	pthread_attr_init(&attr);
	pthread_create(&logging->nack_thread, &attr, nack_thread, logging);
	pthread_attr_destroy(&attr);
	return 0;
error:
	saveerrno = errno;
	if (logging->chan_nack) lc_channel_free(logging->chan_nack);
	if (logging->sock_nack) lc_socket_close(logging->sock_nack);
	free(logging);
	errno = saveerrno;
	return -1;
}

static void *nack_thread(void * _logging)
{
	lc_channel_logging_t * logging = _logging;
	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
	while (1) {
		/* wait for a NACK request for up to a second; if one arrives, process it;
		 * also go through the list of logged messages to see if anything needs
		 * to be removed */
		struct pollfd pfd = {
			.fd = logging->sock_nack->sock,
			.events = POLLIN,
		};
		lc_packet_log_t *oldest = aload(&logging->oldest);
		lc_packet_log_t *newest;
		int ok;
		ok = poll(&pfd, 1, 1000);
		if (ok > 0) {
			pthread_mutex_lock(&logging->mutex);
			/* process this request */
			uint64_t nack;
			lc_seq_t seq;
			if (lc_socket_recv(logging->sock_nack, &nack, sizeof(nack), 0) == -1) break;
			seq = be64toh(nack);
			/* see if we have this to resend */
			oldest = aload(&logging->oldest);
			newest = aload(&logging->newest);
			if (oldest && oldest->seq <= seq && seq <= newest->seq) {
				/* the message may be in the list, and we exit the critical section
				 * now as the only thing other thread may do is to add entries after
				 * logging->newest and we are going to make sure we don't look there;
				 * this improves performance for the other threads if the list gets
				 * very long */
				lc_packet_log_t * end = newest, * msg = NULL;
				/* find this message in the list */
				msg = oldest;
				while (msg && msg->seq < seq && msg != end) msg = msg->next;
				/* MUST unlock before lc_channel_send() */
				pthread_mutex_unlock(&logging->mutex);
				/* if found, resend it (no point checking return
				 * of lc_channel_send() here, as nowhere to send
				 * error. Just continue. */
				if (msg && msg->seq == seq)
					lc_channel_send(logging->chan_data, msg->data, msg->len, 0);
			}
			else pthread_mutex_unlock(&logging->mutex);
		}
		pthread_mutex_lock(&logging->mutex);
		oldest = aload(&logging->oldest);
		if (oldest) {
			time_t expire;
			expire = time(NULL) - logging->n_seconds;
			while (oldest && oldest->time < expire) {
				lc_packet_log_t * go = oldest;
				oldest = aload(&go->next);
				if (!oldest) astor(&logging->newest, NULL);
				free(go);
				oldest = aload(&logging->oldest);
			}
		}
		pthread_mutex_unlock(&logging->mutex);
	}
	return NULL;
}

int lc_channel_nack_handler(lc_channel_t *chan, int n_seconds)
{
	return lc_channel_nack_handler_thr(chan, n_seconds, nack_thread);
}

static int lc_channel_logmsg(lc_channel_logging_t *logging, const void *buf, size_t len, lc_seq_t seq)
{
	lc_packet_log_t * msg;
	msg = calloc(1, sizeof(lc_packet_log_t) + len);
	if (! msg) return -1;
	msg->next =  NULL;
	msg->len = len;
	msg->time = time(NULL);
	msg->seq = seq;
	memcpy(msg->data, buf, len);
	pthread_mutex_lock(&logging->mutex);
	if (logging->newest)
		logging->newest->next = msg;
	else
		astor(&logging->oldest, msg);
	astor(&logging->newest, msg);
	pthread_mutex_unlock(&logging->mutex);
	return 0;
}

int lc_channel_nack_add_log(lc_channel_t *chan, const void *buf, size_t len, lc_seq_t seq)
{
	if (chan && chan->logging)
		return lc_channel_logmsg(chan->logging, buf, len, seq);
	else return 0;
}

ssize_t lc_msg_sendto(int sock, const void *buf, size_t len, struct sockaddr_in6 *sa, int flags)
{
	if (flags & MSG_DROP) return len;
	return sendto(sock, buf, len, flags, (struct sockaddr *)sa, sizeof(struct sockaddr_in6));
}

ssize_t lc_msg_send(lc_channel_t *chan, lc_message_t *msg)
{
	lc_message_head_t *head = NULL;
	char *buf = NULL;
	size_t len = 0;
	size_t ohead = 0;
	ssize_t bytes = 0;
	struct timespec t = {0};
	int state = 0;
	int err = 0;

	if (!chan->sock) return LC_ERROR_SOCKET_REQUIRED;
	if (msg->len > 0 && !msg->data) return LC_ERROR_MESSAGE_EMPTY;

	pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &state);

	head = calloc(1, sizeof(lc_message_head_t));
	if (!head) return LC_ERROR_MALLOC;

	if (msg->timestamp)
		head->timestamp = htobe64(msg->timestamp);
	else if (!clock_gettime(CLOCK_REALTIME, &t))
		head->timestamp = htobe64(t.tv_sec * 1000000000 + t.tv_nsec);

	head->seq = htobe64(add_fetch(&chan->seq, 1));
	lc_getrandom(&head->rnd, sizeof(lc_rnd_t));
	head->len = htobe64(msg->len);
	head->op = msg->op;
	len = msg->len;
	if (chan->coding & LC_CODE_SYMM) {
#ifdef HAVE_LIBSODIUM
		ohead = crypto_secretbox_MACBYTES + crypto_secretbox_NONCEBYTES;
#else
		free(head);
		return (errno = ENOTSUP), -1;
#endif
	}
	buf = calloc(1, sizeof(lc_message_head_t) + len + ohead);
	if (!buf) {
		free(head);
		return LC_ERROR_MALLOC;
	}
	memcpy(buf, head, sizeof(lc_message_head_t));
	if (msg->data) memcpy(buf + sizeof(lc_message_head_t), msg->data, len);
	len += sizeof(lc_message_head_t);

	if (chan->coding & LC_CODE_SYMM) {
#ifdef HAVE_LIBSODIUM
		unsigned char *nonce;
		nonce = (unsigned char *)buf + len + crypto_secretbox_MACBYTES;
		randombytes_buf(nonce, crypto_secretbox_MACBYTES);
		crypto_secretbox_easy((unsigned char *)buf, (unsigned char *)buf,
				len, nonce, chan->key.key);
#endif
	}

	if (chan->logging)
		/* chan->seq could have been changed by another thread in the meantime,
		 * so use head->seq which we know hasn't */
		lc_channel_logmsg(chan->logging, buf, len + ohead, be64toh(head->seq));
	bytes = chan_send(chan, buf, len + ohead, 0, 0);
	if (bytes == -1) err = errno;

	free(head);
	free(buf);
	pthread_setcancelstate(state, NULL);

	if (err) errno = err;
	return bytes;
}

#ifndef IPV6_MULTICAST_ALL
static int lc_socket_group_joined(lc_socket_t *sock, struct in6_addr *grp)
{
	for (lc_grplist_t *g = sock->grps; g; g = g->next) {
		if (!memcmp(&g->grp, grp, sizeof(struct in6_addr))) return 1;
	}
	return 0;
}
#endif

lc_channel_t *lc_channel_by_address(lc_ctx_t *lctx, struct in6_addr *addr)
{
	lc_channel_t *p = NULL;
	if (!pthread_mutex_lock(&lctx->mtx)) {
		for (p = lctx->chan_list; p; p = p->next) {
			if (!memcmp(addr,& p->sa.sin6_addr, sizeof(struct in6_addr)))
				break;
		}
		pthread_mutex_unlock(&lctx->mtx);
	}
	if (!p) errno = ENOENT;
	return p;
}

lc_channel_t *lc_socket_channel_by_address(lc_socket_t *sock, struct in6_addr *addr)
{
	lc_ctx_t *lctx = lc_socket_ctx(sock);
	lc_channel_t *p = NULL;
	if (!pthread_mutex_lock(&lctx->mtx)) {
		for (p = aload(&lctx->chan_list); p; p = aload(&p->next)) {
			if (aload(&p->sock) != sock) continue;
			if (!memcmp(addr,& p->sa.sin6_addr, sizeof(struct in6_addr)))
				break;
		}
		pthread_mutex_unlock(&lctx->mtx);
	}
	if (!p) errno = ENOENT;
	return p;
}

lc_channel_t *lc_channel_by_hash(lc_ctx_t *lctx, unsigned char *hash, size_t hashlen)
{
	lc_channel_t *p = NULL;
	if (!pthread_mutex_lock(&lctx->mtx)) {
		for (p = lctx->chan_list; p; p = p->next) {
			if (!memcmp(p->hash, hash, hashlen)) break;
		}
		pthread_mutex_unlock(&lctx->mtx);
	}
	if (!p) errno = ENOENT;
	return p;
}

/* return channel which matches both the socket and hash */
lc_channel_t *lc_channel_socket_by_hash(lc_socket_t *sock, unsigned char *hash, size_t hashlen)
{
	lc_ctx_t *lctx = sock->ctx;
	lc_channel_t *p = NULL;
	if (!pthread_mutex_lock(&lctx->mtx)) {
		for (p = lctx->chan_list; p; p = p->next) {
			if (p->sock != sock) continue;
			if (!memcmp(p->hash, hash, hashlen)) break;
		}
		pthread_mutex_unlock(&lctx->mtx);
	}
	if (!p) errno = ENOENT;
	return p;
}

#ifdef HAVE_LIBSODIUM
static ssize_t lc_channel_decoded(lc_channel_t *chan, unsigned char *m, unsigned char *c, size_t bytes)
{
	size_t clen = bytes - crypto_secretbox_NONCEBYTES;
	size_t mlen = clen - crypto_secretbox_MACBYTES;
	unsigned char *nonce = c + clen;
	if (bytes < crypto_secretbox_NONCEBYTES + crypto_secretbox_MACBYTES + 1)
		return (errno = EMSGSIZE), -1;
	if (crypto_secretbox_open_easy(m, c, clen, nonce, chan->key.key))
		return (errno = EBADMSG), -1;
	return mlen;
}
#endif

#ifdef	HAVE_LINUX_NETLINK_H
static void handle_netlink_msg(lc_socket_t *sock, struct nlmsghdr *msg)
{
	struct ifinfomsg *ifi = NLMSG_DATA(msg);
	char ifname[IF_NAMESIZE];
	lc_ctx_t *lctx = sock->ctx;
	if (msg->nlmsg_type != RTM_NEWLINK) return;
	/* new link detected, JOIN channels */
	if_indextoname(ifi->ifi_index, ifname);
	if (!pthread_mutex_lock(&lctx->mtx)) {
		for (lc_channel_t *p = lctx->chan_list; p; p = p->next) {
			if (p->sock != sock) continue;
			lc_channel_join(p);
		}
		pthread_mutex_unlock(&lctx->mtx);
	}
}

/* handle netlink event */
static int recv_netlink_event(lc_socket_t *sock, int fd, struct sockaddr_nl *sa)
{
	struct nlmsghdr buf[8192/sizeof(struct nlmsghdr)];
	struct iovec iov = { buf, sizeof(buf) };
	struct msghdr msg = { sa, sizeof(*sa), &iov, 1, NULL, 0, 0 };
	struct nlmsghdr *nh;
	ssize_t len;
	len = recvmsg(fd, &msg, 0);
	if (len == -1) return -1;
	for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, len); nh = NLMSG_NEXT(nh, len)) {
		if (nh->nlmsg_type == NLMSG_DONE) break; /* end of multipart message */
		if (nh->nlmsg_type == NLMSG_ERROR) return -1;
		else handle_netlink_msg(sock, nh);
	}
	return 0;
}
#endif

static ssize_t socket_recvmsg(lc_socket_t *sock, struct msghdr *msg, int flags)
{
	enum {
		FD_NETLNK,
		FD_SOCKET
	};
	int fd = sock->sock;
#ifdef	HAVE_LINUX_NETLINK_H
	/* for IPv6 sockets which aren't bound, watch netlink */
	if (!sock->ifx && sock->type == LC_SOCK_IN6) {
		const nfds_t nfds = 2;
		struct pollfd fds[nfds];
		struct sockaddr_nl sa = {
			.nl_family = AF_NETLINK,
			.nl_groups = RTMGRP_LINK
		};
		int nlfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
		if (nlfd == -1) return -1;
		if (bind(nlfd, (struct sockaddr *) &sa, sizeof(sa))) return -1;
		fds[FD_NETLNK].fd = nlfd;
		fds[FD_NETLNK].events = POLLIN;
		fds[FD_SOCKET].fd = sock->sock;
		fds[FD_SOCKET].events = POLLIN;
		for (;;) {
			fd = poll(fds, nfds, -1);
			if (fd == -1) return -1;
			if (fds[FD_NETLNK].revents) {
				if (recv_netlink_event(sock, nlfd, &sa)) return -1;
			}
			if (fds[FD_SOCKET].revents) {
				fd = fds[FD_SOCKET].fd;
				break;
			}
		}
		close(nlfd);
	}
#endif
	return recvmsg(fd, msg, flags);
}

ssize_t lc_msg_recv(lc_socket_t *sock, lc_message_t *msg)
{
	ssize_t zi = 0;
	struct iovec iov[1];
	struct msghdr msgh = {0};
	struct in6_pktinfo pi = {0};
	struct sockaddr_in6 from;
	unsigned char buf[BUFSIZ];
	struct cmsghdr *cmsg;
	char ctl[CMSG_SPACE(sizeof(struct in6_pktinfo))];
	lc_message_head_t head;
	lc_channel_t *chan = NULL;
	int opt = 1;

#ifndef IPV6_MULTICAST_ALL
recv_again:
#endif
	iov[0].iov_base = buf;
	iov[0].iov_len = sizeof buf;
	msgh.msg_control = ctl;
	msgh.msg_controllen = sizeof ctl;
	msgh.msg_name = &from;
	msgh.msg_namelen = sizeof from;
	msgh.msg_iov = iov;
	msgh.msg_iovlen = 1;
	if (setsockopt(sock->sock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &opt, sizeof opt) == -1)
		return -1;
	pthread_testcancel();
	if ((zi = socket_recvmsg(sock, &msgh, 0)) <= 0) return zi;
	for (cmsg = CMSG_FIRSTHDR(&msgh); cmsg; cmsg = CMSG_NXTHDR(&msgh, cmsg)) {
		if (cmsg->cmsg_type == IPV6_PKTINFO) {
			/* may not be aligned, copy */
			memcpy(&pi, CMSG_DATA(cmsg), sizeof(struct in6_pktinfo));
			/* if we have a channel, we can use it for gap detection */
			chan = lc_channel_by_address(sock->ctx, &pi.ipi6_addr);
#ifndef IPV6_MULTICAST_ALL
			/* destination is group we haven't joined - drop it */
			if (!lc_socket_group_joined(sock, &pi.ipi6_addr)) goto recv_again;
#endif
			break;
		}
	}
	if (sock->ccoding & LC_CODE_SYMM) {
#ifdef HAVE_LIBSODIUM
		chan = lc_channel_by_address(sock->ctx, &pi.ipi6_addr);
		if (!chan) return -1;
		zi = lc_channel_decoded(chan, buf, buf, zi);
#else
		return (errno = ENOTSUP), -1;
#endif
	}
	if (lc_msg_init_size(msg, (size_t)zi - sizeof(lc_message_head_t)) == -1)
		return -1;
	memcpy(&head, buf, sizeof(lc_message_head_t));
	msg->dst = pi.ipi6_addr;
	msg->src = (&from)->sin6_addr;
	msg->seq = be64toh(head.seq);
	msg->rnd = be64toh(head.rnd);
	msg->timestamp = be64toh(head.timestamp);
	msg->op = head.op;
	if (chan && chan->gap_detect) do_gap_detection(chan->gap_detect, msg->seq);
	if (zi > (ssize_t)sizeof(lc_message_head_t)) {
		memcpy(msg->data, buf + sizeof(lc_message_head_t), msg->len);
	}
	return zi;
}

int lc_socket_listen_cancel(lc_socket_t *sock)
{
	if (sock->thread) {
		if (pthread_cancel(sock->thread))
			return LC_ERROR_THREAD_CANCEL;
		if (pthread_join(sock->thread, NULL))
			return LC_ERROR_THREAD_JOIN;
		sock->thread = 0;
	}
	return 0;
}

static void lc_op_pong_handler(lc_socket_call_t *sc, lc_message_t *msg)
{
	if (sc->callback_msg) sc->callback_msg(msg);
}

static void lc_op_ping_handler(lc_socket_call_t *sc, lc_message_t *msg)
{
	(void) sc; /* unused */
	int opt = LC_OP_PONG;

	/* received PING, echo PONG back to same channel */
	lc_msg_set(msg, LC_ATTR_OPCODE, &opt);
	lc_msg_send(msg->chan, msg);
}

static void lc_op_data_handler(lc_socket_call_t *sc, lc_message_t *msg)
{
	/* callback to message handler */
	if (sc->callback_msg) sc->callback_msg(msg);
}

static ssize_t lc_socket_recvmsg_if(lc_socket_t *sock, struct msghdr *msg, int flags,
		struct in6_pktinfo *pi, lc_channel_t **dst)
{
	char ctl[CMSG_SPACE(sizeof(struct in6_pktinfo))];
	struct cmsghdr *cmsg;
	ssize_t bytes;
	int opt = 1;

	/* We're only interested in packets arriving on the socket->ifx
	 * interface. If we bind to an interface-specific address, we will get no
	 * multicast packets. If bound to either INADDR_ANY or the multicast
	 * group address, we receive packets on all interfaces. So, we need to
	 * filter by extracting the receiving interface from ancillary data */

	if (sock->type == LC_SOCK_IN6) {
		if (setsockopt(sock->sock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &opt, sizeof opt) == -1)
			return -1;

		/* provide control buffer if caller hasn't */
		if (!msg->msg_control) {
			msg->msg_control = ctl;
			msg->msg_controllen = sizeof ctl;
		}
	}
recv_again:
	for (;;) {
		bytes = socket_recvmsg(sock, msg, flags);
		if (bytes == -1) return -1;
		if (sock->type != LC_SOCK_IN6) {
			if (dst) {
				lc_tlv_hash_t *tlv = msg->msg_iov[0].iov_base;
				*dst = lc_channel_socket_by_hash(sock, tlv->value, sizeof tlv->value);
			}
			return bytes;
		}
		for (cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) {
			if (cmsg->cmsg_type == IPV6_PKTINFO) {
				memcpy(pi, CMSG_DATA(cmsg), sizeof(struct in6_pktinfo));
#ifndef IPV6_MULTICAST_ALL
				if (!lc_socket_group_joined(sock, &pi->ipi6_addr)) {
					if (flags & MSG_PEEK) {
						/* read & drop packet */
						int beijing = flags^MSG_PEEK;
						recvmsg(sock->sock, msg, beijing);
					}
				}
				else
#endif
				if (!sock->ifx || sock->ifx == pi->ipi6_ifindex) {
					if (sock->chan_active || dst) {
						lc_channel_t *chan;
						chan = lc_socket_channel_by_address(sock, &pi->ipi6_addr);
						if (dst) *dst = chan;
						if (sock->chan_active) {
							/* for lc_channel_* calls, check channel */
							if (chan != sock->chan_active) {
								if (flags & MSG_DROP) goto recv_again;
								return (errno = ECHRNG), -1;
							}
						}
					}
					return bytes;
				}
				else break;
			}
		}
	}
	/* not reached */
}

static ssize_t scatter_msg_parts(unsigned char *buf, size_t len, struct msghdr *msg, ssize_t rc)
{
	unsigned char *ptr = buf;
	for (int v = 0; v < (int)msg->msg_iovlen; v++) {
		memcpy(msg->msg_iov[v].iov_base, ptr, MIN(msg->msg_iov[v].iov_len, len));
		ptr += msg->msg_iov[v].iov_len;
		len -= msg->msg_iov[v].iov_len;
	}
	return rc;
}

#ifdef HAVE_LIBLCRQ
static int lc_channel_rq_init(lc_channel_t *chan, size_t len)
{
	if (!chan->rq) chan->rq = rq_init(len, chan->T);
	if (!chan->rq) return -1;
	if (!chan->rq_blk) {
		/* alloc ESI + symbol buffer */
		const uint16_t Z = rq_Z(chan->rq);
		const uint16_t nesi = rq_KP(chan->rq) + RQ_OVERHEAD;
		const uint32_t M = nesi * (uint32_t)Z;
		chan->ESI = calloc(Z, nesi * sizeof(uint32_t));
		if (!chan->ESI) goto err_free_rq;
		memset(chan->ESI, ~0, M * sizeof(uint32_t));
		chan->rq_blk = calloc(Z, (size_t)nesi * chan->T);
		if (!chan->rq_blk) goto err_free_ESI;
		if (chan->syms == 0) chan->orig = rq_K(chan->rq);
	}
	return 0;
err_free_ESI:
	free(chan->ESI);
	chan->ESI = NULL;
err_free_rq:
	rq_free(chan->rq);
	chan->rq = NULL;
	return -1;
}

static int set_oid_status_done(lc_channel_t *chan, unsigned char *oid)
{
	const int oidlen = sizeof chan->oid / sizeof chan->oid[0];
	int idx = -1;
	for (int i = 0; i < oidlen; i++) {
		if (!memcmp(oid, chan->oid[i].hash, HASHSIZE)) {
			idx = i;
			break;
		}
	}
	if (idx == -1) return -1;
	chan->oid[idx].status = 1; /* done */
	return 0;
}

static ssize_t decode_buffer(lc_channel_t *chan, struct msghdr *msg, unsigned char *buf, ssize_t len)
{
	len = lc_channel_decoded(chan, buf, buf, len);
	if (len == -1) return -1;
	return scatter_msg_parts(buf, len, msg, len);
}

static ssize_t channel_rq_decode_blocks(lc_channel_t *chan, struct msghdr *msg,
		unsigned char *buf, size_t len)
{
	const uint16_t OH = RQ_OVERHEAD;
	const uint16_t KP = rq_KP(chan->rq);
	const uint16_t K = rq_K(chan->rq);
	const uint16_t Z = rq_Z(chan->rq);
	const uint16_t T = rq_T(chan->rq);
	const uint16_t nesi = KP + OH;
	ssize_t zi = rq_F(chan->rq);
	size_t blocksz = (uint32_t)T * K;
	size_t sz = len;
	uint8_t *dec = buf, *enc = chan->rq_blk;
	if (chan->filter) sz -= sizeof(lc_token_t);
	for (uint8_t sbn = 0; sbn < Z; sbn++) {
		if (!chan->orig) memcpy(dec, enc, MIN(sz, blocksz)); /* original symbols, no decoding */
		else {
			if (sz < blocksz) {
				uint8_t * temp = malloc(blocksz);
				if (! temp) {
					zi = -1; break;
				}
				if (rq_decode(chan->rq, temp, enc, ESIr(sbn), nesi) == -1) {
					zi = -1; break;
				}
				memcpy(dec, temp, sz);
				free(temp);
			} else {
				if (rq_decode(chan->rq, dec, enc, ESIr(sbn), nesi) == -1) {
					zi = -1; break;
				}
			}
		}
		dec += blocksz; enc += blocksz; sz -= blocksz;
	}
	channel_free_rq(chan);
	if (chan->coding & LC_CODE_SYMM) return decode_buffer(chan, msg, buf, zi);
	return scatter_msg_parts(buf, len, msg, zi);
}

/* if oid \notin chan->oid:
 *  - Append(chan->oid, oid)
 *  - oid.status = 0;
 *  - return 0 (ok)
 * if oid \in chan->oid:
 *  if oid.status == 1 return -1, EAGAIN (drop packet)
 *  else return 0 (ok)
 */
static int check_oid(lc_channel_t *chan, unsigned char *oid)
{
	const int oidlen = sizeof chan->oid / sizeof chan->oid[0];
	int idx = -1;
	for (int i = 0; i < oidlen; i++) {
		if (!memcmp(oid, chan->oid[i].hash, HASHSIZE)) {
			idx = i;
			break;
		}
	}
	if (idx == -1) {
		/* new object */
		chan->oid_cur = (chan->oid_cur + 1) % oidlen;
		chan->oid[chan->oid_cur].status = 0;
		memcpy(chan->oid[chan->oid_cur].hash, oid, HASHSIZE);
		return 0;
	}
	if (chan->oid[idx].status) {
		/* we've processed this object, drop */
		channel_free_rq(chan);
		return (errno = EAGAIN), -1;
	}
	return 0;
}

/* store the received symbol:
 * - if ESI < K, store at blk[sbn][esi]
 * - if ESI >= K, store in next available repair slot >= K
 * - if ESI >= K and all slots >= K used, backfill K
 * - update bitmap, ESI list
 * - if received symbols >= KP + OH, decode and return */
static ssize_t channel_msg_decode(lc_channel_t *chan, struct msghdr *msg,
		uint8_t *oidbuf, size_t len)
{
	const uint64_t F = rq_F(chan->rq);
	const uint16_t OH = RQ_OVERHEAD;
	const uint16_t KP = rq_KP(chan->rq);
	const uint16_t K = rq_K(chan->rq);
	const uint16_t Z = rq_Z(chan->rq);
	const uint16_t T = rq_T(chan->rq);
	const uint16_t nesi = KP + OH;
	const uint32_t M = nesi * (uint32_t)Z;
	unsigned char *oid = oidbuf;
	uint8_t *buf = oidbuf + HASHSIZE;
	uint8_t *sym = buf + sizeof(rq_pid_t);
	uint8_t sbn;
	uint32_t esi;
	rq_pid_t pid = *(rq_pid_t *)(buf);
	rq_oti_t oti = 0;
	/* rq_scheme_t scheme = 0; */
	int idx;

	if (check_oid(chan, oid)) return -1;
#if HAVE_RQ_OTI
	if (chan->coding & LC_CODE_FEC_OTI) {
		oti = *(rq_oti_t *)(buf);
		/* scheme = *(rq_scheme_t *)(buf + sizeof oti); */
		pid = *(rq_pid_t *)(buf + OTI_OH);
		sym += OTI_OH;
		if (rq_oti_F(oti) != F || rq_oti_T(oti) != T) return (errno = EAGAIN), -1;
	}
#endif
	sbn = rq_pid2sbn(pid);
	esi = rq_pid2esi(pid);
	if (sbn >= Z) return (errno = EAGAIN), -1;
	if (esi < K) {
		if (ESI(sbn, esi) == esi) return (errno = EAGAIN), -1;
		idx = esi;
		chan->orig--;
	}
	else for (idx = nesi - 1; idx; idx--) {
		if (ESI(sbn, idx) == esi) return (errno = EAGAIN), -1;
		if (ESI(sbn, idx) == UINT32_MAX) break;
	}
	if (ESI(sbn, idx) == UINT32_MAX) {
		chan->syms++;
		ESI(sbn, idx) = esi;
		memcpy(BLK(sbn, idx), sym, T);
	}
	if (chan->orig && chan->syms < M) return (errno = EAGAIN), -1;
	set_oid_status_done(chan, oid);
	return channel_rq_decode_blocks(chan, msg, oidbuf, len);
}
#endif

#ifdef HAVE_LIBSODIUM
static ssize_t channel_msg_decrypt(lc_channel_t *chan, struct msghdr *msg,
		unsigned char *buf, size_t len)
{
	const size_t clen = len - crypto_secretbox_NONCEBYTES;
	unsigned char *nonce = buf + clen;
	unsigned char *ptr;
	ssize_t rc = len - crypto_secretbox_NONCEBYTES - crypto_secretbox_MACBYTES;
	size_t off = (chan->coding & LC_CODE_FEC_RQ) ? HASHSIZE : 0;
	if (chan->coding & LC_CODE_FEC_OTI) off += OTI_OH;
	ptr = buf + off;
	if (len < crypto_secretbox_NONCEBYTES + crypto_secretbox_MACBYTES + 1)
		return (errno = EMSGSIZE), -1;
	if (crypto_secretbox_open_easy(ptr, ptr, clen - off, nonce, chan->key.key))
		return (errno = EBADMSG), -1;
	return scatter_msg_parts(ptr, len, msg, rc);
}

static int channel_filter_check(lc_filter_t *filter, lc_token_t *token, uint8_t *key, unsigned char *sig,
		unsigned char *payload, size_t len)
{
	/* check token signature */
	if (crypto_sign_verify_detached(token->sig, (unsigned char *)token, 64, key) != 0) {
		return (errno = EPROTO), -1;
	}
	/* check message sig */
	crypto_sign_state state;
	crypto_sign_init(&state);
	crypto_sign_update(&state, payload, len);
	if (crypto_sign_final_verify(&state, sig, token->bearkey) != 0) {
		return (errno = EBADMSG), -1;
	}
	/* check capbits */
	if ((token->capbits & filter->capbits) != filter->capbits) {
		return (errno = EACCES), -1;
	}
	/* check expiry */
	if (token->expires) {
		struct timespec now;
		if (clock_gettime(CLOCK_REALTIME, &now) == -1) return -1;
		if ((uint64_t)now.tv_sec > token->expires) return (errno = EKEYEXPIRED), -1;
	}
	return 0;
}

static int channel_filter_drop(lc_channel_t *chan, lc_token_t *token, unsigned char *sig,
		unsigned char *payload, size_t len)
{
	lc_filter_t *filter = chan->filter;
	lc_keyring_t *keyring = filter->keyring;
	if (!keyring) return (errno = ENOKEY), -1;
	/* find token signer key in filter keyring */
	if (keyring->key) {
		for (size_t n = 0; n < keyring->nkeys; n++) {
			uint8_t *k = keyring->key[n];
			if (!k) return (errno = ENOKEY), -1;
			if (!memcmp(k, token->signkey, sizeof token->signkey)) {
				return channel_filter_check(filter, token, k, sig, payload, len);
			}
		}
	}
	return (errno = EKEYREJECTED), -1;
}
#endif /* HAVE_LIBSODIUM */

/* recv data prior to encoding */
static ssize_t socket_recvmsg_data(lc_socket_t *sock, struct msghdr *msg, size_t len, int flags,
		lc_channel_t **dst)
{
	/* provide a buffer large enough to recv encrypted data */
	size_t buflen = MAX(len, sock->T) + OTI_OH + HASHSIZE + sizeof(lc_token_t) +
		crypto_secretbox_NONCEBYTES + crypto_secretbox_MACBYTES + crypto_sign_BYTES;
	unsigned char *buf;
	unsigned char *payload;
	char ctl[CMSG_SPACE(sizeof(struct in6_pktinfo))];
	struct in6_pktinfo pi = {0};
	struct msghdr emsg = {0};
	const int headers = (sock->type == LC_SOCK_IN6) ? 0 : 1;
	struct iovec iov[2 + headers];
	lc_tlv_hash_t tlv_hash;
	lc_channel_t *chan;
	ssize_t rc;
	int v = 0;
	buf = malloc(buflen);
	if (!buf) return -1;
	if (sock->type == LC_SOCK_IN6) {
		emsg.msg_control = ctl;
		emsg.msg_controllen = sizeof ctl;
	}
	else {
		iov[v].iov_base = &tlv_hash;
		iov[v++].iov_len = sizeof tlv_hash;
	}
	iov[v].iov_len = buflen;
	iov[v++].iov_base = buf;
	emsg.msg_iov = iov;
	emsg.msg_iovlen = v;
recv_again:
	payload = buf;
	rc = lc_socket_recvmsg_if(sock, &emsg, flags, &pi, dst);
	if (rc == -1) goto err_free_buf;
	if (flags & MSG_PEEK) goto err_free_buf;
	if (sock->type == LC_SOCK_IN6) {
		chan = lc_socket_channel_by_address(sock, &pi.ipi6_addr);
	}
	else {
		chan = lc_channel_socket_by_hash(sock, tlv_hash.value, HASHSIZE);
		if (rc > (ssize_t)sizeof tlv_hash) rc -= sizeof tlv_hash;
	}
	if (chan) {
#ifdef HAVE_LIBSODIUM
		if (chan->filter) {
			lc_token_t *token = (lc_token_t *)payload;
			unsigned char *sig = payload + rc - crypto_sign_BYTES;
			size_t plen = rc - crypto_sign_BYTES;
			if (channel_filter_drop(chan, token, sig, payload, plen)) goto recv_again;
			rc -= (crypto_sign_BYTES + sizeof(lc_token_t));
			payload += sizeof(lc_token_t);
		}
#endif
		if (!chan->coding) {
			scatter_msg_parts(payload, rc, msg, rc);
			goto err_free_buf;
		}
		if (chan->coding & LC_CODE_FEC_RQ) {
#ifdef HAVE_LIBLCRQ
#if HAVE_RQ_OTI
			ssize_t F = len;
			if (chan->coding & LC_CODE_SYMM) {
				F += OH_SYMM, len += OH_SYMM;
			}
			if (chan->coding & LC_CODE_FEC_OTI) {
				rq_oti_t oti;
				oti = *(rq_oti_t *)(payload + HASHSIZE); /* skip OID */
				if (rq_oti_F(oti) > len) return (errno = EMSGSIZE), -1;
				F = rq_oti_F(oti);
			}
#endif
			if (lc_channel_rq_init(chan, F) == -1) {
				rc = -1;
				goto err_free_buf;
			}
			rc = channel_msg_decode(chan, msg, payload, buflen);
			if (rc == -1) {
				if (errno == EAGAIN) goto recv_again;
				goto err_free_buf;
			}
#endif /* HAVE_LIBLCRQ */
		}
		else if (chan->coding & LC_CODE_SYMM) {
#ifdef HAVE_LIBSODIUM
			rc = channel_msg_decrypt(chan, msg, payload, rc);
			if (rc == -1) goto err_free_buf;
#else
			return (errno = ENOTSUP), -1;
#endif
		}
	}
err_free_buf:
	free(buf);
	return rc;
}

#ifdef HAVE_LIBSODIUM
static ssize_t lc_socket_recvmsg_encoded(lc_socket_t *sock, struct msghdr *msg, int flags,
		lc_channel_t **dst)
{
	size_t len = 0;
	for (int i = 0; i < (int)msg->msg_iovlen; i++) {
		len += msg->msg_iov[i].iov_len;
	}
	return socket_recvmsg_data(sock, msg, len, flags, dst);
}
#endif

#if !(HAVE_RECVMMSG)
/* wrapper for recvmsg for systems with no recvmmsg() - timeout ignored */
static int recvmmsg(int sockfd, struct mmsghdr *msgvec, unsigned int vlen,
		int flags, struct timespec *timeout)
{
	(void)timeout; /* ignored */
	ssize_t rc;
	int msgs = 0;
	int waitfor = 0;
	if (flags & MSG_WAITFORONE) {
		flags &= ~MSG_WAITFORONE;
		waitfor = 1;
	}
	for (unsigned int u = 0; u < vlen; u++) {
		rc = recvmsg(sockfd, &msgvec[u].msg_hdr, flags);
		if (rc == -1) {
			if (msgs > 0) break;
			else return -1;
		}
		if (waitfor) {
			flags |= MSG_DONTWAIT;
			waitfor ^= waitfor;
		}
		msgvec[u].msg_len = rc;
		msgs++;
	}
	return msgs;
}
#endif

#ifdef HAVE_LIBSODIUM
static int lc_socket_recvmmsg_coded(lc_socket_t *sock, struct mmsghdr *msgvec, unsigned int vlen,
		int flags, struct timespec *timeout)
{
	(void)timeout; /* ignored */
	ssize_t rc;
	int msgs = 0;
	int waitfor = 0;
	if (flags & MSG_WAITFORONE) {
		flags &= ~MSG_WAITFORONE;
		waitfor = 1;
	}
	for (unsigned int u = 0; u < vlen; u++) {
		rc = lc_socket_recvmsg_encoded(sock, &msgvec[u].msg_hdr, flags, NULL);
		if (rc == -1) {
			if (msgs > 0) break;
			else return -1;
		}
		if (waitfor) {
			flags |= MSG_DONTWAIT;
			waitfor ^= waitfor;
		}
		msgvec[u].msg_len = rc;
		msgs++;
	}
	return msgs;
}
#endif

int lc_socket_recvmmsg(lc_socket_t *sock, struct mmsghdr *msgvec, unsigned int vlen,
		int flags, struct timespec *timeout)
{
	if (sock->ccoding & LC_CODE_SYMM) {
#ifdef HAVE_LIBSODIUM
		return lc_socket_recvmmsg_coded(sock, msgvec, vlen, flags, timeout);
#else
		return (errno = ENOTSUP), -1;
#endif
	}
	return recvmmsg(sock->sock, msgvec, vlen, flags, timeout);
}

ssize_t lc_socket_multi_recvmsg(lc_socket_t *sock, struct msghdr *msg, int flags, lc_channel_t **dst)
{
	if (sock->ifx || sock->chan_active || dst || sock->ccoding) {
		size_t len = 0;
		for (int i = 0; i < (int)msg->msg_iovlen; i++) {
			len += msg->msg_iov[i].iov_len;
		}
		return socket_recvmsg_data(sock, msg, len, flags, dst);
	}
	return socket_recvmsg(sock, msg, flags);
}

ssize_t lc_socket_recvmsg(lc_socket_t *sock, struct msghdr *msg, int flags)
{
	return lc_socket_multi_recvmsg(sock, msg, flags, NULL);
}

ssize_t lc_socket_multi_recv(lc_socket_t *sock, void *buf, size_t len, int flags, lc_channel_t **dst)
{
	struct iovec iov = { .iov_base = buf, .iov_len = len };
	struct msghdr msg = { .msg_iov = &iov, .msg_iovlen = 1 };
	return lc_socket_multi_recvmsg(sock, &msg, flags, dst);
}

ssize_t lc_socket_recv(lc_socket_t *sock, void *buf, size_t len, int flags)
{
	return lc_socket_multi_recv(sock, buf, len, flags, NULL);
}

/* peek at packet header to get OTI params */
int lc_channel_oti_peek(lc_channel_t *chan, rq_oti_t *oti, rq_scheme_t *scheme)
{
#if (HAVE_LIBLCRQ && HAVE_RQ_OTI)
	const size_t sz = ((chan->coding & LC_CODE_SYMM) ?
		IPV6_MIN_MTU : sizeof *oti + sizeof *scheme) + HASHSIZE;
	char buf[sz];
	ssize_t rc;
	rc = recv(chan->sock->sock, buf, sz, MSG_PEEK);
	if (rc >= (ssize_t)(sizeof *oti + sizeof *scheme)) {
		memcpy(oti, buf + HASHSIZE, sizeof(rq_oti_t));
		memcpy(scheme, buf + HASHSIZE + sizeof *oti, sizeof *scheme);
		if (rq_oti_T(*oti) != chan->T) return (errno = EBADMSG), -1;
		chan->rq = rq_init(rq_oti_F(*oti), chan->T);
	}
	return rc;
#else
	(void)chan, (void)oti, (void) scheme;
	return (errno = ENOTSUP), -1;
#endif
}

ssize_t lc_channel_recv(lc_channel_t *chan, void *buf, size_t len, int flags)
{
	ssize_t rc;
	if (!chan || !buf || !len) return (errno = EINVAL), -1;
	if (!chan->sock) return (errno = ENOTSOCK), -1;
	chan->sock->chan_active = chan;
	rc = lc_socket_recv(chan->sock, buf, len, flags);
	chan->sock->chan_active = NULL;
	return rc;

}

static ssize_t channel_recvmsg_encoded(lc_channel_t *chan, struct msghdr *msg, size_t len, int flags)
{
	char buf[len];
	char *ptr = buf;
	ssize_t rc;
	if ((rc = lc_channel_recv(chan, buf, len, flags)) == -1) return -1;
	/* scatter parts */
	for (int v = 0; v < (int)msg->msg_iovlen; v++) {
		memcpy(msg->msg_iov[v].iov_base, ptr, msg->msg_iov[v].iov_len);
		ptr += msg->msg_iov[v].iov_len;
	}
	return rc;
}

ssize_t lc_channel_recvmsg(lc_channel_t *chan, struct msghdr *msg, int flags)
{
	if (!chan->sock) return (errno = ENOTSOCK), -1;
	if (chan->coding) {
		size_t len = 0;
		for (int i = 0; i < (int)msg->msg_iovlen; i++) {
			len += msg->msg_iov[i].iov_len;
		}
		return channel_recvmsg_encoded(chan, msg, len, flags);
	}
	chan->sock->chan_active = chan;
	ssize_t rc = lc_socket_recvmsg(chan->sock, msg, flags);
	chan->sock->chan_active = NULL;
	return rc;
}

static void process_msg(lc_socket_call_t *sc, lc_message_t *msg)
{
	lc_channel_t *chan;

	inet_ntop(AF_INET6, &msg->dst, msg->dstaddr, INET6_ADDRSTRLEN);
	inet_ntop(AF_INET6, &msg->src, msg->srcaddr, INET6_ADDRSTRLEN);

	/* update channel stats */
	chan = lc_channel_by_address(sc->sock->ctx, &msg->dst);
	if (chan) {
		int (*logger)(lc_channel_t *, lc_message_t *, void *logdb) = aload(&lc_msg_logger);
		msg->chan = chan;
		lc_seq_t seq = aload(&chan->seq);
		astor(&chan->seq, (msg->seq > seq) ? msg->seq + 1 : seq + 1);
		chan->rnd = msg->rnd;
		if (logger) logger(chan, msg, NULL);
	}

	/* opcode handler */
	if (msg->op < LC_OP_MAX && lc_op_handler[msg->op])
		lc_op_handler[msg->op](sc, msg);

	/* callback to message handler */
	if (sc->callback_msg) sc->callback_msg(msg);
}

void *lc_socket_listen_thread(void *arg)
{
	ssize_t len;
	lc_message_t msg = {0};
	lc_socket_call_t *sc = arg;

	pthread_cleanup_push(free, arg);
	pthread_cleanup_push(lc_msg_free, &msg);
	while(1) {
		len = lc_msg_recv(sc->sock, &msg);
		if (len > 0) {
			msg.bytes = len;
			process_msg(sc, &msg);
		}
		if (len < 0) {
			lc_msg_free(&msg);
			if (sc->callback_err) sc->callback_err(len);
		}
		lc_msg_free(&msg);
	}
	/* not reached */
	pthread_cleanup_pop(0);
	pthread_cleanup_pop(0);

	return NULL;
}

int lc_socket_listen(lc_socket_t *sock, void (*callback_msg)(lc_message_t*),
					void (*callback_err)(int))
{
	pthread_attr_t attr = {0};
	lc_socket_call_t *sc;

	if (!sock) return LC_ERROR_SOCKET_REQUIRED;
	if (sock->thread) return LC_ERROR_SOCKET_LISTENING;

	sc = calloc(1, sizeof(lc_socket_call_t));
	if (!sc) return LC_ERROR_MALLOC;
	sc->sock = sock;
	sc->callback_msg = callback_msg;
	sc->callback_err = callback_err;

	pthread_attr_init(&attr);
	pthread_create(&sock->thread, &attr, &lc_socket_listen_thread, sc);
	pthread_attr_destroy(&attr);

	return 0;
}

static int lc_channel_membership_all(int sock, int opt, struct ipv6_mreq *req)
{
	struct ifaddrs *ifaddr, *ifa;
	int rc = (opt == IPV6_JOIN_GROUP) ? LC_ERROR_MCAST_JOIN : LC_ERROR_MCAST_PART;

	if (getifaddrs(&ifaddr) == -1) return -1;
	for (ifa = ifaddr; ifa; ifa = ifa->ifa_next) {
		if (!(ifa->ifa_flags & IFF_MULTICAST)
		   || ifa->ifa_addr == NULL
		   || ifa->ifa_addr->sa_family != AF_INET6) continue;

		req->ipv6mr_interface = if_nametoindex(ifa->ifa_name);

		if (!setsockopt(sock, IPPROTO_IPV6, opt, req, sizeof(struct ipv6_mreq))) {
			rc = 0; /* report success if we joined anything */
		}
	}
	freeifaddrs(ifaddr);

	return rc;
}

int lc_socket_oil_cmp(lc_socket_t *sock, unsigned char *hash)
{
	unsigned char *oil = aload(&sock->oil);
	if (!oil || !hash) goto not_found;
	for (int h = 0; h < BLOOM_HASHES; h++) {
		if (!(oil[hash[h] % BLOOM_LEN])) goto not_found;
	}
	return 0;
not_found:
	return (errno = ENOENT), -1;
}

#ifndef IPV6_MULTICAST_ALL
static void lc_socket_group_add(lc_socket_t *sock, struct in6_addr *grp)
{
	lc_grplist_t *g, *newgrp;
	for (g = sock->grps; g; g = g->next) {
		if (!memcmp(&g->grp, grp, sizeof(struct in6_addr))) return;
		if (!g->next) break;
	}
	newgrp = calloc(1, sizeof(struct lc_grplist_s));
	newgrp->grp = *grp;
	if (!sock->grps) sock->grps = newgrp;
	else g->next = newgrp;
}

static void lc_socket_group_del(lc_socket_t *sock, struct in6_addr *grp)
{
	lc_grplist_t *prev = NULL;
	for (lc_grplist_t *g = sock->grps; g; g = g->next) {
		if (memcmp(g, grp, sizeof(struct in6_addr)) == 0) {
			if (prev) {
				prev->next = (g->next) ? g->next : NULL;
			}
			if (g == sock->grps) sock->grps = NULL;
			free(g);
		}
		prev = g;
	}
}
#endif

int lc_socket_oil_add(lc_socket_t *sock, unsigned char *hash)
{
	const unsigned max = 1 << (CHAR_BIT - 1);
	if (!sock->oil) {
		unsigned char *oil = malloc(BLOOM_LEN);
		if (!oil) return -1;
		memset(oil, 0, BLOOM_LEN);
		astor(&sock->oil, oil);
	}
	/* if hash not in filter, add it */
	if (lc_socket_oil_cmp(sock, hash)) {
		for (int h = 0; h < BLOOM_HASHES; h++) {
			unsigned char c = aload(&sock->oil[hash[h] % BLOOM_LEN]);
			while ((c < max) && !CAE(&sock->oil[hash[h] % BLOOM_LEN], &c, c + 1));
		}
	}
	return 0;
}

int lc_socket_oil_del(lc_socket_t *sock, unsigned char *hash)
{
	if (lc_socket_oil_cmp(sock, hash)) return -1;
	for (int h = 0; h < BLOOM_HASHES; h++) {
		if (sock->oil[hash[h] % BLOOM_LEN])
			sock->oil[hash[h] % BLOOM_LEN]--;
	}
	return 0;
}

static int lc_channel_membership_in6(lc_channel_t *chan, int opt, struct ipv6_mreq *req)
{
	int s = chan->sock->sock;
	if (opt == IPV6_JOIN_GROUP) {
#ifndef IPV6_MULTICAST_ALL
		lc_socket_group_add(chan->sock, &chan->sa.sin6_addr);
#endif
		lc_socket_oil_add(chan->sock, chan->hash);
	}
	else {
#ifndef IPV6_MULTICAST_ALL
		lc_socket_group_del(chan->sock, &chan->sa.sin6_addr);
#endif
		lc_socket_oil_del(chan->sock, chan->hash);
	}
	if (chan->sock->ifx) {
		req->ipv6mr_interface = chan->sock->ifx;
		return setsockopt(s, IPPROTO_IPV6, opt, req, sizeof(struct ipv6_mreq));
	}
	return lc_channel_membership_all(s, opt, req);
}

static int lc_channel_action_in6(lc_channel_t *chan, int opt)
{
	struct ipv6_mreq req = {0};
	if(!chan->sock) return LC_ERROR_SOCKET_REQUIRED;
	req.ipv6mr_multiaddr = chan->sa.sin6_addr;
	return lc_channel_membership_in6(chan, opt, &req);
}

static int lc_channel_action(lc_channel_t *chan, int opt)
{
	lc_socket_t *sock = chan->sock;
	if(!chan->sock) return LC_ERROR_SOCKET_REQUIRED;
	if (chan->sock->type == LC_SOCK_PAIR) sock = sock->pair;
	if (opt == IPV6_JOIN_GROUP) {
		lc_socket_oil_add(sock, chan->hash);
	}
	else {
		lc_socket_oil_del(sock, chan->hash);
	}
	switch (chan->sock->type) {
		case LC_SOCK_IN6:
			return lc_channel_action_in6(chan, opt);
		case LC_SOCK_PAIR:
			return 0;
		default:
			return (errno = ENOSYS), -1;
	}
}

int lc_channel_part(lc_channel_t *chan)
{
	return lc_channel_action(chan, IPV6_LEAVE_GROUP);
}

int lc_channel_join(lc_channel_t *chan)
{
	return lc_channel_action(chan, IPV6_JOIN_GROUP);
}

int lc_channel_unbind(lc_channel_t *chan)
{
	chan->sock->bound--;
	chan->sock = NULL;
	return 0;
}

static int lc_socket_bind_addr(lc_socket_t *sock, short port)
{
	const int opt = 1;
	struct sockaddr_in6 any = {
		.sin6_family = AF_INET6,
		.sin6_addr = IN6ADDR_ANY_INIT,
		.sin6_port = port
	};
	if ((setsockopt(sock->sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) == -1)
		return LC_ERROR_SETSOCKOPT;

#ifdef SO_REUSEPORT
	if ((setsockopt(sock->sock, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt))) == -1)
		return LC_ERROR_SETSOCKOPT;
#endif
	if (bind(sock->sock, (struct sockaddr *)&any, sizeof(struct sockaddr_in6)) == -1) {
		/* ignore EINVAL "socket already bound" and EADDRINUSE errors */
		if (errno != EINVAL && errno != EADDRINUSE) return LC_ERROR_SOCKET_BIND;
	}
	return 0;
}

int lc_channel_bind(lc_socket_t *sock, lc_channel_t *chan)
{
	int rc = 0;

	/* Librecast sockets can have multiple channels bound to them, but we
	 * only need to call lc_socket_bind_addr() the first time */

	if (sock->type == LC_SOCK_IN6) {
		lc_channel_in6addr(chan); /* generate group address (IPv6 socket only) */
		rc = (sock->bound) ? 0 : lc_socket_bind_addr(sock, chan->sa.sin6_port);
	}

	if (!rc) {
		astor(&chan->sock, sock);
		sock->bound++;
		chan->sock->ccoding |= chan->coding;
#ifdef HAVE_LIBLCRQ
		if (chan->T > sock->T) sock->T = chan->T;
#endif
	}

	return rc;
}

static lc_channel_t * lc_channel_ins(lc_ctx_t *ctx, lc_channel_t *chan)
{
	chan->next = aload(&ctx->chan_list);
	chan->readers = 1;
	CAE_loop(&ctx->chan_list, &chan->next, chan);
	return chan;
}

lc_channel_t * lc_channel_sidehash(lc_channel_t *base, unsigned char *key, size_t keylen)
{
#ifndef HASH_TYPE
	return (errno = ENOTSUP), NULL;
#else
	lc_ctx_t *ctx = base->ctx;
	lc_channel_t *side = lc_channel_copy(ctx, base);
	if (!side) return NULL;
	hash_generic_key(side->hash, HASHSIZE, base->hash, HASHSIZE, key, keylen);
	/* force regeneration of IPv6 group address */
	memset(&side->sa, 0, sizeof(struct sockaddr_in6));
	lc_channel_in6addr(side);
	return side;
#endif
}

lc_channel_t * lc_channel_sideband(lc_channel_t *base, uint64_t band)
{
	struct in6_addr *in;
	uint64_t *ptr;
	lc_ctx_t *ctx = base->ctx;
	lc_channel_t *side = lc_channel_copy(ctx, base);
	if (!side) return NULL;
	in = &side->sa.sin6_addr;
	ptr = (uint64_t *)&in->s6_addr[8];
	*ptr = band;
	return side;
}

lc_channel_t * lc_channel_copy(lc_ctx_t *ctx, lc_channel_t *chan)
{
	lc_channel_t *copy = malloc(sizeof(lc_channel_t));
	if (!copy) return NULL;
	memset(copy, 0, sizeof(lc_channel_t));
	memcpy(copy->hash, chan->hash, HASHSIZE);
	copy->ctx = ctx;
	copy->sa = chan->sa;
	copy->coding = chan->coding;
	copy->key = chan->key;
	copy->pek = chan->pek;
	copy->sek = chan->sek;
	copy->ssk = chan->ssk;
	if (clock_gettime(CLOCK_TAI, &chan->byt_reset)) {
		free(copy);
		return NULL;
	}
	return lc_channel_ins(ctx, copy);
}

lc_channel_t * lc_channel_nnew(lc_ctx_t *ctx, unsigned char *s, size_t len)
{
	lc_channel_t *chan;
	chan = malloc(sizeof(lc_channel_t));
	if (!chan) return NULL;
	memset(chan, 0, sizeof(lc_channel_t));
	hash_generic(chan->hash, HASHSIZE, s, len);
	chan->ctx = ctx;
	chan->bps_out = ctx->bps_out;
	chan->bps_in = ctx->bps_in;
	chan->coding = ctx->coding;
	chan->key = ctx->key;
	chan->pek = ctx->pek;
	chan->sek = ctx->sek;
	chan->psk = ctx->psk;
	chan->ssk = ctx->ssk;
	if (clock_gettime(CLOCK_TAI, &chan->byt_reset)) {
		free(chan);
		return NULL;
	}
#ifdef HAVE_LIBLCRQ
	chan->T = LC_DEFAULT_RQ_T;
	chan->rq_oh = ctx->rq_oh;
#endif
	return lc_channel_ins(ctx, chan);
}

lc_channel_t * lc_channel_new(lc_ctx_t *ctx, char *s)
{
	return lc_channel_nnew(ctx, (unsigned char *)s, strlen(s));
}

lc_channel_t *lc_channel_init(lc_ctx_t *ctx, struct sockaddr_in6 *sa)
{
	lc_channel_t *chan;
	chan = lc_channel_nnew(ctx, (unsigned char *)&sa->sin6_addr + 2, 14);
	if (!chan) return NULL;
	chan->sa = *sa;
	return chan;
}

lc_channel_t *lc_channel_init_grp(lc_ctx_t *ctx, struct in6_addr *grp, short port)
{
	struct sockaddr_in6 sa = {
		.sin6_family = AF_INET6,
		.sin6_port = htons(port)
	};
	sa.sin6_addr = *grp;
	return lc_channel_init(ctx, &sa);
}

void lc_hashtoaddr(struct in6_addr *addr, unsigned char *hash, size_t hashlen, unsigned char flags)
{
	addr->s6_addr[0] = 0xff;  /* multicast */
	addr->s6_addr[1] = flags; /* set flags */
	memcpy(&addr->s6_addr[2], hash, MIN(hashlen, 14)); /* 112 bits (14 bytes) for hash */
}

lc_channel_t * lc_channel_hash(lc_ctx_t *ctx, unsigned char *hash, size_t hashlen, unsigned char flags,
		short port)
{
	struct sockaddr_in6 sa = {
		.sin6_family = AF_INET6,
		.sin6_port = htons(port)
	};
	lc_hashtoaddr(&sa.sin6_addr, hash, hashlen, flags);
	return lc_channel_init(ctx, &sa);
}

lc_channel_t *lc_channel_random(lc_ctx_t *ctx)
{
	unsigned char buf[14];
	if (lc_getrandom(buf, sizeof buf) != sizeof buf) return NULL;
	return lc_channel_nnew(ctx, buf, sizeof buf);
}

#ifndef IPV6_MULTICAST_ALL
static int lc_socket_groups_free(lc_socket_t *sock)
{
	lc_grplist_t *tmp;
	for (lc_grplist_t *g = sock->grps; g; ) {
		tmp = g;
		g = g->next;
		free(tmp);
	}
	return 0;
}
#endif

/* remove socket from ctx list */
static void socket_unlink(lc_socket_t *sock)
{
	pthread_mutex_lock(&sock->ctx->mtx);
	lc_socket_t *prev = NULL;
	lc_socket_t *next = aload(&sock->next);
	for (lc_socket_t *p = aload(&sock->ctx->sock_list); p; p = aload(&p->next)) {
		if (p == sock) {
			lc_socket_t **ptr = (prev) ? &prev->next : &sock->ctx->sock_list;
			CAE_loop(ptr, &p, next);
			break;
		}
		prev = p;
	}
	pthread_mutex_unlock(&sock->ctx->mtx);
}

void lc_socket_close(lc_socket_t *sock)
{
	int err = errno;
	if (!sock) return;
#ifndef IPV6_MULTICAST_ALL
	lc_socket_groups_free(sock);
#endif
	lc_socket_listen_cancel(sock);
	socket_unlink(sock);
	int deleted = 0;
	if (CAE(&sock->deleted, &deleted, DELETE_LATER))
		lc_socket_release(sock);
	errno = err;
}

inline static void ctx_free(lc_ctx_t *ctx)
{
	int deleted = aload(&ctx->deleted);
	CAE(&ctx->deleted, &deleted, DELETE_LATER);
	lc_ctx_release(ctx);
}

void lc_ctx_free(lc_ctx_t *ctx)
{
	int err = errno;
	if (ctx) {
		void *p;
		if (ctx->q) lc_ctx_qpool_free(ctx);
		while ((p = aload(&ctx->sock_list))) {
			lc_socket_close(p);
		}
		while ((p = aload(&ctx->chan_list))) {
			lc_channel_free(p);
		}
		while ((p = aload(&ctx->router_list))) {
			lc_router_free(p);
		}
		if (ctx->sock >= 0) close(ctx->sock);
		if (ctx->mdex) mdex_free(ctx->mdex);
		pthread_mutex_destroy(&ctx->mtx);
		ctx_free(ctx);
	}
	errno = err;
}

int lc_socket_bind(lc_socket_t *sock, unsigned int ifx)
{
	if (setsockopt(sock->sock, IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifx, sizeof ifx) == -1) {
		return -1;
	}
	sock->ifx = ifx;
	return 0;
}

static lc_socket_t * lc_socket_nnew(lc_ctx_t *ctx, int type)
{
	lc_socket_t *sock;
	int s = 0, i, err = 0;

	sock = calloc(1, sizeof(lc_socket_t));
	if (!sock) return NULL;
	sock->ctx = ctx;
	sock->type = type;
	sock->readers = 1;
	sock->ttl = DEFAULT_MULTICAST_HOPS;
	if (type == LC_SOCK_IN6) {
		s = socket(AF_INET6, SOCK_DGRAM, 0);
		if (s == -1) {
			err = errno;
			goto err_free_sock;
		}
		sock->sock = s;
#ifdef IPV6_MULTICAST_ALL
		/* available in Linux 4.2 onwards */
		i = 0;
		if (setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_ALL, &i, sizeof i) == -1) {
			goto err_close_s;
		}
#endif
		i = 1;
		if (setsockopt(s, IPPROTO_IPV6, IPV6_RECVPKTINFO, &i, sizeof i) == -1) {
			goto err_close_s;
		}
		i = DEFAULT_MULTICAST_LOOP;
		if (setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &i, sizeof i) == -1) {
			goto err_close_s;
		}
		i = DEFAULT_MULTICAST_HOPS;
		if (setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &i, sizeof i) == -1) {
			goto err_close_s;
		}
		if (ctx->ifx && lc_socket_bind(sock, ctx->ifx)) goto err_close_s;
	}
	sock->next = aload(&ctx->sock_list);
	CAE_loop(&ctx->sock_list, &sock->next, sock);
	return sock;
err_close_s:
	err = errno;
	if (s) close(s);
err_free_sock:
	free(sock);
	return (errno = err), NULL;
}

lc_socket_t * lc_socket_new(lc_ctx_t *ctx)
{
	return lc_socket_nnew(ctx, LC_SOCK_IN6);
}

int lc_socketpair(lc_ctx_t *ctx, lc_socket_t *sock[2])
{
	int sv[2];
	int err;
	sock[0] = lc_socket_nnew(ctx, LC_SOCK_PAIR);
	if (!sock[0]) return -1;
	sock[1] = lc_socket_nnew(ctx, LC_SOCK_PAIR);
	if (!sock[1]) goto err_sock_close;
	err = socketpair(AF_UNIX, SOCK_DGRAM, 0, sv);
	if (err) goto err_sock_close;
	for (int i = 0; i < 2; i++) {
		sock[i]->sock = sv[i];
		sock[i^0]->pair = sock[i^1]; /* pair our socks */
	}
	return 0;
err_sock_close:
	err = errno;
	for (int i = 0; i < 2; i++) lc_socket_close(sock[0]);
	errno = err;
	return -1;
}

q_t *lc_ctx_q(lc_ctx_t *ctx)
{
	return ctx->q;
}

int lc_ctx_qpool_free(lc_ctx_t *ctx)
{
	int err = 0, rc = 0;
	if (q_pool_destroy(ctx->tid, ctx->nthreads))
		err = errno, rc = -1;
	free(ctx->tid); ctx->tid = NULL;
	free(ctx->q); ctx->q = NULL;
	if (err) errno = err;
	return rc;
}

int lc_ctx_qpool_resize(lc_ctx_t *ctx, int nthreads)
{
	pthread_t *tid;
	int rc = 0, newtids;
	if (nthreads == ctx->nthreads) return 0;
	if (nthreads < ctx->nthreads) {
		newtids = ctx->nthreads - nthreads;
		rc = q_pool_destroy(&ctx->tid[nthreads], newtids);
		if (rc) return -1;
		ctx->nthreads = nthreads;
	}
	tid = realloc(ctx->tid, sizeof(pthread_t) * nthreads);
	if (!tid) return -1;
	ctx->tid = tid;
	if (nthreads > ctx->nthreads) {
		newtids = nthreads - ctx->nthreads;
		memset(&tid[ctx->nthreads], 0, sizeof(pthread_t) * newtids);
		rc = q_pool_create(&ctx->tid[ctx->nthreads], newtids, &q_job_seek, ctx->q);
		ctx->nthreads = nthreads - rc;
	}
	return (ctx->nthreads == nthreads) ? 0 : -1;
}

int lc_ctx_qpool_init(lc_ctx_t *ctx, int nthreads)
{
	int err;
	ctx->q = malloc(sizeof(q_t));
	if (!ctx->q) return -1;
	if (q_init(ctx->q)) {
		err = errno;
		goto err_free_ctx_q;
	}
	ctx->tid = calloc(nthreads, sizeof(pthread_t));
	if (!ctx->tid) {
		err = errno;
		goto err_q_free;
	}
	ctx->nthreads = nthreads - q_pool_create(ctx->tid, nthreads, &q_job_seek, ctx->q);
	return (ctx->nthreads == nthreads) ? 0 : -1;
err_q_free:
	q_free(ctx->q);
err_free_ctx_q:
	free(ctx->q);
	return (errno = err), -1;
}

void lc_ctx_release(lc_ctx_t *ctx)
{
	/* decrease reference count, fetch updated value */
	int refs = add_fetch(&ctx->readers, -1);
	int deleted = aload(&ctx->deleted);
	if (!refs && deleted == DELETE_LATER) {
		/* no readers left, try to delete */
		if (CAE(&ctx->deleted, &deleted, DELETE_IN_PROGRESS)) {
			free(ctx);
		}
	}
}

void lc_socket_release(lc_socket_t *sock)
{
	/* decrease reference count, fetch updated value */
	int refs = add_fetch(&sock->readers, -1);
	int deleted = aload(&sock->deleted);
	if (!refs && deleted == DELETE_LATER) {
		/* no readers left, try to delete */
		if (CAE(&sock->deleted, &deleted, DELETE_IN_PROGRESS)) {
			if (sock->sock) close(sock->sock);
			if (sock->oil) free(sock->oil);
			free(sock);
		}
	}
}

void lc_channel_release(lc_channel_t *chan)
{
	/* decrease reference count, fetch updated value */
	int refs = add_fetch(&chan->readers, -1);
	int deleted = aload(&chan->deleted);
	if (!refs && deleted == DELETE_LATER) {
		/* no readers left, try to delete */
		if (CAE(&chan->deleted, &deleted, DELETE_IN_PROGRESS)) {
			free(chan);
		}
	}
}

lc_ctx_t *lc_ctx_acquire(lc_ctx_t *ctx)
{
	if (!ctx || aload(&ctx->deleted)) return NULL;
	fetch_add(&ctx->readers, 1); /* increase reference count */
	return ctx;
}

lc_socket_t *lc_socket_acquire(lc_socket_t *sock)
{
	if (!sock || aload(&sock->deleted)) return NULL;
	fetch_add(&sock->readers, 1); /* increase reference count */
	return sock;
}

lc_channel_t *lc_channel_acquire(lc_channel_t *chan)
{
	if (!chan || aload(&chan->deleted)) return NULL;
	fetch_add(&chan->readers, 1); /* increase reference count */
	return chan;
}

lc_ctx_t * lc_ctx_new(void)
{
	lc_ctx_t *ctx;

	if (!(ctx = calloc(1, sizeof(lc_ctx_t)))) return NULL; /* errno set by calloc */
	ctx->next = aload(&ctx_list);
	astor(&ctx_list, ctx);
	ctx->sock = -1;
	ctx->readers = 1;
#ifdef HAVE_LIBLCRQ
	ctx->rq_oh = -1;
#endif
	if (pthread_mutex_init(&ctx->mtx, NULL)) {
		free(ctx); return NULL;
	}

	return ctx;
}

int lc_ctx_debug(lc_ctx_t *ctx, int debug)
{
	if (debug != -1) ctx->debug = debug;
	return ctx->debug;
}

FILE *lc_ctx_stream(lc_ctx_t *ctx, FILE *stream)
{
	if (stream) ctx->stream = stream;
	return ctx->stream;
}
