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

#define _GNU_SOURCE /* required for struct in6_pktinfo */
#include "config.h"
#include "mld_pvt.h"
#include "log.h"

#ifdef __linux__
# include <linux/netlink.h>
# include <linux/rtnetlink.h>
# include <linux/filter.h>
# include <linux/if_packet.h>
# include <linux/if_ether.h>
#endif
#if HAVE_ASM_TYPES_H
# include <asm/types.h>
#endif
#if HAVE_SYS_PARAM_H
#include <sys/param.h>
#endif
#if HAVE_SYS_SOCKET_H
# include <sys/socket.h>
#endif
#if HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
#if HAVE_NETINET6_IP6_MROUTE_H
# define OS_NETBSD 1
# include <netinet6/ip6_mroute.h>
#endif
#ifdef HAVE_FCNTL_H
# include <fcntl.h>
#endif
#ifdef HAVE_NET_BPF_H
# include <net/bpf.h>
# ifndef sock_filter
#  define sock_filter bpf_insn
#  define sock_fprog  bpf_program
# endif
#endif
#ifdef HAVE_SYS_IOCTL_H
# include <sys/ioctl.h>
#endif

#include <arpa/inet.h>
#include <errno.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <netdb.h>
#include <netinet/icmp6.h>
#include <netinet/ip6.h>
#include <poll.h>
#include <signal.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#ifdef HAVE_ENDIAN_H
# include <endian.h>
#elif defined(HAVE_SYS_ENDIAN_H)
# include <sys/endian.h>
#elif defined(HAVE_LIBKERN_OSBYTEORDER_H)
# include <libkern/OSByteOrder.h>
# define be64toh(x) OSSwapBigToHostInt64(x)
#endif

/* BPF filter for MLD types - we are interested in icmpv6 types 130-132 and 143 */
/* tcpdump -dd 'ip6[6] == 0 and
 *    ((ip6[40:4] == 0x3a000502 and ip6[44:2] == 0) or
 *     (ip6[40:4] == 0x3a000100 and ip6[44:4] == 0x05020000)) and
 * (ip6[48] == 130 or ip6[48] == 131 or ip6[48] == 132 or ip6[48] == 143)'
 * notes:
 * ip6[6] == 0: first header is an option header
 *   ip6[40:4] == 0x3a000502: router alert, next header ICMP6 and Option header 0
 *   ip6[44:2] == 0: alert type MLD
 * -or-
 *   ip6[40:4] == 0x3a000100: router alert, next header ICMP6 and Option header 1
 *   ip6[44:4] == 0x05020000: alert type MLD
 * ip6[48]: ICMP6 type in ICMP6 header */
static struct sock_filter BPF_MLD[] = {
	{ 0x28, 0, 0, 0x0000000c },
	{ 0x15, 0, 15, 0x000086dd },
	{ 0x30, 0, 0, 0x00000014 },
	{ 0x15, 0, 13, 0x00000000 },
	{ 0x20, 0, 0, 0x00000036 },
	{ 0x15, 0, 2, 0x3a000502 },
	{ 0x28, 0, 0, 0x0000003a },
	{ 0x15, 3, 9, 0x00000000 },
	{ 0x15, 0, 8, 0x3a000100 },
	{ 0x20, 0, 0, 0x0000003a },
	{ 0x15, 0, 6, 0x05020000 },
	{ 0x30, 0, 0, 0x0000003e },
	{ 0x15, 3, 0, 0x00000082 },
	{ 0x15, 2, 0, 0x00000083 },
	{ 0x15, 1, 0, 0x00000084 },
	{ 0x15, 0, 1, 0x0000008f },
	{ 0x6, 0, 0, 0x00040000 },
	{ 0x6, 0, 0, 0x00000000 },
};

static void mld_query_send(mld_t *mld, unsigned int ifx, struct in6_addr *saddr);
static inline void mld_watch_event_push(mld_t *mld, unsigned int ifx, struct in6_addr *addr, int type);

void mld_loglevel_set(int level)
{
	loglevel = level;
}

static void freewatchlist(mld_watch_t *w)
{
	for (mld_watch_t *p = w; w; p = w, w = w->next, free(p));
}

static void freegrplist(mld_grp_list_t *g)
{
	for (mld_grp_list_t *p = g; g; p = g, g = g->next, free(p));
}

static void freeifacelist(mld_iface_t *i)
{
	for (mld_iface_t *p = i; i; p = i, i = i->next, free(p)) {
		if (i->grp) freegrplist(i->grp);
	}
}

static mld_iface_t *mld_get_iface(const mld_t *mld, const unsigned int ifx)
{
	for (mld_iface_t *i = aload(&mld->iface); i; i = aload(&i->next)) {
		if (i->ifx == ifx) return i;
	}
	return NULL;
}

mld_grp_list_t *mld_get_grp(const mld_t *mld, const unsigned int ifx, const struct in6_addr *grp)
{
	mld_iface_t *i = mld_get_iface(mld, ifx);
	if (!i) {
		errno = ENODEV;
		return NULL;
	}
	for (mld_grp_list_t *g = i->grp; g; g = g->next) {
		if (!memcmp(&g->addr, grp, sizeof(struct in6_addr))) return g;
	}
	return NULL;
}

void mld_free(mld_t *mld)
{
	int err = errno;
	freewatchlist(mld->watch);
	freeifacelist(mld->iface);
	sem_destroy(&mld->sem_mld);
	sem_destroy(&mld->sem_state);
#if HAVE_NETLINK
	sem_destroy(&mld->sem_netlink);
#endif
	free(mld);
	errno = err;
}

int mld_del_iface(mld_t *mld, unsigned int ifx)
{
	mld_iface_t *i, *p = NULL;
	for (i = mld->iface; i; i = i->next) {
		if (i->ifx == ifx) break;
		p = i;
	}
	if (!i) {
		errno = ENODEV;
		return -1;
	}
#if OS_NETBSD
	close(i->bpf);
#endif
	if (p) {
		p->next = i->next;
	}
	else {
		mld->iface = i->next;
	}
	if (i->grp) freegrplist(i->grp);
	free(i);
	mld_watch_event_push(mld, ifx, NULL, MLD_EVENT_IFDOWN);
	return 0;
}

/* join MLD2_CAPABLE_ROUTERS on interface ifx */
static int mld_join_iface(mld_t *mld, unsigned int ifx)
{
	struct ipv6_mreq req = {0};
	struct sockaddr_in6 *llocal;
	struct ifaddrs *ifaddr = NULL;
	mld_iface_t *iface = mld_get_iface(mld, ifx);
	assert(iface);

	if (inet_pton(AF_INET6, MLD2_CAPABLE_ROUTERS, &(req.ipv6mr_multiaddr)) != 1) {
		return -1;
	}
	req.ipv6mr_interface = ifx;
	if (getifaddrs(&ifaddr)) {
		ERROR("%s() getifaddrs: %s\n", __func__, strerror(errno));
		return -1;
	}
	/* find link-local address for ifx and bind to perform JOIN */
	for (struct ifaddrs *ifa = ifaddr; ifa; ifa = ifa->ifa_next) {
		if (if_nametoindex(ifa->ifa_name) != ifx) {
			continue;
		}
		if (ifa->ifa_addr->sa_family !=AF_INET6) {
			continue;
		}
		llocal = ((struct sockaddr_in6 *)ifa->ifa_addr);
		if (!IN6_IS_ADDR_LINKLOCAL(&llocal->sin6_addr)) {
			continue;
		}
		if (!(ifa->ifa_flags & IFF_MULTICAST)) {
			continue;
		}
		int sock = aload(&mld->sock);
		if (!setsockopt(sock, IPPROTO_IPV6, IPV6_JOIN_GROUP, &req, sizeof(req))) {
			DEBUG("MLD listening on interface %s[%u]\n", ifa->ifa_name, ifx);
			memcpy(&iface->llink, llocal, sizeof(struct sockaddr_in6));
		}
		else {
			ERROR("%s() setsockopt: %s\n", __func__, strerror(errno));
		}
		astor(&mld->sock, sock);
	}
	freeifaddrs(ifaddr);
	/* send MLD2 General Queries */
	mld_query_send(mld, ifx, NULL);
	return 0;
}

#if OS_NETBSD
static int mld_bpf_init(char *ifname)
{
	struct sock_fprog prog = {
		.bf_len = sizeof(BPF_MLD) / sizeof(BPF_MLD[0]),
		.bf_insns = BPF_MLD,
	};
	struct ifreq ifr = {0,};
	int bpf;
	const int opt = 1;
	bpf = open("/dev/bpf", O_RDONLY);
	if (bpf == -1) goto exit_0;
	strcpy(ifr.ifr_name, ifname);
	if (ioctl(bpf, BIOCSETIF, &ifr) == -1) goto exit_1;
	if (ioctl(bpf, BIOCIMMEDIATE, &opt)) goto exit_1;
	if (ioctl(bpf, BIOCSETF, &prog) == -1) goto exit_1;
exit_0:
	return bpf;
exit_1:
	close(bpf);
	return bpf;
}
#endif

int mld_add_iface(mld_t *mld, unsigned int ifx, char *ifname)
{
	mld_iface_t *iface;
	if (strlen(ifname) >= 16)
		return (errno = ENAMETOOLONG), -1;
	if (mld_get_iface(mld, ifx))
		return (errno = ENODEV), -1;
	iface = calloc(1, sizeof(mld_iface_t));
	if (!iface) return -1;
	astor(&iface->next, aload(&mld->iface));
	iface->ifx = ifx;
	strcpy(iface->ifname, ifname);
#if OS_NETBSD
	iface->bpf = mld_bpf_init(ifname);
#endif
	astor(&mld->iface, iface);
	mld_join_iface(mld, ifx);
	DEBUG("ifx %u added\n", ifx);
	mld_watch_event_push(mld, ifx, NULL, MLD_EVENT_IFUP);
	return 0;
}

int mld_add_iface_ifx(mld_t *mld, unsigned int ifx)
{
	char ifname[IF_NAMESIZE];
	if (!if_indextoname(ifx, ifname)) {
		ERROR("%s() if_indextoname: %s\n", __func__, strerror(errno));
		return -1;
	}
	return mld_add_iface(mld, ifx, ifname);
}


void *mld_state_ifx_del(void *arg)
{
	struct mld_txn_s *txn = arg;
#if 0
	if (!if_indextoname(txn->ifx, ifname)) {
		ERROR("trying to delete unknown ifx %u", txn->ifx);
		txn->err = errno;
		txn->ret = -1;
		return arg;
	}
#endif
	txn->ret = mld_del_iface(txn->mld, txn->ifx);
	txn->err = errno;
	return arg;
}

void *mld_state_ifx_add(void *arg)
{
	struct mld_txn_s *txn = arg;
	char ifname[IF_NAMESIZE];
	if (!if_indextoname(txn->ifx, ifname)) {
		txn->err = errno;
		txn->ret = -1;
		return arg;
	}
	txn->ret = mld_add_iface(txn->mld, txn->ifx, ifname);
	txn->err = errno;
	return arg;
}

/* create a grp list for each multicast capable interface */
void *mld_state_ifx_add_all(void *arg)
{
	mld_t *mld = (mld_t *)arg;
	struct ifaddrs *ifa = NULL, *ifap = NULL;
	unsigned int ifx;
	if (getifaddrs(&ifa) == -1) {
		ERROR("%s() getifaddrs(): %s\n", __func__, strerror(errno));
		return NULL;
	}
	for (ifap = ifa; ifap; ifap = ifap->ifa_next) {
		if (!(ifap->ifa_flags & IFF_MULTICAST)) continue;
		if (ifap->ifa_addr == NULL) continue;
		if (ifap->ifa_addr->sa_family != AF_INET6) continue;
		ifx = if_nametoindex(ifap->ifa_name);
		mld_add_iface(mld, ifx, ifap->ifa_name);
	}
	freeifaddrs(ifa);
	return arg;
}

static int mld_state_grp_del_entry(mld_iface_t *iface, struct in6_addr *addr)
{
	mld_grp_list_t *grp, *p = NULL;
	for (grp = iface->grp; grp; p = grp, grp = grp->next) {
		if (!memcmp(&grp->addr, addr, sizeof(struct in6_addr))) break;
	}
	if (!grp) {
		errno = ENOENT;
		return -1;
	}
	if (p) {
		p->next = grp->next;
	}
	else {
		iface->grp = grp->next;
	}
	free(grp);
	return 0;
}

static void *mld_state_grp_del(void *arg)
{
	struct mld_txn_s *txn = arg;
	mld_iface_t *iface = mld_get_iface(txn->mld, txn->ifx);
	if (!iface) {
		txn->err = ENODEV;
		txn->ret = -1;
		return arg;
	}
	txn->ret = mld_state_grp_del_entry(iface, txn->addr);
	txn->err = errno;
	return arg;
}

static int mld_state_grp_check(struct mld_txn_s *txn)
{
	mld_grp_list_t *grp;
	if ((grp = mld_get_grp(txn->mld, txn->ifx, txn->addr))) {
		struct timespec ts = {0};
		mld_iface_t *iface = mld_get_iface(txn->mld, txn->ifx);
		if (!iface) return (errno = ENODEV), -1;
		if (clock_gettime(CLOCK_REALTIME, &ts) == -1) return -1;
		if (grp->expires.tv_sec <= ts.tv_sec) { /* expired, delete */
			return mld_state_grp_del_entry(iface, txn->addr);
		}
		else {
			txn->expires.tv_sec = grp->expires.tv_sec;
			txn->expires.tv_nsec = grp->expires.tv_nsec;
			return 1;
		}
	}
	return 0;
}

static void *mld_state_grp_cmp(void *arg)
{
	struct mld_txn_s *txn = arg;
	mld_iface_t *iface;
	if (!txn->ifx) {
		/* check all interfaces, return 1 if *any* match */
		txn->ret = 0;
		for (mld_iface_t *i = txn->mld->iface; i; i = i->next) {
			txn->ifx = i->ifx;
			txn->ret = mld_state_grp_check(txn);
			if (txn->ret) break;
		}
	}
	else if (!(iface = mld_get_iface(txn->mld, txn->ifx))) {
		txn->err = ENODEV;
		txn->ret = -1;
	}
	else {
		txn->ret = mld_state_grp_check(txn);
		txn->err = errno;
	}
	return arg;
}

static void *mld_state_grp_add(void *arg)
{
	struct mld_txn_s *txn;
	if (!arg) return NULL;
	txn = arg;
	mld_grp_list_t *grp;
	mld_iface_t *iface = mld_get_iface(txn->mld, txn->ifx);
	if (!iface) {
		txn->err = ENODEV;
		txn->ret = -1;
		return arg;
	}
	grp = mld_get_grp(txn->mld, txn->ifx, txn->addr);
	if (!grp) {
		/* not found, add group */
		grp = calloc(1, sizeof(mld_grp_list_t));
		if (!grp) {
			txn->ret = -1;
			return arg;
		}
		grp->next = iface->grp;
		memcpy(&grp->addr, txn->addr, sizeof(struct in6_addr));
		iface->grp = grp;
	}
	else {
		txn->ret = 1;
	}
	grp->expires.tv_sec = txn->expires.tv_sec;
	grp->expires.tv_nsec = txn->expires.tv_nsec;

#if DEBUG_ON
	char strgrp[INET6_ADDRSTRLEN];
	char ifname[IF_NAMESIZE];
	if_indextoname(txn->ifx, ifname);
	inet_ntop(AF_INET6, &grp->addr, strgrp, INET6_ADDRSTRLEN);
	//DEBUG("%s(): %s on %s[%u]", __func__, strgrp, ifname, txn->ifx);
#endif

	return arg;
}

void mld_watch_del(mld_watch_t *watch)
{
	mld_watch_t *next = aload(&watch->next);
	mld_watch_t *prev = aload(&watch->prev);
	if (prev) astor(&prev->next, next);
	else astor(&watch->mld->watch, next);
	free(watch);
}

mld_watch_t *mld_watch_add(mld_t *mld, unsigned int ifx, struct in6_addr *addr,
		void (*f)(mld_watch_t *), void *arg, int flags)
{
	mld_watch_t *watch = malloc(sizeof(mld_watch_t));
	if (!watch) return NULL;
	memset(watch, 0, sizeof(mld_watch_t));
	watch->mld = mld;
	watch->ifx = ifx;
	watch->arg = arg;
	watch->flags = flags;
	watch->f = f;
	if (addr) memcpy(&watch->grp, addr, sizeof(struct in6_addr));

	if (mld->watch) {
		watch->next = mld->watch;
		mld->watch->prev = watch;
	}
	astor(&mld->watch, watch);

	return watch;
}

mld_t *mld_watch_mld(mld_watch_t *watch)
{
	return watch->mld;
}

unsigned int mld_watch_ifx(mld_watch_t *watch)
{
	return watch->ifx;
}

struct in6_addr *mld_watch_grp(mld_watch_t *watch)
{
	return &watch->grp;
}

void *mld_watch_arg(mld_watch_t *watch)
{
	return watch->arg;
}

pthread_t mld_watch_tid(mld_watch_t *watch)
{
	return watch->tid;
}

int mld_watch_flags(mld_watch_t *watch)
{
	return watch->flags;
}

void mld_wait_callback(mld_watch_t *watch)
{
	sem_post(mld_watch_arg(watch));
}

void *mld_wait_callback_thread(void *arg)
{
	mld_watch_t *watch;
	if (!arg) return NULL;
	watch = (mld_watch_t *)arg;
	watch->f((mld_watch_t *)arg);
	free(arg);
	return NULL;
}

int mld_wait(mld_t *mld, unsigned int ifx, struct in6_addr *addr, int flags)
{
	sem_t sem_watch;
	int rc;
	if ((rc = mld_filter_grp_cmp(mld, ifx, addr)) == -1) return -1;
	if (rc == 1) return 0;
	if (flags & MLD_DONTWAIT) {
		errno = EWOULDBLOCK;
		return -1;
	}
	if (sem_init(&sem_watch, 0, 0)) return -1;
	if (mld_watch_add(mld, ifx, addr, &mld_wait_callback, &sem_watch, 0)) {
		rc = sem_wait(&sem_watch);
	}
	else rc = -1;
	sem_destroy(&sem_watch);
	return rc;
}

/* check for watchers, run callbacks in detached thread */
static void *mld_watch_event(void *arg)
{
	pthread_attr_t attr;
	mld_txn_t *txn;
	mld_watch_t *event;
	if (!arg) return NULL;
	if (pthread_attr_init(&attr)) return NULL;
	if (pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED))
		goto err_attr_destroy;
	txn = (mld_txn_t *)arg;
	for (mld_watch_t *w = aload(&txn->mld->watch); w; w = aload(&w->next)) {
		if (w->ifx && txn->ifx && txn->ifx != w->ifx) continue;
		if (w->grp.s6_addr[0] && txn->addr && memcmp(txn->addr, &w->grp, sizeof(struct in6_addr)))
			continue;
		if (!(txn->type & w->flags)) continue;
		if (!(event = malloc(sizeof(mld_watch_t)))) break;
		memcpy(event, w, sizeof(mld_watch_t));
		event->flags = txn->type;
		event->ifx = txn->ifx;
		if (txn->addr) memcpy(&event->grp, txn->addr, sizeof(struct in6_addr));
		if (pthread_create(&w->tid, &attr, &mld_wait_callback_thread, event))
			free(event);
	}
	free(txn->addr);
	free(txn);
err_attr_destroy:
	pthread_attr_destroy(&attr);
	return NULL;
}

/* push watch event */
static inline void mld_watch_event_push(mld_t *mld, unsigned int ifx, struct in6_addr *addr, int type)
{
	struct in6_addr *grp = NULL;
	mld_txn_t *txn;
	if (!mld->q[MLD_WATCH]) return;
	txn = calloc(1, sizeof (mld_txn_t));
	if (!txn) return;
	if (addr) {
		grp = malloc(sizeof(struct in6_addr));
		if (!grp) goto err_free_txn;
		memcpy(grp, addr, sizeof(struct in6_addr));
	}
	txn->mld = mld;
	txn->ifx = ifx;
	txn->addr = grp;
	txn->type = type;
	job_push_new(mld->q[MLD_WATCH], &mld_watch_event, txn, sizeof(*txn), NULL, 0);
	return;
err_free_txn:
	free(txn);
}

/* check every grp on every interface for expiry */
static void *mld_state_timer_check(void *arg)
{
	mld_t *mld = (mld_t *)arg;
	struct timespec now = {0};
	if (clock_gettime(CLOCK_REALTIME, &now) == -1) return NULL;
	for (mld_iface_t *i = mld->iface; i; i = i->next) {
		for (mld_grp_list_t *g = i->grp, *p = NULL; g;) {
			if (g->expires.tv_sec <= now.tv_sec) {
				mld_grp_list_t *tmp = g;
				/* group has expired - delete and notify watchers */
				mld_watch_event_push(mld, i->ifx, &g->addr, MLD_EVENT_PART);
				if (p) {
					p->next = g->next;
				}
				else {
					i->grp = g->next;
				}
				g = g->next;
				free(tmp);
				continue;
			}
			else if (g->expires.tv_sec - now.tv_sec <= MLD2_QRI / 1000) {
				/* group is about to expire, send Query */
				mld_query_send(mld, i->ifx, &g->addr);
			}
			p = g, g = g->next;
		}
	}
	sem_post(&mld->sem_state); /* allow next timer job */
	return arg;
}

static void txn_done(void *arg)
{
	struct mld_txn_s *txn = (struct mld_txn_s *)arg;
	sem_post(&txn->done);
}

static int txn_init(struct mld_txn_s *txn, mld_t *mld, unsigned int ifx, struct in6_addr *addr,
		struct timespec *ts)
{
	if (sem_init(&txn->done, 0, 0)) return -1;
	txn->mld = mld;
	txn->ifx = ifx;
	txn->addr = addr;
	if (ts) {
		txn->expires.tv_sec = ts->tv_sec;
		txn->expires.tv_nsec = ts->tv_nsec;
	}
	return 0;
}

static int txn_wait(struct mld_txn_s *txn)
{
	if (sem_wait(&txn->done) || sem_destroy(&txn->done)) return -1;
	errno = txn->err;
	return txn->ret;
}

int mld_filter_ifx_del(mld_t *mld, unsigned int ifx)
{
	struct mld_txn_s txn = {0};
	if (txn_init(&txn, mld, ifx, NULL, NULL) == -1) return -1;
	if (!job_push_new(mld->q[MLD_STATE], &mld_state_ifx_del, &txn, sizeof(txn), &txn_done, 0))
		return -1;
	return txn_wait(&txn);
}

int mld_filter_ifx_add(mld_t *mld, unsigned int ifx)
{
	struct mld_txn_s txn = {0};
	if (txn_init(&txn, mld, ifx, NULL, NULL) == -1) return -1;
	if (!job_push_new(mld->q[MLD_STATE], &mld_state_ifx_add, &txn, sizeof(txn), &txn_done, 0))
		return -1;
	return txn_wait(&txn);
}

int mld_filter_grp_cmp(mld_t *mld, unsigned int ifx, struct in6_addr *addr)
{
	struct mld_txn_s txn = {0};
	if (!addr) {
		errno = EINVAL;
		return -1;
	}
	/* push job to STATE queue */
	if (txn_init(&txn, mld, ifx, addr, NULL) == -1) return -1;
	if (!job_push_new(mld->q[MLD_STATE], &mld_state_grp_cmp, &txn, sizeof(txn), &txn_done, 0))
		return -1;
	return txn_wait(&txn);
}

int mld_filter_timer_get(mld_t *mld, unsigned int ifx, struct in6_addr *addr, struct timespec *ts)
{
	struct mld_txn_s txn = {0};
	if (!addr) {
		errno = EINVAL;
		return -1;
	}
	/* push job to STATE queue */
	if (txn_init(&txn, mld, ifx, addr, ts) == -1) return -1;
	if (!job_push_new(mld->q[MLD_STATE], &mld_state_grp_cmp, &txn, sizeof(txn), &txn_done, 0))
		return -1;
	txn_wait(&txn);
	ts->tv_sec = txn.expires.tv_sec;
	ts->tv_nsec = txn.expires.tv_nsec;
	return txn.ret;
}

int mld_filter_grp_del(mld_t *mld, unsigned int ifx, struct in6_addr *addr)
{
	struct mld_txn_s txn = {0};

	assert(mld->q[MLD_STATE]); /* mld_start() MUST have been called first */
	if (!addr) {
		errno = EINVAL;
		return -1;
	}

	/* push job to STATE queue */
	if (txn_init(&txn, mld, ifx, addr, NULL) == -1) return -1;
	if (!job_push_new(mld->q[MLD_STATE], &mld_state_grp_del, &txn, sizeof(txn), &txn_done, 0))
		return -1;
	return txn_wait(&txn);
}

int mld_filter_timer_set(mld_t *mld, unsigned int ifx, struct in6_addr *addr, struct timespec *ts)
{
	struct mld_txn_s txn = {0};

	assert(mld->q[MLD_STATE]); /* mld_start() MUST have been called first */
	if (!addr) {
		errno = EINVAL;
		return -1;
	}
	/* push job to STATE queue to update timer */
	if (txn_init(&txn, mld, ifx, addr, ts) == -1) return -1;
	if (!job_push_new(mld->q[MLD_STATE], &mld_state_grp_add, &txn, sizeof(txn), &txn_done, 0))
		return -1;
	return txn_wait(&txn);
}

int mld_filter_timer_set_s(mld_t *mld, unsigned int ifx, struct in6_addr *addr, int s)
{
	struct timespec ts = {0};
	if (clock_gettime(CLOCK_REALTIME, &ts) == -1) return -1;
	ts.tv_sec += s;
	return mld_filter_timer_set(mld, ifx, addr, &ts);
}

int mld_filter_grp_add(mld_t *mld, unsigned int ifx, struct in6_addr *addr)
{
	mld_watch_event_push(mld, ifx, addr, MLD_EVENT_JOIN);
	return mld_filter_timer_set_s(mld, ifx, addr, MLD_TIMEOUT);
}

/* return true if we are the active Querier for mif
 * passing ts as current time is optional, as calling routines often already
 * have the current time */
static int mld_is_querier(mld_t *mld, unsigned int ifx, struct timespec *ts)
{
	mld_iface_t *iface = mld_get_iface(mld, ifx);
	struct timespec now;
	if (!iface) return 0;
	if (ts) now.tv_sec = ts->tv_sec;
	else if (clock_gettime(CLOCK_REALTIME, &now) == -1) return -1;
	return ((aload(&iface->qseen.tv_sec) + MLD2_OTHER_QUERIER_TIMEOUT) > now.tv_sec) ? 0 : 1;
}

void mld_query_msg_free(mld_msg_t *msg)
{
	if (msg) free(msg->cmsgbuf);
}

/* set length of msgh->msg_controllen */
static int mld_query_cmsglen(mld_msg_t *msg)
{
	int rc;
	if ((rc = inet6_opt_init(NULL, 0)) == -1) {
		ERROR("inet6_opt_init failed\n");
		return -1;
	}
	msg->extlen = (socklen_t)rc;
	assert(msg->extlen > 0);
	if ((rc = inet6_opt_append(NULL, 0, msg->extlen, IP6OPT_ROUTER_ALERT, 2, 2, NULL)) == -1) {
		ERROR("inet6_opt_append failed\n");
		return -1;
	}
	msg->extlen = (socklen_t)rc;
	assert(msg->extlen > 0);
	if ((rc = inet6_opt_finish(NULL, 0, msg->extlen)) == -1) {
		ERROR("inet6_opt_finish failed\n");
		return -1;
	}
	msg->extlen = (socklen_t)rc;
	msg->msgh.msg_controllen = CMSG_SPACE(msg->extlen);
	return 0;
}

int mld_query_msg(mld_t *mld, unsigned int ifx, struct in6_addr *saddr, mld_msg_t *msg)
{
	(void)mld; /* unused */
	(void)ifx; /* unused */
	void *databufp = NULL, *extbuf;
	uint16_t racode = 0;
	uint8_t qrv = (uint8_t)MLD2_ROBUSTNESS;
	int offset;

	if (mld_query_cmsglen(msg) == -1) return -1;
	msg->cmsgbuf = malloc(msg->msgh.msg_controllen);
	if (!msg->cmsgbuf) return -1;
	memset(msg->cmsgbuf, 0, msg->msgh.msg_controllen);
	msg->dst.sin6_family = AF_INET6;
	msg->qmsg.type = 130;
	msg->qmsg.mrc = htobe16(MLD2_QRI);
	assert(MLD2_ROBUSTNESS <= 0xf); /* QRV is lowest 3 bits in network byte order */
	msg->qmsg.bits = htons(qrv);
	msg->qmsg.qqic = MLD2_QI;
	msg->iov[0].iov_base = &msg->qmsg;
	msg->iov[0].iov_len = sizeof(mld_query_msg_t);
	msg->msgh.msg_name = &msg->dst;
	msg->msgh.msg_namelen = sizeof(struct sockaddr_in6);
	msg->msgh.msg_iov = msg->iov;
	msg->msgh.msg_iovlen = 1;
	msg->msgh.msg_control = msg->cmsgbuf;
	msg->cmsgh = CMSG_FIRSTHDR(&msg->msgh);
	msg->cmsgh->cmsg_len = CMSG_LEN(msg->extlen);
	msg->cmsgh->cmsg_level = IPPROTO_IPV6;
	msg->cmsgh->cmsg_type = IPV6_HOPOPTS;
	extbuf = CMSG_DATA(msg->cmsgh);
	if ((offset = inet6_opt_init(extbuf, msg->extlen)) == -1) {
		ERROR("inet6_opt_init");
		goto err_free_cmsgbuf;
	}
	if ((offset = inet6_opt_append(extbuf, msg->extlen, offset,
		IP6OPT_ROUTER_ALERT, 2, 2, &databufp)) == -1)
	{
		ERROR("inet6_opt_append\n");
		goto err_free_cmsgbuf;
	}
	inet6_opt_set_val(databufp, 0, &racode, sizeof(racode));
	if (inet6_opt_finish(extbuf, msg->extlen, offset) == -1) {
		ERROR("inet6_opt_finish\n");
		goto err_free_cmsgbuf;
	}
	if (saddr) {
		/* Multicast Address Specific Query */
		memcpy(&msg->qmsg.addr, saddr, sizeof(struct in6_addr));
		memcpy(&msg->dst.sin6_addr, saddr, sizeof(struct in6_addr));
	}
	else {
		/* MLDv2 General Query */
		memset(&msg->dst.sin6_addr, 0, sizeof(struct in6_addr));
		if (inet_pton(AF_INET6, MLD2_ALL_NODES, &msg->dst.sin6_addr) != 1) {
			ERROR("inet_pton\n");
			goto err_free_cmsgbuf;
		}
		memcpy(&msg->qmsg.addr, &msg->dst.sin6_addr, sizeof(struct in6_addr));
	}
	return 0;
err_free_cmsgbuf:
	free(msg->cmsgbuf);
	return -1;
}

static void mld_query_send(mld_t *mld, unsigned int ifx, struct in6_addr *saddr)
{
	mld_msg_t msg = {0};
	mld_iface_t *iface;
	struct timespec now = {0};
	int sock = aload(&mld->sock);
	int opt = 1;

	iface = mld_get_iface(mld, ifx);
	if (!iface) return;
	if (saddr) {
		/* Multicast Address Specific Query - check timer */
		if (clock_gettime(CLOCK_REALTIME, &now) == -1) return;
		/* check if we are the Querier (for General queries we've already done this) */
		if (!mld_is_querier(mld, ifx, &now)) return;
		mld_grp_list_t *g;
		g = mld_get_grp(mld, ifx, saddr);
		if (!g) return;
		if (now.tv_sec - g->qlast.tv_sec < MLD2_QRI / 1000)
			return;
		g->qlast.tv_sec = now.tv_sec;
	}
	if (mld_query_msg(mld, ifx, saddr, &msg) == -1) return;
	if ((setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) == -1) {
		ERROR("setsockopt(SO_REUSEADDR)\n");
	}
#ifdef SO_REUSEPORT
	if ((setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt))) == -1) {
		ERROR("setsockopt(SO_REUSEPORT)\n");
	}
#endif
#ifdef SO_BINDTODEVICE
	/* if a previous call had a bind() and ifx is different from what it had
	 * at the time, the next setsockopt(IPV6_MULTICAST_IF) will fail; this
	 * works around it */
	opt = 0;
	if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, &opt, sizeof(opt)) == -1)
		ERROR("setsockopt(SO_BINDTODEVICE)\n");
#endif
	if (saddr) {
		/* bind iff Address Specific Query and address is NOT local scope
		 * FreeBSD fails if we bind() when sending to local scope */
		if (!IN6_IS_ADDR_MC_LINKLOCAL(saddr)) {
			if (bind(sock, (struct sockaddr *)&iface->llink, sizeof(struct sockaddr_in6)) == -1) {
				ERROR("%s: bind: %s\n", __func__, strerror(errno));
				goto err_mld_query_msg_free;
			}
		}
	}
	if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifx, sizeof(ifx)) == -1)
		ERROR("setsockopt\n");

	for (int i = 0; i < MLD2_ROBUSTNESS; i++) {
		if (sendmsg(sock, &msg.msgh, 0) == -1) ERROR("sendmsg\n");
	}
	astor(&mld->sock, sock);
err_mld_query_msg_free:
	mld_query_msg_free(&msg);
}

int mld_filter_grp_part(mld_t *mld, unsigned int ifx, struct in6_addr *addr)
{
	mld_query_send(mld, ifx, addr);
	/* set expiry for group to Query Response Interval */
	return mld_filter_timer_set_s(mld, ifx, addr, MLD2_QRI / 1000);
}

/* extract interface number from ancillary control data */
unsigned int interface_index(struct msghdr *msg)
{
	struct cmsghdr *cmsg;
	struct in6_pktinfo pi = {0};
	unsigned int ifx = 0;
	for (cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) {
		if (cmsg->cmsg_type == IPV6_PKTINFO) {
			/* may not be aligned, copy */
			memcpy(&pi, CMSG_DATA(cmsg), sizeof pi);
			ifx = pi.ipi6_ifindex;
			break;
		}
	}
	assert(ifx);
	return ifx;
}

static void mld_address_record(mld_t *mld, unsigned int ifx, mld_addr_rec_t *rec)
{
	struct in6_addr grp = rec->addr;
#ifdef DEBUG_ON
	char strgrp[INET6_ADDRSTRLEN] = {0};
	char ifname[IF_NAMESIZE] = {0};
	if_indextoname(ifx, ifname);
	inet_ntop(AF_INET6, &grp, strgrp, INET6_ADDRSTRLEN);
#endif

	//DEBUG("%s type=%u\n", __func__, rec->type);

	/* XXX: we're ignoring SSM here */
	switch (rec->type) {
		case BLOCK_OLD_SOURCES:
		case MODE_IS_INCLUDE:
		case CHANGE_TO_INCLUDE_MODE:
			DEBUG("INCLUDE %s received on %s(%u)\n", strgrp, ifname, ifx);
			mld_filter_grp_part(mld, ifx, &grp);
			break;
		case MODE_IS_EXCLUDE:
		case CHANGE_TO_EXCLUDE_MODE:
			DEBUG("EXCLUDE %s received on %s(%u)\n", strgrp, ifname, ifx);
			mld_filter_grp_add(mld, ifx, &grp);
			break;
	}
}

#ifndef NDEBUG
static
#endif
void mld_query_handler(mld_t *mld, unsigned int ifx, struct ip6_hdr *ip6h)
{
	mld_iface_t *iface;
	struct in6_addr *src = (struct in6_addr *)((char *)ip6h + offsetof(struct ip6_hdr, ip6_src));

	if (!IN6_IS_ADDR_LINKLOCAL(src)) {
		DEBUG("MLD Query source is not link local - ignoring\n");
		return;
	}

	// TODO ignore if hop limit != 1

#if DEBUG_ON
	char straddrgrp[INET6_ADDRSTRLEN] = {0};
	char ifname[IF_NAMESIZE] = {0};
	inet_ntop(AF_INET6, src, straddrgrp, INET6_ADDRSTRLEN);
	if_indextoname(ifx, ifname);
	DEBUG("processing MLD Query from %s on %s(%u)\n", straddrgrp, ifname, ifx);
#endif
	iface = mld_get_iface(mld, ifx);
	if (!iface) return;

	/* Querier Election - compare lowest 64 bits in big-endian order lowest wins */
	uint64_t srclo64 = *(uint64_t *)&src->s6_addr[8];
	uint64_t loclo64 = *(uint64_t *)&iface->llink.sin6_addr.s6_addr[8];
	if (be64toh(srclo64) < be64toh(loclo64)) {
		/* other node is Querier - set timer */
		struct timespec now;
		if (!clock_gettime(CLOCK_REALTIME, &now) && mld_is_querier(mld, ifx, &now) == 1) {
			char ifname[IF_NAMESIZE];
			if_indextoname(ifx, ifname);
			DEBUG("Querier detected. Entering Non-Querier state on %s(%u)\n", ifname, ifx);
		}
		astor(&iface->qseen.tv_sec, now.tv_sec);
	}
}

static void mld2_listen_report(mld_t *mld, unsigned int ifx, struct mld_hdr *mldh)
{
	struct icmp6_hdr *icmpv6 = (struct icmp6_hdr *)mldh;
	uint16_t recs = ntohs(icmpv6->icmp6_data16[1]);
	mld_addr_rec_t *mrec = (mld_addr_rec_t *)((char *)mldh + offsetof(struct mld_hdr, mld_addr));
	DEBUG("processing mld2 listen report with %u records\n", recs);
	while (recs--) mld_address_record(mld, ifx, mrec++);
}

static void mld_msg_handler(mld_t *mld, unsigned int ifx, struct ip6_hdr *ip6h, struct mld_hdr *mldh)
{
	struct in6_addr *mld_addr = (struct in6_addr *)((char *)mldh + offsetof(struct mld_hdr, mld_addr));
	switch (mldh->mld_type) {
		case MLD_LISTENER_QUERY:
			DEBUG("MLD_LISTENER_QUERY received on %u\n", ifx);
			mld_query_handler(mld, ifx, ip6h);
			break;
		case MLD2_LISTENER_REPORT:
			DEBUG("MLD2_LISTENER_REPORT received on %u\n", ifx);
			mld2_listen_report(mld, ifx, mldh);
			break;
		case MLD_LISTENER_REPORT:
			DEBUG("MLD_LISTENER_REPORT received on %u\n", ifx);
			// TODO: set mldv1 compat mode
			mld_filter_grp_add(mld, ifx, mld_addr);
			break;
		case MLD_LISTENER_DONE:
			DEBUG("MLD_LISTENER_DONE received on %u\n", ifx);
			// TODO: set mldv1 compat mode
			mld_filter_grp_part(mld, ifx, mld_addr);
			break;
		default:
			DEBUG("unhandled icmpv6 type %u received on %u\n", mldh->mld_type, ifx);
	}
}

#ifdef __linux__
/* on Linux, unlike BSD, we can listen for BPF packets across ALL interfaces on
 * a single socket. */
static int mld_listen(mld_t *mld)
{
	char buf[32767] = {0};
	struct sockaddr_ll sll = {0};
	struct sock_fprog bpf = {
		.len = sizeof(BPF_MLD) / sizeof(BPF_MLD[0]),
		.filter = BPF_MLD,
	};
	struct mld_hdr *mldh;
	struct ip6_hdr *ip6h;
	struct iovec iov[3] = {0};
	struct msghdr msg = {0};
	char ethh[14] = {0};
	char ipv6[48] = {0};
	ssize_t byt;
	unsigned int ifx;
	int rc = -1, sock;

	sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
	if (sock == -1) return -1;
	if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf)) == -1) {
		ERROR("setsockopt(SO_ATTACH_FILTER)\n");
		goto err_close_sock;
	}

	astor(&mld->listen_sock, sock);
	/* prepare scatter/gather structs */
	iov[0].iov_base = &ethh;
	iov[0].iov_len = sizeof ethh;
	iov[1].iov_base = ipv6;
	iov[1].iov_len = sizeof ipv6;
	iov[2].iov_base = buf;
	iov[2].iov_len = sizeof buf;
	msg.msg_name = &sll;
	msg.msg_namelen = sizeof sll;
	msg.msg_iov = iov;
	msg.msg_iovlen = sizeof iov / sizeof iov[0];
	msg.msg_flags = 0;
	while (1) {
		sock = aload(&mld->listen_sock);
		byt = recvmsg(sock, &msg, 0);
		if (byt == -1) {
			ERROR("read/recvmsg");
			goto err_close_sock;
		}
		mldh = (struct mld_hdr *)(buf);
		ip6h = (struct ip6_hdr *)&ipv6;
		ifx = (unsigned int)sll.sll_ifindex;
		DEBUG("received type %u on %u (%zi bytes)\n", mldh->mld_icmp6_hdr.icmp6_type, ifx, byt);
		if (ifx) mld_msg_handler(mld, ifx, ip6h, mldh);
	}
err_close_sock:
	close(sock);
	return rc;
}
#endif

#ifndef __linux__
/* return interface id matching BPF file descriptor */
static inline unsigned int bpffd2ifx(mld_t *mld, int bpffd)
{
	for (mld_iface_t *iface = mld->iface; iface; iface = iface->next) {
		if (iface->bpf == bpffd) return iface->ifx;
	}
	return 0;
}

/* on BSD, we poll() and read() an array of /dev/bpf filehandles (one per NIC)
 * BPF filehandles MUST be bound to a single NIC with BIOCSETIF */
static int mld_listen(mld_t *mld)
{
	struct pollfd *fds;
	struct mld_hdr *mldh;
	struct ip6_hdr *ip6h;
	struct bpf_hdr *bpfh;
	char straddr[INET6_ADDRSTRLEN] = "";
	char *buf;
	ssize_t byt;
	unsigned int blen = 32767;
	unsigned int ifx;
	int err = 0, nfds = 0, rc = 0;

	for (mld_iface_t *iface = mld->iface; iface; iface = iface->next) nfds++;
	if (!nfds) return -1;

	/* create a buffer of correct size for BPF */
	if (ioctl(mld->iface->bpf, BIOCGBLEN, &blen) == -1) return -1;
	buf = malloc((size_t)blen);
	if (!buf) return -1;
	memset(buf, 0, (size_t)blen);

	/* create array of pollfd structures */
	fds = calloc(nfds, sizeof (struct pollfd));
	if (!fds) { err = errno; rc = -1; goto err_free_buf; }
	mld_iface_t *iface = mld->iface;
	for (int i = 0; i < nfds; i++) {
		fds[i].fd = iface->bpf;
		fds[i].events = POLLIN;
		iface = iface->next;
	}

	while (nfds > 0) {
		rc = poll(fds, (nfds_t) nfds, 1000);
		pthread_testcancel();
		if (rc > 0) {
			for (int i = 0; i < nfds; i++) {
				if (fds[i].revents & POLLIN) {
					byt = read(fds[i].fd, buf, (size_t)blen);
					if (byt > 0) {
						bpfh = (struct bpf_hdr *)buf;
						ip6h = (struct ip6_hdr *)(buf + bpfh->bh_hdrlen + 14);
						mldh = (struct mld_hdr *)(buf + bpfh->bh_hdrlen + 62);
						inet_ntop(AF_INET6, &(mldh->mld_addr), straddr, sizeof straddr);
						DEBUG("%u: %s(%zi bytes)", mldh->mld_icmp6_hdr.icmp6_type, straddr, byt);
						ifx = bpffd2ifx(mld, fds[i].fd);
						if (ifx) mld_msg_handler(mld, ifx, ip6h, mldh);
					}
				} else if (fds[i].revents & (POLLHUP | POLLERR)) {
					/* socket closed or other error, stop checking it */
					nfds--;
					if (i != nfds) fds[i] = fds[nfds];
					i--;
				}
			}
		}
		if (rc < 0 && errno != EINTR) {
			err = errno;
			break;
		}
	}
	free(fds);
	if (nfds == 0 && err == 0) {
		err = EIO;
		rc = -1;
	}
err_free_buf:
	free(buf);
	if (err) errno = err;
	return rc;
}
#endif

static void *mld_thread_listen(void *arg)
{
	mld_t *mld = (mld_t *)arg;
	/* release semaphore - we're good to go */
	if (sem_post(&mld->sem_mld) == -1) goto err_close_sock;
	/* wait until state thread is ready */
	if (sem_wait(&mld->sem_state) == -1) goto err_close_sock;
	mld_listen(mld);
	return NULL;
err_close_sock:
	close(mld->sock);
	return NULL;
}

#if HAVE_NETLINK
static void mld_handle_netlink_msg(mld_t *mld, struct nlmsghdr *msg)
{
	struct ifinfomsg *ifi = NLMSG_DATA(msg);
	switch (msg->nlmsg_type) {
		case RTM_NEWLINK:
			if (ifi->ifi_flags & IFF_UP) {
				mld_filter_ifx_add(mld, ifi->ifi_index);
			}
			else {
				mld_filter_ifx_del(mld, ifi->ifi_index);
			}
			break;
		case RTM_DELLINK:
			DEBUG("RTM_DELLINK\n");
			mld_filter_ifx_del(mld, ifi->ifi_index);
			break;
		default:
			DEBUG("unknown netlink event type: %i\n", msg->nlmsg_type);
			break;
	}
}

static void *mld_thread_netlink(void *arg)
{
	mld_t *mld = (mld_t *)arg;
	/* 8192 to avoid message truncation on platforms with page size > 4096 */
	struct nlmsghdr buf[8192/sizeof(struct nlmsghdr)];
	struct iovec iov = { buf, sizeof(buf) };
	struct nlmsghdr *nh;
	struct sockaddr_nl sa;
	struct msghdr msg = { &sa, sizeof(sa), &iov, 1, NULL, 0, 0 };
	ssize_t len;
	int fd;

	memset(&sa, 0, sizeof(sa));
	sa.nl_family = AF_NETLINK;
	sa.nl_groups = RTMGRP_LINK;
	if ((fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) == -1) return NULL;
	if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) == -1) {
		close(fd);
		return NULL;
	}

	sem_post(&mld->sem_netlink);
	while (1) {
		len = recvmsg(fd, &msg, 0);
		if (len == -1) continue;
		for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, (size_t)len); nh = NLMSG_NEXT(nh, len)) {
			/* The end of multipart message */
			if (nh->nlmsg_type == NLMSG_DONE) continue;
			if (nh->nlmsg_type == NLMSG_ERROR) ERROR("NLMSG_ERROR\n");
			else mld_handle_netlink_msg(mld, nh);
		}
	}

	return arg;
}
#endif

static void *mld_thread_timer(void *arg)
{
	mld_t *mld = (mld_t *)arg;
	while (1) {
		sleep(1);
		/* we use a semaphore to ensure we only queue a maximum of one
		 * job at a time, even when STATE thread is busy */
		if (sem_trywait(&mld->sem_state) == -1) continue;
		job_push_new(mld->q[MLD_STATE], &mld_state_timer_check, mld, sizeof(*mld), NULL, 0);
	}
	return arg;
}

mld_t *mld_init(int flags)
{
	(void)flags; /* TODO implement flags */
	mld_t *mld;
	int err = 0;
	if (!(mld = malloc(sizeof(mld_t)))) return NULL;
	memset(mld, 0, sizeof(mld_t));
	if (sem_init(&mld->sem_mld, 0, 0) == -1) {
		err = errno; goto err_free_mld;
	}
	if (sem_init(&mld->sem_state, 0, 0) == -1) {
		err = errno; goto err_sem_mld_destroy;
	}
#if HAVE_NETLINK
	if (sem_init(&mld->sem_netlink, 0, 0) == -1) {
		err = errno; goto err_sem_state_destroy;
	}
#endif
	mld->listen_sock = -1;
	return mld;
#if HAVE_NETLINK
err_sem_state_destroy:
	sem_destroy(&mld->sem_state);
#endif
err_sem_mld_destroy:
	sem_destroy(&mld->sem_mld);
err_free_mld:
	free(mld);
	if (err) errno = err;
	return NULL;
}

void mld_stop(mld_t *mld)
{
	for (int i = 0; i < MLD_THREADS; i++) if (mld->q[i]) job_queue_destroy(mld->q[i]);
#ifdef __linux__
	int sock = aload(&mld->listen_sock);
	if (sock >= 0) {
		close(sock);
		astor(&mld->listen_sock, -1);
	}
#else
	for (mld_iface_t *iface = mld->iface; iface; iface = iface->next) {
		if (iface->bpf >= 0) {
			close(iface->bpf);
			iface->bpf = -1;
		}
	}
#endif
}

mld_t *mld_start(mld_t *mld)
{
	struct timespec ts;
	const int opt = 1;
	int err = 0;
	if (!mld) mld = mld_init(0);
	if (!mld) return NULL;
	for (int i = 0; i < MLD_THREADS; i++) {
		mld->q[i] = job_queue_create(1);
		if (!mld->q[i]) goto err_mld_stop;
	}
#if HAVE_NETLINK
	mld->job[MLD_NETLINK] = job_push_new(mld->q[MLD_NETLINK],
			&mld_thread_netlink, mld, sizeof(*mld), NULL, 0);
	if (!mld->job[MLD_NETLINK]) goto err_mld_stop;
	if (sem_wait(&mld->sem_netlink)) goto err_mld_stop; /* make sure netlink is ready */
#endif

	/* start listen thread and wait for it to initialize */
	int sock = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
	if (mld->sock == -1) goto err_mld_stop;
	if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &opt, sizeof(opt)))
		goto err_mld_stop;
	astor(&mld->sock, sock);
	mld->job[MLD_LISTEN] = job_push_new(mld->q[MLD_LISTEN],
			&mld_thread_listen, mld, sizeof(*mld), NULL, 0);
	if (!mld->job[MLD_LISTEN]) goto err_mld_stop;
	if (clock_gettime(CLOCK_REALTIME, &ts) == -1) goto err_mld_stop;
	ts.tv_nsec += MLD_LISTEN_THREAD_STARTUP;
	if (ts.tv_nsec > 999999999L) {
		ts.tv_nsec -= 1000000000L;
		ts.tv_sec++;
	}
	if (sem_timedwait(&mld->sem_mld, &ts) == -1) goto err_mld_stop;
	/* push existing interfaces to STATE thread */
	mld_state_ifx_add_all(mld);
	/* let mld_thread_listen() proceed */
	if (sem_post(&mld->sem_state) == -1) goto err_mld_stop;
	/* first mld_thread_timer() run */
	if (sem_post(&mld->sem_state) == -1) goto err_mld_stop;
	/* start timer thread */
	if (job_push_new(mld->q[MLD_TIMER], &mld_thread_timer, mld, sizeof(*mld), NULL, 0))
		return mld;
err_mld_stop:
	err = errno;
	mld_stop(mld);
	errno = err;
	return NULL;
}
