/* Distributed Checksum Clearinghouse
 *
 * reject messages contain URLs that resolve to DNS blacklisted IP addresses
 *
 * Copyright (c) 2005 by Rhyolite Software
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND RHYOLITE SOFTWARE DISCLAIMS ALL
 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL RHYOLITE SOFTWARE
 * BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
 * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
 * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
 * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
 * SOFTWARE.
 *
 * Rhyolite Software DCC 1.2.74-1.17 $Revision$
 */

#include "ck.h"
#include "dcc_heap_debug.h"
#ifndef DCC_WIN32
#include <sys/wait.h>
#include <arpa/inet.h>
#endif
#ifdef HAVE_RESOLV_H
#include <resolv.h>
#endif
#ifdef HAVE_ARPA_NAMESER_H
#include <arpa/nameser.h>
#endif

#ifdef DCC_WIN32
#undef HELPERS
#undef MX_DNSBL
#else
#define HELPERS
#endif /* DCC_WIN32 */

/* need BIND 4 compatible bits and pieces for MX checking */
#if !defined(T_MX) || !defined(C_IN) || !defined(PACKETSZ)
#undef MX_DNSBL
#endif


DNSBL *dnsbls;

static u_char dnsbl_have_ipv6, dnsbl_have_ipv4;
static int dnsbl_debug;
static u_char some_flags;

#define MAX_MSG_SECS 1000
#ifndef RES_TIMEOUT
#define RES_TIMEOUT 3
#endif
static int msg_secs = 20;		/* total seconds/mail message */
static int url_secs = 5;		/* total seconds/host name */


static u_char threaded;

static const char *dnsbl_progpath;


#ifdef HELPERS
static DCC_SOCKU rslvr_su;
static SOCKET rslvr_soc = INVALID_SOCKET;
static int rslvr_stop_write_fd = -1;
static int rslvr_stop_read_fd = -1;
#define RSLVR_PAT "rslvr=%d,%d"
static int rslvr_sn;
static u_char rslvrs_running;
static int idle_rslvrs, total_rslvrs, rslvr_failures;
static int max_rslvrs;
static int rslvr_argc = 1;
static char const **rslvr_argv;
#endif /* HELPERS */

typedef struct {
    u_int	magic2;
    u_int	sn;
    DNSBL_HIT	type;
    DNSBL_DOM   dom;			/* target for logging */
    char	id[24];
    union {
	DNSBL_DOM   nm;
	struct in6_addr addr;
    } tgt;
    u_int	magic1;
} DNSBL_RSLVR_REQ;

typedef struct {
    u_int	magic1;
    u_int	sn;
    DNSBL_HIT	type;
    u_char	mx;
    DNSBL_DOM	probe;
    u_int	magic2;
} DNSBL_RSLVR_RESP;
#define RSLVR_MAGIC1 0xbeefdead
#define RSLVR_MAGIC2 0xdeadbeef


#ifdef HELPERS
static void rslvr_work(SOCKET, int) NRATTRIB;
#endif



/* add to the argv list for the helper processes */
#ifndef HELPERS
#define save_arg(entry)
#else
static void
save_arg(const char *entry)
{
	char const **new_arg;
	int i;

	new_arg = dcc_malloc(sizeof(char *) * (rslvr_argc+2+1));
	if (rslvr_argv) {
		for (i = 0; i < rslvr_argc; ++i)
			new_arg[i] = rslvr_argv[i];
		dcc_free(rslvr_argv);
	} else {
		new_arg[0] = dnsbl_progpath;
	}
	rslvr_argv = new_arg;

	rslvr_argv[rslvr_argc] = "-B";
	rslvr_argv[++rslvr_argc] = entry;
	rslvr_argv[++rslvr_argc] = 0;
}
# endif /* HELPERS */


/* Parse a string of the form "domain[,[IPaddr][,name|ipv4|ipv6]]"
 *	Strings starting with "set:" are special.
 *	    set:debug		more logging
 *	    set:msg_secs=S	total seconds checking blacklists/message
 *	    set:url_secs=S	total seconds per host name
 *	    set:[no-]envelope	envelope sender client IP address checks
 *	    set:[no-]body	body URL checks
 *	    set:[no-]MX		MX checks
 *
 *	    set:rslvr=soc,fd	DNS resolver process
 */
u_char					/* 0=bad */
dcc_parse_dnsbl(DCC_EMSG emsg,
		const char *entry,
		const char *progpath)
{
	static u_char cur_flags = DNSBL_FGS;
	const char *parm;
	DNSBL *dp;
	const char *ip;			/* "hit" IP address of this blacklist */
	DNSBL_DOM ip_buf;
	enum DNSBL_TYPE bl_type;
	int error1, error2, dom_len, ip_addr_len;
	char *p;
#ifdef HELPERS
	SOCKET soc;
	int fd;
#endif
	int i;

	dnsbl_progpath = progpath;

	if (!CSTRCMP(entry, "set:")) {
		parm = entry+STRZ("set ");
		if (!CSTRCMP(parm, "debug")) {
			++dnsbl_debug;
			save_arg(entry);
			return 1;

		} else if (!CSTRCMP(parm, "envelope")) {
			cur_flags |= DNSBL_FG_ENVELOPE;
			save_arg(entry);
			return 1;

		} else if (!CSTRCMP(parm, "no-envelope")
			   || !CSTRCMP(parm, "no_envelope")) {
			cur_flags &= ~DNSBL_FG_ENVELOPE;
			save_arg(entry);
			return 1;

		} else if (!CSTRCMP(parm, "body")) {
			cur_flags |= DNSBL_FG_BODY;
			save_arg(entry);
			return 1;

		} else if (!CSTRCMP(parm, "no-body")
			   || !CSTRCMP(parm, "no_body")) {
			cur_flags &= ~DNSBL_FG_BODY;
			save_arg(entry);
			return 1;

		} else if (!CSTRCMP(parm, "mx")) {
#ifdef MX_DNSBL
			cur_flags |= DNSBL_FG_MX;
			save_arg(entry);
			return 1;
#else
			dcc_pemsg(EX_USAGE, emsg,
				  "MX DNS blacklists not supported");
			return 0;
#endif

		} else if (!CSTRCMP(parm, "no-mx")
			   || !CSTRCMP(parm, "no_mx")) {
			cur_flags &= ~DNSBL_FG_MX;
			save_arg(entry);
			return 1;

#ifdef HELPERS
		} else if (2 == sscanf(parm, RSLVR_PAT, &soc, &fd)) {
			rslvr_work(soc, fd);
#endif

		} else if (!CSTRCMP(parm, "msg_secs=")
			   || !CSTRCMP(parm, "msg-secs=")) {
			parm += STRZ("msg_secs=");
			i = strtoul(parm, &p, 0);
			if (i < 1 || i > MAX_MSG_SECS || *p != '\0') {
				dcc_pemsg(EX_USAGE, emsg,
					  "bad number of seconds in \"-B %s\"",
					  entry);
				return 0;
			}
			if (msg_secs != i) {
				msg_secs= i;
				save_arg(entry);
			}
			return 1;


		} else if (!CSTRCMP(parm, "url-secs=")
			   || !CSTRCMP(parm, "url_secs=")
			   || !CSTRCMP(parm, "dom_secs=")) {
			parm += STRZ("url_secs=");
			i = strtoul(parm, &p, 0);
			if (i < 1 || i > MAX_MSG_SECS || *p != '\0') {
				dcc_pemsg(EX_USAGE, emsg,
					  "bad number of seconds in \"-B %s\"",
					  entry);
				return 0;
			}
			url_secs = i;
			save_arg(entry);
			return 1;

		} else {
			dcc_pemsg(EX_USAGE, emsg, "unrecongized  \"-B %s\"",
				  entry);
			return 0;
		}
	}

	bl_type = DNSBL_TYPE_IPV4;
	ip = strchr(entry, ',');
	if (!ip) {
		dom_len = strlen(entry);
	} else {
		dom_len = ip - entry;
		++ip;

		/* notice trailing ",name" or ",addr" */
		p = strchr(ip, ',');
		if (!p) {
			dnsbl_have_ipv4 = 1;
		} else {
			++p;
			if (!CSTRCMP(p, "name")) {
				bl_type = DNSBL_TYPE_NAME;
			} else if (!CSTRCMP(p, "IPV4")) {
				bl_type = DNSBL_TYPE_IPV4;
				dnsbl_have_ipv4 = 1;
			} else if (!CSTRCMP(p, "IPV6")) {
				bl_type = DNSBL_TYPE_IPV6;
				dnsbl_have_ipv6 = 1;
			} else {
				dcc_pemsg(EX_NOHOST, emsg,
					  "unknown blacklist type in \"%s\"",
					  entry);
				return 0;
			}
			STRLIMCPY(ip_buf, min(ISZ(ip_buf), p-ip), ip);
			ip = ip_buf;
		}
	}
	if (!ip || *ip == '\0') {
		ip = "127.0.0.2";
		dnsbl_have_ipv4 = 1;
	}
	if (dom_len < 1) {
		dcc_pemsg(EX_NOHOST, emsg,
			  "invalid DNS blacklist \"%s\"", entry);
		return 0;
	}

	/* first try to interpret the target address as IPv4 */
	dcc_host_lock();
	if (dcc_get_host(ip, 0, &error1)) {
		ip_addr_len = sizeof(struct in_addr)*4;
	} else if (dcc_get_host(ip, 1, &error2)) {
		ip_addr_len = sizeof(struct in6_addr)*4;
	} else {
		dcc_host_unlock();
		dcc_pemsg(EX_NOHOST, emsg,
			  "invalid DNS blacklist IP address \"%s\": %s",
			  ip, DCC_HSTRERROR(error1));
		return 0;
	}

	if (dom_len >= ISZ(dp->dom) - ip_addr_len) {
		dcc_host_unlock();
		dcc_pemsg(EX_NOHOST, emsg,
			  "DNS blacklist name \"%s\" too long", entry);
		return 0;
	}

	dp = dcc_malloc(sizeof(*dp));
	memset(dp, 0, sizeof(*dp));
	dp->fwd = dnsbls;
	dnsbls = dp;

	dp->su = dcc_hostaddrs[0];
	dcc_host_unlock();

	dp->bl_type = bl_type;
	dp->flags = cur_flags;
	some_flags |= cur_flags;
	memcpy(dp->dom, entry, dom_len);
	dp->dom_len = dom_len;

	if (!threaded)
		threaded = dcc_dnsbl_lock_init();

	save_arg(entry);
	return 1;
}



u_char					/* 1=logging indicated */
dcc_dnsbl_log_print(const DNSBL_WORK *bl,
		    void(lp)(void *, const char*, ...), void *ctxtp)
{
	const char *mx;

	if (!bl)
		return 0;

	mx = bl->mx ? " MX" : "";

	if (bl->timeouts)
		lp(ctxtp, "insufficent time for %d DNS blacklist checks;"
		   " %s first not checked\n",
		   bl->timeouts,
		   bl->timeout_dom);

	switch (bl->hit) {
	case DNSBL_HIT_NONE:
		return 0;
	case DNSBL_HIT_MTA:
		if (dnsbl_debug)
			lp(ctxtp, "DNSBL check foreclosed by MTA");
		return 0;
	case DNSBL_HIT_CLIENT:
		lp(ctxtp, "SMTP client blacklist hit %s\n", bl->probe);
		return 1;
	case DNSBL_HIT_MAIL_HOST:
		lp(ctxtp, "SMTP envelope sender%s blacklist hit %s\n",
		   mx, bl->probe);
		return 1;
	case DNSBL_HIT_URL:
		lp(ctxtp, "body URL%s %s blacklist hit %s\n",
		   mx, bl->dom, bl->probe);
		return 1;
	}

	return 0;
}



/* start timer before we start to check DNS blacklists
 *	give up if it has already expired */
static u_char				/* 0=already too much time spent */
msg_secs_start(DNSBL_WORK *bl)
{
	time_t now;

	if (bl->msg_secs < 0)
		return 0;

	if (bl->msg_secs == 0) {
		bl->msg_secs = -1;
		if (bl->timeout_dom[0] == '\0')
			strcpy(bl->timeout_dom, bl->dom);
		if (dnsbl_debug > 0)
			dcc_trace_msg("%s DNSBL"
				      " exhausted %d msg_secs before %s",
				      bl->id, msg_secs, bl->dom);
		return 0;
	}

	now = time(0);
	bl->start = now;
	bl->stop = now + url_secs;
	return 1;
}



/* see if we have run out of time */
static u_char				/* 0=timeout, 1=keep looking */
msg_secs_ck(DNSBL_WORK *bl)
{
	time_t now;

	if (bl->msg_secs < 0)
		return 0;

	now = time(0);
	if (now <= bl->stop)
		return 1;

	/* we are out of time for at least the current domain */
	if (bl->timeout_dom[0] == '\0')
		strcpy(bl->timeout_dom, bl->dom);
	++bl->timeouts;
	if (bl->msg_secs <= now - bl->start) {
		bl->msg_secs = -1;
		if (dnsbl_debug > 0)
			dcc_trace_msg("%s DNSBL exhausted %d msg_secs for %s",
				      bl->id, msg_secs, bl->dom);
	} else {
		if (dnsbl_debug > 0)
			dcc_trace_msg("%s DNSBL exhausted %d url_secs for %s",
				      bl->id, url_secs, bl->dom);
	}
	return 0;
}



/* account for time used */
static void
msg_secs_fin(DNSBL_WORK *bl)
{
	time_t used;

	if (bl->hit != DNSBL_HIT_NONE)
		return;

	if (bl->msg_secs > 0) {
		used = time(0) - bl->start;
		if (used > 0)		/* handle clock jumps */
			bl->msg_secs -= used;
		if (bl->msg_secs < 0)
			bl->msg_secs = 0;
	}
}



static void
dnsbl_res_init(void)
{
#ifdef HAVE__RES
	int res_retrans, res_retry;
	int retry_retrans, ratio;
#endif

	if (url_secs > msg_secs)
		url_secs =  msg_secs;

#ifdef HAVE__RES
	/* limit resolver delays to as much as we are willing to wait */
	if (!_res.options & RES_INIT)
		res_init();
	res_retry = _res.retry;
	res_retrans = _res.retrans;
	if (!res_retry)
		res_retry = 4;
	if (!res_retrans)
		res_retrans = RES_TIMEOUT;

	/* Arrange to have the resolver library take a little longer than
	 * our timeout so that can recognize a resolver timeout.
	 * Some versions of some wrappers such as getipnodebyname()
	 * answer with HOST_NOT_FOUND when they mean TRY_AGAIN. */
	retry_retrans = res_retry * res_retrans;
	if (retry_retrans > url_secs+2) {
		ratio = retry_retrans/(url_secs+1);
		res_retry /= ratio;
		if (res_retry < 1)
			res_retry = 1;
		res_retrans = (url_secs+2 + res_retry-1)/res_retry;
		if (res_retrans < RES_TIMEOUT)
			res_retrans = RES_TIMEOUT;
		retry_retrans = res_retry * res_retrans;
	}

	_res.retry = res_retry;
	_res.retrans = res_retrans;
#endif /* !HAVE__RES */
}



/* get ready to handle a mail message */
void
dcc_dnsbl_init(DCC_GOT_CKS *cks, DCC_CLNT_CTXT *ctxt, const char *id,
	       int max_work UATTRIB)
{
	DNSBL_WORK *bl;

	if (!dnsbls)
		return;

	if ((bl = cks->dnsbl) == 0) {
#ifdef HELPERS
		max_rslvrs = max_work;
#endif
		bl = dcc_malloc(sizeof(*cks->dnsbl));
		memset(bl, 0, sizeof(*cks->dnsbl));
		cks->dnsbl = bl;
		if (!threaded)
			dnsbl_res_init();
	}

	bl->hit = DNSBL_HIT_NONE;
	bl->msg_secs = msg_secs;
	bl->dom[bl->dom_len = 0] = '\0';
	bl->timeouts = 0;
	bl->timeout_dom[0] = '\0';
	bl->probe[0] = '\0';
	bl->id = id ? id : "";

	 bl->ctxt = ctxt;
}



/* look for a host name or IP address in a DNS blacklist.
 *	These DNS operations should be done with local default values for
 *	RES_DEFNAMES, RES_DNSRCH, and RES_NOALIASES because the blacklist
 *	might be something local and strange. */
static int				/* -1=out of time, 0=miss, 1=hit */
lookup(DNSBL_WORK *bl,
       const char *probe,		/* check this name in blacklisting */
       const DNSBL *dp,
       u_char mx)
{
	char str[INET6_ADDRSTRLEN+1+6+1];
	int error;

	if (!msg_secs_ck(bl))
		return -1;

	dcc_host_lock();
	if (!dcc_get_host(probe, dp->su.sa.sa_family == AF_INET ? 0 : 1,
			  &error)) {
		if (!msg_secs_ck(bl))
			return -1;
		if (dnsbl_debug > 1)
			dcc_trace_msg("%s DNSBL%s %s gethostbyname(%s): %s",
				      bl->id, mx ? " MX" : "",
				      bl->dom, probe, DCC_HSTRERROR(error));
		dcc_host_unlock();
		return 0;
	}

	if (DCC_SU_SA_EQ(&dcc_hostaddrs[0], &dp->su)) {
		dcc_host_unlock();
		if (dnsbl_debug > 1)
			dcc_trace_msg("%s DNSBL%s hit %s gethostbyname(%s)=%s",
				      bl->id, mx ? " MX" : "", bl->dom, probe,
				      dcc_su2str_opt(&dp->su, 0, 0));
		return 1;
	}

	if (dnsbl_debug > 1) {
		if (dcc_hostaddrs[0].sa.sa_family == AF_INET) {
			if (!DCC_INET_NTOP(AF_INET,
					   &dcc_hostaddrs[0].ipv4,
					   str, sizeof(str)))
				strcpy(str, "???");
		} else {
			if (!DCC_INET_NTOP(AF_INET6,
					   &dcc_hostaddrs[0].ipv6,
					   str, sizeof(str)))
				strcpy(str, "???");
		}
		dcc_trace_msg("%s DNSBL%s miss %s gethostbyname(%s)=%s",
			      bl->id, mx ? " MX" : "", bl->dom, probe, str);
	}
	dcc_host_unlock();
	return 0;
}



/* check one IPv4 address against the DNS blacklists */
static int				/* -1=out of time, 0=miss, 1=hit */
dnsbl_ipv4(DNSBL_WORK *bl,
	   char *probe, int probe_len,
	   const u_char *bp,
	   u_char flags,
	   u_char mx)
{
	const DNSBL *dp;
	int ret;

	for (dp = dnsbls; dp; dp = dp->fwd) {
		if (dp->bl_type != DNSBL_TYPE_IPV4)
			continue;
		if (!(dp->flags & flags))
			continue;
		snprintf(probe, probe_len, "%d.%d.%d.%d.%s",
			 bp[3], bp[2], bp[1], bp[0], dp->dom);
		ret = lookup(bl, probe, dp, mx);
		if (ret)
			return ret;
	}
	return 0;
}



/* check one IPv6 address against the DNS blacklists */
static int				/* -1=out of time, 0=miss, 1=hit */
dnsbl_ipv6(DNSBL_WORK *bl,
	   char *probe, int probe_len,
	   const u_char *bp,
	   u_char flags,
	   u_char mx)
{
	const DNSBL *dp;
	int ret;

	for (dp = dnsbls; dp; dp = dp->fwd) {
		if (dp->bl_type != DNSBL_TYPE_IPV6)
			continue;
		if (!(dp->flags & flags))
			continue;
		snprintf(probe, probe_len,
			 "%d.%d.%d.%d.%d.%d.%d.%d.%d.%d.%d.%d.%d.%d.%d.%d.%s",
			 bp[15], bp[14], bp[13], bp[12],
			 bp[11], bp[10], bp[9], bp[8],
			 bp[7], bp[6], bp[5], bp[4],
			 bp[3], bp[2], bp[1], bp[0],
			 dp->dom);
		ret = lookup(bl, probe, dp, mx);
		if (ret)
			return ret;
	}
	return 0;
}



/* convert a name to an IP address so that the IP address can be checked
 *	in a DNS blacklist.
 *	These DNS operations need RES_DEFNAMES and RES_DNSRCH off and
 *	RES_NOALIASES on when the name is an MX host.
 */
static u_char				/* 0=failed */
dnsbl_get_host(const char *dom, u_char use_ipv6, int *errorp, u_char no_search)
{
	u_long save_options;
	u_char result;

	dcc_host_lock();
#ifdef HAVE__RES
	save_options = _res.options;
	if (no_search) {
		_res.options &= ~(RES_DEFNAMES | RES_DNSRCH);
		_res.options |= RES_NOALIASES;
	}
#endif
	result = dcc_get_host(dom, use_ipv6, errorp);
#ifdef HAVE__RES
	if (no_search)
		_res.options = save_options;
#endif
	return result;
}



/* look for a domain name in the DNS blacklists */
static int				/* -1=out of time, 0=miss, 1=hit */
dnsbl_name_sub(DNSBL_WORK *bl,
	       char *probe, int probe_len,  /* put DNS blacklist probe here */
	       const char *dom,		/* see if this domain is blacklisted */
	       u_char flags,		/* type of lookup */
	       u_char mx)		/* 1=MX server */
{
	const DNSBL *dp;
	const DCC_SOCKU *sup;
	struct in_addr ipv4[4];
	struct in6_addr ipv6[4];
	int ret, i, error;

	/* give up if none of the DNS blacklists allow this kind of search */
	if (!(some_flags & flags))
		return -1;

	/* check the name in any host name DNS blacklists we have */
	for (dp = dnsbls; dp; dp = dp->fwd) {
		if (dp->bl_type != DNSBL_TYPE_NAME)
			continue;
		if (!(dp->flags & flags))
			continue;
		/* truncate on the left */
		i = probe_len-1-1-dp->dom_len;
		if (i > 0) {
			i = strlen(dom) - i;
			if (i < 0)
				i = 0;
			snprintf(probe, probe_len, "%s.%s",
				 dom+i, dp->dom);
			ret = lookup(bl, probe, dp, mx);
			if (ret)
				return ret;
			if (!msg_secs_ck(bl))
				return -1;
		}
	}

	/* try IPv4 second */
	if (dnsbl_have_ipv4) {
		if (!msg_secs_ck(bl))
			return -1;
		if (!dnsbl_get_host(dom, 0, &error, mx)) {
			dcc_host_unlock();
			if (!msg_secs_ck(bl))
				return -1;
			if (dnsbl_debug > 1)
				dcc_trace_msg("%s DNSBL%s"
					      " gethostbyname(%s): %s",
					      bl->id, mx ? " MX" : "",
					      dom, DCC_HSTRERROR(error));
		} else {
			/* Try several of the IP addresses for the domain.
			 * gethostbyname() often returns pointers to static
			 * buffers that are changed by the next call.
			 * That forces us to save any IP addresses we want to
			 * check before we check them */
			for (sup = dcc_hostaddrs, i = 0;
			     sup < dcc_hostaddrs_end && i < DIM(ipv4);
			     ++sup, ++i) {
				ipv4[i] = sup->ipv4.sin_addr;
			}
			dcc_host_unlock();
			/* check the addresses in all of the DNS blacklists */
			do {
				ret = dnsbl_ipv4(bl, probe, probe_len,
						 (u_char *)&ipv4[--i],
						 flags, mx);
				if (ret)
					return ret;
			} while (i > 0);
		}
	}

	/* try IPv6 if we have any */
	if (dnsbl_have_ipv6) {
		if (!msg_secs_ck(bl))
			return -1;
		if (!dnsbl_get_host(dom, 1, &error, mx)) {
			if (!msg_secs_ck(bl))
				return -1;
			dcc_host_unlock();
			if (dnsbl_debug > 1)
				dcc_trace_msg("%s DNSBL%s"
					      " gethostbyname(%s): %s",
					      bl->id, mx ? " MX" : "",
					      dom, DCC_HSTRERROR(error));
		} else {
			for (sup = dcc_hostaddrs, i = 0;
			     sup < dcc_hostaddrs_end && i < DIM(ipv6);
			     ++sup, ++i) {
				ipv6[i] = sup->ipv6.sin6_addr;
			}
			dcc_host_unlock();
			do {
				ret = dnsbl_ipv6(bl, probe, probe_len,
						 (u_char *)&ipv6[--i],
						 flags, mx);
				if (ret)
					return ret;
			} while (i > 0);
		}
	}

	return 0;
}



static int				/* -1=timeout, 0=miss, 1=hit */
dnsbl_addr(DNSBL_WORK *bl,
	   char *probe, int probe_len,	/* build DNS blacklist probe here */
	   const struct in6_addr *addr,
	   u_char flags)
{
	struct in_addr ipv4;
	int ret;

	/* try IPv4 first */
	if (dnsbl_have_ipv4
	    && dcc_ipv6toipv4(&ipv4, addr)) {
		ret = dnsbl_ipv4(bl, probe, probe_len, (u_char *)&ipv4,
				 flags, 0);
		if (ret)
			return ret;
	}

	if (dnsbl_have_ipv6)
		return dnsbl_ipv6(bl, probe, probe_len, (u_char *)addr,
				  flags, 0);
	return 0;
}



/* look for a domain name in the DNS blacklists, including its MX servers */
#ifndef MX_DNSBL
#define dnsbl_name(bl,p,pl,dom,flags) dnsbl_name_sub(bl,p,pl,dom,flags,0)
#else /* MX_DNSBL */
static int				/* -1=out of time, 0=miss, 1=hit */
dnsbl_name(DNSBL_WORK *bl,
	       char *probe, int probe_len,  /* put DNS blacklist probe here */
	       const char *dom,		/* see if this domain is blacklisted */
	       u_char flags)		/* type of lookup */
{
	union {
	    u_char  buf[PACKETSZ+20];
	    HEADER  hdr;
	} answer;
	DNSBL_DOM mx;
	u_char *ap, *eom;
	int cnt, ret, skip, type;

	ret = dnsbl_name_sub(bl, probe, probe_len, dom, flags, 0);
	if (ret)
		return ret;

	/* check MX servers if supported by at least one DNS blacklist */
	if (!(some_flags & DNSBL_FG_MX))
		return 0;

	ret = res_query(dom, C_IN, T_MX, answer.buf, sizeof(answer.buf));
	if (ret < 0) {
		if (dnsbl_debug > 1)
			dcc_trace_msg("DNSBL MX res_query(%s): %s",
				      dom, hstrerror(h_errno));
		return 0;
	}

	ap = &answer.buf[HFIXEDSZ];
	if (ret > ISZ(answer.buf))
		ret = ISZ(answer.buf);
	eom = &answer.buf[ret];

	/* skip the question */
	cnt = ntohs(answer.hdr.qdcount);
	while (--cnt >= 0) {
		skip = dn_skipname(ap, eom);
		if (skip < 0) {
			if (dnsbl_debug > 1)
				dcc_trace_msg("DNSBL MX dn_skipname(%s)=%d",
					      dom, skip);
			return 0;
		}
		ap += skip+QFIXEDSZ;
	}

	cnt = ntohs(answer.hdr.ancount);
	while (--cnt >= 0 && ap < eom) {
		skip = dn_expand(answer.buf, eom, ap, mx, sizeof(mx));
		if (skip < 0) {
			if (dnsbl_debug > 1)
				dcc_trace_msg("DNSBL MX dn_expand(%s)=%d",
					      dom, skip);
			return 0;
		}

		ap += skip;
		GETSHORT(type, ap);
		ap += 2+4;		/* skip class and TTL */
		GETSHORT(skip, ap);	/* get rdlength */
		if (type != T_MX) {
			ap += skip;
			continue;
		}
		ap += 2;		/* skip preference */
		skip = dn_expand(answer.buf, eom, ap, mx, sizeof(mx)-1);
		if (skip < 0) {
			if (dnsbl_debug > 1)
				dcc_trace_msg("DNSBL MX dn_expand(%s)=%d",
					      dom, skip);
			return 0;
		}
		ap += skip;

		ret = dnsbl_name_sub(bl, probe, probe_len, mx, flags, 1);
		if (ret) {
			if (ret > 0)
				bl->mx = 1;
			return ret;
		}
	}

	return 0;
}
#endif /* MX_DNSBL */



#ifdef HELPERS
/* resolver helper processes to deal with typical single threaded
 *	resolver libraries */
static void
stop_rslvrs(u_char locked)
{
	if (!locked)
		dcc_dnsbl_lock();

	if (rslvr_stop_write_fd >= 0) {
		close(rslvr_stop_write_fd);
		rslvr_stop_write_fd = -1;
	}
	if (rslvr_stop_read_fd >= 0) {
		close(rslvr_stop_read_fd);
		rslvr_stop_read_fd = -1;
	}
	if (rslvr_soc != INVALID_SOCKET) {
		closesocket(rslvr_soc);
		rslvr_soc = INVALID_SOCKET;
	}
	if (rslvrs_running) {
		int status;

		while (0 <= waitpid(0, &status, WNOHANG))
			continue;
		rslvrs_running = 0;
		idle_rslvrs = 0;
	}

	rslvr_failures = 0;

	if (!locked)
		dcc_dnsbl_unlock();
}



/* must be called with the mutex */
static u_char
start_rslvrs(const DNSBL_WORK *bl)
{
	int fds[2];
	struct in6_addr ipv6_loopback;
	struct in_addr ipv4_loopback;
	int soc_len;

	stop_rslvrs(1);

	/* create a new socket with the same choice of IPv4 or IPv6
	 * as the context's socket */
	if (bl->ctxt->soc == INVALID_SOCKET
	    && dcc_clnt_soc_open(0, bl->ctxt))
		return 0;
	if (bl->ctxt->use_ipv6) {
		memset(&ipv6_loopback, 0, sizeof(ipv6_loopback));
		ipv6_loopback.s6_addr32[3] = ntohl(1);
		dcc_mk_su(&rslvr_su, AF_INET6, &ipv6_loopback, 0);
	} else {
		ipv4_loopback.s_addr = ntohl(0x7f000001);
		dcc_mk_su(&rslvr_su, AF_INET, &ipv4_loopback, 0);
	}
	if (!dcc_udp_bind(0, &rslvr_soc, &rslvr_su, 0)) {
		dcc_error_msg("DNSBL bind(): %s", ERROR_STR());
		return 0;
	}
	soc_len = sizeof(rslvr_su);
	if (0 > getsockname(rslvr_soc, &rslvr_su.sa, &soc_len)) {
		dcc_error_msg("DNSBL getsockname(): %s", ERROR_STR());
		stop_rslvrs(1);
		return 0;
	}

	/* give the resolver child processes an FD that will go dead
	 * if the parent dies or otherwise closes the other end */
	if (0 > pipe(fds)) {
		dcc_error_msg("DNSBL pipe(): %s", ERROR_STR());
		stop_rslvrs(1);
		return 0;
	}
	rslvr_stop_write_fd = fds[1];
	rslvr_stop_read_fd = fds[0];

	if (-1 == fcntl(rslvr_stop_write_fd, F_SETFL,
			fcntl(rslvr_stop_write_fd, F_GETFL, 0) | O_NONBLOCK)) {
		dcc_error_msg("DNSBL rslvr fcntl(O_NONBLOCK): %s", ERROR_STR());
		stop_rslvrs(1);
		return 0;
	}
	if (0 > fcntl(rslvr_stop_write_fd, F_SETFD, FD_CLOEXEC)) {
		dcc_error_msg("DNSBL rslvr fcntl(FD_CLOEXEC): %s", ERROR_STR());
		stop_rslvrs(1);
		return 0;
	}

	rslvrs_running = 1;
	return 1;
}



/* Start a new resolver helper process.
 *	The mutex must be locked.  It is unlocked on failure */
static u_char
new_rslvr(const DNSBL_WORK *bl)
{
	pid_t pid;
	char buf[sizeof("set:")+sizeof(RSLVR_PAT)+8+8];

	if (!rslvrs_running
	    && !start_rslvrs(bl)) {
		dcc_dnsbl_unlock();
		return 0;
	}

	pid = fork();
	if (pid < 0) {
		dcc_error_msg("DNSBL fork(): %s", ERROR_STR());
		stop_rslvrs(1);
		dcc_dnsbl_unlock();
		return 0;
	}

	if (pid != 0) {
		/* this is the parent */
		++total_rslvrs;
		return 1;
	}

	/* This is the child
	 * exec() to get rid of the other threads */
	rslvr_soc = dup(rslvr_soc);	/* turn off FD_CLOEXEC */
	snprintf(buf, sizeof(buf), "set:"RSLVR_PAT,
		 rslvr_soc, rslvr_stop_read_fd);
	save_arg(buf);
	execv(dnsbl_progpath, (char * const *)rslvr_argv);
	/* This process should continue eventually at rslvr_work() */
	dcc_logbad(EX_UNAVAILABLE, "exec(%s -B %s): %s",
		   dnsbl_progpath, buf, ERROR_STR());
}



/* the resolver helper processes run this forever */
static void
rslvr_work(SOCKET soc, int fd)
{
	fd_set rfds;
	DCC_SOCKLEN_T su_len;
	DCC_SOCKU su;
	DNSBL_RSLVR_REQ req;
	DNSBL_RSLVR_RESP resp;
	DNSBL_WORK bl;
	int soc_len;
	int i;

	/* Ensure FDs are small enough for select() */
	if (soc >= (int)FD_SETSIZE) {
		rslvr_soc = dup(soc);
		close(soc);
	} else {
		rslvr_soc = soc;
	}
	if (fd >= (int)FD_SETSIZE) {
		rslvr_stop_read_fd = dup(fd);
		close(fd);
	} else {
		rslvr_stop_read_fd = fd;
	}

	soc_len = sizeof(rslvr_su);
	if (0 > getsockname(rslvr_soc, &rslvr_su.sa, &soc_len))
		dcc_logbad(EX_IOERR, "DNSBL getsockname(): %s", ERROR_STR());

	if (dnsbl_debug > 1)
		dcc_trace_msg("DNSBL resolver starting on %s",
			      dcc_su2str(&rslvr_su));

	dnsbl_res_init();
	url_secs = MAX_MSG_SECS;

	FD_ZERO(&rfds);
	for (;;) {
		FD_SET(rslvr_soc, &rfds);
		FD_SET(rslvr_stop_read_fd, &rfds);
		i = select(max(rslvr_soc,rslvr_stop_read_fd)+1, &rfds, 0, 0, 0);
		if (i < 0) {
			if (DCC_SELECT_NERROR())
				continue;
			dcc_logbad(EX_OSERR, "DNSBL wokr select(): %s",
				   ERROR_STR());
		}

		if (FD_ISSET(rslvr_stop_read_fd, &rfds)) {
			if (dnsbl_debug > 1)
				dcc_trace_msg("DNSBL resolver on %s stopping",
					      dcc_su2str(&rslvr_su));
			exit(0);
		}

		if (!FD_ISSET(rslvr_soc, &rfds))
			continue;

		/* process a request */
		su_len = sizeof(su);
		i = recvfrom(rslvr_soc, &req, ISZ(req), 0, &su.sa, &su_len);
		if (i < 0) {
			if (DCC_BLOCK_ERROR())
				continue;
			dcc_logbad(EX_IOERR, "DNSBL work recvfrom(): %s",
				   ERROR_STR());
		}

		/* we might get stray packets because we are usng UDP */
		if (!DCC_SU_SA_EQ(&rslvr_su, &su)) {
			if (dnsbl_debug != 0)
				dcc_trace_msg("DNSBL from %s instead of %s",
					      dcc_su2str(&su),
					      dcc_su2str(&rslvr_su));
			continue;
		}
		if (i != ISZ(req)) {
			if (dnsbl_debug != 0)
				dcc_trace_msg("DNSBL work recvfrom()=%d", i);
			continue;
		}
		if (req.magic1 != RSLVR_MAGIC1
		    || req.magic2 != RSLVR_MAGIC2) {
			if (dnsbl_debug != 0)
				dcc_trace_msg("DNSBL work recvfrom()"
					      " magic1=%#08x magic2=%#08x",
					      req.magic1, req.magic2);
			continue;
		}

		resp.magic1 = RSLVR_MAGIC1;
		resp.magic2 = RSLVR_MAGIC2;
		resp.sn = req.sn;

		bl.msg_secs = MAX_MSG_SECS;
		strcpy(bl.dom, req.dom);
		bl.id = req.id;
		bl.hit = DNSBL_HIT_NONE;
		bl.mx = 0;
		bl.start = time(0);
		bl.stop = bl.start+MAX_MSG_SECS;

		switch (req.type) {
		case DNSBL_HIT_CLIENT:
			if (0 < dnsbl_addr(&bl, resp.probe, ISZ(resp.probe),
					   &req.tgt.addr,
					   DNSBL_FG_ENVELOPE))
				resp.type = req.type;
			else
				resp.type = DNSBL_HIT_NONE;
			break;
		case DNSBL_HIT_MAIL_HOST:   /* envelope mail_from */
			if (0 < dnsbl_name(&bl, resp.probe, ISZ(resp.probe),
					   req.tgt.nm, DNSBL_FG_ENVELOPE))
				resp.type = req.type;
			else
				resp.type = DNSBL_HIT_NONE;
			break;
		case DNSBL_HIT_URL:	/* URL in body */
			if (0 < dnsbl_name(&bl, resp.probe, ISZ(resp.probe),
					   req.tgt.nm, DNSBL_FG_BODY))
				resp.type = req.type;
			else
				resp.type = DNSBL_HIT_NONE;
			break;
		default:
			dcc_logbad(EX_SOFTWARE,"%s DNSBL work unknown type %d",
				   bl.id, req.type);
			continue;
		}

		i = sendto(rslvr_soc, &resp, ISZ(resp), 0,
			   &su.sa, DCC_SU_LEN(&su));
		if (i != ISZ(resp)) {
			if (i < 0)
				dcc_error_msg("%s DNSBL work sendto(): %s",
					      bl.id, ERROR_STR());
			else
				dcc_error_msg("%s DNSBL work sendto()=%d",
					      bl.id, i);
			continue;
		}
	}
}



/* ask a helper process to do some DNS work */
static u_char				/* seem to have sick helper process */
ask_rslvr(DCC_SOCKU *send_su, DNSBL_WORK *bl,
	  DNSBL_RSLVR_REQ *req, const void *data)
{
	DCC_SOCKLEN_T su_len;
	DCC_SOCKU recv_su;
	DNSBL_RSLVR_RESP resp;
	int i;

	/* skip it if we have spent too much time checking blacklists */
	if (!msg_secs_ck(bl))
		return 1;

#ifdef DCC_UDP_CONNECT
	dcc_clnt_disconnect(0, bl->ctxt);
#endif
	if (bl->ctxt->soc == INVALID_SOCKET
	    && dcc_clnt_soc_open(0, bl->ctxt))
		return 0;
	i = sendto(bl->ctxt->soc, req, ISZ(*req),
		   0, &send_su->sa, DCC_SU_LEN(send_su));
	if (i != ISZ(*req)) {
		if (dnsbl_debug > 1) {
			if (i < 0)
				dcc_trace_msg("%s DNSBL ask sendto(): %s",
					      bl->id, ERROR_STR());
			else
				dcc_trace_msg("%s DNSBL ask sendto()=%d",
					      bl->id, i);
			return 0;
		}
	}

	for (;;) {
		i = bl->stop - time(0);
		if (i < 0)
			i = 0;
		i = dcc_select_poll(0, bl->ctxt->soc, 1,
				    i*DCC_USECS + DCC_USECS/4);
		if (i < 0) {
			dcc_error_msg("%s DNSBL select_poll: %s",
				      bl->id, ERROR_STR());
			return 0;
		}
		if (i == 0) {
			/* give up if resolver did not answer */
			if (dnsbl_debug > 2)
				dcc_trace_msg("%s DNSBL no resolver answer",
					      bl->id);
			bl->stop = 0;
			msg_secs_ck(bl);
			return 0;
		}

		su_len = sizeof(recv_su);
		i = recvfrom(bl->ctxt->soc, &resp, ISZ(resp),
			     0, &recv_su.sa, &su_len);
		/* because we are usng UDP, we might get stray packets */
		if (i != ISZ(resp)) {
			if (i < 0) {
				dcc_trace_msg("%s DNSBL ask recvfrom(): %s",
					      bl->id, ERROR_STR());
				if (DCC_BLOCK_ERROR())
					continue;
				msg_secs_ck(bl);
				return 0;
			}
			if (dnsbl_debug > 1)
				dcc_trace_msg("%s DNSBL work recvfrom()=%d",
					      bl->id, i);
			continue;
		}
		if (!DCC_SU_SA_EQ(send_su, &recv_su)) {
			if (dnsbl_debug != 0)
				dcc_trace_msg("DNSBL work recvfrom(%s)"
					      " instead of %s",
					      dcc_su2str(&recv_su),
					      dcc_su2str(send_su));
			continue;
		}
		if (resp.magic1 != RSLVR_MAGIC1
		    || resp.magic2 != RSLVR_MAGIC2
		    || resp.sn != req->sn) {
			if (dnsbl_debug >1 )
				dcc_trace_msg("DNSBL work recvfrom(%s)"
					      " magic1=%#08x magic2=%#08x"
					      " sn=%d",
					      dcc_su2str(&recv_su),
					      resp.magic1, resp.magic2,
					      resp.sn);
			continue;
		}

		switch (resp.type) {
		case DNSBL_HIT_NONE:
			break;
		case DNSBL_HIT_MTA:
			dcc_logbad(EX_SOFTWARE, "unknown DNSBL hit");
			break;
		case DNSBL_HIT_CLIENT:
			STRLIMCPY(bl->probe, sizeof(bl->probe), resp.probe);
			if (dnsbl_debug > 1)
				dcc_trace_msg("%s DNSBL"
					      " client blacklist hit %s\n",
					      bl->id, bl->probe);
			bl->hit = resp.type;
			break;
		case DNSBL_HIT_MAIL_HOST:
			STRLIMCPY(bl->dom, sizeof(bl->dom), data);
			STRLIMCPY(bl->probe, sizeof(bl->probe), resp.probe);
			if (dnsbl_debug > 1)
				dcc_trace_msg("%s DNSBL envelope sender"
					      " blacklist hit %s with %s\n",
					      bl->id, bl->dom, bl->probe);
			bl->hit = resp.type;
			break;
		case DNSBL_HIT_URL:
			STRLIMCPY(bl->probe, sizeof(bl->probe), resp.probe);
			if (dnsbl_debug > 1)
				dcc_trace_msg("%s DNSBL body URL"
					      " %s blacklist hit %s\n",
					      bl->id, bl->dom, bl->probe);
			bl->hit = resp.type;
			break;
		}
		return 1;
	}
}



/* find a resolver process to check for something in the DNS blacklists */
static void
use_rslvr(DNSBL_WORK *bl,
	  DNSBL_HIT type,		/* look for this */
	  const void *data)
{
	DNSBL_RSLVR_REQ req;
	DCC_SOCKU send_su;
	u_char result;
	int i;

	req.magic1 = RSLVR_MAGIC1;
	req.magic2 = RSLVR_MAGIC2;
	dcc_dnsbl_lock();
	req.sn = ++rslvr_sn;
	if (idle_rslvrs > 0) {
		--idle_rslvrs;
		send_su = rslvr_su;
		dcc_dnsbl_unlock();
	} else if (total_rslvrs >= max_rslvrs) {
		send_su = rslvr_su;
		dcc_dnsbl_unlock();
		if (dnsbl_debug > 0)
			dcc_trace_msg("%s DNSBL no idle resolvers", bl->id);
	} else {
		if (!new_rslvr(bl))
			return;
		send_su = rslvr_su;
		dcc_dnsbl_unlock();
	}

	strcpy(req.dom, bl->dom);
	STRLIMCPY(req.id, sizeof(req.id), bl->id);
	req.type = type;
	switch (type) {
	case DNSBL_HIT_CLIENT:
		memcpy(&req.tgt.addr, data, sizeof(req.tgt.addr));
		break;
	case DNSBL_HIT_MAIL_HOST:
		i = strlen(data);
		if (i > ISZ(req.tgt.nm)-1)
			i = ISZ(req.tgt.nm)-1;
		memcpy(req.tgt.nm, data, i);
		req.tgt.nm[i] = '\0';
		break;
	case DNSBL_HIT_URL:
		strcpy(req.tgt.nm, bl->dom);
		break;
	default:
		dcc_logbad(EX_SOFTWARE, "unknown DNSBL type %d", type);
	}

	result = ask_rslvr(&send_su, bl, &req, data);

	dcc_dnsbl_lock();
	++idle_rslvrs;
	if (result) {
		rslvr_failures = 0;
	} else if (++rslvr_failures > 5) {
		if (dnsbl_debug)
			dcc_trace_msg("%s DNSBL restarting resolvers", bl->id);
		stop_rslvrs(1);
	}
	dcc_dnsbl_unlock();

	msg_secs_fin(bl);
}
#endif /* HELPERS */



void
dcc_dnsbl_url(DNSBL_WORK *bl)
{
	/* nothing to do if no DNS blacklists have been configured
	 * or if we already have a hit */
	if (!bl || bl->hit != DNSBL_HIT_NONE
	    || bl->dom_len == 0
	    || !(some_flags & DNSBL_FG_BODY))
		return;

	bl->dom[bl->dom_len] = '\0';

	/* or if we have already spent too much time checking blacklists */
	if (!msg_secs_start(bl))
		return;

#ifdef HELPERS
	if (threaded) {
		use_rslvr(bl, DNSBL_HIT_URL, 0);
		return;
	}
#endif

	if (0 < dnsbl_name(bl, bl->probe, sizeof(bl->probe), bl->dom,
			   DNSBL_FG_BODY)) {
		bl->hit = DNSBL_HIT_URL;
		if (dnsbl_debug > 1)
			dcc_trace_msg("%s DNSBL"
				      " body URL %s blacklist hit %s\n",
				      bl->id, bl->dom, bl->probe);
	}
	msg_secs_fin(bl);
}



void
dcc_sender_dnsbl(DNSBL_WORK *bl, const struct in6_addr *addr)
{
	char *p;

	/* nothing to do if no DNS blacklists have been configured
	 * or if we already have a hit */
	if (!bl || bl->hit != DNSBL_HIT_NONE
	    || !(some_flags & DNSBL_FG_ENVELOPE))
		return;

	p = bl->dom;
	strcpy(p, "sender ");
	p += STRZ("sender ");
	if (!DCC_INET_NTOP(AF_INET6, addr, p, sizeof(bl->dom)-STRZ("sender ")))
		strcpy(p, "???");

	/* or if we have already spent too much time checking blacklists */
	if (!msg_secs_start(bl))
		return;

#ifdef HELPERS
	if (threaded) {
		use_rslvr(bl, DNSBL_HIT_CLIENT, addr);
		return;
	}
#endif

	if (0 < dnsbl_addr(bl, bl->probe, sizeof(bl->probe), addr,
			   DNSBL_FG_ENVELOPE)) {
		bl->hit = DNSBL_HIT_CLIENT;
		if (dnsbl_debug > 1)
			dcc_trace_msg("%s DNSBL envelope"
				      " sender %s blacklist hit %s\n",
				      bl->id, bl->dom, bl->probe);
	}
	msg_secs_fin(bl);
}



void
dcc_mail_host_dnsbl(DNSBL_WORK *bl, const char *host)
{
	/* nothing to do if no DNS blacklists have been configured
	 * or if we already have a hit
	 * or if we have already spent too much time checking blacklists */
	if (!bl || bl->hit != DNSBL_HIT_NONE
	    || !(some_flags & DNSBL_FG_ENVELOPE)
	    || !host || *host == '\0')
		return;

	/* or if we have already spent too much time checking blacklists */
	STRLIMCPY(bl->dom, sizeof(bl->dom), host);
	if (!msg_secs_start(bl))
		return;

#ifdef HELPERS
	if (threaded) {
		use_rslvr(bl, DNSBL_HIT_MAIL_HOST, host);
		return;
	}
#endif

	if (0 < dnsbl_name(bl, bl->probe, sizeof(bl->probe), host,
			   DNSBL_FG_ENVELOPE)) {
		bl->hit = DNSBL_HIT_MAIL_HOST;
		if (dnsbl_debug > 1)
			dcc_trace_msg("%s DNSBL"
				      " client blacklist hit %s\n",
				      bl->id, bl->probe);
	}
	msg_secs_fin(bl);
}
