/*
* treewalk.c - written by ale in milano on 27 May 2024
* Look up a domain and its parents as described in DMARCbis.

Copyright (C) 2024 Alessandro Vesely

This file is part of zdkimfilter

zdkimfilter is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

zdkimfilter 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 General Public License for more details.

You should have received a copy of the GNU General Public License version 3
along with zdkimfilter.  If not, see <http://www.gnu.org/licenses/>.

Additional permission under GNU GPLv3 section 7:

If you modify zdkimfilter, or any covered part of it, by linking or combining
it with OpenSSL, OpenDKIM, Sendmail, or any software developed by The Trusted
Domain Project or Sendmail Inc., containing parts covered by the applicable
licence, the licensor of zdkimfilter grants you additional permission to convey
the resulting work.
*/
#define _GNU_SOURCE
#include <config.h>
#if !ZDKIMFILTER_DEBUG
#define NDEBUG
#endif
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <ctype.h>
#include <syslog.h>
#include <poll.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>
#include <resolv.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <arpa/nameser.h>
#include <netinet/in.h>

#include "treewalk.h"
#include "util.h"
#include <assert.h>

static logfun_t do_report = &stderrlog;

void clear_treewalk(tree_walk *tw)
{
	if (tw)
	{
		for (unsigned n = 0; n < tw->ndomains; ++n)
			free(tw->dd[n].dr.rua);
		free(tw->free_domain);
		free(tw->different_od);
		free(tw);
	}
}

static tree_walk *get_subdomains(char const *domain, int verbose)
/*
* domain must be a valid, normalized utf-8 domain name. 
*/
{
	char const *d = domain, *dar[MAX_SUBDOMAINS];
	unsigned ndom = 0;
	for(;;)
	{
		char const *s = d;
		int ch;
		while ((ch = (unsigned char)*d) != 0 && ch != '.')
		{
			if (ch & 0x80)
			{
				int len = utf8length(ch);
				if (len < 1 && verbose >= 8)
				{
					do_report(LOG_ERR, "DNS: invalid domain: %s", domain);
					return NULL;
				}
				d += len;
			}
			else
				d += 1;
		}

		dar[ndom] = s;
		if (ndom >= MAX_SUBDOMAINS - 1)
			memmove(&dar[1], &dar[2], (MAX_SUBDOMAINS - 2)*sizeof dar[0]);
		else
			++ndom;

		if (ch == '.')
			++d;
		else
		{
			assert(ch == 0);
			break;
		}
	}

	if (ndom == 0)
	{
		do_report(LOG_ERR, "DNS: NULL domain: %s", domain);
		return NULL;
	}

	size_t const size = sizeof(tree_walk) + ndom*sizeof(domain_data);
	tree_walk *tw = malloc(size);
	if (tw)
	{
		memset(tw, 0, size);
		tw->ndomains = ndom;
		for (unsigned i = 0; i < ndom; ++i)
			tw->dd[i].domain = dar[i];
	}
	return tw;
}

typedef union sockaddr_union
{
	struct sockaddr sa;
	struct sockaddr_in s4;
	struct sockaddr_in6 s6;
} sockaddr_union;

static int
already_present(uint16_t id, uint16_t const *queries, unsigned q)
{
	if (id == 0)
		return 1;

	for (unsigned c = 0; c < q; ++c)
		if (id == queries[c])
			return 1;

	return 0;
}

/*
* copied from res_mkquery.c
*/
static int res_nopt(
	int n0,             /* current offset in buffer */
	unsigned char *buf, /* buffer to put query */
	int buflen,          /* size of buffer */
	int anslen)         /* UDP answer buffer size */
{
	HEADER *hp;
	unsigned char *cp, *ep;
	uint16_t flags = 0;

	hp = (HEADER *)(void *)buf;
	cp = buf + n0;
	ep = buf + buflen;
	if ((ep - cp) < 1 + RRFIXEDSZ)
		return (-1);
	*cp++ = 0;              /* "." */
	ns_put16(ns_t_opt, cp); /* TYPE */
	cp += INT16SZ;
	if (anslen > 0xffff)
		anslen = 0xffff;
	ns_put16(anslen, cp);   /* CLASS = UDP payload size */
	cp += INT16SZ;
	*cp++ = NOERROR;        /* extended RCODE */
	*cp++ = 0;              /* EDNS version */

	ns_put16(flags, cp);
	cp += INT16SZ;
	ns_put16(0, cp);        /* RDLEN */
	cp += INT16SZ;

	hp->arcount = htons(ntohs(hp->arcount) + 1);
	return (cp - buf);
}

static int
send_txt_query(resolver_state *rs, int fd, char const *dname,
	uint16_t *queries, unsigned q)
/*
* Encode and send the query.  Save the id in the queries array.
* The id's are saved in network order, although comparison and
* increments are done in host order.  Return 1 on success.
*/
{
#if defined TEST_RESOLV
	if (isatty(fileno(stdout)))
	{
		printf("sending query: %s\n", dname);
	}
#endif

	if (rs == NULL || rs->statep == NULL)
	{
		do_report(LOG_CRIT, "DNS: init not done");
		return 0;
	}

	ip_u *server = &rs->servers[rs->cur_ns];
	sockaddr_union u;
	memset(&u, 0, sizeof u);

	socklen_t addrlen;
	if (server->ip == 4)
	{
		addrlen = sizeof u.s4;
		u.s4.sin_family = AF_INET;
		u.s4.sin_port = htons(NS_DEFAULTPORT);
		memcpy(&u.s4.sin_addr, server->u.ipv4, NS_INADDRSZ);
	}
	else
	{
		addrlen = sizeof u.s6;
		u.s6.sin6_family = AF_INET6;
		u.s6.sin6_port = htons(NS_DEFAULTPORT);
		memcpy(&u.s6.sin6_addr, server->u.ipv6, NS_IN6ADDRSZ);
	}

#ifdef HAVE_C_VARARRAYS
	unsigned const dlen = strlen(dname);
	// res_nmkquery uses more buffer than the size it returns
	unsigned size = NS_HFIXEDSZ + NS_QFIXEDSZ + 32 + dlen + 20;
	union
	{
		uint16_t id;
		unsigned char buf[size];
	} w;
#else
	union
	{
		uint16_t id;
		unsigned char buf[1024];
	} w;
	unsigned size = sizeof w;
#endif

	memset(&w, 0, size);
	int len = res_nmkquery(rs->statep,
		ns_o_query, dname, ns_c_in, ns_t_txt,
		NULL, 0, NULL, w.buf, size);

	if (len > 0) // add opt record for edns0
		len = res_nopt(len, w.buf, size, 1200);
	if (len < 0 && rs->parm->verbose >= 8)
	{
		do_report(LOG_ERR, "DNS: res_nmkquery failed: %s", strerror(errno));
		return 0;
	}




	while (already_present(w.id, queries, q))
		w.id += 1;

	queries[q] = w.id;
	do
	{
		int sent = sendto(fd, w.buf, len, 0, &u.sa, addrlen);
		if (sent < 0)
		{
			if (errno == EINTR || errno == EAGAIN)
				continue;

			if (rs->parm->verbose >= 8)
				do_report(LOG_ERR, "DNS: sendto failed: %s", strerror(errno));
			return 0;
		}

		len -= sent;
	} while (len > 0);

	return 1;
}

//// dmarc

typedef struct tag_value
{
	char *tag;
	char *value;
} tag_value;

static char *next_tag(char *buf, tag_value *tv)
/*
* buf points to a tag-list, grammar in Section 3.2 of RFC 6376.
* Return NULL on invalid grammar, otherwise the pointer to the next
* tag+value pair.
*/
{
	assert(buf);
	assert(tv);

	char *p = buf;
	while (isspace((unsigned char)*p))
		++p;

	int ch;
	tv->tag = p; // tag-name
	while (isalnum(ch = (unsigned char)*p) || ch == '_')
		*p++ = tolower(ch);

	if (ch != '=' && !isspace(ch))
		return NULL;

	*p++ = 0;
	if (ch != '=')
	{
		p = skip_fws(p);
		if (p == NULL || *p != '=')
			return NULL;

		++p;
	}

	if ((p = tv->value = skip_fws(p)) == NULL)
		return NULL;

	char *s = NULL;

	// value: EXCLAMATION to TILDE except SEMICOLON
	// whitespace allowed except at begin/end.
	while (((ch = *(unsigned char*)p) >= 0x21 &&
		ch <= 0x7e && ch != ';') || isspace(ch))
	{
		if (isspace(ch))
		{
			if (s == NULL)
				s = p;
		}
		else
			s = NULL;
		++p;
	}

	if (s)
		*s = 0;

	if (ch == 0)
		return p;

	if (ch == ';')
	// consume trailing white space after tag
	{
		*p++ = 0;
		while (isspace(*(unsigned char*)p))
			++p;

		return p;
	}

	// invalid charcter in value
	return NULL;
}

static int none_quarantine_reject(char const *p)
{
	static char const *nqr[3] = {"none", "quarantine", "reject"};
	for (int i = 0; i < 3; ++i)
		if (strcasecmp(p, nqr[i]) == 0)
			return nqr[i][0];

	return 0;
}

static inline int relax_strict(char const *p)
{
	int ch = tolower(*(unsigned char*)p);
	if (p[1] == 0 && (ch == 'r' || ch == 's'))
		return ch;

	return 0;
}

static const char rua_sentinel[] = ",z:;";

static inline char const *nqr_to_string(int p)
{
	return p == 'r'? "reject": p == 'q'? "quarantine": "none";
}

int check_remove_sentinel(char *rua)
// 0 if ok
{
	if (rua)
	{
		size_t len = strlen(rua);
		if (len >= sizeof rua_sentinel)
		{
			size_t off = len - sizeof rua_sentinel + 1;
			if (strcmp(rua + off, rua_sentinel) == 0)
			{
				rua[off] = 0;
				return 0;
			}
		}
	}

	return -1;
}

static inline int is_last(char *p)
{
	int const ch = (unsigned char)p[1];
	return ch == 0 || ch == ',' || isspace(ch);
}

char *adjust_rua(char **ruain, char **badout)
/*
* ruain must point to a heap-allocated (presumably by parse_dmarc) string
* having rua_sentinel.  The function removes "mailto:" and any spaces, moving
* unsupported URI to badout --it must be either NULL or a pointer initialized
* to NULL, in order for the caller to know if it was set.
*
* The value pointed by ruain is set to NULL and possibly freed.  The list of
* addresses is returned.  Both *badout and the returned value are on the heap.
*/
{
	assert(ruain);
	assert(*ruain);

	char *rua = *ruain;
	*ruain = NULL;
	if (check_remove_sentinel(rua))
	{
		if (badout)
			*badout = rua; // log it
		else
			free(rua);
		return NULL;
	}

	size_t len = strlen(rua), glen = 0, blen = 0;
	char good[len+1], buf[len+1], bad[len+1];

	char *start = rua, *p = start, *out = &buf[0];
	bool seen_colon = false, seen_at = false, is_good = true;
	for (;;)
	{
		int ch = *(unsigned char*)p;

		/*
		* atext are Printable US-ASCII characters not including specials.
		* Used for atoms:
		*
		* ALPHA / DIGIT / "!#$%&'*+-/=?^_`{|}~"
		*
		* specials are those excluded: "()<>[]:;@\,." and DQUOTE.
		*/
		// rfc5322 atext:             kept             removed
		if (ch != 0 &&  //            "!#%+-/=^_{}~", "$&'*?`|"
			(isalnum(ch) || strchr(".@:!#%+-/=^_{}~", ch) != NULL))
		{
			switch (ch)
			{
				case ':':
					if (seen_colon || seen_at) is_good = false;
					seen_colon = true;
					if (p - start != 6 ||
						strincmp(&buf[0], "mailto", 6) != 0 || is_last(p) ||
						strchr("@,", (unsigned char)p[1]) != NULL)
							is_good = false;
					break;

				case '@':
					if (seen_at || !seen_colon) is_good = false;
					seen_at = true;
					if (is_last(p) || !isalnum((unsigned char)p[1])) is_good = false;
					break;

				case '.':
					if (seen_at && (is_last(p) || p[1] == '.')) is_good = false;
					break;

				// Exclamation point is not considered a size limit any more
				case '!':
					if (!seen_at || !isdigit((unsigned char)p[1])) is_good = false;
					break;

				default:
					if (seen_at && ch != '-' && !isalnum(ch)) is_good = false;
					if (seen_colon == seen_at)
						ch = tolower(ch);
					break;
			}
			*out++ = ch;
			++p;
			continue;
		}
		else if (seen_colon && (ch & 0x80) != 0)
		// cannot happen (yet), by tag=value definition.
		{
			int len = utf8length(ch);
			if (len > 0)
				while (--len > 0 &&
					((ch = *(unsigned const char*)++p) & 0xc0) == 0x80)
						*out++ = ch;
			if (len == 0)
				continue;
		}

		if (isspace(ch))
		{
			++p;
			while (isspace(ch = *(unsigned char*)p))
				++p;

			if (out == &buf[0])
			{
				start = p;
				continue;
			}

			if (ch != 0 && ch != ',')
				*out++ = ' '; // show a space to explain why is bad
		}

		if (ch == 0 || ch == ',')
		{
			size_t l = out - &buf[0];
			assert(l <= len);
			if (is_good && seen_colon && seen_at)
			{
				assert(l > 7);

				l -= 7; // "mailto:"
				if (glen > 0 && good[glen-1] != ',')
					good[glen++] = ',';
				memcpy(&good[glen], &buf[7], l);
				glen += l;
			}
			else if (l > 0)
			{
				if (blen > 0 && bad[blen-1] != ',')
					bad[blen++] = ',';
				memcpy(&bad[blen], &buf[0], l);
				blen += l;
			}
			assert(glen <= len);
			assert(blen <= len);

			if (ch == 0)
				break;

			is_good = true;
			seen_at = seen_colon = false;
			out = &buf[0];
			start = ++p;
			continue;
		}

		is_good = false;
		*out++ = ch;
		++p;
	}

	if (blen && badout && (*badout = malloc(blen + 1)) != NULL)
	{
		memcpy(*badout, &bad[0], blen);
		(*badout)[blen] = 0;
	}

	p = NULL;
	if (glen > 0)
	{
		if (glen <= len)
			p = rua; // rua_sentinel was there before getting len
		else
		{
			free(rua);
			p = malloc(glen + sizeof rua_sentinel);
			assert(0);
		}

		if (p)
		{
			memcpy(p, &good[0], glen);
			memcpy(p + glen, rua_sentinel, sizeof rua_sentinel);
		}
	}
	else
		free(rua);

	return p;
}

char* write_dmarc_rec(dmarc_rec const *dmarc, int all)
// writes only some tags; returns strdup'd value
{
	char buf[80];
	unsigned len = 0;

	if (dmarc->opt_adkim || all)
		len = snprintf(buf, sizeof buf, "adkim=%c; ", dmarc->adkim);

	if ((dmarc->opt_aspf || all) && len < sizeof buf)
		len += snprintf(&buf[len], sizeof buf - len,
			"aspf=%c; ", dmarc->aspf);

	if ((dmarc->opt_p || all) && len < sizeof buf)
		len += snprintf(&buf[len], sizeof buf - len,
			"p=%s; ", nqr_to_string(dmarc->p));

	if ((dmarc->opt_sp || all) && len < sizeof buf)
		len += snprintf(&buf[len], sizeof buf - len,
			"sp=%s; ", nqr_to_string(dmarc->sp));

	if ((dmarc->opt_pct || all) && len < sizeof buf)
		len += snprintf(&buf[len], sizeof buf - len,
			"pct=%d; ", dmarc->pct);

	if (len + sizeof rua_sentinel < sizeof buf)
		strcat(&buf[len], rua_sentinel);
	len += sizeof rua_sentinel - 1;

	return len < sizeof buf? strdup(buf): NULL;
}

int parse_dmarc(char *record, dmarc_rec *dmarc)
/*
* Check the record (discard ruf=, fo=, which don't play on reception).
* Return 0 for non-DMARC record, -1 for garbled text, or 1 if dmarc_rec
* filled ok.
*/
{
	tag_value tv = {NULL, NULL};
	char *p = next_tag(record, &tv);

	if (p == NULL || strcmp(tv.tag, "v") != 0 || strcmp(tv.value, "DMARC1") != 0)
		return 0;

	// default
	memset(dmarc, 0, sizeof *dmarc);
	dmarc->adkim = dmarc->aspf = 'r';
	dmarc->pct = 100;
	dmarc->t = 'n';
	dmarc->psd = 'u';
	dmarc->found = 1;

	if (*p == 0) // no other tag (e.g. _report._dmarc
		return 1;

	while ((p = next_tag(p, &tv)) != NULL)
	{
		if (strcmp(tv.tag, "p") == 0)
		{
			int p = none_quarantine_reject(tv.value);
			if (p)
			{
				dmarc->p = p;
				dmarc->opt_p = dmarc->opt_p? 2: 1;
			}
		}
		else if (strcmp(tv.tag, "pct") == 0)
		{
			char *t = NULL;
			unsigned long pct = strtoul(tv.value, &t, 10);
			if (t && *t == 0 && pct <= 100)
			{
				dmarc->pct = (unsigned char) pct;
				dmarc->opt_pct = dmarc->opt_pct? 2: 1;
			}
		}
		else if (strcmp(tv.tag, "sp") == 0)
		{
			int sp = none_quarantine_reject(tv.value);
			if (sp)
			{
				dmarc->sp = sp;
				dmarc->opt_sp = dmarc->opt_sp? 2: 1;
			}
		}
		else if (strcmp(tv.tag, "np") == 0)
		{
			int np = none_quarantine_reject(tv.value);
			if (np)
			{
				dmarc->np = np;
				dmarc->opt_np = dmarc->opt_np? 2: 1;
			}
		}
		else if (strcmp(tv.tag, "adkim") == 0)
		{
			int a = relax_strict(tv.value);
			if (a)
			{
				dmarc->adkim = a;
				dmarc->opt_adkim = dmarc->opt_adkim? 2: 1;
			}
		}
		else if (strcmp(tv.tag, "aspf") == 0)
		{
			int a = relax_strict(tv.value);
			if (a)
			{
				dmarc->aspf = a;
				dmarc->opt_aspf = dmarc->opt_aspf? 2: 1;
			}
		}
		else if (strcmp(tv.tag, "psd") == 0)
		{
			if (tv.value[1] == 0 && strchr("ynuYNU", tv.value[0]))
			{
				dmarc->psd = tolower(tv.value[0]);
				dmarc->opt_psd = dmarc->opt_psd? 2: 1;
			}
		}
		else if (strcmp(tv.tag, "rua") == 0)
		{
			if (dmarc->rua == NULL)
			{
				size_t len = strlen(tv.value) + sizeof rua_sentinel;
				if ((dmarc->rua = malloc(len)) == NULL)
					return -1;
				strcat(strcpy(dmarc->rua, tv.value), rua_sentinel);
			}
		}

		if (*p == 0)
			return 1;
	}

	return -1;
}

static int
read_one_dmarc_record(resolver_state *rs, int fd, tree_walk *tw, uint16_t *queries)
/*
* Return -1 for DNS error, to try next server; otherwise 0 if the
* packet did not match the query, possibly the response to another query
* or a dns poisoning attack, to still wait for the response; return 1
* when the correct response is received, and set tw accordingly.
*/
{
	unsigned available;
#if defined SO_NREAD
	socklen_t optlen = sizeof(available);
	int err = getsockopt(fd, SOL_SOCKET, SO_NREAD, &available, &optlen);
#else
	int err = ioctl(fd, FIONREAD, &available);
#endif
	if (err < 0 || available < NS_HFIXEDSZ)
	{
		if (rs->parm->verbose >= 8)
			do_report(LOG_CRIT, "DNS: cannot get size of packet: %s",
				strerror(errno));
		return -1;
	}

	sockaddr_union u;
	memset(&u, 0, sizeof u);

	socklen_t addrlen = sizeof u;

	union dns_buffer
	{
#ifdef HAVE_C_VARARRAYS
		unsigned char answer[available];
#else
		// Way too large, but RFC 8085 says this is the max UDP payload
		unsigned char answer[65527];
#endif
		HEADER h;
	} buf;

	ssize_t size = recvfrom(fd, &buf, available, 0, &u.sa, &addrlen);
	if (size < 0)
	{
		if (rs->parm->verbose >= 8)
			do_report(LOG_ERR, "DNS: recvfrom failure: %s", strerror(errno));
		return -1;
	}

	ip_u *server = &rs->servers[rs->cur_ns];
#if defined TEST_RESOLV
	if (isatty(fileno(stdout)))
	{
		char recv[INET6_ADDRSTRLEN];
		printf("recvfrom server[%d]: %s:%d %zd bytes\n", rs->cur_ns,
			inet_ntop(u.sa.sa_family,
				u.sa.sa_family == AF_INET? (void*)&u.s4.sin_addr: (void*)&u.s6.sin6_addr,
				recv, sizeof recv),
			ntohs(*(in_port_t*)&u.sa.sa_data[0]), size);
	}
#endif

	if ((addrlen != sizeof u.s4 && addrlen != sizeof u.s6) ||
		memcmp(server->u.ip_data, &u.sa.sa_data[addrlen == sizeof u.s4? 2: 6],
			addrlen == sizeof u.s4? 4: 16) != 0 ||
		u.sa.sa_data[0] != 0 || u.sa.sa_data[1] != 53)
	{
		char recv[INET6_ADDRSTRLEN];
		char serv[INET6_ADDRSTRLEN];
		if (rs->parm->verbose >= 8)
			do_report(LOG_ERR, "DNS: mismatch: received from %s:%d not %s:53",
				inet_ntop(u.sa.sa_family,
					u.sa.sa_family == AF_INET? (void*)&u.s4.sin_addr: (void*)&u.s6.sin6_addr,
					recv, sizeof recv), ntohs(*(in_port_t*)&u.sa.sa_data[0]),
				inet_ntop(server->ip == 4? AF_INET: AF_INET6,
					server->ip == 4? (void*)&server->u.ipv4: (void*)&server->u.ipv6,
					serv, sizeof serv));
		return 0;
	}

	if (size < HFIXEDSZ ||
		(unsigned)size > sizeof buf ||
		ntohs(buf.h.qdcount) != 1)
	{
		if (rs->parm->verbose >= 8)
		{
			char reason[256];
			int offs = snprintf(reason, sizeof reason, "pk size=%zu", size);
			if (size < HFIXEDSZ)
				offs += snprintf(reason + offs, sizeof reason - offs,
					" < %u", HFIXEDSZ);
			else if ((unsigned)size > sizeof buf)
				offs += snprintf(reason + offs, sizeof reason - offs,
					" < %zu", sizeof buf);
			if (ntohs(buf.h.qdcount) != 1)
				offs += snprintf(reason + offs, sizeof reason - offs,
					", %d questions", buf.h.qdcount);

			do_report(LOG_ERR, "DNS: unusable response: %s", reason);
		}
		return -1;
	}

	// queries are already in network order
	unsigned q_ndx;
	for (q_ndx = 0; q_ndx < tw->ndomains; ++q_ndx)
		if (queries[q_ndx] == buf.h.id)
			break;

	if (q_ndx >= tw->ndomains)
	{
		if (rs->parm->verbose >= 8)
		{
			if (tw->attempt == 0)
			{
				do_report(LOG_DEBUG,
					"DNS: stray packet: response matches none of %d queries",
					tw->ndomains);
			}
			else
			{
				do_report(LOG_DEBUG,
					"DNS: unmatched packet: probably timed out response of prior attempt");
			}
		}
		return 0;
	}

	if (buf.h.rcode == ns_r_nxdomain ||
		(buf.h.rcode == ns_r_noerror && ntohs(buf.h.ancount) < 1))
	{
		tw->dd[q_ndx].status = dmarc_none;
		return 1;
	}

	unsigned char *cp = &buf.answer[HFIXEDSZ];
	unsigned char *const eom = &buf.answer[size];

	/*
	* question:
	* QNAME, dn_expand returns the length of the compressed name;
	* QTYPE, 16 bit;
	* QCLASS, 16 bit
	*/
	unsigned dlen = strlen(tw->dd[q_ndx].domain);
	char expand[NS_MAXDNAME + 1];
	int n = dn_expand(buf.answer, eom, cp, expand, sizeof expand); //name
	if (n < 0 || (unsigned)n > sizeof expand ||
		strncasecmp(expand, "_dmarc.", 7) != 0 ||
		strncasecmp(&expand[7], tw->dd[q_ndx].domain, dlen) != 0)
	{
		if (rs->parm->verbose >= 8)
			do_report(LOG_ERR,
				"DNS: mismatch domain: %.*s instead of _dmarc.%s",
				n, expand, tw->dd[q_ndx].domain);
		return -1;
	}

	cp += n;
	if (cp + 2*NS_INT16SZ > eom ||
		ns_get16(cp) != ns_t_txt ||
			ns_get16(cp + INT16SZ) != ns_c_in)
	{
		if (rs->parm->verbose >= 8)
			do_report(LOG_ERR,
				"DNS: mismatch question: size %d, type %d, class %d",
				n, ns_get16(cp), ns_get16(cp + INT16SZ));
		return -1;
	}

	/*
	* answers:
	* NAME, should be the same name as QNAME;
	* TYPE, 16 bit, can be QTYPE, or CNAME
	* CLASS, 16 bit;
	* TTL, 32 bit, not checked;
	* RDLENGTH, 16 bit, size of RDATA;
	* RDATA
	*/
	cp += 2*INT16SZ;
	unsigned ancount = ntohs(buf.h.ancount);
	int found = 0, garbled = 0;
	while (ancount --> 0)
	{
		n = dn_expand(buf.answer, eom, cp, expand, sizeof expand);
		if (n < 0 || cp + n + 3*INT16SZ + INT32SZ > eom)
		{
			if (rs->parm->verbose >= 8)
			{
				if (n < 0)
					do_report(LOG_ERR,
						"DNS: dn_expand error: %s", strerror(errno));
				else
					do_report(LOG_ERR, "DNS: inconsistent length%s",
						buf.h.tc? ": truncated": "");
			}
			return -3;
		}

		uint16_t type = ns_get16(cp + n);
		uint16_t class = ns_get16(cp + n + INT16SZ);
		uint16_t rdlength = ns_get16(cp + n + 2*INT16SZ + INT32SZ); // (skip ttl)

		cp += n + 3*INT16SZ + INT32SZ;
		if (type != ns_t_txt || class != ns_c_in)
		{
			cp += rdlength;
			continue;
		}

		char txt[rdlength];
		char *p = &txt[0];
		char *const end = p + rdlength;

		// TXT-DATA consists of one or more <character-string>s.
		// <character-string> is a single length octet followed by that number
		// of characters.  RFC 1035

		while (rdlength > 0 && p < end)
		{
			unsigned sl = *(unsigned char*)cp++;
			rdlength -= 1;
			if (p + sl >= end || sl > rdlength)
				break;

			memcpy(p, cp, sl);
			p += sl;
			cp += sl;
			rdlength -= sl;
		}

		if (rdlength == 0 && p < end)
		{
			*p = 0;
#if defined TEST_RESOLV
			if (isatty(fileno(stdout)))
				printf("_dmarc.%s -> %s\n", tw->dd[q_ndx].domain, txt);
#endif
			dmarc_rec *dr = &tw->dd[q_ndx].dr;
			int rtc = parse_dmarc(txt, dr);
			if (rtc == 0)
				continue; // not a DMARC record

			found += 1;
			if (dr->p == 0)
				dr->p = 'n';
			if (dr->sp == 0)
				dr->sp = dr->p;
			if (dr->np == 0)
				dr->np = dr->sp;
			if (rtc < 0)
				garbled += 1;
		}
	}

	if (found == 1)
	{
		tw->nrecords += 1;
		if (garbled)
			tw->dd[q_ndx].status = dmarc_garbled;
		else
			tw->dd[q_ndx].status = dmarc_ok;
	}
	else
	{
		if (buf.h.tc)
			do_report(LOG_WARNING, "DNS: truncated packet, possible miss");
		tw->dd[q_ndx].status = dmarc_none;
	}

	return 1;
}

static const char dmarc_prefix[] = "_dmarc.";
static int do_resolve_tree(resolver_state *rs, tree_walk *tw)
/*
* Fill in tw with data from the DNS.  Return 0 if all well.
* On error, return -1, the error is already reported.
*/
{
	int fd = socket(rs->af, SOCK_DGRAM, 0);
	if (fd < 0)
	{
		do_report(LOG_CRIT, "DNS: cannot open IPv%d socket: %s",
			rs->af == AF_INET? 4: 6, strerror(errno));
		return -1;
	}

	unsigned tot_sent = 0, tot_recv = 0;

	unsigned const ndom = tw->ndomains;
	unsigned attempts = rs->attempts;

	unsigned const length = strlen(tw->dd[0].domain) + sizeof dmarc_prefix;
	while (attempts --> 0)
	{
		unsigned nservers = rs->nservers;
		for (unsigned i = 0; i < nservers; ++i)
		{
			rs->cur_ns = (i + rs->good_ns) % nservers;

			uint16_t queries[ndom];
			memset(queries, 0, sizeof queries);

			time_t current_time;
			time(&current_time);

			unsigned q;
			for (q = 0; q < ndom; ++q)
			{
				if (queries[q])
					continue;

				char qbuf[length];
				strcpy(qbuf, dmarc_prefix);
				strcpy(&qbuf[sizeof dmarc_prefix - 1], tw->dd[q].domain);
				if ((send_txt_query(rs, fd, qbuf, queries, q)) == 0)
				{
					break;  // how come it couldn't send?
				}
				++tot_sent;
			}

			if (q < ndom)
			{
				++tw->attempt;
				continue;  // try next server??
			}

			time_t final_time = current_time + rs->timeout;
			unsigned nreplies = ndom;

			while (nreplies > 0)
			{
				errno = 0;
				time(&current_time);
				int left = final_time - current_time; // should be < 30
				if (left <= 0)
					break;

				struct pollfd pfd = {0};
				pfd.fd = fd;
				pfd.events = POLLIN;

				int ready;
				while ((ready = poll(&pfd, 1, 1000*left)) < 0)
					if (errno != EINTR && errno != EAGAIN)
						break;

				if (ready == 1 && (pfd.revents & POLLIN))
				{
					int rtc = read_one_dmarc_record(rs, fd, tw, queries);
					if (rtc < 0)
						break;

					if (rtc > 0)
					{
						rs->good_ns = rs->cur_ns;
						nreplies -= 1;
						++tot_recv;
					}
					continue;
				}

				if (ready == 0) // timeout
				{
					if (rs->parm->verbose >= 8)
					{
						char recv[INET6_ADDRSTRLEN];
						do_report(LOG_NOTICE,
							"DNS: timeout server[%d]: %s ("
							"got %d/%d responses for %s)",
							rs->cur_ns,
							inet_ntop(rs->servers[rs->cur_ns].ip == 4? AF_INET: AF_INET6,
								rs->servers[rs->cur_ns].u.ip_data,
								recv, sizeof recv),
							tot_recv, ndom, tw->dd[0].domain);
					}
					break;
				}

				if (ready < 0 && rs->parm->verbose >= 8)
					do_report(LOG_ERR,
						"DNS: poll error: rtc=%d, revents=%s%s%s errno=%d: %s",
						ready,
						(pfd.revents & POLLIN)  ? "POLLIN "  : "",
						(pfd.revents & POLLHUP) ? "POLLHUP " : "",
						(pfd.revents & POLLERR) ? "POLLERR " : "",
						errno, strerror(errno));
				break;
			}

			++tw->attempt;
			if (nreplies == 0)
			{
				close(fd);
				return 0;
			}
		}
	}

	close(fd);
	do_report(LOG_ERR, "DNS: sent %d, recv %d for %s",
		tot_sent, tot_recv, tw->dd[0].domain);
	return -1;
}

static int
(*resolve_tree)(resolver_state *rs, tree_walk *tw) = &do_resolve_tree;

static int fake_resolve_tree(resolver_state *rs, tree_walk *tw)
/*
* Fill in tw with data from the KEYFILE.  Return 0 if all well.
* On error, return -1.
*
* POLICYFILE contains query| one char| value.
*/
{
	FILE *fp = fopen("KEYFILE", "r");
	if (fp == NULL)
		return -1;

	unsigned const ndom = tw->ndomains;
	char qbuf[NS_MAXDNAME + 1];
	char buf[NS_MAXDNAME + 1024]; // allow plenty of space to read KEYFILE

	for (unsigned q = 0; q < ndom; ++q)
	{
		strcpy(qbuf, dmarc_prefix);
		strcpy(&qbuf[sizeof dmarc_prefix - 1], tw->dd[q].domain);
		unsigned sublength = strlen(qbuf);
		rewind(fp);

		int found = 0, garbled = 0;
		while (!feof(fp) && !ferror(fp))
		{
			char *s = fgets(buf, sizeof buf, fp);
			if (s && strncmp(buf, qbuf, sublength) == 0)
			{
				char *end = strchr(buf, '\n');
				if (end)
					*end = 0;

				dmarc_rec *dr = &tw->dd[q].dr;
				int rtc = parse_dmarc(buf + sublength + 1, dr);
				if (rtc)
				{
					found += 1;
					if (dr->p == 0)
						dr->p = 'n';
					if (dr->sp == 0)
						dr->sp = dr->p;
					if (dr->np == 0)
						dr->np = dr->sp;
					if (rtc < 0)
						garbled += 1;
				}
			}
		}
		if (ferror(fp))
		{
			fclose(fp);
			return -1;
		}

		if (found == 1)
		{
			tw->nrecords += 1;
			if (garbled)
				tw->dd[q].status = dmarc_garbled;
			else
				tw->dd[q].status = dmarc_ok;
		}
		else
			tw->dd[q].status = dmarc_none;
	}

	fclose(fp);
	return 0;
	(void)rs;
}

static const char* explain_h_errno(void)
{
	switch(h_errno)
	{
		/* netdb.h: Possible values left in `h_errno'.  */
		case HOST_NOT_FOUND: return "HOST_NOT_FOUND: Authoritative Answer Host not found.";
		case TRY_AGAIN: return "TRY_AGAIN: Non-Authoritative Host not found, or SERVERFAIL.";
		case NO_RECOVERY: return "NO_RECOVERY: Non recoverable errors, FORMERR, REFUSED, NOTIMP.";
		case NO_DATA: return "NO_DATA: Valid name, no data record of requested type.";
		default: return "Unexpected h_errno.";
	}
}

static int do_check_domain_exists(resolver_state *rs, char const *domain)
/*
* Just check existence.  Return 0 if doesn't exist, 1 if does, -1 for
* error.
*/
{
	assert(rs && rs->statep);

	unsigned char buf[512];
	int rc = res_nquery(rs->statep, domain, ns_c_in, ns_t_a,
		buf, sizeof buf);

	if (rc < 0)
	{
		switch (h_errno)
		{
			case HOST_NOT_FOUND: // The specified host is unknown.
				return 0;

			case NO_DATA: // The requested name is valid but does not
				// have an IP address.  Another type of request to the
				// name server for this domain may return an answer.
				return 1;

			case NO_RECOVERY: // A nonrecoverable name server error occurred.
			case TRY_AGAIN: // A temporary error occurred on an authoritative name server.
			default:
				if (rs->parm->verbose >= 8)
					do_report(LOG_ERR,
						"DNS: h_errno=%d (%s) querying %s",
						h_errno, explain_h_errno(), domain);
				return -1;
		}
	}

	return 1;
}

static int do_domain_exists(resolver_state *rs, tree_walk *tw)
/*
* Check existence, possibly cached of the domain where tree walk started.
*  Return 0 if doesn't exist, 1 if does, -1 for error.
*/
{
	assert(rs && rs->statep);
	assert(tw);

	if (tw->top_domain_exists >= 0)
		return tw->top_domain_exists;

	return tw->top_domain_exists = check_domain_exists(rs, tw->dd[0].domain);
}

static int (*domain_exists)(resolver_state *, tree_walk *) = do_domain_exists;
static int (*my_check_domain_exists)(resolver_state *, char const *) = do_check_domain_exists;

// public function
int check_domain_exists(resolver_state *rs, char const *domain)
{
	return my_check_domain_exists(rs, domain);
}


static int fake_check_domain_exists(resolver_state *rs, char const *domain)
{
	FILE *fp = fopen("KEYFILE", "r");
	if (fp == NULL)
		return -1;

	unsigned length = strlen(domain);
	char buf[1024];

	while (!feof(fp) && !ferror(fp))
	{
		char *dom = fgets(buf, sizeof buf, fp);
		if (dom)
		{
			char *space = strchr(dom, ' ');
			if (space)
			{
				*space = 0;
				unsigned dom_length = space - dom;
				if (length <= dom_length)
				{
					unsigned diff = dom_length - length;
					if (strcmp(domain, dom + diff) == 0)
					{
						fclose(fp);
						return 1;
					}
				}
			}
		}
	}

	if (ferror(fp))
	{
		fclose(fp);
		return -1;
	}

	fclose(fp);
	return 0;

	(void)rs;
}

static int fake_domain_exists(resolver_state *rs, tree_walk *tw)
{
	assert(rs);
	assert(tw);

	if (tw->top_domain_exists >= 0)
		return tw->top_domain_exists;

	return tw->top_domain_exists = fake_check_domain_exists(rs, tw->dd[0].domain);
}

void set_treewalk_faked(void)
{
	resolve_tree = &fake_resolve_tree;
	domain_exists = &fake_domain_exists;
	my_check_domain_exists = &fake_check_domain_exists;
}


tree_walk *treewalk(resolver_state *rs, char *domain)
{

	tree_walk *tw = get_subdomains(domain, rs->parm->verbose);
	if (tw == NULL)
	{
		return NULL;
	}

	tw->top_domain_exists = -1;
	if (resolve_tree(rs, tw))
	{
		clear_treewalk(tw);
		return NULL;
	}

	unsigned nrecords = tw->nrecords;
	if (nrecords > 0)
	{
		unsigned last_ndx = 2*MAX_SUBDOMAINS, policy;
		if (tw->dd[0].status != dmarc_none)
			policy = 0;
		else
			policy = 2*MAX_SUBDOMAINS;

		char psd = 'u';
		unsigned ndomains = tw->ndomains;
		for (unsigned i = 0; i < ndomains; ++i)
		{
			if (tw->dd[i].status != dmarc_none)
			{
				psd = tw->dd[i].dr.psd;
				if (psd == 'n')
				{
					tw->org = i;
					break;
				}
				if (psd == 'y')
				{
#if 0  // see https://mailarchive.ietf.org/arch/msg/dmarc/WS8mjaCMORXCCo9JA4FgB4wKX_w
					if (last_ndx < MAX_SUBDOMAINS)
						tw->org = last_ndx;
					else
#endif
					{
						if (policy > MAX_SUBDOMAINS)
							policy = i;

						// org has no DMARC record
						if (i > 0)
							tw->org = i - 1;
						else
							tw->org = 0;
					}
					break;
				}
				last_ndx = i;
			}
		}
		if (psd == 'u')
			tw->org = last_ndx;

		if (policy < MAX_SUBDOMAINS)
			tw->policy = policy;
		else if (tw->dd[tw->org].status != dmarc_none)
			tw->policy = tw->org;

		dmarc_rec *dr = &tw->dd[tw->policy].dr;
		if (tw->policy == 0)
			tw->effective_p = dr->p;
		else
		{
			int exists;
			if (dr->sp == dr->np ||
				(exists = domain_exists(rs, tw)) == 1)
					tw->effective_p = dr->sp;
			else if (exists == 0)
				tw->effective_p = dr->np;
			else
			{
				clear_treewalk(tw);
				tw = NULL;
			}
		}
	}

	/*
	* In case domain is not malloc'ed, caller should set this to NULL
	* before calling clear_treewalk.
	*/
	if (tw)
		tw->free_domain = domain;

	return tw;
}

int top_domain_exists(resolver_state *rs, tree_walk *tw)
{
	return domain_exists(rs, tw);
}

static int check_ipv4_c_or_m(unsigned char ip[16])
{
	for (int i = 0; i < 10; ++i)
		if (ip[i] != 0)
			return 6;

	/*
	* IPv4-compatible and IPv4-mapped addresses (RFC 4291) 
	* are to be stored in the IPv4 database.
	*/
	if (ip[10] != ip[11] || (ip[10] != 0xffU && ip[10] != 0))
		return 6;

	if (ip[10] == 0 && ip[12] == 0 && ip[13] == 0 &&
		ip[14] == 0 && ip[15] == 0)
			return 0;

	memcpy(&ip[0], &ip[12], 4);
	return 4;
}

static void ipv4_to_ipv6(unsigned char ip[16])
{
	// 127.0.0.1 translated to ::ffff:127.0.0.1, not ::1
	// see https://stackoverflow.com/questions/49793630/is-ffff127-0-0-1-localhost/78602303
	memcpy(&ip[12], &ip[0], 4);
	memset(&ip[0], 0, 10);
	ip[10] = ip[11] = 0xffU;
}

static int my_inet_pton(char const *p, ip_u *u)
/*
* Like inet_pton, return 1 on success, 0 or -1 on error.
* Guess address family.
*/
{
	assert(p);
	assert(u);

	memset(u, 0, sizeof *u);
	if (strchr(p, ':') == NULL)
	{
		int rtc = inet_pton(AF_INET, p, u->u.ipv4);
		if (rtc == 1)
			u->ip = 4;
		return rtc;
	}

	int rtc = inet_pton(AF_INET6, p, u->u.ipv6);
	if (rtc <= 0)
		return rtc;

	u->ip = check_ipv4_c_or_m(u->u.ipv6);

	return rtc;
}

static void
check_options(char const *p, unsigned *timeout, unsigned *attempts)
{	//                       012345678
	char *q = strcasestr(p, "timeout:"), *t = 0;
	long l;
	/*
	* "The value for this option is silently capped to 30" is stated
	* in resolv.conf(5).  We need a value that, multiplied by 1000,
	* still fits an integer, for poll() argument.
	*/
	if (q && (l = strtoul(q+8, &t, 10)) > 0 && l < INT_MAX &&
		(isspace(*(unsigned char *)t) || *t == 0))
			*timeout = (unsigned)l;

	//                 0123456789
	q = strcasestr(p, "attempts:");
	if (q && (l = strtoul(q+9, &t, 10)) > 0 && l < 5 &&
		(isspace(*(unsigned char *)t) || *t == 0))
			*attempts = (unsigned)l;
}

void clear_resolv(resolver_state *rs)
{
	if (rs && rs->statep)
	{
		res_nclose(rs->statep);
		free(rs->statep);
	}

	free(rs);
}

resolver_state *init_resolv(parm_t *parm, char const *resolv_conf)
{
	res_state statep = calloc(1, sizeof *statep);
	if (statep)
	{
		if (res_ninit(statep) == 0)
		{
			statep->options &= ~RES_DNSRCH; // don't search local tree

#if defined RES_USE_EDNS0
			statep->options |= RES_USE_EDNS0; // extension 0
#endif
#if defined RES_TRUSTAD
			statep->options |= RES_TRUSTAD; // pass Authentic Data (AD) bit
#endif
#if defined RES_USE_DNSSEC
			statep->options &= ~RES_USE_DNSSEC; // Don't want to see it
#endif
		}
		else
		{
			free(statep);
			statep = NULL;
		}
	}

	if (statep == NULL)
	{
		do_report(LOG_CRIT, "DNS: res_ninit failed: %s", strerror(errno));
		return NULL;
	}

	int nns = 0;
	ip_u ns[MAX_NAMESERVERS];
	unsigned timeout = 5, attempts = 2;
	FILE *fp = resolv_conf? fopen(resolv_conf, "r"): NULL;
	if (fp)
	{
		char buf[1024], *p, *start;
		while ((p = fgets(buf, sizeof buf, fp)) != NULL)
		{
			int ch;
			for (start = p; (ch = *(unsigned char*)start) != 0; ++start)
				if (!isspace(ch))
					break;

			if (ch == '#' || ch == ';')
				continue;

			for (p = start; (ch = *(unsigned char*)p) != 0; ++p)
			{
				*p = tolower(ch);
				if (isspace(ch))
				{
					*p++ = 0;
					break;
				}
			}

			if (strcmp(start, "options") == 0)
			{
				check_options(p, &timeout, &attempts);
				continue;
			}

			if (strcmp(start, "nameserver"))
				continue;

			do // multiple addresses on a line?
			{
				while ((ch = *(unsigned char*)p) != 0 && isspace(ch))
					++p;

				char *q;

				for (q = p; (ch = *(unsigned char*)q) != 0 && !isspace(ch); ++q)
					continue;

				*q = 0;
				if (q > p && 
					nns < MAX_NAMESERVERS &&
					my_inet_pton(p, &ns[nns]) == 1)
						++nns;

				p = q + 1;
			} while (ch != 0);
		}
		fclose(fp);
	}

	if (nns == 0 && my_inet_pton("127.0.0.1", &ns[0]) == 1)
		nns = 1;

	char *more_opts = getenv("RES_OPTIONS");
	if (more_opts)
		check_options(more_opts, &timeout, &attempts);

	unsigned size = sizeof(resolver_state) + nns*sizeof(ip_u);
	resolver_state *rs = malloc(size);
	if (rs)
	{
		memset(rs, 0, size);

		int have_4 = 0;
		rs->statep = statep;
		rs->parm = parm;
		rs->timeout = timeout;
		rs->attempts = attempts;
		rs->nservers = nns;
		rs->af = AF_INET;
		for (int i = 0; i < nns; ++i)
		{
			rs->servers[i] = ns[i];
			if (ns[i].ip == 6)
				rs->af = AF_INET6;
			else
				have_4 = 1;
		}
		if (rs->af == AF_INET6 && have_4)
			for (int i = 0; i < nns; ++i)
				if (ns[i].ip == 4)
				{
					ipv4_to_ipv6(rs->servers[i].u.ip_data);
					rs->servers[i].ip = 6;
				}
	}

	return rs;
}

static int
is_relaxed_aligned(resolver_state *rs, tree_walk *tw, char const *domain)
/*
* For relaxed alignment, two domains are aligned iff they have the
* same org domain.  That is, our org domain must be a parent of the
* domain under test, and there must be no psd= determined boundary
* in between.
*/
{
	char const *org = tw->dd[tw->org].domain;
	unsigned orglen = strlen(org), dlen = strlen(domain);
	if (dlen < orglen)
		return 0; // not aligned

	char const *tail = domain + dlen - orglen;
	if (strcmp(org, tail))
		return 0;

	int rtc = 1;
	if (dlen > orglen)
	/*
	* Must check that domain has the same org domain as the author
	* domain in tw.  Since the tail matches, the only case org
	* domains can differ is if there is a psd= in some DMARC record
	* between domain and org.
	*/
	{
		tree_walk *twa = get_subdomains(domain, rs->parm->verbose);
		if (twa)
		{
			unsigned skip = tw->ndomains - tw->org;
			twa->ndomains -= skip;
			if (resolve_tree(rs, twa))
				rtc = -1;
			else if (twa->nrecords)
				for (unsigned u = 0; u < twa->ndomains; ++u)
				{
					char psd = twa->dd[u].dr.psd;
					if (psd == 'n' || psd == 'y')
					{
						rtc = 0;
						break;
					}
				}
			assert(twa->free_domain == NULL);
			clear_treewalk(twa);
		}
		else
			rtc = -1;
	}

	return rtc;
}

int is_subdomain(char const *domain, char const *org_domain)
{
	if (!domain || !org_domain)
		return 0;

	unsigned dlen = strlen(domain);
	unsigned olen = strlen(org_domain);

	return dlen >= olen && strincmp(domain + dlen - olen, org_domain, olen) == 0;
}

int
is_aligned(resolver_state *rs, tree_walk *tw, identifier_type id, char const *domain)
/*
* Domain names are expected to be normalized.  Return 1 if domain is
* aligned, 0 if not, -1 on error.
*
* Issue a warning if the result differs from PSL.
*/
{
	assert(id == identifier_dkim || id == identifier_spf);

	if (tw == NULL || tw->nrecords == 0)
		return 0;

	int align;
	if (id == identifier_dkim)
		align = tw->dd[tw->policy].dr.adkim;
	else
		align = tw->dd[tw->policy].dr.aspf;

	if (align == 's')
		return strcmp(domain, tw->dd[0].domain) == 0;

	int rtc = is_relaxed_aligned(rs, tw, domain);
	if (rtc == 0 && rs->parm->verbose >= 5 && tw->different_od &&
		is_subdomain(domain, tw->different_od))
			do_report(LOG_NOTICE,
				"Tree Walk from %s finds %s, while PSL had %s: "
				"%s identifier %s becomes not aligned this way",
				tw->dd[0].domain, tw->dd[tw->org].domain, tw->different_od,
				id == identifier_dkim? "DKIM": "SPF", domain);

	return rtc;
}

dmarc_rec *get_policy_record(tree_walk *tw)
{
	if (tw == NULL || tw->nrecords == 0)
		return NULL;
	return &tw->dd[tw->policy].dr;
}

#if defined TEST_RESOLV
// gcc -W -Wall -g -DTEST_RESOLV treewalk.c parm.o util.o ../libopendkim/dkim-mailparse.o -lidn2 -lresolv
int main(int argc, char *argv[])
{
	parm_t parm;
	memset(&parm, 0, sizeof parm);
	parm.verbose = 10;

	do_report = stderrlog;

	if (argc > 1)
	{
		int fake = 0;
		if (strcmp(argv[1], "-f") == 0)
		{
			fake = 1;
			set_treewalk_faked();
		}

		resolver_state *rs = init_resolv(&parm, fake? NULL: argv[1]);
		if (rs)
		{
			char ip[INET6_ADDRSTRLEN];
			if (fake == 0)
			{
				printf("timeout=%u, attempts=%u\n%u servers:\n",
					rs->timeout, rs->attempts, rs->nservers);
				for (unsigned u = 0; u < rs->nservers; ++u)
				{
					int af = rs->servers[u].ip;
					printf("%u:  %s\n", u,
						inet_ntop(af == 4? AF_INET: AF_INET6,
							af == 4? rs->servers[u].u.ipv4: rs->servers[u].u.ipv6,
							ip, sizeof ip));
				}
			}

			if (argc > 2)
			{
				tree_walk *tw = treewalk(rs, argv[2]);
				if (tw)
				{
					tw->free_domain = NULL;
					printf("%u domains, %u records:\n",
						tw->ndomains, tw->nrecords);
					for (unsigned u = 0; u < tw->ndomains; ++u)
						printf("%u: %s%s %s\n", u,
							u == tw->org? "ORG ": "",
							u == tw->policy? "POLICY ": "",
							tw->dd[u].domain);
					printf("effective p: %c\n",
						isprint((unsigned char)tw->effective_p)?
						tw->effective_p: '-');

					for (int i = 3; i < argc; ++i)
					{
						int isa = is_aligned(rs, tw, identifier_dkim, argv[i]);
						printf("%s IS%s aligned with %s\n",
							argv[i], isa? "": " NOT", argv[2]);
#if 0
						if (tw->dd[tw->policy].dr.aspf != tw->dd[tw->policy].dr.adkim)
						{
							isa = is_aligned(rs, tw, identifier_spf, argv[i]);
							printf("%s IS%s aligned with %s\n",
								argv[i], isa? "": " NOT", argv[2]);
						}
#endif
					}
					clear_treewalk(tw);
				}
			}
			clear_resolv(rs);
		}
	}

	return 0;
}
#endif
