/*
 * hdhomerun_discover.c
 *
 * Copyright © 2006-2022 Silicondust USA Inc. <www.silicondust.com>.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "hdhomerun.h"

struct hdhomerun_discover2_device_type_t {
	struct hdhomerun_discover2_device_type_t *next;
	uint32_t device_type;
};

struct hdhomerun_discover2_device_if_t {
	struct hdhomerun_discover2_device_if_t *next;
	struct sockaddr_storage ip_addr;
	char *base_url;
	char *lineup_url;
	char *storage_url;
	uint8_t priority;
};

struct hdhomerun_discover2_device_t {
	struct hdhomerun_discover2_device_t *next;
	struct hdhomerun_discover2_device_if_t *if_list;
	struct hdhomerun_discover2_device_type_t *type_list;
	uint32_t device_id;
	uint8_t tuner_count;
	char *device_auth;
	char *storage_id;
};

struct hdhomerun_discover_sock_t {
	struct hdhomerun_discover_sock_t *next;
	struct hdhomerun_discover_sock_t *recv_next;
	struct hdhomerun_sock_t *sock;
	struct sockaddr_storage local_ip;
	uint32_t ifindex;
	uint32_t ipv4_subnet_mask;
	bool active;
};

struct hdhomerun_discover_t {
	struct hdhomerun_discover2_device_t *device_list;
	struct hdhomerun_discover_sock_t *ipv6_socks;
	struct hdhomerun_discover_sock_t *ipv4_socks;
	struct hdhomerun_discover_sock_t *ipv6_localhost;
	struct hdhomerun_discover_sock_t *ipv4_localhost;
	struct hdhomerun_pkt_t tx_pkt;
	struct hdhomerun_pkt_t rx_pkt;
	struct hdhomerun_debug_t *dbg;
};

static uint8_t hdhomerun_discover_ipv6_linklocal_multicast_ip[16] = { 0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x76 };
static uint8_t hdhomerun_discover_ipv6_sitelocal_multicast_ip[16] = { 0xFF, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x76 };

static void hdhomerun_discover_sock_free(struct hdhomerun_discover_sock_t *dss)
{
	hdhomerun_sock_destroy(dss->sock);
	free(dss);
}

static uint16_t hdhomerun_discover_get_local_port(struct hdhomerun_discover_t *ds)
{
	if (ds->ipv6_socks) {
		return hdhomerun_sock_getsockname_port(ds->ipv6_socks->sock);
	}
	if (ds->ipv4_socks) {
		return hdhomerun_sock_getsockname_port(ds->ipv4_socks->sock);
	}
	if (ds->ipv6_localhost) {
		return hdhomerun_sock_getsockname_port(ds->ipv6_localhost->sock);
	}
	if (ds->ipv4_localhost) {
		return hdhomerun_sock_getsockname_port(ds->ipv4_localhost->sock);
	}
	return 0;
}

static void hdhomerun_discover_sock_add_ipv6(void *arg, uint32_t ifindex, const struct sockaddr *local_ip, uint8_t cidr)
{
	if (local_ip->sa_family != AF_INET6) {
		return;
	}

	struct hdhomerun_discover_t *ds = (struct hdhomerun_discover_t *)arg;
	struct sockaddr_in6 *local_ip_in6 = (struct sockaddr_in6 *)local_ip;

	char local_ip_str[64];
	hdhomerun_sock_sockaddr_to_ip_str(local_ip_str, local_ip, true);
	if (hdhomerun_sock_sockaddr_is_addr(local_ip)) {
		hdhomerun_debug_printf(ds->dbg, "discover: local ip %s/%u\n", local_ip_str, cidr);
	}

	struct hdhomerun_discover_sock_t **pprev = &ds->ipv6_socks;
	struct hdhomerun_discover_sock_t *p = ds->ipv6_socks;
	while (p) {
		struct sockaddr_in6 *p_ip = (struct sockaddr_in6 *)&p->local_ip;
		if ((p->ifindex == ifindex) && (memcmp(p_ip->sin6_addr.s6_addr, local_ip_in6->sin6_addr.s6_addr, 16) == 0)) {
			p->active = true;
			return;
		}

		pprev = &p->next;
		p = p->next;
	}

	/* Create socket. */
	struct hdhomerun_discover_sock_t *dss = (struct hdhomerun_discover_sock_t *)calloc(sizeof(struct hdhomerun_discover_sock_t), 1);
	if (!dss) {
		hdhomerun_debug_printf(ds->dbg, "discover: resource error\n");
		return;
	}

	dss->sock = hdhomerun_sock_create_udp_ex(AF_INET6);
	if (!dss->sock) {
		hdhomerun_debug_printf(ds->dbg, "discover: failed to allocate socket (%d)\n", hdhomerun_sock_getlasterror());
		free(dss);
		return;
	}

	hdhomerun_sock_set_ttl(dss->sock, 64);

	/* Bind socket. */
	local_ip_in6->sin6_port = htons(hdhomerun_discover_get_local_port(ds));

	if (!hdhomerun_sock_bind_ex(dss->sock, local_ip, true)) {
		hdhomerun_debug_printf(ds->dbg, "discover: failed to bind to local ip %s (%d)\n", local_ip_str, hdhomerun_sock_getlasterror());
		hdhomerun_discover_sock_free(dss);
		return;
	}

	/* Success. */
	memcpy(&dss->local_ip, local_ip_in6, sizeof(struct sockaddr_in6));
	dss->ifindex = ifindex;
	dss->active = true;
	*pprev = dss;
}

static void hdhomerun_discover_sock_add_ipv4(void *arg, uint32_t ifindex, const struct sockaddr *local_ip, uint8_t cidr)
{
	if (local_ip->sa_family != AF_INET) {
		return;
	}

	struct hdhomerun_discover_t *ds = (struct hdhomerun_discover_t *)arg;
	struct sockaddr_in *local_ip_in = (struct sockaddr_in *)local_ip;
	uint32_t detected_subnet_mask = 0xFFFFFFFF << (32 - cidr);

	char local_ip_str[64];
	hdhomerun_sock_sockaddr_to_ip_str(local_ip_str, local_ip, true);
	if (hdhomerun_sock_sockaddr_is_addr(local_ip)) {
		hdhomerun_debug_printf(ds->dbg, "discover: local ip %s/%u\n", local_ip_str, cidr);
	}

	struct hdhomerun_discover_sock_t **pprev = &ds->ipv4_socks;
	struct hdhomerun_discover_sock_t *p = ds->ipv4_socks;
	while (p) {
		struct sockaddr_in *p_ip = (struct sockaddr_in *)&p->local_ip;
		if ((p->ifindex == ifindex) && (p_ip->sin_addr.s_addr == local_ip_in->sin_addr.s_addr) && (p->ipv4_subnet_mask == detected_subnet_mask)) {
			p->active = true;
			return;
		}

		pprev = &p->next;
		p = p->next;
	}

	/* Create socket. */
	struct hdhomerun_discover_sock_t *dss = (struct hdhomerun_discover_sock_t *)calloc(sizeof(struct hdhomerun_discover_sock_t), 1);
	if (!dss) {
		hdhomerun_debug_printf(ds->dbg, "discover: resource error\n");
		return;
	}

	dss->sock = hdhomerun_sock_create_udp_ex(AF_INET);
	if (!dss->sock) {
		hdhomerun_debug_printf(ds->dbg, "discover: failed to allocate socket (%d)\n", hdhomerun_sock_getlasterror());
		free(dss);
		return;
	}

	hdhomerun_sock_set_ttl(dss->sock, 64);

	/* Bind socket. */
	local_ip_in->sin_port = htons(hdhomerun_discover_get_local_port(ds));

	if (!hdhomerun_sock_bind_ex(dss->sock, local_ip, true)) {
		hdhomerun_debug_printf(ds->dbg, "discover: failed to bind to local ip %s (%d)\n", local_ip_str, hdhomerun_sock_getlasterror());
		hdhomerun_discover_sock_free(dss);
		return;
	}

	/* Success. */
	memcpy(&dss->local_ip, local_ip_in, sizeof(struct sockaddr_in));
	dss->ifindex = ifindex;
	dss->ipv4_subnet_mask = detected_subnet_mask;
	dss->active = true;
	*pprev = dss;
}

static void hdhomerun_discover_device_if_free(struct hdhomerun_discover2_device_if_t *device_if)
{
	if (device_if->base_url) {
		free(device_if->base_url);
	}
	if (device_if->lineup_url) {
		free(device_if->lineup_url);
	}
	if (device_if->storage_url) {
		free(device_if->storage_url);
	}

	free(device_if);
}

static void hdhomerun_discover_device_free(struct hdhomerun_discover2_device_t *device)
{
	while (device->if_list) {
		struct hdhomerun_discover2_device_if_t *device_if = device->if_list;
		device->if_list = device_if->next;
		hdhomerun_discover_device_if_free(device_if);
	}

	while (device->type_list) {
		struct hdhomerun_discover2_device_type_t *device_type = device->type_list;
		device->type_list = device_type->next;
		free(device_type);
	}

	if (device->device_auth) {
		free(device->device_auth);
	}
	if (device->storage_id) {
		free(device->storage_id);
	}

	free(device);
}

static void hdhomerun_discover_free_device_list(struct hdhomerun_discover_t *ds)
{
	while (ds->device_list) {
		struct hdhomerun_discover2_device_t *device = ds->device_list;
		ds->device_list = device->next;
		hdhomerun_discover_device_free(device);
	}
}

void hdhomerun_discover_destroy(struct hdhomerun_discover_t *ds)
{
	hdhomerun_discover_free_device_list(ds);

	while (ds->ipv6_socks) {
		struct hdhomerun_discover_sock_t *dss = ds->ipv6_socks;
		ds->ipv6_socks = dss->next;
		hdhomerun_discover_sock_free(dss);
	}

	while (ds->ipv4_socks) {
		struct hdhomerun_discover_sock_t *dss = ds->ipv4_socks;
		ds->ipv4_socks = dss->next;
		hdhomerun_discover_sock_free(dss);
	}

	free(ds);
}

struct hdhomerun_discover_t *hdhomerun_discover_create(struct hdhomerun_debug_t *dbg)
{
	struct hdhomerun_discover_t *ds = (struct hdhomerun_discover_t *)calloc(1, sizeof(struct hdhomerun_discover_t));
	if (!ds) {
		return NULL;
	}

	ds->dbg = dbg;
	return ds;
}

static void hdhomerun_discover_sock_detect_ipv6(struct hdhomerun_discover_t *ds)
{
	struct hdhomerun_discover_sock_t *default_dss = ds->ipv6_socks;
	if (!default_dss) {
		/* Create a routable socket (always first entry). */
		struct sockaddr_in6 ipv6_addr;
		memset(&ipv6_addr, 0, sizeof(ipv6_addr));
		ipv6_addr.sin6_family = AF_INET6;
		hdhomerun_discover_sock_add_ipv6(ds, 0, (const struct sockaddr *)&ipv6_addr, 0);

		default_dss = ds->ipv6_socks;
		if (!default_dss) {
			return;
		}
	}

	struct hdhomerun_discover_sock_t *p = default_dss->next;
	while (p) {
		p->active = false;
		p = p->next;
	}

	hdhomerun_local_ip_info2(AF_INET6, hdhomerun_discover_sock_add_ipv6, ds);
}

static void hdhomerun_discover_sock_detect_ipv4(struct hdhomerun_discover_t *ds)
{
	struct hdhomerun_discover_sock_t *default_dss = ds->ipv4_socks;
	if (!default_dss) {
		/* Create a routable socket (always first entry). */
		struct sockaddr_in ipv4_addr;
		memset(&ipv4_addr, 0, sizeof(ipv4_addr));
		ipv4_addr.sin_family = AF_INET;
		hdhomerun_discover_sock_add_ipv4(ds, 0, (const struct sockaddr *)&ipv4_addr, 0);

		default_dss = ds->ipv4_socks;
		if (!default_dss) {
			return;
		}
	}

	struct hdhomerun_discover_sock_t *p = default_dss->next;
	while (p) {
		p->active = false;
		p = p->next;
	}

	hdhomerun_local_ip_info2(AF_INET, hdhomerun_discover_sock_add_ipv4, ds);
}

static void hdhomerun_discover_sock_detect_ipv6_localhost(struct hdhomerun_discover_t *ds)
{
	if (ds->ipv6_localhost) {
		return;
	}

	struct hdhomerun_discover_sock_t *dss = (struct hdhomerun_discover_sock_t *)calloc(sizeof(struct hdhomerun_discover_sock_t), 1);
	if (!dss) {
		return;
	}

	dss->sock = hdhomerun_sock_create_udp_ex(AF_INET6);
	if (!dss->sock) {
		free(dss);
		return;
	}

	struct sockaddr_in6 *sock_addr = (struct sockaddr_in6 *)&dss->local_ip;
	sock_addr->sin6_family = AF_INET6;
	sock_addr->sin6_addr.s6_addr[15] = 1;
	sock_addr->sin6_port = htons(hdhomerun_discover_get_local_port(ds));

	if (!hdhomerun_sock_bind_ex(dss->sock, (const struct sockaddr *)sock_addr, true)) {
		hdhomerun_discover_sock_free(dss);
		return;
	}

	dss->active = true;
	ds->ipv6_localhost = dss;
}

static void hdhomerun_discover_sock_detect_ipv4_localhost(struct hdhomerun_discover_t *ds)
{
	if (ds->ipv4_localhost) {
		return;
	}

	struct hdhomerun_discover_sock_t *dss = (struct hdhomerun_discover_sock_t *)calloc(sizeof(struct hdhomerun_discover_sock_t), 1);
	if (!dss) {
		return;
	}

	dss->sock = hdhomerun_sock_create_udp_ex(AF_INET);
	if (!dss->sock) {
		free(dss);
		return;
	}

	struct sockaddr_in *sock_addr = (struct sockaddr_in *)&dss->local_ip;
	sock_addr->sin_family = AF_INET;
	sock_addr->sin_addr.s_addr = htonl(0x7F000001);
	sock_addr->sin_port = htons(hdhomerun_discover_get_local_port(ds));
	dss->ipv4_subnet_mask = 0xFF000000;

	if (!hdhomerun_sock_bind_ex(dss->sock, (const struct sockaddr *)sock_addr, true)) {
		hdhomerun_discover_sock_free(dss);
		return;
	}

	dss->active = true;
	ds->ipv4_localhost = dss;
}

static void hdhomerun_discover_sock_detect_all_from_flags(struct hdhomerun_discover_t *ds, uint32_t flags)
{
	if (flags & HDHOMERUN_DISCOVER_FLAGS_IPV6_LOCALHOST) {
		hdhomerun_discover_sock_detect_ipv6_localhost(ds);
	}
	if (flags & (HDHOMERUN_DISCOVER_FLAGS_IPV6_GENERAL | HDHOMERUN_DISCOVER_FLAGS_IPV6_LINKLOCAL)) {
		hdhomerun_discover_sock_detect_ipv6(ds);
	}
	if (flags & HDHOMERUN_DISCOVER_FLAGS_IPV4_LOCALHOST) {
		hdhomerun_discover_sock_detect_ipv4_localhost(ds);
	}
	if (flags & HDHOMERUN_DISCOVER_FLAGS_IPV4_GENERAL) {
		hdhomerun_discover_sock_detect_ipv4(ds);
	}
}

static void hdhomerun_discover_sock_flush_list(struct hdhomerun_discover_t *ds, struct hdhomerun_discover_sock_t *list)
{
	struct hdhomerun_pkt_t *rx_pkt = &ds->rx_pkt;
	hdhomerun_pkt_reset(rx_pkt);

	struct hdhomerun_discover_sock_t *dss = list;
	while (dss) {
		if (!dss->active) {
			dss = dss->next;
			continue;
		}

		while (1) {
			struct sockaddr_storage remote_addr;
			size_t length = rx_pkt->limit - rx_pkt->end;
			if (!hdhomerun_sock_recvfrom_ex(dss->sock, &remote_addr, rx_pkt->end, &length, 0)) {
				break;
			}
		}

		dss = dss->next;
	}
}

static void hdhomerun_discover_sock_recv_list_append_list(struct hdhomerun_discover_sock_t **recv_list, struct hdhomerun_discover_sock_t *list)
{
	struct hdhomerun_discover_sock_t *dss = list;
	while (dss) {
		if (!dss->active) {
			dss = dss->next;
			continue;
		}

		dss->recv_next = *recv_list;
		*recv_list = dss;
		dss = dss->next;
	}
}

static bool hdhomerun_discover_send_request(struct hdhomerun_discover_t *ds, struct hdhomerun_discover_sock_t *dss, const struct sockaddr *remote_addr, const uint32_t device_types[], size_t device_types_count, uint32_t device_id)
{
	if (device_types_count == 0) {
		return false;
	}

	struct hdhomerun_pkt_t *tx_pkt = &ds->tx_pkt;
	hdhomerun_pkt_reset(tx_pkt);

	if (device_types_count == 1) {
		hdhomerun_pkt_write_u8(tx_pkt, HDHOMERUN_TAG_DEVICE_TYPE);
		hdhomerun_pkt_write_var_length(tx_pkt, 4);
		hdhomerun_pkt_write_u32(tx_pkt, device_types[0]);
	} else {
		hdhomerun_pkt_write_u8(tx_pkt, HDHOMERUN_TAG_MULTI_TYPE);
		hdhomerun_pkt_write_var_length(tx_pkt, device_types_count * 4);
		while (device_types_count--) {
			hdhomerun_pkt_write_u32(tx_pkt, *device_types++);
		}
	}

	if (device_id != HDHOMERUN_DEVICE_ID_WILDCARD) {
		hdhomerun_pkt_write_u8(tx_pkt, HDHOMERUN_TAG_DEVICE_ID);
		hdhomerun_pkt_write_var_length(tx_pkt, 4);
		hdhomerun_pkt_write_u32(tx_pkt, device_id);
	}

	hdhomerun_pkt_seal_frame(tx_pkt, HDHOMERUN_TYPE_DISCOVER_REQ);

	char local_ip_str[64];
	char remote_ip_str[64];
	hdhomerun_sock_sockaddr_to_ip_str(local_ip_str, (const struct sockaddr *)&dss->local_ip, true);
	hdhomerun_sock_sockaddr_to_ip_str(remote_ip_str, remote_addr, true);
	hdhomerun_debug_printf(ds->dbg, "discover: send to %s via %s\n", remote_ip_str, local_ip_str);

	return hdhomerun_sock_sendto_ex(dss->sock, remote_addr, tx_pkt->start, tx_pkt->end - tx_pkt->start, 0);
}

static void hdhomerun_discover_send_ipv6_targeted(struct hdhomerun_discover_t *ds, const struct sockaddr *target_addr, uint32_t flags, const uint32_t device_types[], size_t device_types_count, uint32_t device_id, struct hdhomerun_discover_sock_t **recv_list)
{
	struct hdhomerun_discover_sock_t *default_dss = ds->ipv6_socks;
	if (!default_dss) {
		return;
	}

	hdhomerun_discover_sock_flush_list(ds, ds->ipv6_socks);
	hdhomerun_discover_sock_recv_list_append_list(recv_list, ds->ipv6_socks);

	struct sockaddr_in6 target_addr_in;
	memcpy(&target_addr_in, target_addr, sizeof(target_addr_in));
	target_addr_in.sin6_port = htons(HDHOMERUN_DISCOVER_UDP_PORT);

	/* ipv6 linklocal - send out every interface if the scope id isn't provided */
	if (hdhomerun_sock_sockaddr_is_ipv6_linklocal(target_addr) && (target_addr_in.sin6_scope_id == 0)) {
		struct hdhomerun_discover_sock_t *dss = default_dss->next;
		while (dss) {
			if (!dss->active) {
				dss = dss->next;
				continue;
			}

			if (!hdhomerun_sock_sockaddr_is_ipv6_linklocal((const struct sockaddr *)&dss->local_ip)) {
				dss = dss->next;
				continue;
			}

			struct sockaddr_in6 *local_addr_in = (struct sockaddr_in6 *)&dss->local_ip;
			target_addr_in.sin6_scope_id = local_addr_in->sin6_scope_id;

			if (!hdhomerun_discover_send_request(ds, dss, (const struct sockaddr *)&target_addr_in, device_types, device_types_count, device_id)) {
				dss = dss->next;
				continue;
			}

			dss = dss->next;
		}

		return;
	}

	hdhomerun_discover_send_request(ds, default_dss, (const struct sockaddr *)&target_addr_in, device_types, device_types_count, device_id);
}

static void hdhomerun_discover_send_ipv4_targeted(struct hdhomerun_discover_t *ds, const struct sockaddr *target_addr, uint32_t flags, const uint32_t device_types[], size_t device_types_count, uint32_t device_id, struct hdhomerun_discover_sock_t **recv_list)
{
	struct hdhomerun_discover_sock_t *default_dss = ds->ipv4_socks;
	if (!default_dss) {
		return;
	}

	hdhomerun_discover_sock_flush_list(ds, ds->ipv4_socks);
	hdhomerun_discover_sock_recv_list_append_list(recv_list, ds->ipv4_socks);

	struct sockaddr_in target_addr_in;
	memcpy(&target_addr_in, target_addr, sizeof(target_addr_in));
	target_addr_in.sin_port = htons(HDHOMERUN_DISCOVER_UDP_PORT);

	uint32_t target_ipv4 = ntohl(target_addr_in.sin_addr.s_addr);
	bool local_subnet_send = false;

	struct hdhomerun_discover_sock_t *dss = default_dss->next;
	while (dss) {
		if (!dss->active) {
			dss = dss->next;
			continue;
		}

		if (dss->ipv4_subnet_mask == 0) {
			dss = dss->next;
			continue;
		}

		struct sockaddr_in *local_ip_in = (struct sockaddr_in *)&dss->local_ip;
		uint32_t local_ipv4 = ntohl(local_ip_in->sin_addr.s_addr);

		if ((target_ipv4 & dss->ipv4_subnet_mask) != (local_ipv4 & dss->ipv4_subnet_mask)) {
			dss = dss->next;
			continue;
		}

		if (!hdhomerun_discover_send_request(ds, dss, (const struct sockaddr *)&target_addr_in, device_types, device_types_count, device_id)) {
			dss = dss->next;
			continue;
		}

		local_subnet_send = true;
		dss = dss->next;
	}

	if (local_subnet_send) {
		return;
	}

	if (hdhomerun_sock_sockaddr_is_ipv4_autoip(target_addr)) {
		return;
	}

	hdhomerun_discover_send_request(ds, default_dss, (const struct sockaddr *)&target_addr_in, device_types, device_types_count, device_id);
}

static void hdhomerun_discover_send_ipv6_multicast(struct hdhomerun_discover_t *ds, uint32_t flags, const uint32_t device_types[], size_t device_types_count, uint32_t device_id, struct hdhomerun_discover_sock_t **recv_list)
{
	struct hdhomerun_discover_sock_t *default_dss = ds->ipv6_socks;
	if (!default_dss) {
		return;
	}

	hdhomerun_discover_sock_flush_list(ds, ds->ipv6_socks);
	hdhomerun_discover_sock_recv_list_append_list(recv_list, ds->ipv6_socks);

	if (flags & HDHOMERUN_DISCOVER_FLAGS_IPV6_LINKLOCAL) {
		struct sockaddr_in6 sock_addr_in;
		memset(&sock_addr_in, 0, sizeof(sock_addr_in));
		sock_addr_in.sin6_family = AF_INET6;
		memcpy(sock_addr_in.sin6_addr.s6_addr, hdhomerun_discover_ipv6_linklocal_multicast_ip, 16);
		sock_addr_in.sin6_port = htons(HDHOMERUN_DISCOVER_UDP_PORT);

		struct hdhomerun_discover_sock_t *dss = default_dss->next;
		while (dss) {
			if (!dss->active) {
				dss = dss->next;
				continue;
			}

			if (!hdhomerun_sock_sockaddr_is_ipv6_linklocal((const struct sockaddr *)&dss->local_ip)) {
				dss = dss->next;
				continue;
			}

			hdhomerun_sock_set_ipv6_multicast_ifindex(dss->sock, dss->ifindex);

			struct sockaddr_in6 *local_ip_in = (struct sockaddr_in6 *)&dss->local_ip;
			sock_addr_in.sin6_scope_id = local_ip_in->sin6_scope_id;

			if (!hdhomerun_discover_send_request(ds, dss, (const struct sockaddr *)&sock_addr_in, device_types, device_types_count, device_id)) {
				dss = dss->next;
				continue;
			}

			dss = dss->next;
		}
	}

	if (flags & HDHOMERUN_DISCOVER_FLAGS_IPV6_GENERAL) {
		struct sockaddr_in6 sock_addr_in;
		memset(&sock_addr_in, 0, sizeof(sock_addr_in));
		sock_addr_in.sin6_family = AF_INET6;
		memcpy(sock_addr_in.sin6_addr.s6_addr, hdhomerun_discover_ipv6_sitelocal_multicast_ip, 16);
		sock_addr_in.sin6_port = htons(HDHOMERUN_DISCOVER_UDP_PORT);

		struct hdhomerun_discover_sock_t *dss = default_dss->next;
		while (dss) {
			if (!dss->active) {
				dss = dss->next;
				continue;
			}

			if (hdhomerun_sock_sockaddr_is_ipv6_linklocal((const struct sockaddr *)&dss->local_ip)) {
				dss = dss->next;
				continue;
			}

			hdhomerun_sock_set_ipv6_multicast_ifindex(dss->sock, dss->ifindex);

			struct sockaddr_in6 *local_ip_in = (struct sockaddr_in6 *)&dss->local_ip;
			sock_addr_in.sin6_scope_id = local_ip_in->sin6_scope_id;

			if (!hdhomerun_discover_send_request(ds, dss, (const struct sockaddr *)&sock_addr_in, device_types, device_types_count, device_id)) {
				dss = dss->next;
				continue;
			}

			dss = dss->next;
		}
	}
}

static void hdhomerun_discover_send_ipv4_broadcast(struct hdhomerun_discover_t *ds, uint32_t flags, const uint32_t device_types[], size_t device_types_count, uint32_t device_id, struct hdhomerun_discover_sock_t **recv_list)
{
	struct hdhomerun_discover_sock_t *default_dss = ds->ipv4_socks;
	if (!default_dss) {
		return;
	}

	hdhomerun_discover_sock_flush_list(ds, ds->ipv4_socks);
	hdhomerun_discover_sock_recv_list_append_list(recv_list, ds->ipv4_socks);

	struct sockaddr_in sock_addr_in;
	memset(&sock_addr_in, 0, sizeof(sock_addr_in));
	sock_addr_in.sin_family = AF_INET;
	sock_addr_in.sin_addr.s_addr = htonl(0xFFFFFFFF);
	sock_addr_in.sin_port = htons(HDHOMERUN_DISCOVER_UDP_PORT);

	struct hdhomerun_discover_sock_t *dss = default_dss->next;
	while (dss) {
		if (!dss->active) {
			dss = dss->next;
			continue;
		}

#if defined(IP_ONESBCAST)
		struct sockaddr_in *local_ip_in = (struct sockaddr_in *)&dss->local_ip;
		uint32_t local_ip_val = ntohl(local_ip_in->sin_addr.s_addr);

		uint32_t subnet_broadcast = local_ip_val | ~dss->ipv4_subnet_mask;
		if ((subnet_broadcast == 0) || (subnet_broadcast >= 0xE0000000)) {
			dss = dss->next;
			continue;
		}

		hdhomerun_sock_set_ipv4_onesbcast(dss->sock, 1);
		sock_addr_in.sin_addr.s_addr = htonl(subnet_broadcast);
#endif

		bool send_ok = hdhomerun_discover_send_request(ds, dss, (const struct sockaddr *)&sock_addr_in, device_types, device_types_count, device_id);
		hdhomerun_sock_set_ipv4_onesbcast(dss->sock, 0);
		if (!send_ok) {
			dss = dss->next;
			continue;
		}

		dss = dss->next;
	}
}

static void hdhomerun_discover_send_ipv6_localhost(struct hdhomerun_discover_t *ds, const struct sockaddr *target_addr, uint32_t flags, const uint32_t device_types[], size_t device_types_count, uint32_t device_id, struct hdhomerun_discover_sock_t **recv_list)
{
	struct hdhomerun_discover_sock_t *localhost_dss = ds->ipv6_localhost;
	if (!localhost_dss) {
		return;
	}

	hdhomerun_discover_sock_flush_list(ds, localhost_dss);
	hdhomerun_discover_sock_recv_list_append_list(recv_list, localhost_dss);

	struct sockaddr_in6 sock_addr_in;
	if (target_addr) {
		memcpy(&sock_addr_in, target_addr, sizeof(sock_addr_in));
	} else {
		memset(&sock_addr_in, 0, sizeof(sock_addr_in));
		sock_addr_in.sin6_family = AF_INET6;
		sock_addr_in.sin6_addr.s6_addr[15] = 1;
	}

	sock_addr_in.sin6_port = htons(HDHOMERUN_DISCOVER_UDP_PORT);
	hdhomerun_discover_send_request(ds, localhost_dss, (const struct sockaddr *)&sock_addr_in, device_types, device_types_count, device_id);
}

static void hdhomerun_discover_send_ipv4_localhost(struct hdhomerun_discover_t *ds, const struct sockaddr *target_addr, uint32_t flags, const uint32_t device_types[], size_t device_types_count, uint32_t device_id, struct hdhomerun_discover_sock_t **recv_list)
{
	struct hdhomerun_discover_sock_t *localhost_dss = ds->ipv4_localhost;
	if (!localhost_dss) {
		return;
	}

	hdhomerun_discover_sock_flush_list(ds, localhost_dss);
	hdhomerun_discover_sock_recv_list_append_list(recv_list, localhost_dss);

	struct sockaddr_in sock_addr_in;
	if (target_addr) {
		memcpy(&sock_addr_in, target_addr, sizeof(sock_addr_in));
	} else {
		memset(&sock_addr_in, 0, sizeof(sock_addr_in));
		sock_addr_in.sin_family = AF_INET;
		sock_addr_in.sin_addr.s_addr = htonl(0x7F000001);
	}

	sock_addr_in.sin_port = htons(HDHOMERUN_DISCOVER_UDP_PORT);
	hdhomerun_discover_send_request(ds, localhost_dss, (const struct sockaddr *)&sock_addr_in, device_types, device_types_count, device_id);
}

static uint8_t hdhomerun_discover_compute_device_if_priority(struct hdhomerun_discover2_device_if_t *device_if)
{
	if (hdhomerun_sock_sockaddr_is_ipv6_localhost((const struct sockaddr *)&device_if->ip_addr)) {
		return 0; /* highest priority */
	}

	if (hdhomerun_sock_sockaddr_is_ipv4_localhost((const struct sockaddr *)&device_if->ip_addr)) {
		return 1;
	}

	if (device_if->ip_addr.ss_family == AF_INET6) {
		if (hdhomerun_sock_sockaddr_is_ipv6_global((const struct sockaddr *)&device_if->ip_addr)) {
			return 2;
		}

		if (!hdhomerun_sock_sockaddr_is_ipv6_linklocal((const struct sockaddr *)&device_if->ip_addr)) {
			return 3;
		}

		return 4;
	}

	if (!hdhomerun_sock_sockaddr_is_ipv4_autoip((const struct sockaddr *)&device_if->ip_addr)) {
		return 5;
	}

	return 6;
}

static void hdhomerun_discover_recv_internal_device_type(struct hdhomerun_discover2_device_t *device, struct hdhomerun_pkt_t *rx_pkt)
{
	uint32_t device_type = hdhomerun_pkt_read_u32(rx_pkt);
	if ((device_type == 0) || (device_type == HDHOMERUN_DEVICE_TYPE_WILDCARD)) {
		return;
	}

	struct hdhomerun_discover2_device_type_t **pprev = &device->type_list;
	struct hdhomerun_discover2_device_type_t *p = device->type_list;
	while (p) {
		if (p->device_type >= device_type) {
			if (p->device_type > device_type) {
				break;
			}
			return;
		}

		pprev = &p->next;
		p = p->next;
	}

	struct hdhomerun_discover2_device_type_t *new_type = (struct hdhomerun_discover2_device_type_t *)calloc(sizeof(struct hdhomerun_discover2_device_type_t), 1);
	if (!new_type) {
		return;
	}

	new_type->device_type = device_type;
	new_type->next = *pprev;
	*pprev = new_type;
}

static void hdhomerun_discover_recv_internal_string(char **poutput, struct hdhomerun_pkt_t *rx_pkt, size_t len)
{
	if (*poutput) {
		return;
	}

	char *str = (char *)malloc(len + 1);
	if (!str) {
		return;
	}

	hdhomerun_pkt_read_mem(rx_pkt, str, len);
	str[len] = 0;

	*poutput = str;
}

static void hdhomerun_discover_recv_internal_auth_bin(char **poutput, struct hdhomerun_pkt_t *rx_pkt, size_t len)
{
	if (*poutput) {
		return;
	}
	if (len != 18) {
		return;
	}

	char *str = (char *)malloc(24 + 1);
	if (!str) {
		return;
	}

	int i;
	for (i = 0; i < 24; i += 4) {
		static const char hdhomerun_discover_recv_base64_encode_table[64 + 1] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";

		uint32_t raw24;
		raw24 = (uint32_t)hdhomerun_pkt_read_u8(rx_pkt) << 16;
		raw24 |= (uint32_t)hdhomerun_pkt_read_u8(rx_pkt) << 8;
		raw24 |= (uint32_t)hdhomerun_pkt_read_u8(rx_pkt) << 0;
		str[i + 0] = hdhomerun_discover_recv_base64_encode_table[(raw24 >> 18) & 0x3F];
		str[i + 1] = hdhomerun_discover_recv_base64_encode_table[(raw24 >> 12) & 0x3F];
		str[i + 2] = hdhomerun_discover_recv_base64_encode_table[(raw24 >> 6) & 0x3F];
		str[i + 3] = hdhomerun_discover_recv_base64_encode_table[(raw24 >> 0) & 0x3F];
	}

	str[24] = 0;

	*poutput = str;
}

static void hdhomerun_discover_recv_fixup_tuner_count(struct hdhomerun_discover_t *ds, struct hdhomerun_discover2_device_t *device)
{
	if (!hdhomerun_discover2_device_is_type(device, HDHOMERUN_DEVICE_TYPE_TUNER)) {
		return;
	}

	if (device->tuner_count > 0) {
		return;
	}

	switch (device->device_id >> 20) {
	case 0x102:
		device->tuner_count = 1;
		break;

	case 0x100:
	case 0x101:
	case 0x121:
		device->tuner_count = 2;
		break;

	default:
		break;
	}
}

static void hdhomerun_discover_recv_fixup_base_url(struct hdhomerun_discover_t *ds, struct hdhomerun_discover2_device_t *device)
{
	if (!hdhomerun_discover2_device_is_type(device, HDHOMERUN_DEVICE_TYPE_TUNER)) {
		return;
	}

	struct hdhomerun_discover2_device_if_t *device_if = device->if_list;
	if (device_if->base_url) {
		return;
	}

	if (device_if->ip_addr.ss_family != AF_INET) {
		return;
	}

	device_if->base_url = (char *)malloc(32);
	if (!device_if->base_url) {
		return;
	}

	struct sockaddr_in *sock_addr_in = (struct sockaddr_in *)&device_if->ip_addr;
	uint32_t ip = ntohl(sock_addr_in->sin_addr.s_addr);

	hdhomerun_sprintf(device_if->base_url, device_if->base_url + 32, "http://%u.%u.%u.%u:80",
		(ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, (ip >> 0) & 0xFF
	);
}

static bool hdhomerun_discover_recv_internal(struct hdhomerun_discover_t *ds, struct hdhomerun_pkt_t *rx_pkt, struct hdhomerun_discover2_device_t *device)
{
	uint16_t type;
	if (hdhomerun_pkt_open_frame(rx_pkt, &type) <= 0) {
		return false;
	}
	if (type != HDHOMERUN_TYPE_DISCOVER_RPY) {
		return false;
	}

	struct hdhomerun_discover2_device_if_t *device_if = device->if_list;

	while (1) {
		uint8_t tag;
		size_t len;
		uint8_t *next = hdhomerun_pkt_read_tlv(rx_pkt, &tag, &len);
		if (!next) {
			break;
		}

		switch (tag) {
		case HDHOMERUN_TAG_DEVICE_TYPE:
			if (len != 4) {
				break;
			}
			hdhomerun_discover_recv_internal_device_type(device, rx_pkt);
			break;

		case HDHOMERUN_TAG_MULTI_TYPE:
			while (len >= 4) {
				hdhomerun_discover_recv_internal_device_type(device, rx_pkt);
				len -= 4;
			}
			break;

		case HDHOMERUN_TAG_DEVICE_ID:
			if (len != 4) {
				break;
			}
			device->device_id = hdhomerun_pkt_read_u32(rx_pkt);
			break;

		case HDHOMERUN_TAG_TUNER_COUNT:
			if (len != 1) {
				break;
			}
			device->tuner_count = hdhomerun_pkt_read_u8(rx_pkt);
			break;

		case HDHOMERUN_TAG_DEVICE_AUTH_STR:
			hdhomerun_discover_recv_internal_string(&device->device_auth, rx_pkt, len);
			break;

		case HDHOMERUN_TAG_DEVICE_AUTH_BIN_DEPRECATED:
			hdhomerun_discover_recv_internal_auth_bin(&device->device_auth, rx_pkt, len);
			break;

		case HDHOMERUN_TAG_BASE_URL:
			hdhomerun_discover_recv_internal_string(&device_if->base_url, rx_pkt, len);
			break;

		case HDHOMERUN_TAG_STORAGE_ID:
			hdhomerun_discover_recv_internal_string(&device->storage_id, rx_pkt, len);
			break;

		case HDHOMERUN_TAG_LINEUP_URL:
			hdhomerun_discover_recv_internal_string(&device_if->lineup_url, rx_pkt, len);
			break;

		case HDHOMERUN_TAG_STORAGE_URL:
			hdhomerun_discover_recv_internal_string(&device_if->storage_url, rx_pkt, len);
			break;

		default:
			break;
		}

		rx_pkt->pos = next;
	}

	if (!device->type_list) {
		return false;
	}

	/*
	 * Fixup for old firmware.
	 */
	hdhomerun_discover_recv_fixup_tuner_count(ds, device);
	hdhomerun_discover_recv_fixup_base_url(ds, device);

	/*
	 * Success
	 */
	device_if->priority = hdhomerun_discover_compute_device_if_priority(device_if);
	return true;
}

static void hdhomerun_discover_recv_merge_device_type(struct hdhomerun_discover2_device_t *device, struct hdhomerun_discover2_device_type_t *new_type)
{
	struct hdhomerun_discover2_device_type_t **pprev = &device->type_list;
	struct hdhomerun_discover2_device_type_t *p = device->type_list;
	while (p) {
		if (p->device_type >= new_type->device_type) {
			if (p->device_type > new_type->device_type) {
				break;
			}
			free(new_type);
			return;
		}

		pprev = &p->next;
		p = p->next;
	}

	new_type->next = *pprev;
	*pprev = new_type;
}

static void hdhomerun_discover_recv_merge_device_types(struct hdhomerun_discover2_device_t *device, struct hdhomerun_discover2_device_type_t *type_list)
{
	while (type_list) {
		struct hdhomerun_discover2_device_type_t *new_type = type_list;
		type_list = new_type->next;
		hdhomerun_discover_recv_merge_device_type(device, new_type);
	}
}

static void hdhomerun_discover_recv_merge_device_if(struct hdhomerun_discover2_device_t *device, struct hdhomerun_discover2_device_if_t *device_if)
{
	struct hdhomerun_discover2_device_if_t **pprev = &device->if_list;
	struct hdhomerun_discover2_device_if_t *p = device->if_list;
	while (p) {
		if (p->priority < device_if->priority) {
			pprev = &p->next;
			p = p->next;
			continue;
		}
		if (p->priority > device_if->priority) {
			break;
		}

		char *p_base_url = p->base_url;
		char *device_if_base_url = device_if->base_url;
		if (!p_base_url) {
			p_base_url = "";
		}
		if (!device_if_base_url) {
			device_if_base_url = "";
		}

		int cmp = strcmp(p_base_url, device_if_base_url);
		if (cmp < 0) {
			pprev = &p->next;
			p = p->next;
			continue;
		}
		if (cmp > 0) {
			break;
		}

		/* Duplicate */
		hdhomerun_discover_device_if_free(device_if);
		return;
	}

	device_if->next = *pprev;
	*pprev = device_if;
}

static void hdhomerun_discover_recv_merge_device(struct hdhomerun_discover_t *ds, struct hdhomerun_discover2_device_t *device)
{
	struct hdhomerun_discover2_device_t **pprev = &ds->device_list;
	struct hdhomerun_discover2_device_t *p = ds->device_list;
	while (p) {
		if (p->device_id < device->device_id) {
			pprev = &p->next;
			p = p->next;
			continue;
		}
		if (p->device_id > device->device_id) {
			break;
		}

		char *p_storage_id = p->storage_id;
		char *device_storage_id = device->storage_id;
		if (!p_storage_id) {
			p_storage_id = "";
		}
		if (!device_storage_id) {
			device_storage_id = "";
		}

		int cmp = strcmp(p_storage_id, device_storage_id);
		if (cmp < 0) {
			pprev = &p->next;
			p = p->next;
			continue;
		}
		if (cmp > 0) {
			break;
		}

		/* Merge */
		struct hdhomerun_discover2_device_type_t *type_list = device->type_list;
		device->type_list = NULL;
		hdhomerun_discover_recv_merge_device_types(p, type_list);

		struct hdhomerun_discover2_device_if_t *device_if = device->if_list;
		device->if_list = NULL;
		hdhomerun_discover_recv_merge_device_if(p, device_if);

		hdhomerun_discover_device_free(device);
		return;
	}

	device->next = *pprev;
	*pprev = device;
}

static bool hdhomerun_discover_recvfrom_match_flags(const struct sockaddr *remote_addr, uint32_t flags)
{
	if (remote_addr->sa_family == AF_INET6) {
		if (hdhomerun_sock_sockaddr_is_ipv6_localhost(remote_addr)) {
			return (flags & HDHOMERUN_DISCOVER_FLAGS_IPV6_LOCALHOST) != 0;
		}

		if (hdhomerun_sock_sockaddr_is_ipv6_linklocal(remote_addr)) {
			return (flags & HDHOMERUN_DISCOVER_FLAGS_IPV6_LINKLOCAL) != 0;
		}

		return (flags & HDHOMERUN_DISCOVER_FLAGS_IPV6_GENERAL) != 0;
	}

	if (remote_addr->sa_family == AF_INET) {
		if (hdhomerun_sock_sockaddr_is_ipv4_localhost(remote_addr)) {
			return (flags & HDHOMERUN_DISCOVER_FLAGS_IPV4_LOCALHOST) != 0;
		}

		return (flags & HDHOMERUN_DISCOVER_FLAGS_IPV4_GENERAL) != 0;
	}

	return false;
}

static bool hdhomerun_discover_recvfrom_match_device_type(struct hdhomerun_discover2_device_t *device, const uint32_t device_types_match[], size_t device_types_count)
{
	if (device_types_count == 0) {
		return false;
	}

	if (device_types_match[0] == HDHOMERUN_DEVICE_TYPE_WILDCARD) {
		return true;
	}

	while (device_types_count--) {
		if (hdhomerun_discover2_device_is_type(device, *device_types_match++)) {
			return true;
		}
	}

	return false;
}

static bool hdhomerun_discover_recvfrom_match_device_id(struct hdhomerun_discover2_device_t *device, uint32_t device_id_match)
{
	if (device_id_match == HDHOMERUN_DEVICE_ID_WILDCARD) {
		return true;
	}
	
	return (device->device_id == device_id_match);
}

static bool hdhomerun_discover_recvfrom(struct hdhomerun_discover_t *ds, struct hdhomerun_discover_sock_t *dss, uint32_t flags, const uint32_t device_types_match[], size_t device_types_count, uint32_t device_id_match)
{
	struct hdhomerun_pkt_t *rx_pkt = &ds->rx_pkt;
	hdhomerun_pkt_reset(rx_pkt);
	bool activity = false;

	struct sockaddr_storage remote_addr;
	size_t length = rx_pkt->limit - rx_pkt->end;
	if (!hdhomerun_sock_recvfrom_ex(dss->sock, &remote_addr, rx_pkt->end, &length, 0)) {
		return activity;
	}

	rx_pkt->end += length;
	activity = true;

	char local_ip_str[64];
	char remote_ip_str[64];
	hdhomerun_sock_sockaddr_to_ip_str(local_ip_str, (const struct sockaddr *)&dss->local_ip, true);
	hdhomerun_sock_sockaddr_to_ip_str(remote_ip_str, (const struct sockaddr *)&remote_addr, true);
	hdhomerun_debug_printf(ds->dbg, "discover: recv from %s via %s\n", remote_ip_str, local_ip_str);

	if (!hdhomerun_discover_recvfrom_match_flags((const struct sockaddr *)&remote_addr, flags)) {
		return activity;
	}

	struct hdhomerun_discover2_device_t *device = (struct hdhomerun_discover2_device_t *)calloc(sizeof(struct hdhomerun_discover2_device_t), 1);
	struct hdhomerun_discover2_device_if_t *device_if = (struct hdhomerun_discover2_device_if_t *)calloc(sizeof(struct hdhomerun_discover2_device_if_t), 1);
	if (!device || !device_if) {
		if (device) {
			free(device);
		}
		if (device_if) {
			free(device_if);
		}
		return activity;
	}

	device->if_list = device_if;
	device_if->ip_addr = remote_addr;

	if (!hdhomerun_discover_recv_internal(ds, rx_pkt, device)) {
		hdhomerun_discover_device_free(device);
		return activity;
	}

	if (!hdhomerun_discover_recvfrom_match_device_type(device, device_types_match, device_types_count)) {
		hdhomerun_discover_device_free(device);
		return activity;
	}

	if (!hdhomerun_discover_recvfrom_match_device_id(device, device_id_match)) {
		hdhomerun_discover_device_free(device);
		return activity;
	}

	hdhomerun_discover_recv_merge_device(ds, device);
	return activity;
}

static void hdhomerun_discover2_find_devices_debug_log_results(struct hdhomerun_discover_t *ds)
{
	if (!ds->dbg) {
		return;
	}

	struct hdhomerun_discover2_device_t *device = ds->device_list;
	while (device) {
		struct hdhomerun_discover2_device_if_t *device_if = device->if_list;
		if (device->device_id) {
			hdhomerun_debug_printf(ds->dbg, "discover: found %08X %s\n", device->device_id, device_if->base_url);
			device = device->next;
			continue;
		}

		if (device->storage_id) {
			hdhomerun_debug_printf(ds->dbg, "discover: found %s %s\n", device->storage_id, device_if->base_url);
			device = device->next;
			continue;
		}

		hdhomerun_debug_printf(ds->dbg, "discover: found %s\n", device_if->base_url);
		device = device->next;
	}
}

static uint32_t hdhomerun_discover2_find_devices_targeted_flags(const struct sockaddr *target_addr)
{
	if (target_addr->sa_family == AF_INET6) {
		if (hdhomerun_sock_sockaddr_is_ipv6_localhost(target_addr)) {
			return HDHOMERUN_DISCOVER_FLAGS_IPV6_LOCALHOST;
		}

		if (hdhomerun_sock_sockaddr_is_ipv6_linklocal(target_addr)) {
			return HDHOMERUN_DISCOVER_FLAGS_IPV6_LINKLOCAL;
		}

		return HDHOMERUN_DISCOVER_FLAGS_IPV6_GENERAL;
	}

	if (target_addr->sa_family == AF_INET) {
		if (hdhomerun_sock_sockaddr_is_ipv4_localhost(target_addr)) {
			return HDHOMERUN_DISCOVER_FLAGS_IPV4_LOCALHOST;
		}

		return HDHOMERUN_DISCOVER_FLAGS_IPV4_GENERAL;
	}

	return 0;
}

static int hdhomerun_discover2_find_devices_targeted_internal(struct hdhomerun_discover_t *ds, const struct sockaddr *target_addr, const uint32_t device_types[], size_t device_types_count, uint32_t device_id)
{
	uint32_t flags = hdhomerun_discover2_find_devices_targeted_flags(target_addr);
	if (flags == 0) {
		return -1;
	}

	hdhomerun_discover_free_device_list(ds);
	hdhomerun_discover_sock_detect_all_from_flags(ds, flags);

	int attempt;
	for (attempt = 0; attempt < 2; attempt++) {
		struct hdhomerun_discover_sock_t *recv_list = NULL;
		if (flags & HDHOMERUN_DISCOVER_FLAGS_IPV6_LOCALHOST) {
			hdhomerun_discover_send_ipv6_localhost(ds, target_addr, flags, device_types, device_types_count, device_id, &recv_list);
		}
		if (flags & (HDHOMERUN_DISCOVER_FLAGS_IPV6_GENERAL | HDHOMERUN_DISCOVER_FLAGS_IPV6_LINKLOCAL)) {
			hdhomerun_discover_send_ipv6_targeted(ds, target_addr, flags, device_types, device_types_count, device_id, &recv_list);
		}
		if (flags & HDHOMERUN_DISCOVER_FLAGS_IPV4_LOCALHOST) {
			hdhomerun_discover_send_ipv4_localhost(ds, target_addr, flags, device_types, device_types_count, device_id, &recv_list);
		}
		if (flags & HDHOMERUN_DISCOVER_FLAGS_IPV4_GENERAL) {
			hdhomerun_discover_send_ipv4_targeted(ds, target_addr, flags, device_types, device_types_count, device_id, &recv_list);
		}
		if (!recv_list) {
			return -1;
		}

		uint64_t timeout = getcurrenttime() + 200;

		while (1) {
			bool activity = false;

			struct hdhomerun_discover_sock_t *dss = recv_list;
			while (dss) {
				activity |= hdhomerun_discover_recvfrom(ds, dss, flags, device_types, device_types_count, device_id);
				dss = dss->recv_next;
			}

			if (ds->device_list) {
				hdhomerun_discover2_find_devices_debug_log_results(ds);
				return 1;
			}
			if (activity) {
				continue;
			}
			if (getcurrenttime() >= timeout) {
				break;
			}
			msleep_approx(16);
		}
	}

	hdhomerun_discover2_find_devices_debug_log_results(ds);
	return (ds->device_list) ? 1 : 0;
}

static int hdhomerun_discover2_find_devices_broadcast_internal(struct hdhomerun_discover_t *ds, uint32_t flags, const uint32_t device_types[], size_t device_types_count, uint32_t device_id)
{
	hdhomerun_discover_free_device_list(ds);
	hdhomerun_discover_sock_detect_all_from_flags(ds, flags);

	int attempt;
	for (attempt = 0; attempt < 2; attempt++) {
		struct hdhomerun_discover_sock_t *recv_list = NULL;
		if (flags & HDHOMERUN_DISCOVER_FLAGS_IPV6_LOCALHOST) {
			hdhomerun_discover_send_ipv6_localhost(ds, NULL, flags, device_types, device_types_count, device_id, &recv_list);
		}
		if (flags & (HDHOMERUN_DISCOVER_FLAGS_IPV6_GENERAL | HDHOMERUN_DISCOVER_FLAGS_IPV6_LINKLOCAL)) {
			hdhomerun_discover_send_ipv6_multicast(ds, flags, device_types, device_types_count, device_id, &recv_list);
		}
		if (flags & HDHOMERUN_DISCOVER_FLAGS_IPV4_LOCALHOST) {
			hdhomerun_discover_send_ipv4_localhost(ds, NULL, flags, device_types, device_types_count, device_id, &recv_list);
		}
		if (flags & HDHOMERUN_DISCOVER_FLAGS_IPV4_GENERAL) {
			hdhomerun_discover_send_ipv4_broadcast(ds, flags, device_types, device_types_count, device_id, &recv_list);
		}
		if (!recv_list) {
			return -1;
		}

		uint64_t timeout = getcurrenttime() + 200;

		while (1) {
			bool activity = false;

			struct hdhomerun_discover_sock_t *dss = recv_list;
			while (dss) {
				activity |= hdhomerun_discover_recvfrom(ds, dss, flags, device_types, device_types_count, device_id);
				dss = dss->recv_next;
			}

			if ((device_id != HDHOMERUN_DEVICE_ID_WILDCARD) && ds->device_list) {
				hdhomerun_discover2_find_devices_debug_log_results(ds);
				return 1;
			}
			if (activity) {
				continue;
			}
			if (getcurrenttime() >= timeout) {
				break;
			}
			msleep_approx(16);
		}
	}

	hdhomerun_discover2_find_devices_debug_log_results(ds);
	return (ds->device_list) ? 1 : 0;
}

int hdhomerun_discover2_find_devices_targeted(struct hdhomerun_discover_t *ds, const struct sockaddr *target_addr, const uint32_t device_types[], size_t device_types_count)
{
	return hdhomerun_discover2_find_devices_targeted_internal(ds, target_addr, device_types, device_types_count, HDHOMERUN_DEVICE_ID_WILDCARD);
}

int hdhomerun_discover2_find_devices_broadcast(struct hdhomerun_discover_t *ds, uint32_t flags, const uint32_t device_types[], size_t device_types_count)
{
	return hdhomerun_discover2_find_devices_broadcast_internal(ds, flags, device_types, device_types_count, HDHOMERUN_DEVICE_ID_WILDCARD);
}

int hdhomerun_discover2_find_device_id_targeted(struct hdhomerun_discover_t *ds, const struct sockaddr *target_addr, uint32_t device_id)
{
	if ((device_id == 0) || (device_id == HDHOMERUN_DEVICE_ID_WILDCARD)) {
		return -1;
	}
	if (!hdhomerun_discover_validate_device_id(device_id)) {
		return - 1;
	}

	uint32_t device_types[1];
	device_types[0] = HDHOMERUN_DEVICE_TYPE_WILDCARD;

	return hdhomerun_discover2_find_devices_targeted_internal(ds, target_addr, device_types, 1, device_id);
}

int hdhomerun_discover2_find_device_id_broadcast(struct hdhomerun_discover_t *ds, uint32_t flags, uint32_t device_id)
{
	if ((device_id == 0) || (device_id == HDHOMERUN_DEVICE_ID_WILDCARD)) {
		return -1;
	}
	if (!hdhomerun_discover_validate_device_id(device_id)) {
		return -1;
	}

	uint32_t device_types[1];
	device_types[0] = HDHOMERUN_DEVICE_TYPE_WILDCARD;

	return hdhomerun_discover2_find_devices_broadcast_internal(ds, flags, device_types, 1, device_id);
}

struct hdhomerun_discover2_device_t *hdhomerun_discover2_iter_device_first(struct hdhomerun_discover_t *ds)
{
	return ds->device_list;
}

struct hdhomerun_discover2_device_t *hdhomerun_discover2_iter_device_next(struct hdhomerun_discover2_device_t *device)
{
	return device->next;
}

struct hdhomerun_discover2_device_if_t *hdhomerun_discover2_iter_device_if_first(struct hdhomerun_discover2_device_t *device)
{
	return device->if_list;
}

struct hdhomerun_discover2_device_if_t *hdhomerun_discover2_iter_device_if_next(struct hdhomerun_discover2_device_if_t *device_if)
{
	return device_if->next;
}

bool hdhomerun_discover2_device_is_legacy(struct hdhomerun_discover2_device_t *device)
{
	switch (device->device_id >> 20) {
	case 0x100: /* TECH-US/TECH3-US */
		return (device->device_id < 0x10040000);

	case 0x120: /* TECH3-EU */
		return (device->device_id < 0x12030000);

	case 0x101: /* HDHR-US */
	case 0x102: /* HDHR-T1-US */
	case 0x103: /* HDHR3-US */
	case 0x111: /* HDHR3-DT */
	case 0x121: /* HDHR-EU */
	case 0x122: /* HDHR3-EU */
		return true;

	default:
		return false;
	}
}

bool hdhomerun_discover2_device_is_type(struct hdhomerun_discover2_device_t *device, uint32_t device_type)
{
	struct hdhomerun_discover2_device_type_t *p = device->type_list;
	while (p) {
		if (p->device_type == device_type) {
			return true;
		}
		p = p->next;
	}

	return false;
}

uint32_t hdhomerun_discover2_device_get_device_id(struct hdhomerun_discover2_device_t *device)
{
	return device->device_id;
}

const char *hdhomerun_discover2_device_get_storage_id(struct hdhomerun_discover2_device_t *device)
{
	return device->storage_id;
}

uint8_t hdhomerun_discover2_device_get_tuner_count(struct hdhomerun_discover2_device_t *device)
{
	return device->tuner_count;
}

const char *hdhomerun_discover2_device_get_device_auth(struct hdhomerun_discover2_device_t *device)
{
	return device->device_auth;
}

bool hdhomerun_discover2_device_if_addr_is_ipv4(struct hdhomerun_discover2_device_if_t *device_if)
{
	return (device_if->ip_addr.ss_family == AF_INET);
}

bool hdhomerun_discover2_device_if_addr_is_ipv6_linklocal(struct hdhomerun_discover2_device_if_t *device_if)
{
	return hdhomerun_sock_sockaddr_is_ipv6_linklocal((const struct sockaddr *)&device_if->ip_addr);
}

uint32_t hdhomerun_discover2_device_if_get_ipv6_linklocal_scope_id(struct hdhomerun_discover2_device_if_t *device_if)
{
	if (!hdhomerun_sock_sockaddr_is_ipv6_linklocal((const struct sockaddr *)&device_if->ip_addr)) {
		return 0;
	}

	const struct sockaddr_in6 *ip_addr_in6 = (const struct sockaddr_in6 *)&device_if->ip_addr;
	return ip_addr_in6->sin6_scope_id;
}

void hdhomerun_discover2_device_if_get_ip_addr(struct hdhomerun_discover2_device_if_t *device_if, struct sockaddr_storage *ip_addr)
{
	*ip_addr = device_if->ip_addr;
}

const char *hdhomerun_discover2_device_if_get_base_url(struct hdhomerun_discover2_device_if_t *device_if)
{
	return device_if->base_url;
}

const char *hdhomerun_discover2_device_if_get_lineup_url(struct hdhomerun_discover2_device_if_t *device_if)
{
	return device_if->lineup_url;
}

const char *hdhomerun_discover2_device_if_get_storage_url(struct hdhomerun_discover2_device_if_t *device_if)
{
	return device_if->storage_url;
}

static void hdhomerun_discover_v2v3_strcpy(char *output, size_t size, const char *src)
{
	if (!src) {
		output[0] = 0;
		return;
	}

	size_t src_len = strlen(src);
	if (src_len >= size) {
		output[0] = 0;
		return;
	}

	memcpy(output, src, src_len + 1);
}

static uint32_t hdhomerun_discover_v2v3_device_type(struct hdhomerun_discover2_device_t *device, uint32_t device_type_match)
{
	if (device_type_match == HDHOMERUN_DEVICE_TYPE_WILDCARD) {
		return device->type_list->device_type;
	}

	if (hdhomerun_discover2_device_is_type(device, device_type_match)) {
		return device_type_match;
	}

	return device->type_list->device_type;
}

int hdhomerun_discover_find_devices_v3(struct hdhomerun_discover_t *ds, uint32_t target_ip, uint32_t device_type_match, uint32_t device_id_match, struct hdhomerun_discover_device_v3_t result_list[], int max_count)
{
	if (target_ip == 0) {
		target_ip = 0xFFFFFFFF;
	}
	if (device_type_match == 0) {
		device_type_match = HDHOMERUN_DEVICE_TYPE_WILDCARD;
	}
	if (device_id_match == 0) {
		device_id_match = HDHOMERUN_DEVICE_ID_WILDCARD;
	}
	if (!hdhomerun_discover_validate_device_id(device_id_match)) {
		return -1;
	}

	uint32_t device_types[1];
	device_types[0] = device_type_match;

	int ret;
	if (target_ip == 0xFFFFFFFF) {
		ret = hdhomerun_discover2_find_devices_broadcast_internal(ds, HDHOMERUN_DISCOVER_FLAGS_IPV4_GENERAL, device_types, 1, device_id_match);
	} else {
		struct sockaddr_in sock_addr_in;
		memset(&sock_addr_in, 0, sizeof(sock_addr_in));
		sock_addr_in.sin_family = AF_INET;
		sock_addr_in.sin_addr.s_addr = htonl(target_ip);
		ret = hdhomerun_discover2_find_devices_targeted_internal(ds, (const struct sockaddr *)&sock_addr_in, device_types, 1, device_id_match);
	}
	if (ret <= 0) {
		return ret;
	}

	struct hdhomerun_discover_device_v3_t *result = result_list;
	struct hdhomerun_discover2_device_t *device = ds->device_list;
	int count = 0;

	while (device && (count < max_count)) {
		struct hdhomerun_discover2_device_if_t *device_if = device->if_list;
		while (device_if) {
			if (device_if->ip_addr.ss_family == AF_INET) {
				break;
			}
			device_if = device_if->next;
		}
		if (!device_if) {
			device = device->next;
			continue;
		}

		struct sockaddr_in *sock_addr_in = (struct sockaddr_in *)&device_if->ip_addr;
		result->ip_addr = ntohl(sock_addr_in->sin_addr.s_addr);
		result->device_type = hdhomerun_discover_v2v3_device_type(device, device_type_match);
		result->device_id = device->device_id;
		result->tuner_count = device->tuner_count;
		result->is_legacy = hdhomerun_discover2_device_is_legacy(device);
		hdhomerun_discover_v2v3_strcpy(result->storage_id, sizeof(result->storage_id), device->storage_id);
		hdhomerun_discover_v2v3_strcpy(result->device_auth, sizeof(result->device_auth), device->device_auth);
		hdhomerun_discover_v2v3_strcpy(result->base_url, sizeof(result->base_url), device_if->base_url);
		hdhomerun_discover_v2v3_strcpy(result->lineup_url, sizeof(result->lineup_url), device_if->lineup_url);
		hdhomerun_discover_v2v3_strcpy(result->storage_url, sizeof(result->storage_url), device_if->storage_url);

		count++;
		result++;
		device = device->next;
	}

	return count;
}

int hdhomerun_discover_find_devices_v2(struct hdhomerun_discover_t *ds, uint32_t target_ip, uint32_t device_type_match, uint32_t device_id_match, struct hdhomerun_discover_device_t result_list[], int max_count)
{
	if (target_ip == 0) {
		target_ip = 0xFFFFFFFF;
	}
	if (device_type_match == 0) {
		device_type_match = HDHOMERUN_DEVICE_TYPE_WILDCARD;
	}
	if (device_id_match == 0) {
		device_id_match = HDHOMERUN_DEVICE_ID_WILDCARD;
	}
	if (!hdhomerun_discover_validate_device_id(device_id_match)) {
		return -1;
	}

	uint32_t device_types[1];
	device_types[0] = device_type_match;

	int ret;
	if (target_ip == 0xFFFFFFFF) {
		ret = hdhomerun_discover2_find_devices_broadcast_internal(ds, HDHOMERUN_DISCOVER_FLAGS_IPV4_GENERAL, device_types, 1, device_id_match);
	} else {
		struct sockaddr_in sock_addr_in;
		memset(&sock_addr_in, 0, sizeof(sock_addr_in));
		sock_addr_in.sin_family = AF_INET;
		sock_addr_in.sin_addr.s_addr = htonl(target_ip);
		ret = hdhomerun_discover2_find_devices_targeted_internal(ds, (const struct sockaddr *)&sock_addr_in, device_types, 1, device_id_match);
	}
	if (ret <= 0) {
		return ret;
	}

	struct hdhomerun_discover_device_t *result = result_list;
	struct hdhomerun_discover2_device_t *device = ds->device_list;
	int count = 0;

	while (device && (count < max_count)) {
		struct hdhomerun_discover2_device_if_t *device_if = device->if_list;
		while (device_if) {
			if (device_if->ip_addr.ss_family == AF_INET) {
				break;
			}
			device_if = device_if->next;
		}
		if (!device_if) {
			device = device->next;
			continue;
		}

		struct sockaddr_in *sock_addr_in = (struct sockaddr_in *)&device_if->ip_addr;
		result->ip_addr = ntohl(sock_addr_in->sin_addr.s_addr);
		result->device_type = hdhomerun_discover_v2v3_device_type(device, device_type_match);
		result->device_id = device->device_id;
		result->tuner_count = device->tuner_count;
		result->is_legacy = hdhomerun_discover2_device_is_legacy(device);
		hdhomerun_discover_v2v3_strcpy(result->device_auth, sizeof(result->device_auth), device->device_auth);
		hdhomerun_discover_v2v3_strcpy(result->base_url, sizeof(result->base_url), device_if->base_url);

		count++;
		result++;
		device = device->next;
	}

	return count;
}

int hdhomerun_discover_find_devices_custom_v3(uint32_t target_ip, uint32_t device_type_match, uint32_t device_id_match, struct hdhomerun_discover_device_v3_t result_list[], int max_count)
{
	struct hdhomerun_discover_t *ds = hdhomerun_discover_create(NULL);
	if (!ds) {
		return -1;
	}

	int ret = hdhomerun_discover_find_devices_v3(ds, target_ip, device_type_match, device_id_match, result_list, max_count);
	hdhomerun_discover_destroy(ds);
	return ret;
}

int hdhomerun_discover_find_devices_custom_v2(uint32_t target_ip, uint32_t device_type_match, uint32_t device_id_match, struct hdhomerun_discover_device_t result_list[], int max_count)
{
	struct hdhomerun_discover_t *ds = hdhomerun_discover_create(NULL);
	if (!ds) {
		return -1;
	}

	int ret = hdhomerun_discover_find_devices_v2(ds, target_ip, device_type_match, device_id_match, result_list, max_count);
	hdhomerun_discover_destroy(ds);
	return ret;
}

bool hdhomerun_discover_validate_device_id(uint32_t device_id)
{
	static uint8_t lookup_table[16] = {0xA, 0x5, 0xF, 0x6, 0x7, 0xC, 0x1, 0xB, 0x9, 0x2, 0x8, 0xD, 0x4, 0x3, 0xE, 0x0};

	uint8_t checksum = 0;

	checksum ^= lookup_table[(device_id >> 28) & 0x0F];
	checksum ^= (device_id >> 24) & 0x0F;
	checksum ^= lookup_table[(device_id >> 20) & 0x0F];
	checksum ^= (device_id >> 16) & 0x0F;
	checksum ^= lookup_table[(device_id >> 12) & 0x0F];
	checksum ^= (device_id >> 8) & 0x0F;
	checksum ^= lookup_table[(device_id >> 4) & 0x0F];
	checksum ^= (device_id >> 0) & 0x0F;

	return (checksum == 0);
}

bool hdhomerun_discover_is_ip_multicast(uint32_t ip_addr)
{
	struct sockaddr_in addr_in;
	memset(&addr_in, 0, sizeof(struct sockaddr_in));
	addr_in.sin_family = AF_INET;
	addr_in.sin_addr.s_addr = htonl(ip_addr);

	return hdhomerun_sock_sockaddr_is_multicast((const struct sockaddr *)&addr_in);
}

bool hdhomerun_discover_is_ip_multicast_ex(const struct sockaddr *ip_addr)
{
	return hdhomerun_sock_sockaddr_is_multicast(ip_addr);
}
