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

/* test 0000-0005
 *
 * Aim:
 * ensure MLD Queries are sent when they're supposed to be
 *
 * Method:
 *
 * - find multicast-capable interface
 * - mld_init()
 * - start query thread, listening for MLD Queries on socket
 * - wait for query thread to release semaphore
 * - mld_start()
 * - ensure MLD2 General Queries sent on startup
 * - create random multicast group (g1)
 * - ensure g1 not in MLD filter for interface before join
 * - JOIN g1
 * - ensure g1 *is* in filter after join
 * - PART g1
 * - ensure g1 is still in filter after PART
 * - ensure MLD Query Address Specific Query received
 * - ensure filter expiry timer set to QRI
 * - PART g1 again
 * - ensure no futher Queries sent
 * - (set QRI timer to simulate another Querier on interface)
 * - (another Querier present - ensure no MLD Queries received)
 * - cancel query thread, clean up
 */

#define _GNU_SOURCE /* required for struct in6_pktinfo */
#include "testnet.h"
#ifdef HAVE_LIBLIBRECAST
#include <librecast.h>
#include <librecast/if.h>
#endif
#include <errno.h>
#include <limits.h>
#include <mld_pvt.h>
#include <fcntl.h>
#include <net/if.h>
#include <netinet/icmp6.h>
#include <sys/types.h>
#include <ifaddrs.h>
#include <unistd.h>
#ifdef __linux__
#include <linux/if_packet.h>
#include <linux/if_ether.h>
#endif
#ifdef HAVE_NET_BPF_H
#include <net/bpf.h>
#include <sys/ioctl.h>
#define sock_filter bpf_insn
#define sock_fprog  bpf_program
#endif
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>

#ifdef HAVE_LIBLIBRECAST
static sem_t sem_query;
static struct in6_addr *addr;
static struct in6_addr allnodes;
static volatile int q_gen;
static volatile int q_add;

/* Thread to count MLD Query packets.
 * Use a BPF filter on a raw socket so we only receive icmp6 type 130.  */
void *thread_query(void *arg)
{
#ifdef __linux__
	(void)arg; /* unused */
	struct iovec iov[3] = {0};
	struct msghdr msg = {0};
#endif
#ifdef HAVE_NET_BPF_H
	unsigned int ifx = *(unsigned int *)arg;
	struct ifreq ifr;
#endif
	struct icmp6_hdr *icmpv6;
	struct mld_hdr mldh = {0};
	char *buf = NULL;
	ssize_t byt;
	int raw = -1;
	/* BPF filter: `tcpdump -dd "ip6[48] == 130"` */
	/* 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'
	 */
	struct sock_filter code[] = {
		{ 0x28, 0, 0, 0x0000000c },
		{ 0x15, 0, 13, 0x000086dd },
		{ 0x30, 0, 0, 0x00000014 },
		{ 0x15, 0, 11, 0x00000000 },
		{ 0x20, 0, 0, 0x00000036 },
		{ 0x15, 0, 2, 0x3a000502 },
		{ 0x28, 0, 0, 0x0000003a },
		{ 0x15, 4, 0, 0x00000000 },
		{ 0x20, 0, 0, 0x00000036 },
		{ 0x15, 0, 5, 0x3a000100 },
		{ 0x20, 0, 0, 0x0000003a },
		{ 0x15, 0, 3, 0x05020000 },
		{ 0x30, 0, 0, 0x0000003e },
		{ 0x15, 0, 1, 0x00000082 },
		{ 0x6, 0, 0, 0x00000074 },
		{ 0x6, 0, 0, 0x00000000 },
	};
	struct sock_fprog bpf = {
#ifdef HAVE_NET_BPF_H
		.bf_len = sizeof(code) / sizeof(code[0]),
		.bf_insns = code,
#else
		.len = sizeof(code) / sizeof(code[0]),
		.filter = code,
#endif
	};
#ifdef HAVE_NET_BPF_H
	struct bpf_hdr *bpfh;
#else
	char ethh[14] = {0};
	char ipv6[48] = {0};
#endif
	unsigned int blen = 32767;

	test_log("query thread starting\n");

#ifdef HAVE_NET_BPF_H
	if_indextoname(ifx, ifr.ifr_name);
	raw = open("/dev/bpf", O_RDONLY);
#endif
#ifdef __linux__
	raw = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
#endif
	if (raw == -1) {
		perror("socket(PF_PACKET)");
		sem_post(&sem_query);
		return NULL;
	}

#ifdef HAVE_NET_BPF_H
	if (ioctl(raw, BIOCGBLEN, &blen) == -1) {
		perror("ioctl(BIOCGBLEN)");
		sem_post(&sem_query);
		goto exit_1;
	}
	buf = malloc((size_t)blen);
	if (!buf) {
		sem_post(&sem_query);
		goto exit_1;
	}
	if (ioctl(raw, BIOCSETIF, &ifr) == -1) {
		perror("ioctl(BIOCSETIF)");
		sem_post(&sem_query);
		goto exit_1;
	}
	int opt = 1;
	if (ioctl(raw, BIOCIMMEDIATE, &opt)) {
		perror("ioctl(BIOCIMMEDIATE)");
		sem_post(&sem_query);
		goto exit_1;
	}
	if (ioctl(raw, BIOCSETF, &bpf) == -1) {
		perror("ioctl(BIOCSETF)");
		sem_post(&sem_query);
		goto exit_1;
	}
#endif
#ifdef __linux__
	if (setsockopt(raw, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf)) == -1) {
		perror("setsockopt(SO_ATTACH_FILTER)");
		sem_post(&sem_query);
		return NULL;
	}
#endif
	sem_post(&sem_query); /* socket ready, let main thread proceed */

#ifdef __linux__
	/* 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 = &mldh;
	iov[2].iov_len = sizeof mldh;
	msg.msg_name = buf;
	msg.msg_namelen = blen;
	msg.msg_iov = iov;
	msg.msg_iovlen = 3;
	msg.msg_flags = 0;
#endif

	while (1) {
		test_log("query thread in loop\n");
#ifdef __linux__
		byt = recvmsg(raw, &msg, 0);
#endif
#ifdef HAVE_NET_BPF_H
		byt = read(raw, buf, (size_t)blen);
		bpfh = (struct bpf_hdr *)buf;
		memcpy(&mldh, (char *)buf + bpfh->bh_hdrlen + 62, sizeof(struct mld_hdr));
#endif
		if (byt == -1) {
			perror("recvmsg");
			break;
		}
		test_log("read %zi bytes\n", byt);
		test_assert(byt > 0, "%zi bytes received on PACKET socket", byt);
		if (byt > 0) {
			icmpv6 = &mldh.mld_icmp6_hdr;
			if (icmpv6->icmp6_type == MLD_LISTENER_QUERY) {
				char straddr[INET6_ADDRSTRLEN];
				inet_ntop(AF_INET6, &mldh.mld_addr, straddr, INET6_ADDRSTRLEN);
				test_log("Query addr: %s\n", straddr);

				/* MLDv2 General Query */
				if (!memcmp(&mldh.mld_addr, &allnodes, sizeof(struct in6_addr))) {
					q_gen++;
				}
				/* Multicast Address Specific Query */
				else if (addr && !memcmp(&mldh.mld_addr, addr, sizeof(struct in6_addr))) {
					q_add++;
				}
			}
		}
	}
	free(buf);
#ifdef HAVE_NET_BPF_H
exit_1:
#endif
	close(raw);
	test_log("query thread exiting\n");
	return NULL;
}
#endif

int main(void)
{
	char name[] = "MLD listen thread";
#ifdef HAVE_LIBLIBRECAST
	mld_t *mld;
	lc_ctx_t *lctx;
	lc_socket_t *sock;
	lc_channel_t *chan;
	pthread_t tid_query;
	unsigned int ifx;
	int rc;

	test_cap_require(CAP_NET_RAW);
	test_name(name);
	test_require_net(TEST_NET_BASIC);

	ifx = get_multicast_if();
	test_assert(ifx, "get_multicast_if() - find multicast capable interface");
	if (!ifx) return TEST_WARN;

	mld = mld_init(0);
	test_assert(mld != NULL, "mld_t allocated");
	if (!mld) return test_status;

	if (inet_pton(AF_INET6, MLD2_ALL_NODES, &allnodes) != 1) {
		perror("inet_pton (set allnodes)");
		return test_status;
	}

	/* start query thread */
	rc = sem_init(&sem_query, 0, 0);
	test_assert(rc == 0, "sem_init returned %i", rc);
	if (rc) goto err_mld_free;
	rc = pthread_create(&tid_query, NULL, thread_query, &ifx);
	test_assert(rc == 0, "pthread_create returned %i", rc);
	if (rc) goto err_sem_query_destroy;
	rc = sem_wait(&sem_query);
	test_assert(rc == 0, "sem_wait returned %i", rc);
	if (rc) goto err_sem_query_destroy;

	test_log("main thread: query thread is ready. Starting MLD\n");

	if (!mld_start(mld)) {
		test_assert(0, "mld_start() failed: %s", strerror(errno));
		goto err_sem_query_destroy;
	}
	/* ensure all threads created */
	for (int i = 0; i < MLD_THREADS; i++) assert(mld->q[i]);

	/* MUST send MLD2 General Queries on startup - sleep, then check */
	test_log("main thread sleeping\n");
	usleep(100000);
	test_log("main thread awake\n");
	test_assert(q_gen > 0, "MLD General Queries (%i)", q_gen);

	/* generate a random multicast address */
	lctx = lc_ctx_new();
	test_assert(lctx != NULL, "lc_ctx_new()");
	if (!lctx) goto err_mld_stop;
	chan = lc_channel_random(lctx);
	test_assert(chan != NULL, "lc_channel_random()");
	if (!chan) goto err_ctx_free;
	addr = lc_channel_in6addr(chan);
	test_assert(addr != NULL, "lc_channel_in6addr()");
	if (!addr) goto err_ctx_free;

	/* check for group before joining */
	rc = mld_filter_grp_cmp(mld, ifx, addr);
	test_assert(rc == 0, "check for group before joining on ifx %u", ifx);
	if (rc) goto err_ctx_free;

	/* JOIN group, check again */
	sock = lc_socket_new(lctx);
	test_assert(sock != NULL, "lc_socket_new()");
	if (!sock) goto err_ctx_free;
	rc = lc_socket_bind(sock, ifx);
	test_assert(rc == 0, "lc_socket_bind() returned %i", rc);
	if (rc) goto err_ctx_free;
	rc = lc_channel_bind(sock, chan);
	test_assert(rc == 0, "lc_channel_bind() returned %i", rc);
	if (rc) goto err_ctx_free;
	rc = lc_channel_join(chan);
	test_assert(rc == 0, "lc_channel_join() returned %i", rc);
	if (rc) goto err_ctx_free;

	/* does not receive MLD Address Specific Queries if this is < 2 on Linux */
	sleep(2);

	rc = mld_filter_grp_cmp(mld, ifx, addr);
	test_assert(rc == 1, "check for group after joining on ifx %u", ifx);
	if (rc != 1) goto err_ctx_free;

	/* PART group */
	rc = lc_channel_part(chan);
	test_assert(rc == 0, "lc_channel_part() returned %i", rc);
	if (rc != 0) goto err_ctx_free;

	usleep(10000);
	rc = mld_filter_grp_cmp(mld, ifx, addr);
	/* group is not removed from filter until timer expires */
	test_assert(rc == 1, "group is still in filter after parting");
	if (rc != 1) goto err_ctx_free;
	usleep(500000);

	/* ensure timer has been set to QRI */
	struct timespec ts = {0};
	struct timespec now = {0};
	rc = mld_filter_timer_get(mld, ifx, addr, &ts);
	test_assert(rc == 1, "mld_filter_timer_get() returned %i", rc);
	if (rc != 1) goto err_ctx_free;
	rc = clock_gettime(CLOCK_REALTIME, &now);
	test_assert(rc == 0, "clock_gettime() returned %i", rc);
	if (rc) goto err_ctx_free;

	long int s = ts.tv_sec - now.tv_sec;
	/* allow timer to be within one second of expected value */
	test_assert(s >= MLD2_QRI/1000 - 1 && s <= MLD2_QRI/1000, "timer set to QRI (=%lis) QRI = %i", s, MLD2_QRI / 1000);

	sleep(2); /* a good long snooze to make sure MLD Queries are all dealt with */
	test_assert(q_add > 0, "MLD Address Specific Queries received (%i)", q_add);

	/* PART again, ensure no more Queries sent (max queries per QRI) */
	q_add = 0;
	mld_filter_grp_part(mld, ifx, addr);
	usleep(10000);
	test_assert(q_add == 0, "ensure no MLD Queries received (QRI check) (%i)", q_add);

	/* set querier on ifx, ensure no MLD Query sent */
	q_add = 0;
	mld_iface_t *iface;
	for (iface = mld->iface; iface; iface = iface->next) {
		if (iface->ifx == ifx) break;
	}
	test_assert(iface != NULL, "iface found");
	if (iface) {
		mld_grp_list_t *g = mld_get_grp(mld, ifx, addr);
		if (g) {
			g->qlast.tv_sec = 0; /* clear QRI timer */
			/* set timer as though another Querier is present */
			clock_gettime(CLOCK_REALTIME, &iface->qseen);
			mld_filter_grp_part(mld, ifx, addr);
			usleep(10000);
			test_assert(q_add == 0,
					"another Querier present - ensure no MLD Queries received (%i)",
					q_add);
		}
	}

	// TODO build MLD Query with Link-Local source address lower than real
	// fe80:: (lowest possible link-local address)
	//msg = 
	// link to win the Querier election
	// TODO check MLD is no longer Querier for the link
	// TODO check querier timer

	/* stop query thread */
	pthread_cancel(tid_query);
	pthread_join(tid_query, NULL);
err_ctx_free:
	lc_ctx_free(lctx);
err_mld_stop:
	mld_stop(mld);
err_sem_query_destroy:
	sem_destroy(&sem_query);
err_mld_free:
	mld_free(mld);
	return test_status;
#else
	return test_skip("%s - requires liblibrecast", name);
#endif
}
