/*
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 *
 * 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 http://mozilla.org/MPL/2.0/.
 *
 * See the COPYRIGHT file distributed with this work for additional
 * information regarding copyright ownership.
 */


/*! \file */

#include <config.h>

#include <inttypes.h>
#include <stdbool.h>

#include <isc/mem.h>
#include <isc/print.h>
#include <isc/random.h>
#include <isc/string.h>		/* Required for HP/UX (and others?) */
#include <isc/task.h>
#include <isc/timer.h>
#include <isc/util.h>

#include <dns/callbacks.h>
#include <dns/catz.h>
#include <dns/db.h>
#include <dns/diff.h>
#include <dns/events.h>
#include <dns/journal.h>
#include <dns/log.h>
#include <dns/message.h>
#include <dns/rdataclass.h>
#include <dns/rdatalist.h>
#include <dns/rdataset.h>
#include <dns/result.h>
#include <dns/soa.h>
#include <dns/tcpmsg.h>
#include <dns/timer.h>
#include <dns/tsig.h>
#include <dns/view.h>
#include <dns/xfrin.h>
#include <dns/zone.h>

#include <dst/dst.h>

/*
 * Incoming AXFR and IXFR.
 */

/*%
 * It would be non-sensical (or at least obtuse) to use FAIL() with an
 * ISC_R_SUCCESS code, but the test is there to keep the Solaris compiler
 * from complaining about "end-of-loop code not reached".
 */
#define FAIL(code) \
	do { result = (code);					\
		if (result != ISC_R_SUCCESS) goto failure;	\
	} while (0)

#define CHECK(op) \
	do { result = (op);					\
		if (result != ISC_R_SUCCESS) goto failure;	\
	} while (0)

/*%
 * The states of the *XFR state machine.  We handle both IXFR and AXFR
 * with a single integrated state machine because they cannot be distinguished
 * immediately - an AXFR response to an IXFR request can only be detected
 * when the first two (2) response RRs have already been received.
 */
typedef enum {
	XFRST_SOAQUERY,
	XFRST_GOTSOA,
	XFRST_INITIALSOA,
	XFRST_FIRSTDATA,
	XFRST_IXFR_DELSOA,
	XFRST_IXFR_DEL,
	XFRST_IXFR_ADDSOA,
	XFRST_IXFR_ADD,
	XFRST_IXFR_END,
	XFRST_AXFR,
	XFRST_AXFR_END
} xfrin_state_t;

/*%
 * Incoming zone transfer context.
 */

struct dns_xfrin_ctx {
	unsigned int		magic;
	isc_mem_t		*mctx;
	dns_zone_t		*zone;

	int			refcount;

	isc_task_t 		*task;
	isc_timer_t		*timer;
	isc_socketmgr_t 	*socketmgr;

	int			connects; 	/*%< Connect in progress */
	int			sends;		/*%< Send in progress */
	int			recvs;	  	/*%< Receive in progress */
	bool		shuttingdown;
	isc_result_t		shutdown_result;

	dns_name_t 		name; 		/*%< Name of zone to transfer */
	dns_rdataclass_t 	rdclass;

	bool		checkid;
	dns_messageid_t		id;

	/*%
	 * Requested transfer type (dns_rdatatype_axfr or
	 * dns_rdatatype_ixfr).  The actual transfer type
	 * may differ due to IXFR->AXFR fallback.
	 */
	dns_rdatatype_t 	reqtype;
	isc_dscp_t 		dscp;

	isc_sockaddr_t 		masteraddr;
	isc_sockaddr_t		sourceaddr;
	isc_socket_t 		*socket;

	/*% Buffer for IXFR/AXFR request message */
	isc_buffer_t 		qbuffer;
	unsigned char 		qbuffer_data[512];

	/*% Incoming reply TCP message */
	dns_tcpmsg_t		tcpmsg;
	bool		tcpmsg_valid;

	dns_db_t 		*db;
	dns_dbversion_t 	*ver;
	dns_diff_t 		diff;		/*%< Pending database changes */
	int 			difflen;	/*%< Number of pending tuples */

	xfrin_state_t 		state;
	uint32_t 		end_serial;
	bool 		is_ixfr;

	unsigned int		nmsg;		/*%< Number of messages recvd */
	unsigned int		nrecs;		/*%< Number of records recvd */
	uint64_t		nbytes;		/*%< Number of bytes received */

	unsigned int		maxrecords;	/*%< The maximum number of
						     records set for the zone */

	isc_time_t		start;		/*%< Start time of the transfer */
	isc_time_t		end;		/*%< End time of the transfer */

	dns_tsigkey_t		*tsigkey;	/*%< Key used to create TSIG */
	isc_buffer_t		*lasttsig;	/*%< The last TSIG */
	dst_context_t		*tsigctx;	/*%< TSIG verification context */
	unsigned int		sincetsig;	/*%< recvd since the last TSIG */
	dns_xfrindone_t		done;

	/*%
	 * AXFR- and IXFR-specific data.  Only one is used at a time
	 * according to the is_ixfr flag, so this could be a union,
	 * but keeping them separate makes it a bit simpler to clean
	 * things up when destroying the context.
	 */
	dns_rdatacallbacks_t	axfr;

	struct {
		uint32_t 	request_serial;
		uint32_t 	current_serial;
		dns_journal_t	*journal;

	} ixfr;
};

#define XFRIN_MAGIC		  ISC_MAGIC('X', 'f', 'r', 'I')
#define VALID_XFRIN(x)		  ISC_MAGIC_VALID(x, XFRIN_MAGIC)

/**************************************************************************/
/*
 * Forward declarations.
 */

static isc_result_t
xfrin_create(isc_mem_t *mctx,
	     dns_zone_t *zone,
	     dns_db_t *db,
	     isc_task_t *task,
	     isc_timermgr_t *timermgr,
	     isc_socketmgr_t *socketmgr,
	     dns_name_t *zonename,
	     dns_rdataclass_t rdclass,
	     dns_rdatatype_t reqtype,
	     isc_sockaddr_t *masteraddr,
	     isc_sockaddr_t *sourceaddr,
	     isc_dscp_t dscp,
	     dns_tsigkey_t *tsigkey,
	     dns_xfrin_ctx_t **xfrp);

static isc_result_t axfr_init(dns_xfrin_ctx_t *xfr);
static isc_result_t axfr_makedb(dns_xfrin_ctx_t *xfr, dns_db_t **dbp);
static isc_result_t axfr_putdata(dns_xfrin_ctx_t *xfr, dns_diffop_t op,
				   dns_name_t *name, dns_ttl_t ttl,
				   dns_rdata_t *rdata);
static isc_result_t axfr_apply(dns_xfrin_ctx_t *xfr);
static isc_result_t axfr_commit(dns_xfrin_ctx_t *xfr);
static isc_result_t axfr_finalize(dns_xfrin_ctx_t *xfr);

static isc_result_t ixfr_init(dns_xfrin_ctx_t *xfr);
static isc_result_t ixfr_apply(dns_xfrin_ctx_t *xfr);
static isc_result_t ixfr_putdata(dns_xfrin_ctx_t *xfr, dns_diffop_t op,
				 dns_name_t *name, dns_ttl_t ttl,
				 dns_rdata_t *rdata);
static isc_result_t ixfr_commit(dns_xfrin_ctx_t *xfr);

static isc_result_t xfr_rr(dns_xfrin_ctx_t *xfr, dns_name_t *name,
			   uint32_t ttl, dns_rdata_t *rdata);

static isc_result_t xfrin_start(dns_xfrin_ctx_t *xfr);

static void xfrin_connect_done(isc_task_t *task, isc_event_t *event);
static isc_result_t xfrin_send_request(dns_xfrin_ctx_t *xfr);
static void xfrin_send_done(isc_task_t *task, isc_event_t *event);
static void xfrin_recv_done(isc_task_t *task, isc_event_t *event);
static void xfrin_timeout(isc_task_t *task, isc_event_t *event);

static void maybe_free(dns_xfrin_ctx_t *xfr);

static void
xfrin_fail(dns_xfrin_ctx_t *xfr, isc_result_t result, const char *msg);
static isc_result_t
render(dns_message_t *msg, isc_mem_t *mctx, isc_buffer_t *buf);

static void
xfrin_logv(int level, const char *zonetext, isc_sockaddr_t *masteraddr,
	   const char *fmt, va_list ap)
     ISC_FORMAT_PRINTF(4, 0);

static void
xfrin_log1(int level, const char *zonetext, isc_sockaddr_t *masteraddr,
	   const char *fmt, ...)
     ISC_FORMAT_PRINTF(4, 5);

static void
xfrin_log(dns_xfrin_ctx_t *xfr, int level, const char *fmt, ...)
     ISC_FORMAT_PRINTF(3, 4);

/**************************************************************************/
/*
 * AXFR handling
 */

static isc_result_t
axfr_init(dns_xfrin_ctx_t *xfr) {
	isc_result_t result;

	xfr->is_ixfr = false;

	if (xfr->db != NULL)
		dns_db_detach(&xfr->db);

	CHECK(axfr_makedb(xfr, &xfr->db));
	dns_rdatacallbacks_init(&xfr->axfr);
	CHECK(dns_db_beginload(xfr->db, &xfr->axfr));
	result = ISC_R_SUCCESS;
 failure:
	return (result);
}

static isc_result_t
axfr_makedb(dns_xfrin_ctx_t *xfr, dns_db_t **dbp) {
	isc_result_t result;

	result = dns_db_create(xfr->mctx, /* XXX */
			       "rbt",	/* XXX guess */
			       &xfr->name,
			       dns_dbtype_zone,
			       xfr->rdclass,
			       0, NULL, /* XXX guess */
			       dbp);
	if (result == ISC_R_SUCCESS) {
		dns_zone_rpz_enable_db(xfr->zone, *dbp);
		dns_zone_catz_enable_db(xfr->zone, *dbp);
	}
	return (result);
}

static isc_result_t
axfr_putdata(dns_xfrin_ctx_t *xfr, dns_diffop_t op,
	     dns_name_t *name, dns_ttl_t ttl, dns_rdata_t *rdata)
{
	isc_result_t result;

	dns_difftuple_t *tuple = NULL;

	if (rdata->rdclass != xfr->rdclass)
		return(DNS_R_BADCLASS);

	CHECK(dns_zone_checknames(xfr->zone, name, rdata));
	CHECK(dns_difftuple_create(xfr->diff.mctx, op,
				   name, ttl, rdata, &tuple));
	dns_diff_append(&xfr->diff, &tuple);
	if (++xfr->difflen > 100)
		CHECK(axfr_apply(xfr));
	result = ISC_R_SUCCESS;
 failure:
	return (result);
}

/*
 * Store a set of AXFR RRs in the database.
 */
static isc_result_t
axfr_apply(dns_xfrin_ctx_t *xfr) {
	isc_result_t result;
	uint64_t records;

	CHECK(dns_diff_load(&xfr->diff, xfr->axfr.add, xfr->axfr.add_private));
	xfr->difflen = 0;
	dns_diff_clear(&xfr->diff);
	if (xfr->maxrecords != 0U) {
		result = dns_db_getsize(xfr->db, xfr->ver, &records, NULL);
		if (result == ISC_R_SUCCESS && records > xfr->maxrecords) {
			result = DNS_R_TOOMANYRECORDS;
			goto failure;
		}
	}
	result = ISC_R_SUCCESS;
 failure:
	return (result);
}

static isc_result_t
axfr_commit(dns_xfrin_ctx_t *xfr) {
	isc_result_t result;

	CHECK(axfr_apply(xfr));
	CHECK(dns_db_endload(xfr->db, &xfr->axfr));

	result = ISC_R_SUCCESS;
 failure:
	return (result);
}

static isc_result_t
axfr_finalize(dns_xfrin_ctx_t *xfr) {
	isc_result_t result;

	CHECK(dns_zone_replacedb(xfr->zone, xfr->db, true));

	result = ISC_R_SUCCESS;
 failure:
	return (result);
}

/**************************************************************************/
/*
 * IXFR handling
 */

static isc_result_t
ixfr_init(dns_xfrin_ctx_t *xfr) {
	isc_result_t result;
	char *journalfile;

	if (xfr->reqtype != dns_rdatatype_ixfr) {
		xfrin_log(xfr, ISC_LOG_ERROR,
			  "got incremental response to AXFR request");
		return (DNS_R_FORMERR);
	}

	xfr->is_ixfr = true;
	INSIST(xfr->db != NULL);
	xfr->difflen = 0;

	journalfile = dns_zone_getjournal(xfr->zone);
	if (journalfile != NULL)
		CHECK(dns_journal_open(xfr->mctx, journalfile,
				       DNS_JOURNAL_CREATE, &xfr->ixfr.journal));

	result = ISC_R_SUCCESS;
 failure:
	return (result);
}

static isc_result_t
ixfr_putdata(dns_xfrin_ctx_t *xfr, dns_diffop_t op,
	     dns_name_t *name, dns_ttl_t ttl, dns_rdata_t *rdata)
{
	isc_result_t result;
	dns_difftuple_t *tuple = NULL;

	if (rdata->rdclass != xfr->rdclass)
		return(DNS_R_BADCLASS);

	if (op == DNS_DIFFOP_ADD)
		CHECK(dns_zone_checknames(xfr->zone, name, rdata));
	CHECK(dns_difftuple_create(xfr->diff.mctx, op,
				   name, ttl, rdata, &tuple));
	dns_diff_append(&xfr->diff, &tuple);
	if (++xfr->difflen > 100)
		CHECK(ixfr_apply(xfr));
	result = ISC_R_SUCCESS;
 failure:
	return (result);
}

/*
 * Apply a set of IXFR changes to the database.
 */
static isc_result_t
ixfr_apply(dns_xfrin_ctx_t *xfr) {
	isc_result_t result;
	uint64_t records;

	if (xfr->ver == NULL) {
		CHECK(dns_db_newversion(xfr->db, &xfr->ver));
		if (xfr->ixfr.journal != NULL)
			CHECK(dns_journal_begin_transaction(xfr->ixfr.journal));
	}
	CHECK(dns_diff_apply(&xfr->diff, xfr->db, xfr->ver));
	if (xfr->maxrecords != 0U) {
		result = dns_db_getsize(xfr->db, xfr->ver, &records, NULL);
		if (result == ISC_R_SUCCESS && records > xfr->maxrecords) {
			result = DNS_R_TOOMANYRECORDS;
			goto failure;
		}
	}
	if (xfr->ixfr.journal != NULL) {
		result = dns_journal_writediff(xfr->ixfr.journal, &xfr->diff);
		if (result != ISC_R_SUCCESS)
			goto failure;
	}
	dns_diff_clear(&xfr->diff);
	xfr->difflen = 0;
	result = ISC_R_SUCCESS;
 failure:
	return (result);
}

static isc_result_t
ixfr_commit(dns_xfrin_ctx_t *xfr) {
	isc_result_t result;

	CHECK(ixfr_apply(xfr));
	if (xfr->ver != NULL) {
		/* XXX enter ready-to-commit state here */
		if (xfr->ixfr.journal != NULL)
			CHECK(dns_journal_commit(xfr->ixfr.journal));
		dns_db_closeversion(xfr->db, &xfr->ver, true);
		dns_zone_markdirty(xfr->zone);
	}
	result = ISC_R_SUCCESS;
 failure:
	return (result);
}

/**************************************************************************/
/*
 * Common AXFR/IXFR protocol code
 */

/*
 * Handle a single incoming resource record according to the current
 * state.
 */
static isc_result_t
xfr_rr(dns_xfrin_ctx_t *xfr, dns_name_t *name, uint32_t ttl,
       dns_rdata_t *rdata)
{
	isc_result_t result;

	xfr->nrecs++;

	if (rdata->type == dns_rdatatype_none ||
	    dns_rdatatype_ismeta(rdata->type))
		FAIL(DNS_R_FORMERR);

 redo:
	switch (xfr->state) {
	case XFRST_SOAQUERY:
		if (rdata->type != dns_rdatatype_soa) {
			xfrin_log(xfr, ISC_LOG_ERROR,
				  "non-SOA response to SOA query");
			FAIL(DNS_R_FORMERR);
		}
		xfr->end_serial = dns_soa_getserial(rdata);
		if (!DNS_SERIAL_GT(xfr->end_serial, xfr->ixfr.request_serial) &&
		    !dns_zone_isforced(xfr->zone)) {
			xfrin_log(xfr, ISC_LOG_DEBUG(3),
				  "requested serial %u, "
				  "master has %u, not updating",
				  xfr->ixfr.request_serial, xfr->end_serial);
			FAIL(DNS_R_UPTODATE);
		}
		xfr->state = XFRST_GOTSOA;
		break;

	case XFRST_GOTSOA:
		/*
		 * Skip other records in the answer section.
		 */
		break;

	case XFRST_INITIALSOA:
		if (rdata->type != dns_rdatatype_soa) {
			xfrin_log(xfr, ISC_LOG_ERROR,
				  "first RR in zone transfer must be SOA");
			FAIL(DNS_R_FORMERR);
		}
		/*
		 * Remember the serial number in the initial SOA.
		 * We need it to recognize the end of an IXFR.
		 */
		xfr->end_serial = dns_soa_getserial(rdata);
		if (xfr->reqtype == dns_rdatatype_ixfr &&
		    ! DNS_SERIAL_GT(xfr->end_serial, xfr->ixfr.request_serial)
		    && !dns_zone_isforced(xfr->zone))
		{
			/*
			 * This must be the single SOA record that is
			 * sent when the current version on the master
			 * is not newer than the version in the request.
			 */
			xfrin_log(xfr, ISC_LOG_DEBUG(3),
				  "requested serial %u, "
				  "master has %u, not updating",
				  xfr->ixfr.request_serial, xfr->end_serial);
			FAIL(DNS_R_UPTODATE);
		}
		if (xfr->reqtype == dns_rdatatype_axfr)
			xfr->checkid = false;
		xfr->state = XFRST_FIRSTDATA;
		break;

	case XFRST_FIRSTDATA:
		/*
		 * If the transfer begins with one SOA record, it is an AXFR,
		 * if it begins with two SOAs, it is an IXFR.
		 */
		if (xfr->reqtype == dns_rdatatype_ixfr &&
		    rdata->type == dns_rdatatype_soa &&
		    xfr->ixfr.request_serial == dns_soa_getserial(rdata)) {
			xfrin_log(xfr, ISC_LOG_DEBUG(3),
				  "got incremental response");
			CHECK(ixfr_init(xfr));
			xfr->state = XFRST_IXFR_DELSOA;
		} else {
			xfrin_log(xfr, ISC_LOG_DEBUG(3),
				  "got nonincremental response");
			CHECK(axfr_init(xfr));
			xfr->state = XFRST_AXFR;
		}
		goto redo;

	case XFRST_IXFR_DELSOA:
		INSIST(rdata->type == dns_rdatatype_soa);
		CHECK(ixfr_putdata(xfr, DNS_DIFFOP_DEL, name, ttl, rdata));
		xfr->state = XFRST_IXFR_DEL;
		break;

	case XFRST_IXFR_DEL:
		if (rdata->type == dns_rdatatype_soa) {
			uint32_t soa_serial = dns_soa_getserial(rdata);
			xfr->state = XFRST_IXFR_ADDSOA;
			xfr->ixfr.current_serial = soa_serial;
			goto redo;
		}
		CHECK(ixfr_putdata(xfr, DNS_DIFFOP_DEL, name, ttl, rdata));
		break;

	case XFRST_IXFR_ADDSOA:
		INSIST(rdata->type == dns_rdatatype_soa);
		CHECK(ixfr_putdata(xfr, DNS_DIFFOP_ADD, name, ttl, rdata));
		xfr->state = XFRST_IXFR_ADD;
		break;

	case XFRST_IXFR_ADD:
		if (rdata->type == dns_rdatatype_soa) {
			uint32_t soa_serial = dns_soa_getserial(rdata);
			if (soa_serial == xfr->end_serial) {
				CHECK(ixfr_commit(xfr));
				xfr->state = XFRST_IXFR_END;
				break;
			} else if (soa_serial != xfr->ixfr.current_serial) {
				xfrin_log(xfr, ISC_LOG_ERROR,
					  "IXFR out of sync: "
					  "expected serial %u, got %u",
					  xfr->ixfr.current_serial, soa_serial);
				FAIL(DNS_R_FORMERR);
			} else {
				CHECK(ixfr_commit(xfr));
				xfr->state = XFRST_IXFR_DELSOA;
				goto redo;
			}
		}
		if (rdata->type == dns_rdatatype_ns &&
		    dns_name_iswildcard(name))
			FAIL(DNS_R_INVALIDNS);
		CHECK(ixfr_putdata(xfr, DNS_DIFFOP_ADD, name, ttl, rdata));
		break;

	case XFRST_AXFR:
		/*
		 * Old BINDs sent cross class A records for non IN classes.
		 */
		if (rdata->type == dns_rdatatype_a &&
		    rdata->rdclass != xfr->rdclass &&
		    xfr->rdclass != dns_rdataclass_in)
			break;
		CHECK(axfr_putdata(xfr, DNS_DIFFOP_ADD, name, ttl, rdata));
		if (rdata->type == dns_rdatatype_soa) {
			CHECK(axfr_commit(xfr));
			xfr->state = XFRST_AXFR_END;
			break;
		}
		break;
	case XFRST_AXFR_END:
	case XFRST_IXFR_END:
		FAIL(DNS_R_EXTRADATA);
		/* NOTREACHED */
		/* FALLTHROUGH */
	default:
		INSIST(0);
		ISC_UNREACHABLE();
	}
	result = ISC_R_SUCCESS;
 failure:
	return (result);
}

isc_result_t
dns_xfrin_create(dns_zone_t *zone, dns_rdatatype_t xfrtype,
		 isc_sockaddr_t *masteraddr, dns_tsigkey_t *tsigkey,
		 isc_mem_t *mctx, isc_timermgr_t *timermgr,
		 isc_socketmgr_t *socketmgr, isc_task_t *task,
		 dns_xfrindone_t done, dns_xfrin_ctx_t **xfrp)
{
	isc_sockaddr_t sourceaddr;
	isc_dscp_t dscp;

	switch (isc_sockaddr_pf(masteraddr)) {
	case PF_INET:
		sourceaddr = *dns_zone_getxfrsource4(zone);
		dscp = dns_zone_getxfrsource4dscp(zone);
		break;
	case PF_INET6:
		sourceaddr = *dns_zone_getxfrsource6(zone);
		dscp = dns_zone_getxfrsource6dscp(zone);
		break;
	default:
		INSIST(0);
		ISC_UNREACHABLE();
	}

	return(dns_xfrin_create3(zone, xfrtype, masteraddr, &sourceaddr,
				 dscp, tsigkey, mctx, timermgr, socketmgr,
				 task, done, xfrp));
}

isc_result_t
dns_xfrin_create2(dns_zone_t *zone, dns_rdatatype_t xfrtype,
		  isc_sockaddr_t *masteraddr, isc_sockaddr_t *sourceaddr,
		  dns_tsigkey_t *tsigkey, isc_mem_t *mctx,
		  isc_timermgr_t *timermgr, isc_socketmgr_t *socketmgr,
		  isc_task_t *task, dns_xfrindone_t done,
		  dns_xfrin_ctx_t **xfrp)
{
	return (dns_xfrin_create3(zone, xfrtype, masteraddr, sourceaddr, -1,
				  tsigkey, mctx, timermgr, socketmgr, task,
				  done, xfrp));
}

isc_result_t
dns_xfrin_create3(dns_zone_t *zone, dns_rdatatype_t xfrtype,
		  isc_sockaddr_t *masteraddr, isc_sockaddr_t *sourceaddr,
		  isc_dscp_t dscp, dns_tsigkey_t *tsigkey, isc_mem_t *mctx,
		  isc_timermgr_t *timermgr, isc_socketmgr_t *socketmgr,
		  isc_task_t *task, dns_xfrindone_t done,
		  dns_xfrin_ctx_t **xfrp)
{
	dns_name_t *zonename = dns_zone_getorigin(zone);
	dns_xfrin_ctx_t *xfr = NULL;
	isc_result_t result;
	dns_db_t *db = NULL;

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

	(void)dns_zone_getdb(zone, &db);

	if (xfrtype == dns_rdatatype_soa || xfrtype == dns_rdatatype_ixfr)
		REQUIRE(db != NULL);

	CHECK(xfrin_create(mctx, zone, db, task, timermgr, socketmgr, zonename,
			   dns_zone_getclass(zone), xfrtype, masteraddr,
			   sourceaddr, dscp, tsigkey, &xfr));

	CHECK(xfrin_start(xfr));

	xfr->done = done;
	if (xfr->done != NULL)
		xfr->refcount++;
	*xfrp = xfr;

 failure:
	if (db != NULL)
		dns_db_detach(&db);
	if (result != ISC_R_SUCCESS) {
		char zonetext[DNS_NAME_MAXTEXT+32];
		dns_zone_name(zone, zonetext, sizeof(zonetext));
		xfrin_log1(ISC_LOG_ERROR, zonetext, masteraddr,
			   "zone transfer setup failed");
	}
	return (result);
}

void
dns_xfrin_shutdown(dns_xfrin_ctx_t *xfr) {
	if (! xfr->shuttingdown)
		xfrin_fail(xfr, ISC_R_CANCELED, "shut down");
}

void
dns_xfrin_attach(dns_xfrin_ctx_t *source, dns_xfrin_ctx_t **target) {
	REQUIRE(target != NULL && *target == NULL);
	source->refcount++;
	*target = source;
}

void
dns_xfrin_detach(dns_xfrin_ctx_t **xfrp) {
	dns_xfrin_ctx_t *xfr = *xfrp;
	INSIST(xfr->refcount > 0);
	xfr->refcount--;
	maybe_free(xfr);
	*xfrp = NULL;
}

static void
xfrin_cancelio(dns_xfrin_ctx_t *xfr) {
	if (xfr->connects > 0) {
		isc_socket_cancel(xfr->socket, xfr->task,
				  ISC_SOCKCANCEL_CONNECT);
	} else if (xfr->recvs > 0) {
		dns_tcpmsg_cancelread(&xfr->tcpmsg);
	} else if (xfr->sends > 0) {
		isc_socket_cancel(xfr->socket, xfr->task,
				  ISC_SOCKCANCEL_SEND);
	}
}

static void
xfrin_reset(dns_xfrin_ctx_t *xfr) {
	REQUIRE(VALID_XFRIN(xfr));

	xfrin_log(xfr, ISC_LOG_INFO, "resetting");

	xfrin_cancelio(xfr);

	if (xfr->socket != NULL)
		isc_socket_detach(&xfr->socket);

	if (xfr->lasttsig != NULL)
		isc_buffer_free(&xfr->lasttsig);

	dns_diff_clear(&xfr->diff);
	xfr->difflen = 0;

	if (xfr->ixfr.journal != NULL)
		dns_journal_destroy(&xfr->ixfr.journal);

	if (xfr->axfr.add_private != NULL)
		(void)dns_db_endload(xfr->db, &xfr->axfr);

	if (xfr->tcpmsg_valid) {
		dns_tcpmsg_invalidate(&xfr->tcpmsg);
		xfr->tcpmsg_valid = false;
	}

	if (xfr->ver != NULL)
		dns_db_closeversion(xfr->db, &xfr->ver, false);
}


static void
xfrin_fail(dns_xfrin_ctx_t *xfr, isc_result_t result, const char *msg) {
	if (result != DNS_R_UPTODATE && result != DNS_R_TOOMANYRECORDS) {
		xfrin_log(xfr, ISC_LOG_ERROR, "%s: %s",
			  msg, isc_result_totext(result));
		if (xfr->is_ixfr)
			/* Pass special result code to force AXFR retry */
			result = DNS_R_BADIXFR;
	}
	xfrin_cancelio(xfr);
	/*
	 * Close the journal.
	 */
	if (xfr->ixfr.journal != NULL)
		dns_journal_destroy(&xfr->ixfr.journal);
	if (xfr->done != NULL) {
		(xfr->done)(xfr->zone, result);
		xfr->done = NULL;
	}
	xfr->shuttingdown = true;
	xfr->shutdown_result = result;
	maybe_free(xfr);
}

static isc_result_t
xfrin_create(isc_mem_t *mctx,
	     dns_zone_t *zone,
	     dns_db_t *db,
	     isc_task_t *task,
	     isc_timermgr_t *timermgr,
	     isc_socketmgr_t *socketmgr,
	     dns_name_t *zonename,
	     dns_rdataclass_t rdclass,
	     dns_rdatatype_t reqtype,
	     isc_sockaddr_t *masteraddr,
	     isc_sockaddr_t *sourceaddr,
	     isc_dscp_t dscp,
	     dns_tsigkey_t *tsigkey,
	     dns_xfrin_ctx_t **xfrp)
{
	dns_xfrin_ctx_t *xfr = NULL;
	isc_result_t result;
	uint32_t tmp;

	xfr = isc_mem_get(mctx, sizeof(*xfr));
	if (xfr == NULL)
		return (ISC_R_NOMEMORY);
	xfr->mctx = NULL;
	isc_mem_attach(mctx, &xfr->mctx);
	xfr->refcount = 0;
	xfr->zone = NULL;
	dns_zone_iattach(zone, &xfr->zone);
	xfr->task = NULL;
	isc_task_attach(task, &xfr->task);
	xfr->timer = NULL;
	xfr->socketmgr = socketmgr;
	xfr->done = NULL;

	xfr->connects = 0;
	xfr->sends = 0;
	xfr->recvs = 0;
	xfr->shuttingdown = false;
	xfr->shutdown_result = ISC_R_UNSET;

	dns_name_init(&xfr->name, NULL);
	xfr->rdclass = rdclass;
	isc_random_get(&tmp);
	xfr->checkid = true;
	xfr->id	= (uint16_t)(tmp & 0xffff);
	xfr->reqtype = reqtype;
	xfr->dscp = dscp;

	/* sockaddr */
	xfr->socket = NULL;
	/* qbuffer */
	/* qbuffer_data */
	/* tcpmsg */
	xfr->tcpmsg_valid = false;

	xfr->db = NULL;
	if (db != NULL)
		dns_db_attach(db, &xfr->db);
	xfr->ver = NULL;
	dns_diff_init(xfr->mctx, &xfr->diff);
	xfr->difflen = 0;

	if (reqtype == dns_rdatatype_soa)
		xfr->state = XFRST_SOAQUERY;
	else
		xfr->state = XFRST_INITIALSOA;
	/* end_serial */

	xfr->nmsg = 0;
	xfr->nrecs = 0;
	xfr->nbytes = 0;
	xfr->maxrecords = dns_zone_getmaxrecords(zone);
	isc_time_now(&xfr->start);

	xfr->tsigkey = NULL;
	if (tsigkey != NULL)
		dns_tsigkey_attach(tsigkey, &xfr->tsigkey);
	xfr->lasttsig = NULL;
	xfr->tsigctx = NULL;
	xfr->sincetsig = 0;
	xfr->is_ixfr = false;

	/* ixfr.request_serial */
	/* ixfr.current_serial */
	xfr->ixfr.journal = NULL;

	xfr->axfr.add = NULL;
	xfr->axfr.add_private = NULL;

	CHECK(dns_name_dup(zonename, mctx, &xfr->name));

	CHECK(isc_timer_create(timermgr, isc_timertype_inactive, NULL, NULL,
			       task, xfrin_timeout, xfr, &xfr->timer));
	CHECK(dns_timer_setidle(xfr->timer,
				dns_zone_getmaxxfrin(xfr->zone),
				dns_zone_getidlein(xfr->zone),
				false));

	xfr->masteraddr = *masteraddr;

	INSIST(isc_sockaddr_pf(masteraddr) == isc_sockaddr_pf(sourceaddr));
	xfr->sourceaddr = *sourceaddr;
	isc_sockaddr_setport(&xfr->sourceaddr, 0);

	/*
	 * Reserve 2 bytes for TCP length at the begining of the buffer.
	 */
	isc_buffer_init(&xfr->qbuffer, &xfr->qbuffer_data[2],
			sizeof(xfr->qbuffer_data) - 2);

	xfr->magic = XFRIN_MAGIC;
	*xfrp = xfr;
	return (ISC_R_SUCCESS);

 failure:
	if (xfr->timer != NULL)
		isc_timer_detach(&xfr->timer);
	if (dns_name_dynamic(&xfr->name))
		dns_name_free(&xfr->name, xfr->mctx);
	if (xfr->tsigkey != NULL)
		dns_tsigkey_detach(&xfr->tsigkey);
	if (xfr->db != NULL)
		dns_db_detach(&xfr->db);
	isc_task_detach(&xfr->task);
	dns_zone_idetach(&xfr->zone);
	isc_mem_putanddetach(&xfr->mctx, xfr, sizeof(*xfr));

	return (result);
}

static isc_result_t
xfrin_start(dns_xfrin_ctx_t *xfr) {
	isc_result_t result;
	CHECK(isc_socket_create(xfr->socketmgr,
				isc_sockaddr_pf(&xfr->sourceaddr),
				isc_sockettype_tcp,
				&xfr->socket));
	isc_socket_setname(xfr->socket, "xfrin", NULL);
#ifndef BROKEN_TCP_BIND_BEFORE_CONNECT
	CHECK(isc_socket_bind(xfr->socket, &xfr->sourceaddr,
			      ISC_SOCKET_REUSEADDRESS));
#endif
	isc_socket_dscp(xfr->socket, xfr->dscp);
	CHECK(isc_socket_connect(xfr->socket, &xfr->masteraddr, xfr->task,
				 xfrin_connect_done, xfr));
	xfr->connects++;
	return (ISC_R_SUCCESS);
 failure:
	xfrin_fail(xfr, result, "failed setting up socket");
	return (result);
}

/* XXX the resolver could use this, too */

static isc_result_t
render(dns_message_t *msg, isc_mem_t *mctx, isc_buffer_t *buf) {
	dns_compress_t cctx;
	bool cleanup_cctx = false;
	isc_result_t result;

	CHECK(dns_compress_init(&cctx, -1, mctx));
	cleanup_cctx = true;
	CHECK(dns_message_renderbegin(msg, &cctx, buf));
	CHECK(dns_message_rendersection(msg, DNS_SECTION_QUESTION, 0));
	CHECK(dns_message_rendersection(msg, DNS_SECTION_ANSWER, 0));
	CHECK(dns_message_rendersection(msg, DNS_SECTION_AUTHORITY, 0));
	CHECK(dns_message_rendersection(msg, DNS_SECTION_ADDITIONAL, 0));
	CHECK(dns_message_renderend(msg));
	result = ISC_R_SUCCESS;
 failure:
	if (cleanup_cctx)
		dns_compress_invalidate(&cctx);
	return (result);
}

/*
 * A connection has been established.
 */
static void
xfrin_connect_done(isc_task_t *task, isc_event_t *event) {
	isc_socket_connev_t *cev = (isc_socket_connev_t *) event;
	dns_xfrin_ctx_t *xfr = (dns_xfrin_ctx_t *) event->ev_arg;
	isc_result_t result = cev->result;
	char sourcetext[ISC_SOCKADDR_FORMATSIZE];
	char signerbuf[DNS_NAME_FORMATSIZE];
	const char *signer = "", *sep = "";
	isc_sockaddr_t sockaddr;
	dns_zonemgr_t * zmgr;
	isc_time_t now;

	REQUIRE(VALID_XFRIN(xfr));

	UNUSED(task);

	INSIST(event->ev_type == ISC_SOCKEVENT_CONNECT);
	isc_event_free(&event);

	xfr->connects--;
	if (xfr->shuttingdown) {
		maybe_free(xfr);
		return;
	}

	zmgr = dns_zone_getmgr(xfr->zone);
	if (zmgr != NULL) {
		if (result != ISC_R_SUCCESS) {
			TIME_NOW(&now);
			dns_zonemgr_unreachableadd(zmgr, &xfr->masteraddr,
						   &xfr->sourceaddr, &now);
			goto failure;
		} else
			dns_zonemgr_unreachabledel(zmgr, &xfr->masteraddr,
						   &xfr->sourceaddr);
	}

	result = isc_socket_getsockname(xfr->socket, &sockaddr);
	if (result == ISC_R_SUCCESS) {
		isc_sockaddr_format(&sockaddr, sourcetext, sizeof(sourcetext));
	} else {
		strlcpy(sourcetext, "<UNKNOWN>", sizeof(sourcetext));
	}

	if (xfr->tsigkey != NULL && xfr->tsigkey->key != NULL) {
		dns_name_format(dst_key_name(xfr->tsigkey->key),
				signerbuf, sizeof(signerbuf));
		sep = " TSIG ";
		signer = signerbuf;
	}

	xfrin_log(xfr, ISC_LOG_INFO, "connected using %s%s%s",
		  sourcetext, sep, signer);

	dns_tcpmsg_init(xfr->mctx, xfr->socket, &xfr->tcpmsg);
	xfr->tcpmsg_valid = true;

	CHECK(xfrin_send_request(xfr));
 failure:
	if (result != ISC_R_SUCCESS)
		xfrin_fail(xfr, result, "failed to connect");
}

/*
 * Convert a tuple into a dns_name_t suitable for inserting
 * into the given dns_message_t.
 */
static isc_result_t
tuple2msgname(dns_difftuple_t *tuple, dns_message_t *msg, dns_name_t **target)
{
	isc_result_t result;
	dns_rdata_t *rdata = NULL;
	dns_rdatalist_t *rdl = NULL;
	dns_rdataset_t *rds = NULL;
	dns_name_t *name = NULL;

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

	CHECK(dns_message_gettemprdata(msg, &rdata));
	dns_rdata_init(rdata);
	dns_rdata_clone(&tuple->rdata, rdata);

	CHECK(dns_message_gettemprdatalist(msg, &rdl));
	dns_rdatalist_init(rdl);
	rdl->type = tuple->rdata.type;
	rdl->rdclass = tuple->rdata.rdclass;
	rdl->ttl = tuple->ttl;
	ISC_LIST_APPEND(rdl->rdata, rdata, link);

	CHECK(dns_message_gettemprdataset(msg, &rds));
	CHECK(dns_rdatalist_tordataset(rdl, rds));

	CHECK(dns_message_gettempname(msg, &name));
	dns_name_init(name, NULL);
	dns_name_clone(&tuple->name, name);
	ISC_LIST_APPEND(name->list, rds, link);

	*target = name;
	return (ISC_R_SUCCESS);

 failure:

	if (rds != NULL) {
		dns_rdataset_disassociate(rds);
		dns_message_puttemprdataset(msg, &rds);
	}
	if (rdl != NULL) {
		ISC_LIST_UNLINK(rdl->rdata, rdata, link);
		dns_message_puttemprdatalist(msg, &rdl);
	}
	if (rdata != NULL)
		dns_message_puttemprdata(msg, &rdata);

	return (result);
}


/*
 * Build an *XFR request and send its length prefix.
 */
static isc_result_t
xfrin_send_request(dns_xfrin_ctx_t *xfr) {
	isc_result_t result;
	isc_region_t region;
	dns_rdataset_t *qrdataset = NULL;
	dns_message_t *msg = NULL;
	dns_difftuple_t *soatuple = NULL;
	dns_name_t *qname = NULL;
	dns_dbversion_t *ver = NULL;
	dns_name_t *msgsoaname = NULL;

	/* Create the request message */
	CHECK(dns_message_create(xfr->mctx, DNS_MESSAGE_INTENTRENDER, &msg));
	CHECK(dns_message_settsigkey(msg, xfr->tsigkey));

	/* Create a name for the question section. */
	CHECK(dns_message_gettempname(msg, &qname));
	dns_name_init(qname, NULL);
	dns_name_clone(&xfr->name, qname);

	/* Formulate the question and attach it to the question name. */
	CHECK(dns_message_gettemprdataset(msg, &qrdataset));
	dns_rdataset_makequestion(qrdataset, xfr->rdclass, xfr->reqtype);
	ISC_LIST_APPEND(qname->list, qrdataset, link);
	qrdataset = NULL;

	dns_message_addname(msg, qname, DNS_SECTION_QUESTION);
	qname = NULL;

	if (xfr->reqtype == dns_rdatatype_ixfr) {
		/* Get the SOA and add it to the authority section. */
		/* XXX is using the current version the right thing? */
		dns_db_currentversion(xfr->db, &ver);
		CHECK(dns_db_createsoatuple(xfr->db, ver, xfr->mctx,
					    DNS_DIFFOP_EXISTS, &soatuple));
		xfr->ixfr.request_serial = dns_soa_getserial(&soatuple->rdata);
		xfr->ixfr.current_serial = xfr->ixfr.request_serial;
		xfrin_log(xfr, ISC_LOG_DEBUG(3),
			  "requesting IXFR for serial %u",
			  xfr->ixfr.request_serial);

		CHECK(tuple2msgname(soatuple, msg, &msgsoaname));
		dns_message_addname(msg, msgsoaname, DNS_SECTION_AUTHORITY);
	} else if (xfr->reqtype == dns_rdatatype_soa)
		CHECK(dns_db_getsoaserial(xfr->db, NULL,
					  &xfr->ixfr.request_serial));

	xfr->checkid = true;
	xfr->id++;
	xfr->nmsg = 0;
	xfr->nrecs = 0;
	xfr->nbytes = 0;
	isc_time_now(&xfr->start);
	msg->id = xfr->id;
	if (xfr->tsigctx != NULL)
		dst_context_destroy(&xfr->tsigctx);

	CHECK(render(msg, xfr->mctx, &xfr->qbuffer));

	/*
	 * Free the last tsig, if there is one.
	 */
	if (xfr->lasttsig != NULL)
		isc_buffer_free(&xfr->lasttsig);

	/*
	 * Save the query TSIG and don't let message_destroy free it.
	 */
	CHECK(dns_message_getquerytsig(msg, xfr->mctx, &xfr->lasttsig));

	isc_buffer_usedregion(&xfr->qbuffer, &region);
	INSIST(region.length <= 65535);

	/*
	 * Record message length and adjust region to include TCP
	 * length field.
	 */
	xfr->qbuffer_data[0] = (region.length >> 8) & 0xff;
	xfr->qbuffer_data[1] = region.length & 0xff;
	region.base -= 2;
	region.length += 2;
	CHECK(isc_socket_send(xfr->socket, &region, xfr->task,
			      xfrin_send_done, xfr));
	xfr->sends++;

 failure:
	if (qname != NULL)
		dns_message_puttempname(msg, &qname);
	if (qrdataset != NULL)
		dns_message_puttemprdataset(msg, &qrdataset);
	if (msg != NULL)
		dns_message_destroy(&msg);
	if (soatuple != NULL)
		dns_difftuple_free(&soatuple);
	if (ver != NULL)
		dns_db_closeversion(xfr->db, &ver, false);
	return (result);
}

static void
xfrin_send_done(isc_task_t *task, isc_event_t *event) {
	isc_socketevent_t *sev = (isc_socketevent_t *) event;
	dns_xfrin_ctx_t *xfr = (dns_xfrin_ctx_t *) event->ev_arg;
	isc_result_t result;

	REQUIRE(VALID_XFRIN(xfr));

	UNUSED(task);

	INSIST(event->ev_type == ISC_SOCKEVENT_SENDDONE);

	xfr->sends--;
	xfrin_log(xfr, ISC_LOG_DEBUG(3), "sent request data");
	CHECK(sev->result);

	CHECK(dns_tcpmsg_readmessage(&xfr->tcpmsg, xfr->task,
				     xfrin_recv_done, xfr));
	xfr->recvs++;
 failure:
	isc_event_free(&event);
	if (result != ISC_R_SUCCESS)
		xfrin_fail(xfr, result, "failed sending request data");
}


static void
xfrin_recv_done(isc_task_t *task, isc_event_t *ev) {
	dns_xfrin_ctx_t *xfr = (dns_xfrin_ctx_t *) ev->ev_arg;
	isc_result_t result;
	dns_message_t *msg = NULL;
	dns_name_t *name;
	dns_tcpmsg_t *tcpmsg;
	dns_name_t *tsigowner = NULL;

	REQUIRE(VALID_XFRIN(xfr));

	UNUSED(task);

	INSIST(ev->ev_type == DNS_EVENT_TCPMSG);
	tcpmsg = ev->ev_sender;
	isc_event_free(&ev);

	xfr->recvs--;
	if (xfr->shuttingdown) {
		maybe_free(xfr);
		return;
	}

	CHECK(tcpmsg->result);

	xfrin_log(xfr, ISC_LOG_DEBUG(7), "received %u bytes",
		  tcpmsg->buffer.used);

	CHECK(isc_timer_touch(xfr->timer));

	CHECK(dns_message_create(xfr->mctx, DNS_MESSAGE_INTENTPARSE, &msg));

	CHECK(dns_message_settsigkey(msg, xfr->tsigkey));
	CHECK(dns_message_setquerytsig(msg, xfr->lasttsig));

	msg->tsigctx = xfr->tsigctx;
	xfr->tsigctx = NULL;

	dns_message_setclass(msg, xfr->rdclass);

	if (xfr->nmsg > 0)
		msg->tcp_continuation = 1;

	result = dns_message_parse(msg, &tcpmsg->buffer,
				   DNS_MESSAGEPARSE_PRESERVEORDER);

	if (result == ISC_R_SUCCESS)
		dns_message_logpacket2(msg, "received message from",
				       &tcpmsg->address,
				       DNS_LOGCATEGORY_XFER_IN,
				       DNS_LOGMODULE_XFER_IN,
				       ISC_LOG_DEBUG(10), xfr->mctx);
	else
		xfrin_log(xfr, ISC_LOG_DEBUG(10), "dns_message_parse: %s",
			  dns_result_totext(result));

	if (result != ISC_R_SUCCESS || msg->rcode != dns_rcode_noerror ||
	    msg->opcode != dns_opcode_query ||msg->rdclass != xfr->rdclass ||
	    (xfr->checkid && msg->id != xfr->id)) {
		if (result == ISC_R_SUCCESS && msg->rcode != dns_rcode_noerror)
			result = ISC_RESULTCLASS_DNSRCODE + msg->rcode; /*XXX*/
		else if (result == ISC_R_SUCCESS &&
			 msg->opcode != dns_opcode_query)
			result = DNS_R_UNEXPECTEDOPCODE;
		else if (result == ISC_R_SUCCESS &&
			 msg->rdclass != xfr->rdclass)
			result = DNS_R_BADCLASS;
		else if (result == ISC_R_SUCCESS || result == DNS_R_NOERROR)
			result = DNS_R_UNEXPECTEDID;
		if (xfr->reqtype == dns_rdatatype_axfr ||
		    xfr->reqtype == dns_rdatatype_soa)
			goto failure;
		xfrin_log(xfr, ISC_LOG_DEBUG(3), "got %s, retrying with AXFR",
		       isc_result_totext(result));
 try_axfr:
		dns_message_destroy(&msg);
		xfrin_reset(xfr);
		xfr->reqtype = dns_rdatatype_soa;
		xfr->state = XFRST_SOAQUERY;
		(void)xfrin_start(xfr);
		return;
	}

	/*
	 * Does the server know about IXFR?  If it doesn't we will get
	 * a message with a empty answer section or a potentially a CNAME /
	 * DNAME, the later is handled by xfr_rr() which will return FORMERR
	 * if the first RR in the answer section is not a SOA record.
	 */
	if (xfr->reqtype == dns_rdatatype_ixfr &&
	    xfr->state == XFRST_INITIALSOA &&
	    msg->counts[DNS_SECTION_ANSWER] == 0) {
		xfrin_log(xfr, ISC_LOG_DEBUG(3),
			  "empty answer section, retrying with AXFR");
		goto try_axfr;
	}

	if (xfr->reqtype == dns_rdatatype_soa &&
	    (msg->flags & DNS_MESSAGEFLAG_AA) == 0) {
		FAIL(DNS_R_NOTAUTHORITATIVE);
	}


	result = dns_message_checksig(msg, dns_zone_getview(xfr->zone));
	if (result != ISC_R_SUCCESS) {
		xfrin_log(xfr, ISC_LOG_DEBUG(3), "TSIG check failed: %s",
		       isc_result_totext(result));
		goto failure;
	}

	for (result = dns_message_firstname(msg, DNS_SECTION_ANSWER);
	     result == ISC_R_SUCCESS;
	     result = dns_message_nextname(msg, DNS_SECTION_ANSWER))
	{
		dns_rdataset_t *rds;

		name = NULL;
		dns_message_currentname(msg, DNS_SECTION_ANSWER, &name);
		for (rds = ISC_LIST_HEAD(name->list);
		     rds != NULL;
		     rds = ISC_LIST_NEXT(rds, link))
		{
			for (result = dns_rdataset_first(rds);
			     result == ISC_R_SUCCESS;
			     result = dns_rdataset_next(rds))
			{
				dns_rdata_t rdata = DNS_RDATA_INIT;
				dns_rdataset_current(rds, &rdata);
				CHECK(xfr_rr(xfr, name, rds->ttl, &rdata));
			}
		}
	}
	if (result != ISC_R_NOMORE)
		goto failure;

	if (dns_message_gettsig(msg, &tsigowner) != NULL) {
		/*
		 * Reset the counter.
		 */
		xfr->sincetsig = 0;

		/*
		 * Free the last tsig, if there is one.
		 */
		if (xfr->lasttsig != NULL)
			isc_buffer_free(&xfr->lasttsig);

		/*
		 * Update the last tsig pointer.
		 */
		CHECK(dns_message_getquerytsig(msg, xfr->mctx,
					       &xfr->lasttsig));

	} else if (dns_message_gettsigkey(msg) != NULL) {
		xfr->sincetsig++;
		if (xfr->sincetsig > 100 || xfr->nmsg == 0 ||
		    xfr->state == XFRST_AXFR_END ||
		    xfr->state == XFRST_IXFR_END)
		{
			result = DNS_R_EXPECTEDTSIG;
			goto failure;
		}
	}

	/*
	 * Update the number of messages received.
	 */
	xfr->nmsg++;

	/*
	 * Update the number of bytes received.
	 */
	xfr->nbytes += tcpmsg->buffer.used;

	/*
	 * Take the context back.
	 */
	INSIST(xfr->tsigctx == NULL);
	xfr->tsigctx = msg->tsigctx;
	msg->tsigctx = NULL;

	dns_message_destroy(&msg);

	switch (xfr->state) {
	case XFRST_GOTSOA:
		xfr->reqtype = dns_rdatatype_axfr;
		xfr->state = XFRST_INITIALSOA;
		CHECK(xfrin_send_request(xfr));
		break;
	case XFRST_AXFR_END:
		CHECK(axfr_finalize(xfr));
		/* FALLTHROUGH */
	case XFRST_IXFR_END:
		/*
		 * Close the journal.
		 */
		if (xfr->ixfr.journal != NULL)
			dns_journal_destroy(&xfr->ixfr.journal);

		/*
		 * Inform the caller we succeeded.
		 */
		if (xfr->done != NULL) {
			(xfr->done)(xfr->zone, ISC_R_SUCCESS);
			xfr->done = NULL;
		}
		/*
		 * We should have no outstanding events at this
		 * point, thus maybe_free() should succeed.
		 */
		xfr->shuttingdown = true;
		xfr->shutdown_result = ISC_R_SUCCESS;
		maybe_free(xfr);
		break;
	default:
		/*
		 * Read the next message.
		 */
		CHECK(dns_tcpmsg_readmessage(&xfr->tcpmsg, xfr->task,
					     xfrin_recv_done, xfr));
		xfr->recvs++;
	}
	return;

 failure:
	if (msg != NULL)
		dns_message_destroy(&msg);
	if (result != ISC_R_SUCCESS)
		xfrin_fail(xfr, result, "failed while receiving responses");
}

static void
xfrin_timeout(isc_task_t *task, isc_event_t *event) {
	dns_xfrin_ctx_t *xfr = (dns_xfrin_ctx_t *) event->ev_arg;

	REQUIRE(VALID_XFRIN(xfr));

	UNUSED(task);

	isc_event_free(&event);
	/*
	 * This will log "giving up: timeout".
	 */
	xfrin_fail(xfr, ISC_R_TIMEDOUT, "giving up");
}

static void
maybe_free(dns_xfrin_ctx_t *xfr) {
	uint64_t msecs;
	uint64_t persec;
	const char *result_str;

	REQUIRE(VALID_XFRIN(xfr));

	if (! xfr->shuttingdown || xfr->refcount != 0 ||
	    xfr->connects != 0 || xfr->sends != 0 ||
	    xfr->recvs != 0)
		return;

	INSIST(! xfr->shuttingdown || xfr->shutdown_result != ISC_R_UNSET);

	/* If we're called through dns_xfrin_detach() and are not
	 * shutting down, we can't know what the transfer status is as
	 * we are only called when the last reference is lost.
	 */
	result_str = (xfr->shuttingdown ?
		      isc_result_totext(xfr->shutdown_result) : "unknown");
	xfrin_log(xfr, ISC_LOG_INFO, "Transfer status: %s", result_str);

	/*
	 * Calculate the length of time the transfer took,
	 * and print a log message with the bytes and rate.
	 */
	isc_time_now(&xfr->end);
	msecs = isc_time_microdiff(&xfr->end, &xfr->start) / 1000;
	if (msecs == 0)
		msecs = 1;
	persec = (xfr->nbytes * 1000) / msecs;
	xfrin_log(xfr, ISC_LOG_INFO,
		  "Transfer completed: %d messages, %d records, "
		  "%" PRIu64 " bytes, "
		  "%u.%03u secs (%u bytes/sec)",
		  xfr->nmsg, xfr->nrecs, xfr->nbytes,
		  (unsigned int) (msecs / 1000), (unsigned int) (msecs % 1000),
		  (unsigned int) persec);

	if (xfr->socket != NULL)
		isc_socket_detach(&xfr->socket);

	if (xfr->timer != NULL)
		isc_timer_detach(&xfr->timer);

	if (xfr->task != NULL)
		isc_task_detach(&xfr->task);

	if (xfr->tsigkey != NULL)
		dns_tsigkey_detach(&xfr->tsigkey);

	if (xfr->lasttsig != NULL)
		isc_buffer_free(&xfr->lasttsig);

	dns_diff_clear(&xfr->diff);

	if (xfr->ixfr.journal != NULL)
		dns_journal_destroy(&xfr->ixfr.journal);

	if (xfr->axfr.add_private != NULL)
		(void)dns_db_endload(xfr->db, &xfr->axfr);

	if (xfr->tcpmsg_valid)
		dns_tcpmsg_invalidate(&xfr->tcpmsg);

	if (xfr->tsigctx != NULL)
		dst_context_destroy(&xfr->tsigctx);

	if ((xfr->name.attributes & DNS_NAMEATTR_DYNAMIC) != 0)
		dns_name_free(&xfr->name, xfr->mctx);

	if (xfr->ver != NULL)
		dns_db_closeversion(xfr->db, &xfr->ver, false);

	if (xfr->db != NULL)
		dns_db_detach(&xfr->db);

	if (xfr->zone != NULL)
		dns_zone_idetach(&xfr->zone);

	isc_mem_putanddetach(&xfr->mctx, xfr, sizeof(*xfr));
}

/*
 * Log incoming zone transfer messages in a format like
 * transfer of <zone> from <address>: <message>
 */
static void
xfrin_logv(int level, const char *zonetext, isc_sockaddr_t *masteraddr,
	   const char *fmt, va_list ap)
{
	char mastertext[ISC_SOCKADDR_FORMATSIZE];
	char msgtext[2048];

	isc_sockaddr_format(masteraddr, mastertext, sizeof(mastertext));
	vsnprintf(msgtext, sizeof(msgtext), fmt, ap);

	isc_log_write(dns_lctx, DNS_LOGCATEGORY_XFER_IN,
		      DNS_LOGMODULE_XFER_IN, level,
		      "transfer of '%s' from %s: %s",
		      zonetext, mastertext, msgtext);
}

/*
 * Logging function for use when a xfrin_ctx_t has not yet been created.
 */

static void
xfrin_log1(int level, const char *zonetext, isc_sockaddr_t *masteraddr,
	   const char *fmt, ...)
{
	va_list ap;

	if (isc_log_wouldlog(dns_lctx, level) == false)
		return;

	va_start(ap, fmt);
	xfrin_logv(level, zonetext, masteraddr, fmt, ap);
	va_end(ap);
}

/*
 * Logging function for use when there is a xfrin_ctx_t.
 */

static void
xfrin_log(dns_xfrin_ctx_t *xfr, int level, const char *fmt, ...)
{
	va_list ap;
	char zonetext[DNS_NAME_MAXTEXT+32];

	if (isc_log_wouldlog(dns_lctx, level) == false)
		return;

	dns_zone_name(xfr->zone, zonetext, sizeof(zonetext));

	va_start(ap, fmt);
	xfrin_logv(level, zonetext, &xfr->masteraddr, fmt, ap);
	va_end(ap);
}
