/*
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 *
 * SPDX-License-Identifier: MPL-2.0
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, you can obtain one at https://mozilla.org/MPL/2.0/.
 *
 * See the COPYRIGHT file distributed with this work for additional
 * information regarding copyright ownership.
 */

#include <stdbool.h>
#include <string.h>

#include <isc/list.h>
#include <isc/mem.h>
#include <isc/netaddr.h>
#include <isc/result.h>
#include <isc/string.h>
#include <isc/util.h>

#include <dns/acl.h>
#include <dns/dns64.h>
#include <dns/rdata.h>
#include <dns/rdatalist.h>
#include <dns/rdataset.h>

struct dns_dns64 {
	unsigned char bits[16]; /*
				 * Prefix + suffix bits.
				 */
	dns_acl_t *clients;	/*
				 * Which clients get mapped
				 * addresses.
				 */
	dns_acl_t *mapped;	/*
				 * IPv4 addresses to be mapped.
				 */
	dns_acl_t *excluded;	/*
				 * IPv6 addresses that are
				 * treated as not existing.
				 */
	unsigned int prefixlen; /*
				 * Start of mapped address.
				 */
	unsigned int flags;
	isc_mem_t *mctx;
	ISC_LINK(dns_dns64_t) link;
};

void
dns_dns64_create(isc_mem_t *mctx, const isc_netaddr_t *prefix,
		 unsigned int prefixlen, const isc_netaddr_t *suffix,
		 dns_acl_t *clients, dns_acl_t *mapped, dns_acl_t *excluded,
		 unsigned int flags, dns_dns64_t **dns64p) {
	dns_dns64_t *dns64;
	unsigned int nbytes = 16;

	REQUIRE(prefix != NULL && prefix->family == AF_INET6);
	/* Legal prefix lengths from rfc6052.txt. */
	REQUIRE(prefixlen == 32 || prefixlen == 40 || prefixlen == 48 ||
		prefixlen == 56 || prefixlen == 64 || prefixlen == 96);
	REQUIRE(isc_netaddr_prefixok(prefix, prefixlen) == ISC_R_SUCCESS);
	REQUIRE(dns64p != NULL && *dns64p == NULL);

	if (suffix != NULL) {
		static const unsigned char zeros[16];
		REQUIRE(prefix->family == AF_INET6);
		nbytes = prefixlen / 8 + 4;
		/* Bits 64-71 are zeros. rfc6052.txt */
		if (prefixlen >= 32 && prefixlen <= 64) {
			nbytes++;
		}
		REQUIRE(memcmp(suffix->type.in6.s6_addr, zeros, nbytes) == 0);
	}

	dns64 = isc_mem_get(mctx, sizeof(dns_dns64_t));
	memset(dns64->bits, 0, sizeof(dns64->bits));
	memmove(dns64->bits, prefix->type.in6.s6_addr, prefixlen / 8);
	if (suffix != NULL) {
		memmove(dns64->bits + nbytes, suffix->type.in6.s6_addr + nbytes,
			16 - nbytes);
	}
	dns64->clients = NULL;
	if (clients != NULL) {
		dns_acl_attach(clients, &dns64->clients);
	}
	dns64->mapped = NULL;
	if (mapped != NULL) {
		dns_acl_attach(mapped, &dns64->mapped);
	}
	dns64->excluded = NULL;
	if (excluded != NULL) {
		dns_acl_attach(excluded, &dns64->excluded);
	}
	dns64->prefixlen = prefixlen;
	dns64->flags = flags;
	ISC_LINK_INIT(dns64, link);
	dns64->mctx = NULL;
	isc_mem_attach(mctx, &dns64->mctx);
	*dns64p = dns64;
}

void
dns_dns64_destroy(dns_dns64list_t *list, dns_dns64_t **dns64p) {
	dns_dns64_t *dns64;

	REQUIRE(dns64p != NULL && *dns64p != NULL);

	dns64 = *dns64p;
	*dns64p = NULL;

	ISC_LIST_UNLINK(*list, dns64, link);

	if (dns64->clients != NULL) {
		dns_acl_detach(&dns64->clients);
	}
	if (dns64->mapped != NULL) {
		dns_acl_detach(&dns64->mapped);
	}
	if (dns64->excluded != NULL) {
		dns_acl_detach(&dns64->excluded);
	}
	isc_mem_putanddetach(&dns64->mctx, dns64, sizeof(*dns64));
}

isc_result_t
dns_dns64_aaaafroma(const dns_dns64_t *dns64, const isc_netaddr_t *reqaddr,
		    const dns_name_t *reqsigner, dns_aclenv_t *env,
		    unsigned int flags, unsigned char *a, unsigned char *aaaa) {
	unsigned int nbytes, i;
	int match;

	if ((dns64->flags & DNS_DNS64_RECURSIVE_ONLY) != 0 &&
	    (flags & DNS_DNS64_RECURSIVE) == 0)
	{
		return DNS_R_DISALLOWED;
	}

	if ((dns64->flags & DNS_DNS64_BREAK_DNSSEC) == 0 &&
	    (flags & DNS_DNS64_DNSSEC) != 0)
	{
		return DNS_R_DISALLOWED;
	}

	if (dns64->clients != NULL && reqaddr != NULL) {
		RETERR(dns_acl_match(reqaddr, reqsigner, dns64->clients, env,
				     &match, NULL));
		if (match <= 0) {
			return DNS_R_DISALLOWED;
		}
	}

	if (dns64->mapped != NULL) {
		struct in_addr ina;
		isc_netaddr_t netaddr;

		memmove(&ina.s_addr, a, 4);
		isc_netaddr_fromin(&netaddr, &ina);
		RETERR(dns_acl_match(&netaddr, NULL, dns64->mapped, env, &match,
				     NULL));
		if (match <= 0) {
			return DNS_R_DISALLOWED;
		}
	}

	nbytes = dns64->prefixlen / 8;
	INSIST(nbytes <= 12);
	/* Copy prefix. */
	memmove(aaaa, dns64->bits, nbytes);
	/* Bits 64-71 are zeros. rfc6052.txt */
	if (nbytes == 8) {
		aaaa[nbytes++] = 0;
	}
	/* Copy mapped address. */
	for (i = 0; i < 4U; i++) {
		aaaa[nbytes++] = a[i];
		/* Bits 64-71 are zeros. rfc6052.txt */
		if (nbytes == 8) {
			aaaa[nbytes++] = 0;
		}
	}
	/* Copy suffix. */
	memmove(aaaa + nbytes, dns64->bits + nbytes, 16 - nbytes);
	return ISC_R_SUCCESS;
}

void
dns_dns64_append(dns_dns64list_t *list, dns_dns64_t *dns64) {
	ISC_LIST_APPEND(*list, dns64, link);
}

bool
dns_dns64_aaaaok(const dns_dns64_t *dns64, const isc_netaddr_t *reqaddr,
		 const dns_name_t *reqsigner, dns_aclenv_t *env,
		 unsigned int flags, dns_rdataset_t *rdataset, bool *aaaaok,
		 size_t aaaaoklen) {
	struct in6_addr in6;
	isc_netaddr_t netaddr;
	isc_result_t result;
	int match;
	bool answer = false;
	bool found = false;
	unsigned int i, ok;

	REQUIRE(rdataset != NULL);
	REQUIRE(rdataset->type == dns_rdatatype_aaaa);
	REQUIRE(rdataset->rdclass == dns_rdataclass_in);
	if (aaaaok != NULL) {
		REQUIRE(aaaaoklen == dns_rdataset_count(rdataset));
	}

	for (; dns64 != NULL; dns64 = ISC_LIST_NEXT(dns64, link)) {
		if ((dns64->flags & DNS_DNS64_RECURSIVE_ONLY) != 0 &&
		    (flags & DNS_DNS64_RECURSIVE) == 0)
		{
			continue;
		}

		if ((dns64->flags & DNS_DNS64_BREAK_DNSSEC) == 0 &&
		    (flags & DNS_DNS64_DNSSEC) != 0)
		{
			continue;
		}
		/*
		 * Work out if this dns64 structure applies to this client.
		 */
		if (dns64->clients != NULL) {
			result = dns_acl_match(reqaddr, reqsigner,
					       dns64->clients, env, &match,
					       NULL);
			if (result != ISC_R_SUCCESS) {
				continue;
			}
			if (match <= 0) {
				continue;
			}
		}

		if (!found && aaaaok != NULL) {
			for (i = 0; i < aaaaoklen; i++) {
				aaaaok[i] = false;
			}
		}
		found = true;

		/*
		 * If we are not excluding any addresses then any AAAA
		 * will do.
		 */
		if (dns64->excluded == NULL) {
			answer = true;
			if (aaaaok == NULL) {
				goto done;
			}
			for (i = 0; i < aaaaoklen; i++) {
				aaaaok[i] = true;
			}
			goto done;
		}

		i = 0;
		ok = 0;
		DNS_RDATASET_FOREACH(rdataset) {
			dns_rdata_t rdata = DNS_RDATA_INIT;
			if (aaaaok == NULL || !aaaaok[i]) {
				dns_rdataset_current(rdataset, &rdata);
				memmove(&in6.s6_addr, rdata.data, 16);
				isc_netaddr_fromin6(&netaddr, &in6);

				result = dns_acl_match(&netaddr, NULL,
						       dns64->excluded, env,
						       &match, NULL);
				if (result == ISC_R_SUCCESS && match <= 0) {
					answer = true;
					if (aaaaok == NULL) {
						goto done;
					}
					aaaaok[i] = true;
					ok++;
				}
			} else {
				ok++;
			}
			i++;
		}
		/*
		 * Are all addresses ok?
		 */
		if (aaaaok != NULL && ok == aaaaoklen) {
			goto done;
		}
	}

done:
	if (!found && aaaaok != NULL) {
		for (i = 0; i < aaaaoklen; i++) {
			aaaaok[i] = true;
		}
	}
	return found ? answer : true;
}

/*
 * Posible mapping of IPV4ONLY.ARPA A records into AAAA records
 * for valid RFC6052 prefixes.
 */
static struct {
	const unsigned char aa[16]; /* mapped version of 192.0.0.170 */
	const unsigned char ab[16]; /* mapped version of 192.0.0.171 */
	const unsigned char mask[16];
	const unsigned int plen;
} const prefixes[6] = {
	{ { 0, 0, 0, 0, 192, 0, 0, 170, 0, 0, 0, 0, 0, 0, 0, 0 },
	  { 0, 0, 0, 0, 192, 0, 0, 171, 0, 0, 0, 0, 0, 0, 0, 0 },
	  { 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0 },
	  32 },
	{ { 0, 0, 0, 0, 0, 192, 0, 0, 0, 170, 0, 0, 0, 0, 0, 0 },
	  { 0, 0, 0, 0, 0, 192, 0, 0, 0, 171, 0, 0, 0, 0, 0, 0 },
	  { 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0 },
	  40 },
	{ { 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 170, 0, 0, 0, 0, 0 },
	  { 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 171, 0, 0, 0, 0, 0 },
	  { 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0 },
	  48 },
	{ { 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 170, 0, 0, 0, 0 },
	  { 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 171, 0, 0, 0, 0 },
	  { 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0 },
	  56 },
	{ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 170, 0, 0, 0 },
	  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 171, 0, 0, 0 },
	  { 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0 },
	  64 },
	{ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 170 },
	  { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 171 },
	  { 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255 },
	  96 }
};

static unsigned int
search(const dns_rdata_t *rd1, const dns_rdata_t *rd2, unsigned int plen) {
	unsigned int i = 0, j;
	const unsigned char *c, *m;

	/*
	 * Resume looking for another aa match?
	 */
	if (plen != 0U && rd2 == NULL) {
		while (i < 6U) {
			/* Post increment as we resume on next entry. */
			if (prefixes[i++].plen == plen) {
				break;
			}
		}
	}

	for (; i < 6U; i++) {
		j = 0;
		if (rd2 != NULL) {
			/* Find the right entry. */
			if (prefixes[i].plen != plen) {
				continue;
			}
			/* Does the prefix match? */
			while ((j * 8U) < plen) {
				if (rd1->data[j] != rd2->data[j]) {
					return 0;
				}
				j++;
			}
		}

		/* Match well known mapped addresses. */
		c = (rd2 == NULL) ? prefixes[i].aa : prefixes[i].ab;
		m = prefixes[i].mask;
		for (; j < 16U; j++) {
			if ((rd1->data[j] & m[j]) != (c[j] & m[j])) {
				break;
			}
		}
		if (j == 16U) {
			return prefixes[i].plen;
		}
		if (rd2 != NULL) {
			return 0;
		}
	}
	return 0;
}

isc_result_t
dns_dns64_findprefix(dns_rdataset_t *rdataset, isc_netprefix_t *prefix,
		     size_t *len) {
	dns_rdataset_t outer, inner;
	unsigned int oplen, iplen;
	size_t count = 0;
	struct in6_addr ina6;

	REQUIRE(prefix != NULL && len != NULL && *len != 0U);
	REQUIRE(rdataset != NULL && rdataset->type == dns_rdatatype_aaaa);

	dns_rdataset_init(&outer);
	dns_rdataset_init(&inner);
	dns_rdataset_clone(rdataset, &outer);
	dns_rdataset_clone(rdataset, &inner);

	DNS_RDATASET_FOREACH(&outer) {
		dns_rdata_t rd1 = DNS_RDATA_INIT;
		dns_rdataset_current(&outer, &rd1);
		oplen = 0;
	resume:
		/* Look for a 192.0.0.170 match. */
		oplen = search(&rd1, NULL, oplen);
		if (oplen == 0) {
			continue;
		}

		/* Look for the 192.0.0.171 match. */
		bool matched = false;
		DNS_RDATASET_FOREACH(&inner) {
			dns_rdata_t rd2 = DNS_RDATA_INIT;

			dns_rdataset_current(&inner, &rd2);
			iplen = search(&rd2, &rd1, oplen);
			if (iplen != 0) {
				matched = true;
				INSIST(iplen == oplen);
				if (count >= *len) {
					count++;
					break;
				}

				/* We have a prefix. */
				memset(ina6.s6_addr, 0, sizeof(ina6.s6_addr));
				memmove(ina6.s6_addr, rd1.data, oplen / 8);
				isc_netaddr_fromin6(&prefix[count].addr, &ina6);
				prefix[count].prefixlen = oplen;
				count++;
			}
		}
		/* Didn't find a match look for a different prefix length. */
		if (!matched) {
			goto resume;
		}
	}
	if (count == 0U) {
		return ISC_R_NOTFOUND;
	}
	if (count > *len) {
		*len = count;
		return ISC_R_NOSPACE;
	}
	*len = count;
	return ISC_R_SUCCESS;
}

isc_result_t
dns_dns64_apply(isc_mem_t *mctx, dns_dns64list_t dns64s, unsigned int count,
		dns_message_t *message, dns_aclenv_t *env, isc_sockaddr_t *peer,
		dns_name_t *reqsigner, unsigned int flags, dns_rdataset_t *a,
		dns_rdataset_t **aaaap) {
	isc_result_t result;
	dns_rdatalist_t *aaaalist = NULL;
	isc_buffer_t *buffer = NULL;
	isc_netaddr_t netaddr;

	REQUIRE(aaaap != NULL && *aaaap == NULL);
	REQUIRE(a->type == dns_rdatatype_a);

	isc_netaddr_fromsockaddr(&netaddr, peer);

	isc_buffer_allocate(mctx, &buffer, count * 16 * dns_rdataset_count(a));

	dns_message_gettemprdatalist(message, &aaaalist);
	aaaalist->rdclass = dns_rdataclass_in;
	aaaalist->type = dns_rdatatype_aaaa;

	DNS_RDATASET_FOREACH(a) {
		dns_rdata_t rdata = DNS_RDATA_INIT;
		dns_rdataset_current(a, &rdata);

		ISC_LIST_FOREACH(dns64s, dns64, link) {
			dns_rdata_t *dns64_rdata = NULL;
			isc_region_t r;

			isc_buffer_availableregion(buffer, &r);
			INSIST(r.length >= 16);
			result = dns_dns64_aaaafroma(dns64, &netaddr, reqsigner,
						     env, flags, rdata.data,
						     r.base);
			if (result != ISC_R_SUCCESS) {
				continue;
			}
			isc_buffer_add(buffer, 16);
			isc_buffer_remainingregion(buffer, &r);
			isc_buffer_forward(buffer, 16);
			dns_message_gettemprdata(message, &dns64_rdata);
			dns_rdata_fromregion(dns64_rdata, dns_rdataclass_in,
					     dns_rdatatype_aaaa, &r);
			ISC_LIST_APPEND(aaaalist->rdata, dns64_rdata, link);
		}
	}

	if (!ISC_LIST_EMPTY(aaaalist->rdata)) {
		dns_rdataset_t *aaaa = NULL;
		dns_message_gettemprdataset(message, &aaaa);
		dns_rdatalist_tordataset(aaaalist, aaaa);
		dns_message_takebuffer(message, &buffer);
		aaaa->trust = a->trust;
		*aaaap = aaaa;
		return ISC_R_SUCCESS;
	}

	/* No applicable dns64; free the resources */
	isc_buffer_free(&buffer);
	ISC_LIST_FOREACH(aaaalist->rdata, rdata, link) {
		ISC_LIST_UNLINK(aaaalist->rdata, rdata, link);
		dns_message_puttemprdata(message, &rdata);
	}
	dns_message_puttemprdatalist(message, &aaaalist);

	return ISC_R_NOMORE;
}
