/*
 * Copyright (c) 2016-2024 Apple Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "DNSServerDNSSEC.h"

#include <CoreUtils/CoreUtils.h>
#include <dns_sd.h>
#include <dns_sd_private.h>
#include <mdns/DNSMessage.h>
#include <mdns/dns_relay.h>
#include <mdns/pf.h>
#include <mdns/security.h>
#include <mdns/signed_result.h>
#include <mdns/system.h>
#include <mrc/private.h>
#include <os/feature_private.h>
#include <pcap.h>
#include <Security/SecCertificateRequest.h>
#include <Security/SecTrustSettings.h>

#include CF_RUNTIME_HEADER

#if( TARGET_OS_DARWIN )
	#include <CFNetwork/CFHost.h>
	#include <CoreFoundation/CoreFoundation.h>
	#include <SystemConfiguration/SCPrivate.h>
	#include <dnsinfo.h>
	#include <netdb.h>
	#include <netinet6/in6_var.h>
	#include <netinet6/nd6.h>
	#include <spawn.h>
	#include <xpc/xpc.h>
#endif

#if( TARGET_OS_POSIX )
	#include <sys/resource.h>
	#include <spawn.h>
#endif

#if( !defined( DNSSDUTIL_INCLUDE_DNSCRYPT ) )
	#define DNSSDUTIL_INCLUDE_DNSCRYPT		0
#endif

#if( DNSSDUTIL_INCLUDE_DNSCRYPT )
	#include "tweetnacl.h"	// TweetNaCl from <https://tweetnacl.cr.yp.to/software.html>.
#endif

#if( !defined( MDNSRESPONDER_PROJECT ) )
	#define MDNSRESPONDER_PROJECT		0
#endif

#if( MDNSRESPONDER_PROJECT )
	#include <CoreFoundation/CFXPCBridge.h>
	#include "dnssd_private.h"
	#include <mdns/private.h>
	#include <mdns/tcpinfo.h>
	#include "TestUtils.h"
	// Set ENABLE_DNSSDUTIL_DNSSEC_TEST to 1 to enable DNSSEC test functionality.
	#define ENABLE_DNSSDUTIL_DNSSEC_TEST 1
#endif

//===========================================================================================================================
//	Versioning
//===========================================================================================================================

#define kDNSSDUtilNumVersion	NumVersionBuild( 2, 0, 0, kVersionStageBeta, 0 )

#if( !MDNSRESPONDER_PROJECT && !defined( DNSSDUTIL_SOURCE_VERSION ) )
	#define DNSSDUTIL_SOURCE_VERSION	"0.0.0"
#endif

#define kDNSSDUtilIdentifier		"com.apple.dnssdutil"

//===========================================================================================================================
//	DNS-SD
//===========================================================================================================================

// DNS-SD API flag descriptors

#define kDNSServiceFlagsDescriptors		\
	"\x00" "AutoTrigger\0"				\
	"\x01" "Add\0"						\
	"\x02" "Default\0"					\
	"\x03" "NoAutoRename\0"				\
	"\x04" "Shared\0"					\
	"\x05" "Unique\0"					\
	"\x06" "BrowseDomains\0"			\
	"\x07" "RegistrationDomains\0"		\
	"\x08" "LongLivedQuery\0"			\
	"\x09" "AllowRemoteQuery\0"			\
	"\x0A" "ForceMulticast\0"			\
	"\x0B" "KnownUnique\0"				\
	"\x0C" "ReturnIntermediates\0"		\
	"\x0D" "DenyConstrained\0"			\
	"\x0E" "ShareConnection\0"			\
	"\x0F" "SuppressUnusable\0"			\
	"\x10" "Timeout\0"					\
	"\x11" "IncludeP2P\0"				\
	"\x12" "WakeOnResolve\0"			\
	"\x13" "BackgroundTrafficClass\0"	\
	"\x14" "IncludeAWDL\0"				\
	"\x15" "EnableDNSSEC\0"				\
	"\x16" "UnicastResponse\0"			\
	"\x17" "ValidateOptional\0"			\
	"\x18" "WakeOnlyService\0"			\
	"\x19" "ThresholdOne\0"				\
	"\x1A" "ThresholdFinder\0"			\
	"\x1B" "DenyCellular\0"				\
	"\x1C" "ServiceIndex\0"				\
	"\x1D" "DenyExpensive\0"			\
	"\x1E" "PathEvaluationDone\0"		\
	"\x1F" "AllowExpiredAnswers\0"		\
	"\x00"

#define DNSServiceFlagsToAddRmvStr( FLAGS )		( ( (FLAGS) & kDNSServiceFlagsAdd ) ? "Add" : "Rmv" )

#define kDNSServiceProtocolDescriptors	\
	"\x00" "IPv4\0"						\
	"\x01" "IPv6\0"						\
	"\x04" "UDP\0"						\
	"\x05" "TCP\0"						\
	"\x00"

#define kBadDNSServiceRef		( (DNSServiceRef)(intptr_t) -1 )

//===========================================================================================================================
//	DNS
//===========================================================================================================================

#define kDNSPort_Do53		 53 // See <https://tools.ietf.org/html/rfc1035#section-4.2>.
#define kDNSPort_DoT		853 // See <https://tools.ietf.org/html/rfc7858#section-3.1>.
#define kDNSPort_DoH		443 // See <https://tools.ietf.org/html/rfc8484#section-8.1>.

#define kDNSMaxUDPMessageSize		512
#define kDNSMaxTCPMessageSize		UINT16_MAX
#define kDNSRecordDataLengthMax		UINT16_MAX

//===========================================================================================================================
//	mDNS
//===========================================================================================================================

#define kMDNSPort		5353

#define kDefaultMDNSMessageID		0
#define kDefaultMDNSQueryFlags		0

// Recommended Resource Record TTL values. See <https://tools.ietf.org/html/rfc6762#section-10>.

#define kMDNSRecordTTL_Host			120		// TTL for resource records related to a host name, e.g., A, AAAA, SRV, etc.
#define kMDNSRecordTTL_Other		4500	// TTL for other resource records.

// Maximum mDNS Message Size. See <https://tools.ietf.org/html/rfc6762#section-17>.

#define kMDNSMessageSizeMax		8952	// 9000 B (Ethernet jumbo frame max size) - 40 B (IPv6 header) - 8 B (UDP header)

#define kLocalStr			"\x05" "local"
#define kLocalLabel			( (const uint8_t *) kLocalStr )
#define kLocalName			( (const uint8_t *) kLocalStr )
#define kLocalNameLen		sizeof( kLocalStr )

//===========================================================================================================================
//	Test Address Blocks
//===========================================================================================================================

// IPv4 address block 203.0.113.0/24 (TEST-NET-3) is reserved for documentation. See <https://tools.ietf.org/html/rfc5737>.

#define kDNSServerBaseAddrV4		UINT32_C( 0xCB007100 )	// 203.0.113.0/24

#define kDNSServerReverseIPv4DomainStr		"113.0.203.in-addr.arpa."
#define kDNSServerReverseIPv4DomainName \
	( (const uint8_t *) "\x3" "113" "\x1" "0" "\x3" "203" "\x7" "in-addr" "\x4" "arpa" )

// IPv6 address block 2001:db8::/32 is reserved for documentation. See <https://tools.ietf.org/html/rfc3849>.

static const uint8_t		kDNSServerBaseAddrV6[] =
{
	0x20, 0x01, 0x0D, 0xB8, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00	// 2001:db8:1::/96
};
check_compile_time( sizeof( kDNSServerBaseAddrV6 ) == 16 );

#define kDNSServerReverseIPv6DomainStr		"0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa."
#define kDNSServerReverseIPv6DomainName												\
	( (const uint8_t *) "\x1" "0" "\x1" "0" "\x1" "0" "\x1" "0" "\x1" "0" "\x1" "0"	\
	"\x1" "0" "\x1" "0" "\x1" "0" "\x1" "0" "\x1" "0" "\x1" "0" "\x1" "0" "\x1" "0"	\
	"\x1" "0" "\x1" "0" "\x1" "0" "\x1" "0" "\x1" "1" "\x1" "0" "\x1" "0" "\x1" "0"	\
	"\x1" "8" "\x1" "b" "\x1" "d" "\x1" "0" "\x1" "1" "\x1" "0" "\x1" "0" "\x1" "2"	\
	"\x3" "ip6" "\x4" "arpa" )

static const uint8_t		kMDNSReplierBaseAddrV6[] =
{
	0x20, 0x01, 0x0D, 0xB8, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00	// 2001:db8:2::/96
};
check_compile_time( sizeof( kMDNSReplierBaseAddrV6 ) == 16 );

static const uint8_t		kMDNSReplierLinkLocalBaseAddrV6[] =
{
	0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00	// fe80::/96
};
check_compile_time( sizeof( kMDNSReplierLinkLocalBaseAddrV6 ) == 16 );

// Bad IPv4 and IPv6 Address Blocks
// Used by the DNS server when it needs to respond with intentionally "bad" A/AAAA record data, i.e., IP addresses neither
// in 203.0.113.0/24 nor 2001:db8:1::/120.

#define kDNSServerBadBaseAddrV4		UINT32_C( 0x00000000 )	// 0.0.0.0/24

static const uint8_t		kDNSServerBadBaseAddrV6[] =
{
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00	// ::ffff:0:0/120
};
check_compile_time( sizeof( kDNSServerBadBaseAddrV6 ) == 16 );

#if( TARGET_OS_DARWIN )
// IPv6 Unique Local Address for assigning extra randomly-generated IPv6 addresses to the loopback interface.
// 40-bit Global ID: 0xEDF03555E4 (randomly-generated)
// 16-bit Subnet ID: 0
// See <https://tools.ietf.org/html/rfc4193#section-3.1>.

static const uint8_t		kExtraLoopbackIPv6Prefix[] =
{
	0xFD, 0xED, 0xF0, 0x35, 0x55, 0xE4, 0x00, 0x00	// fded:f035:55e4::/64
};

#define kExtraLoopbackIPv6PrefixBitLen		64
check_compile_time( ( sizeof( kExtraLoopbackIPv6Prefix ) * 8 ) == kExtraLoopbackIPv6PrefixBitLen );
#endif

//===========================================================================================================================
//	DNS Server Domains
//===========================================================================================================================

#define kDNSServerDomain_Default		( (const uint8_t *) "\x01" "d" "\x04" "test" )
#define kDNSServerDomain_DNSSEC			( (const uint8_t *) "\x06" "dnssec" "\x04" "test" )

//===========================================================================================================================
//	Misc.
//===========================================================================================================================

#define kLowerAlphaNumericCharSet			"abcdefghijklmnopqrstuvwxyz0123456789"
#define kLowerAlphaNumericCharSetSize		sizeof_string( kLowerAlphaNumericCharSet )

#if( !defined( kWhiteSpaceCharSet ) )
	#define kWhiteSpaceCharSet		"\t\n\v\f\r "
#endif

#define _RandomStringExact( CHAR_SET, CHAR_SET_SIZE, CHAR_COUNT, OUT_STRING ) \
	RandomString( CHAR_SET, CHAR_SET_SIZE, CHAR_COUNT, CHAR_COUNT, OUT_STRING )

#define kNoSuchRecordStr			"No Such Record"
#define kNoSuchRecordAStr			"No Such Record (A)"
#define kNoSuchRecordAAAAStr		"No Such Record (AAAA)"
#define kNoSuchNameStr				"No Such Name"

#define kRootLabel		( (const uint8_t *) "" )

#if !defined( nw_forget )
	#define nw_forget( X )		ForgetCustom( X, nw_release )
#endif

// When running a Do53 or DoT server for testing on tvOS and iOS, use an alternate server port instead of the standard
// UDP/TCP server ports of 53 and 853 respectively, which may be in use by the Unicast Bonjour discovery proxy.

#if( TARGET_OS_TV || TARGET_OS_IOS || TARGET_OS_OSX )
	#define DNSSDUTIL_TEST_USE_ALTERNATE_SERVER_PORT_FOR_DO53		1
	#define DNSSDUTIL_TEST_USE_ALTERNATE_SERVER_PORT_FOR_DOT		1
#else
	#define DNSSDUTIL_TEST_USE_ALTERNATE_SERVER_PORT_FOR_DO53		0
	#define DNSSDUTIL_TEST_USE_ALTERNATE_SERVER_PORT_FOR_DOT		0
#endif

// Most tests that spawn a DNS server for their testing don't actually care about the port used by the DNS protocol, so
// when we need to use an alternate listening port, just use a random ephemeral port (--port 0). If an alternate
// listening port isn't necessary, just use the default port, which is the common case.

#if( DNSSDUTIL_TEST_USE_ALTERNATE_SERVER_PORT_FOR_DO53 )
	#define DNSSDUTIL_TEST_DNS_SERVER_COMMAND_PREAMBLE		"dnssdutil server --port 0"
#else
	#define DNSSDUTIL_TEST_DNS_SERVER_COMMAND_PREAMBLE		"dnssdutil server"
#endif

// When it's necessary to use a specific alternate port for Do53 for testing, use port 202, which is the AppleTalk Name
// Binding UDP/TCP port (https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml).
// AppleTalk is discontinued, so it should be OK to squat on one of its ports, at least for testing.

#define kDNSPort_Do53Alt		202

// IDs and keys for mDNSResponder preferences. See mDNSResponder(8) man page.

#define kMDNSResponderPrefAppIDStr		"com.apple.mDNSResponder"

#define kMDNSResponderPrefStr_AlwaysAppendSearchDomains		"AlwaysAppendSearchDomains"

//===========================================================================================================================
//	Gerneral Command Options
//===========================================================================================================================

// Command option macros

#define Command( NAME, CALLBACK, SUB_OPTIONS, SHORT_HELP, IS_NOTCOMMON )											\
	CLI_COMMAND_EX( NAME, CALLBACK, SUB_OPTIONS, (IS_NOTCOMMON) ? kCLIOptionFlags_NotCommon : kCLIOptionFlags_None,	\
		(SHORT_HELP), NULL )

#define kRequiredOptionSuffix		" [REQUIRED]"

#define MultiStringOptionEx( SHORT_CHAR, LONG_NAME, VAL_PTR, VAL_COUNT_PTR, ARG_HELP, SHORT_HELP, IS_REQUIRED, LONG_HELP )	\
	CLI_OPTION_MULTI_STRING_EX( SHORT_CHAR, LONG_NAME, VAL_PTR, VAL_COUNT_PTR, ARG_HELP,									\
		(IS_REQUIRED) ? SHORT_HELP kRequiredOptionSuffix : SHORT_HELP,														\
		(IS_REQUIRED) ? kCLIOptionFlags_Required : kCLIOptionFlags_None, LONG_HELP )

#define MultiStringOption( SHORT_CHAR, LONG_NAME, VAL_PTR, VAL_COUNT_PTR, ARG_HELP, SHORT_HELP, IS_REQUIRED ) \
		MultiStringOptionEx( SHORT_CHAR, LONG_NAME, VAL_PTR, VAL_COUNT_PTR, ARG_HELP, SHORT_HELP, IS_REQUIRED, NULL )

#define IntegerOptionEx( SHORT_CHAR, LONG_NAME, VAL_PTR, ARG_HELP, SHORT_HELP, IS_REQUIRED, LONG_HELP )	\
	CLI_OPTION_INTEGER_EX( SHORT_CHAR, LONG_NAME, VAL_PTR, ARG_HELP,									\
		(IS_REQUIRED) ? SHORT_HELP kRequiredOptionSuffix : SHORT_HELP,									\
		(IS_REQUIRED) ? kCLIOptionFlags_Required : kCLIOptionFlags_None, LONG_HELP )

#define IntegerOption( SHORT_CHAR, LONG_NAME, VAL_PTR, ARG_HELP, SHORT_HELP, IS_REQUIRED )	\
	IntegerOptionEx( SHORT_CHAR, LONG_NAME, VAL_PTR, ARG_HELP, SHORT_HELP, IS_REQUIRED, NULL )

#define DoubleOption( SHORT_CHAR, LONG_NAME, VAL_PTR, ARG_HELP, SHORT_HELP, IS_REQUIRED )	\
	CLI_OPTION_DOUBLE_EX( SHORT_CHAR, LONG_NAME, VAL_PTR, ARG_HELP,							\
		(IS_REQUIRED) ? SHORT_HELP kRequiredOptionSuffix : SHORT_HELP,						\
		(IS_REQUIRED) ? kCLIOptionFlags_Required : kCLIOptionFlags_None, NULL )

#define BooleanOption( SHORT_CHAR, LONG_NAME, VAL_PTR, SHORT_HELP ) \
	CLI_OPTION_BOOLEAN( (SHORT_CHAR), (LONG_NAME), (VAL_PTR), (SHORT_HELP), NULL )

#define StringOptionEx( SHORT_CHAR, LONG_NAME, VAL_PTR, ARG_HELP, SHORT_HELP, IS_REQUIRED, LONG_HELP )	\
	CLI_OPTION_STRING_EX( SHORT_CHAR, LONG_NAME, VAL_PTR, ARG_HELP,										\
		(IS_REQUIRED) ? SHORT_HELP kRequiredOptionSuffix : SHORT_HELP,									\
		(IS_REQUIRED) ? kCLIOptionFlags_Required : kCLIOptionFlags_None, LONG_HELP )

#define StringOption( SHORT_CHAR, LONG_NAME, VAL_PTR, ARG_HELP, SHORT_HELP, IS_REQUIRED ) \
	StringOptionEx( SHORT_CHAR, LONG_NAME, VAL_PTR, ARG_HELP, SHORT_HELP, IS_REQUIRED, NULL )

#define CFStringOption( SHORT_CHAR, LONG_NAME, VAL_PTR, ARG_HELP, SHORT_HELP, IS_REQUIRED )	\
	CLI_OPTION_CFSTRING_EX( SHORT_CHAR, LONG_NAME, VAL_PTR, ARG_HELP,						\
		(IS_REQUIRED) ? SHORT_HELP kRequiredOptionSuffix : SHORT_HELP,						\
		(IS_REQUIRED) ? kCLIOptionFlags_Required : kCLIOptionFlags_None, NULL )

// DNS-SD API flag options

static int		gDNSSDFlags						= 0;
static int		gDNSSDFlag_AllowExpiredAnswers	= false;
static int		gDNSSDFlag_BrowseDomains		= false;
static int		gDNSSDFlag_DenyCellular			= false;
static int		gDNSSDFlag_DenyConstrained		= false;
static int		gDNSSDFlag_DenyExpensive		= false;
static int		gDNSSDFlag_ForceMulticast		= false;
static int		gDNSSDFlag_IncludeAWDL			= false;
static int		gDNSSDFlag_KnownUnique			= false;
static int		gDNSSDFlag_NoAutoRename			= false;
static int		gDNSSDFlag_PathEvaluationDone	= false;
static int		gDNSSDFlag_RegistrationDomains	= false;
static int		gDNSSDFlag_ReturnIntermediates	= false;
static int		gDNSSDFlag_Shared				= false;
static int		gDNSSDFlag_SuppressUnusable		= false;
static int		gDNSSDFlag_Timeout				= false;
static int		gDNSSDFlag_UnicastResponse		= false;
static int		gDNSSDFlag_Unique				= false;
static int		gDNSSDFlag_WakeOnResolve		= false;
static int		gDNSSDFlag_EnableDNSSEC			= false;

#define DNSSDFlagsOption()								\
	IntegerOption( 'f', "flags", &gDNSSDFlags, "flags",	\
		"DNSServiceFlags as an integer. This value is bitwise ORed with other single flag options.", false )

#define DNSSDFlagOption( SHORT_CHAR, FLAG_NAME ) \
	BooleanOption( SHORT_CHAR, # FLAG_NAME, &gDNSSDFlag_ ## FLAG_NAME, "Use kDNSServiceFlags" # FLAG_NAME "." )

#define DNSSDFlagsOption_AllowExpiredAnswers()		DNSSDFlagOption( 'X', AllowExpiredAnswers )
#define DNSSDFlagsOption_DenyCellular()				DNSSDFlagOption( 'C', DenyCellular )
#define DNSSDFlagsOption_DenyConstrained()			DNSSDFlagOption( 'R', DenyConstrained)
#define DNSSDFlagsOption_DenyExpensive()			DNSSDFlagOption( 'E', DenyExpensive )
#define DNSSDFlagsOption_EnableDNSSEC()				DNSSDFlagOption( 'D', EnableDNSSEC )
#define DNSSDFlagsOption_ForceMulticast()			DNSSDFlagOption( 'M', ForceMulticast )
#define DNSSDFlagsOption_IncludeAWDL()				DNSSDFlagOption( 'A', IncludeAWDL )
#define DNSSDFlagsOption_KnownUnique()				DNSSDFlagOption( 'K', KnownUnique )
#define DNSSDFlagsOption_NoAutoRename()				DNSSDFlagOption( 'N', NoAutoRename )
#define DNSSDFlagsOption_PathEvalDone()				DNSSDFlagOption( 'P', PathEvaluationDone )
#define DNSSDFlagsOption_ReturnIntermediates()		DNSSDFlagOption( 'I', ReturnIntermediates )
#define DNSSDFlagsOption_Shared()					DNSSDFlagOption( 'S', Shared )
#define DNSSDFlagsOption_SuppressUnusable()			DNSSDFlagOption( 'S', SuppressUnusable )
#define DNSSDFlagsOption_Timeout()					DNSSDFlagOption( 'T', Timeout )
#define DNSSDFlagsOption_UnicastResponse()			DNSSDFlagOption( 'U', UnicastResponse )
#define DNSSDFlagsOption_Unique()					DNSSDFlagOption( 'U', Unique )
#define DNSSDFlagsOption_WakeOnResolve()			DNSSDFlagOption( 'W', WakeOnResolve )

// Interface option

static const char *		gInterface = NULL;

#define InterfaceOption()										\
	StringOption( 'i', "interface", &gInterface, "interface",	\
		"Network interface by name or index. Use index -1 for local-only.", false )

// Connection options

#define kConnectionArg_Normal			""
#define kConnectionArgPrefix_PID		"pid:"
#define kConnectionArgPrefix_UUID		"uuid:"

static const char *		gConnectionOpt = kConnectionArg_Normal;

#define ConnectionOptions()																						\
	{ kCLIOptionType_String, 0, "connection", &gConnectionOpt, NULL, (intptr_t) kConnectionArg_Normal, "type",	\
		kCLIOptionFlags_OptionalArgument, NULL, NULL, NULL, NULL,												\
		"Specifies the type of main connection to use. See " kConnectionSection_Name " below.", NULL }

#define kConnectionSection_Name		"Connection Option"
#define kConnectionSection_Text																							\
	"The default behavior is to create a main connection with DNSServiceCreateConnection() and perform operations on\n"	\
	"the main connection using the kDNSServiceFlagsShareConnection flag. This behavior can be explicitly invoked by\n"	\
	"specifying the connection option without an argument, i.e.,\n"														\
	"\n"																												\
	"    --connection\n"																								\
	"\n"																												\
	"To instead use a delegate connection created with DNSServiceCreateDelegateConnection(), use\n"						\
	"\n"																												\
	"    --connection=pid:<PID>\n"																						\
	"\n"																												\
	"to specify the delegator by PID, or use\n"																			\
	"\n"																												\
	"    --connection=uuid:<UUID>\n"																					\
	"\n"																												\
	"to specify the delegator by UUID.\n"																				\
	"\n"																												\
	"To not use a main connection at all, but instead perform operations on their own implicit connections, use\n"		\
	"\n"																												\
	"    --no-connection\n"

#define ConnectionSection()		CLI_SECTION( kConnectionSection_Name, kConnectionSection_Text )

// Help text for record data options

#define kRDataArgPrefix_Domain			"domain:"
#define kRDataArgPrefix_File			"file:"
#define kRDataArgPrefix_HexString		"hex:"
#define kRDataArgPrefix_IPv4			"ipv4:"
#define kRDataArgPrefix_IPv6			"ipv6:"
#define kRDataArgPrefix_SRV				"srv:"
#define kRDataArgPrefix_String			"string:"
#define kRDataArgPrefix_TXT				"txt:"

#define kRecordDataSection_Name		"Record Data Arguments"
#define kRecordDataSection_Text																							\
	"A record data argument is specified in one of the following formats:\n"											\
	"\n"																												\
	"Format                       Syntax                                   Example\n"									\
	"Domain name                  domain:<domain name>                     domain:demo._test._tcp.local\n"				\
	"File containing record data  file:<file path>                         file:/path/to/binary-rdata-file\n"			\
	"Hexadecimal string           hex:<hex string>                         hex:c0000201 or hex:'C0 00 02 01'\n"		    \
	"IPv4 address                 ipv4:<IPv4 address>                      ipv4:192.0.2.1\n"							\
	"IPv6 address                 ipv6:<IPv6 address>                      ipv6:2001:db8::1\n"							\
	"SRV record                   srv:<priority>,<weight>,<port>,<target>  srv:0,0,64206,example.local\n"				\
	"String                       string:<string>                          string:'\\x09color=red'\n"					\
	"TXT record strings           txt:<comma-delimited strings>            txt:'vers=1.0,lang=en\\,es\\,fr,passreq'\n"	\
	"\n"																												\
	"Note: The string format converts each \\xHH escape sequence into the octet represented by the HH hex digit pair.\n"

#define RecordDataSection()		CLI_SECTION( kRecordDataSection_Name, kRecordDataSection_Text )

// Fallback DNS service option

#if( MDNSRESPONDER_PROJECT )
static const char *		gFallbackDNSService = NULL;

#define kFallbackDNSServiceArgPrefix_DoH		"doh:"
#define kFallbackDNSServiceArgPrefix_DoT		"dot:"

#define FallbackDNSServiceGroup()		CLI_OPTION_GROUP( "Default Fallback DNS Service" )
#define FallbackDNSServiceOption()																						\
	StringOptionEx( 0, "fallback", &gFallbackDNSService, "DNS service", "Default fallback DNS service to set.", false,	\
		"\n"																											\
		"When this option is used, an nw_resolver_config is created for the specified DoH or DoT service.\n"			\
		"DNSServiceSetResolverDefaults() is then used to set the DNS service described by nw_resolver_config as the\n"	\
		"default fallback DNS service for the dnssdutil process.\n"														\
		"\n"																											\
		"To specify a DNS over HTTPS (DoH) service, use\n"																\
		"\n"																											\
		"    --fallback=doh:<URL>\n"																					\
		"\n"																											\
		"Example: --fallback=doh:https://dns.example.com/dns-query\n"													\
		"\n"																											\
		"To specify a DNS over TLS (DoT) service, use\n"																\
		"\n"																											\
		"    --fallback=dot:<hostname>\n"																				\
		"\n"																											\
		"Example: --fallback=dot:dns.example.com\n"																		\
	)
#endif

//===========================================================================================================================
//	Output Formatting
//===========================================================================================================================

#define kOutputFormatStr_JSON		"json"
#define kOutputFormatStr_XML		"xml"
#define kOutputFormatStr_Binary		"binary"

typedef enum
{
	kOutputFormatType_Invalid	= 0,
	kOutputFormatType_JSON		= 1,
	kOutputFormatType_XML		= 2,
	kOutputFormatType_Binary	= 3
	
}	OutputFormatType;

#define FormatOption( SHORT_CHAR, LONG_NAME, VAL_PTR, SHORT_HELP, IS_REQUIRED )			\
	StringOptionEx( SHORT_CHAR, LONG_NAME, VAL_PTR, "format", SHORT_HELP, IS_REQUIRED,	\
		"\n"																			\
		"Use '" kOutputFormatStr_JSON   "' for JavaScript Object Notation (JSON).\n"	\
		"Use '" kOutputFormatStr_XML    "' for property list XML version 1.0.\n"		\
		"Use '" kOutputFormatStr_Binary "' for property list binary version 1.0.\n"		\
		"\n"																			\
	)

//===========================================================================================================================
//	Browse Command Options
//===========================================================================================================================

static char **			gBrowse_ServiceTypes		= NULL;
static size_t			gBrowse_ServiceTypesCount	= 0;
static const char *		gBrowse_Domain				= NULL;
static int				gBrowse_DoResolve			= false;
static int				gBrowse_QueryTXT			= false;
static int				gBrowse_TimeLimitSecs		= 0;
static int 				gBrowse_ValidateResults		= false;
static int 				gBrowse_ResolveDelayMs		= 0;

static CLIOption		kBrowseOpts[] =
{
	InterfaceOption(),
	MultiStringOption(	't', "type",	&gBrowse_ServiceTypes, &gBrowse_ServiceTypesCount, "service type", "Service type(s), e.g., \"_ssh._tcp\".", true ),
	StringOption(		'd', "domain",	&gBrowse_Domain, "domain", "Domain in which to browse for the service type(s).", false ),
	
	CLI_OPTION_GROUP( "Flags" ),
	DNSSDFlagsOption(),
	DNSSDFlagsOption_IncludeAWDL(),
	
	CLI_OPTION_GROUP( "Operation" ),
	ConnectionOptions(),
	BooleanOption(  0 , "resolve",      &gBrowse_DoResolve,       "Resolve service instances." ),
	BooleanOption(  0 , "queryTXT",     &gBrowse_QueryTXT,        "Query TXT records of service instances." ),
	IntegerOption( 'l', "timeLimit",    &gBrowse_TimeLimitSecs,   "seconds", "Specifies the max duration of the browse operation. Use '0' for no time limit.", false ),
	BooleanOption( 'v', "validate",     &gBrowse_ValidateResults, "Validate results." ),
	IntegerOption(  0 , "resolveDelay", &gBrowse_ResolveDelayMs,  "ms", "The amount of time to wait before a resolve or TXT query in milliseconds. (default: 0)", false ),
	
	ConnectionSection(),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	GetAddrInfo Command Options
//===========================================================================================================================

static const char *		gGetAddrInfo_Name			= NULL;
static int				gGetAddrInfo_ProtocolIPv4	= false;
static int				gGetAddrInfo_ProtocolIPv6	= false;
static int				gGetAddrInfo_OneShot		= false;
static int				gGetAddrInfo_TimeLimitSecs	= 0;

static CLIOption		kGetAddrInfoOpts[] =
{
	InterfaceOption(),
	StringOption(  'n', "name", &gGetAddrInfo_Name,			"domain name", "Domain name to resolve.", true ),
	BooleanOption(  0 , "ipv4", &gGetAddrInfo_ProtocolIPv4,	"Use kDNSServiceProtocol_IPv4." ),
	BooleanOption(  0 , "ipv6", &gGetAddrInfo_ProtocolIPv6,	"Use kDNSServiceProtocol_IPv6." ),
	
	CLI_OPTION_GROUP( "Flags" ),
	DNSSDFlagsOption(),
	DNSSDFlagsOption_AllowExpiredAnswers(),
	DNSSDFlagsOption_DenyCellular(),
	DNSSDFlagsOption_DenyConstrained(),
	DNSSDFlagsOption_DenyExpensive(),
	DNSSDFlagsOption_EnableDNSSEC(),
	DNSSDFlagsOption_IncludeAWDL(),
	DNSSDFlagsOption_PathEvalDone(),
	DNSSDFlagsOption_ReturnIntermediates(),
	DNSSDFlagsOption_SuppressUnusable(),
	DNSSDFlagsOption_Timeout(),
	
	CLI_OPTION_GROUP( "Operation" ),
	ConnectionOptions(),
	BooleanOption( 'o', "oneshot",		&gGetAddrInfo_OneShot,			"Finish after first set of results." ),
	IntegerOption( 'l', "timeLimit",	&gGetAddrInfo_TimeLimitSecs,	"seconds", "Maximum duration of the GetAddrInfo operation. Use '0' for no time limit.", false ),
	
#if( MDNSRESPONDER_PROJECT )
	FallbackDNSServiceGroup(),
	FallbackDNSServiceOption(),
#endif
	ConnectionSection(),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	QueryRecord Command Options
//===========================================================================================================================

static const char *		gQueryRecord_Name				= NULL;
static const char *		gQueryRecord_Type				= NULL;
static int				gQueryRecord_AAAAFallback		= false;
static int				gQueryRecord_UseFailover		= false;
static const char *		gQueryRecord_ResolverOverride	= NULL;
static int				gQueryRecord_OneShot			= false;
static int				gQueryRecord_TimeLimitSecs		= 0;
static int				gQueryRecord_RawRData			= false;

static CLIOption		kQueryRecordOpts[] =
{
	InterfaceOption(),
	StringOption( 'n', "name", &gQueryRecord_Name, "domain name", "Full domain name of record to query.", true ),
	StringOption( 't', "type", &gQueryRecord_Type, "record type", "Record type by name (e.g., TXT, SRV, etc.) or number.", true ),
	
	CLI_OPTION_GROUP( "Flags" ),
	DNSSDFlagsOption(),
	DNSSDFlagsOption_AllowExpiredAnswers(),
	DNSSDFlagsOption_DenyCellular(),
	DNSSDFlagsOption_DenyConstrained(),
	DNSSDFlagsOption_DenyExpensive(),
	DNSSDFlagsOption_EnableDNSSEC(),
	DNSSDFlagsOption_ForceMulticast(),
	DNSSDFlagsOption_IncludeAWDL(),
	DNSSDFlagsOption_PathEvalDone(),
	DNSSDFlagsOption_ReturnIntermediates(),
	DNSSDFlagsOption_SuppressUnusable(),
	DNSSDFlagsOption_Timeout(),
	DNSSDFlagsOption_UnicastResponse(),
	
	CLI_OPTION_GROUP( "Attributes" ),
	BooleanOption( 0, "aaaaFallback",     &gQueryRecord_AAAAFallback,     "If a AAAA record doesn't exist, try querying for an A record of the same name and type." ),
	BooleanOption( 0, "useFailover",      &gQueryRecord_UseFailover,      "Use DNS service failover if necessary and applicable." ),
	StringOption(  0, "resolverOverride", &gQueryRecord_ResolverOverride, "UUID", "UUID of libnetwork resolver configuration to use as override.", false ),
	
	CLI_OPTION_GROUP( "Operation" ),
	ConnectionOptions(),
	BooleanOption( 'o', "oneshot",		&gQueryRecord_OneShot,			"Finish after first set of results." ),
	IntegerOption( 'l', "timeLimit",	&gQueryRecord_TimeLimitSecs,	"seconds", "Maximum duration of the query record operation. Use '0' for no time limit.", false ),
	BooleanOption(  0 , "raw",			&gQueryRecord_RawRData,			"Show record data as a hexdump." ),
	
#if( MDNSRESPONDER_PROJECT )
	FallbackDNSServiceGroup(),
	FallbackDNSServiceOption(),
#endif	
	ConnectionSection(),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	Register Command Options
//===========================================================================================================================

static const char *			gRegister_Name			= NULL;
static const char *			gRegister_Type			= NULL;
static const char *			gRegister_Domain		= NULL;
static int					gRegister_Port			= 0;
static const char *			gRegister_TXT			= NULL;
static int					gRegister_LifetimeMs	= -1;
static const char *			gRegister_TimeOfReceipt	= NULL;
static const char *			gRegister_HostKeyHash	= NULL;

static const char **		gAddRecord_Types		= NULL;
static size_t				gAddRecord_TypesCount	= 0;
static const char **		gAddRecord_Data			= NULL;
static size_t				gAddRecord_DataCount	= 0;
static const char **		gAddRecord_TTLs			= NULL;
static size_t				gAddRecord_TTLsCount	= 0;

static const char **		gUpdateRecord_Datas			= NULL;
static size_t				gUpdateRecord_DataCount		= 0;
static const char **		gUpdateRecord_DelaysMs		= NULL;
static size_t				gUpdateRecord_DelayCount	= 0;
static const char **		gUpdateRecord_TTLs			= NULL;
static size_t				gUpdateRecord_TTLCount		= 0;

static CLIOption		kRegisterOpts[] =
{
	InterfaceOption(),
	StringOption(  'n', "name",		&gRegister_Name,	"service name",	"Name of service.", false ),
	StringOption(  't', "type",		&gRegister_Type,	"service type",	"Service type, e.g., \"_ssh._tcp\".", true ),
	StringOption(  'd', "domain",	&gRegister_Domain,	"domain",		"Domain in which to advertise the service.", false ),
	IntegerOption( 'p', "port",		&gRegister_Port,	"port number",	"Service's port number.", true ),
	StringOption(   0 , "txt",		&gRegister_TXT,		"record data",	"The TXT record data. See " kRecordDataSection_Name " below.", false ),
	
	CLI_OPTION_GROUP( "Attributes" ),
	StringOption( 0, "timestamp",	&gRegister_TimeOfReceipt,	"Unix time",	"Time since epoch in seconds to indicate when the service registration request is received, should be used with flag kDNSServiceFlagsNoAutoRename", false ),
	StringOption( 0, "hostKeyHash",	&gRegister_HostKeyHash,		"32-bit hash",	"Unique hostkey hash value.", false ),

	CLI_OPTION_GROUP( "Flags" ),
	DNSSDFlagsOption(),
	DNSSDFlagsOption_IncludeAWDL(),
	DNSSDFlagsOption_KnownUnique(),
	DNSSDFlagsOption_NoAutoRename(),
	
	CLI_OPTION_GROUP( "Operation" ),
	IntegerOption( 'l', "lifetime", &gRegister_LifetimeMs, "ms", "Lifetime of the service registration in milliseconds.", false ),
	
	CLI_OPTION_GROUP( "Options for adding extra record(s) to the registered service with DNSServiceAddRecord()\n" ),
	MultiStringOption(   0, "addType", &gAddRecord_Types, &gAddRecord_TypesCount, "record type", "Type of additional record by name (e.g., TXT, SRV, etc.) or number.", false ),
	MultiStringOptionEx( 0, "addData", &gAddRecord_Data,  &gAddRecord_DataCount,  "record data", "Additional record's data. See " kRecordDataSection_Name " below.", false, NULL ),
	MultiStringOption(   0, "addTTL",  &gAddRecord_TTLs,  &gAddRecord_TTLsCount,  "seconds",     "Time-to-live of additional record in seconds. Use '0' for the system default.", false ),
	
	CLI_OPTION_GROUP( "Options for updating the service's primary TXT record with DNSServiceUpdateRecord()\n" ),
	MultiStringOption( 0, "updateData",  &gUpdateRecord_Datas,    &gUpdateRecord_DataCount,  "record data", "Record data for the record update. See " kRecordDataSection_Name " below.", false ),
	MultiStringOption( 0, "updateDelay", &gUpdateRecord_DelaysMs, &gUpdateRecord_DelayCount, "ms", "Number of milliseconds after registration to wait before the update.", false ),
	MultiStringOption( 0, "updateTTL",   &gUpdateRecord_TTLs,     &gUpdateRecord_TTLCount,   "seconds", "Time-to-live of the updated record. Use '0' for the system default.", false ),
	
	CLI_SECTION( "Rules for multiple extra records",
		"1. The --addType, --addData, and --addTTL options can be specified more than once to add multiple extra records.\n"
		"2. The i-th --addType, --addData, and --addTTL options are used for the i-th extra record.\n"
		"3. The number of --addType options must equal the number of --addData options.\n"
		"4. The number of --addTTL options must equal the number of --addType options or not be specified at all, in\n"
		"   which case a TTL of 0 is used for all extra records.\n"
	),
	CLI_SECTION( "Rules for multiple primary TXT record updates",
		"1. The --updateData, --updateDelay, and --updateTTL options can be specified more than once to specify multiple\n"
		"   primary TXT record updates.\n"
		"2. The i-th --updateData, --updateDelay, and --updateTTL options are used for the i-th update.\n"
		"3. The number of --updateData options must equal the number of --updateDelay options.\n"
		"4. The number of --updateTTL options must equal the number of --updateData options or not be specified at all,\n"
		"   in which case a TTL of 0 is used for all updates.\n"
	),
	RecordDataSection(),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	RegisterRecord Command Options
//===========================================================================================================================

static const char *		gRegisterRecord_Name			= NULL;
static const char *		gRegisterRecord_Type			= NULL;
static const char *		gRegisterRecord_Data			= NULL;
static int				gRegisterRecord_TTL				= 0;
static int				gRegisterRecord_LifetimeMs		= -1;
static const char *		gRegisterRecord_UpdateData		= NULL;
static int				gRegisterRecord_UpdateDelayMs	= 0;
static int				gRegisterRecord_UpdateTTL		= 0;
static const char *		gRegisterRecord_TimeOfReceipt	= NULL;
static const char *		gRegisterRecord_HostKeyHash		= NULL;

static CLIOption		kRegisterRecordOpts[] =
{
	InterfaceOption(),
	StringOption( 'n', "name",	&gRegisterRecord_Name,	"record name",	"Fully qualified domain name of record.", true ),
	StringOption( 't', "type",	&gRegisterRecord_Type,	"record type",	"Record type by name (e.g., TXT, PTR, A) or number.", true ),
	StringOption( 'd', "data",	&gRegisterRecord_Data,	"record data",	"The record data. See " kRecordDataSection_Name " below.", false ),
	IntegerOption( 0 , "ttl",	&gRegisterRecord_TTL,	"seconds",		"Time-to-live in seconds. Use '0' for default.", false ),
	
	CLI_OPTION_GROUP( "Attributes" ),
	StringOption( 0, "timestamp",	&gRegisterRecord_TimeOfReceipt,	"Unix time",	"Time since epoch in seconds to indicate when the record registration request is received.", false ),
	StringOption( 0, "hostKeyHash",	&gRegisterRecord_HostKeyHash,	"32-bit hash",	"Unique hostkey hash value.", false ),

	CLI_OPTION_GROUP( "Flags" ),
	DNSSDFlagsOption(),
	DNSSDFlagsOption_ForceMulticast(),
	DNSSDFlagsOption_IncludeAWDL(),
	DNSSDFlagsOption_KnownUnique(),
	DNSSDFlagsOption_Shared(),
	DNSSDFlagsOption_Unique(),
	
	CLI_OPTION_GROUP( "Operation" ),
	IntegerOption( 'l', "lifetime", &gRegisterRecord_LifetimeMs, "ms", "Lifetime of the service registration in milliseconds.", false ),
	
	CLI_OPTION_GROUP( "Options for updating the registered record with DNSServiceUpdateRecord()\n" ),
	StringOption(  0 , "updateData",	&gRegisterRecord_UpdateData,	"record data",	"Record data for the record update.", false ),
	IntegerOption( 0 , "updateDelay",	&gRegisterRecord_UpdateDelayMs,	"ms",			"Number of milliseconds after registration to wait before record update.", false ),
	IntegerOption( 0 , "updateTTL",		&gRegisterRecord_UpdateTTL,		"seconds",		"Time-to-live of the updated record.", false ),
	
	RecordDataSection(),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	Resolve Command Options
//===========================================================================================================================

static char *		gResolve_Name			= NULL;
static char *		gResolve_Type			= NULL;
static char *		gResolve_Domain			= NULL;
static int			gResolve_TimeLimitSecs	= 0;

static CLIOption		kResolveOpts[] =
{
	InterfaceOption(),
	StringOption( 'n', "name",		&gResolve_Name,		"service name", "Name of the service instance to resolve.", true ),
	StringOption( 't', "type",		&gResolve_Type,		"service type", "Type of the service instance to resolve.", true ),
	StringOption( 'd', "domain",	&gResolve_Domain,	"domain", "Domain of the service instance to resolve.", true ),
	
	CLI_OPTION_GROUP( "Flags" ),
	DNSSDFlagsOption(),
	DNSSDFlagsOption_ForceMulticast(),
	DNSSDFlagsOption_IncludeAWDL(),
	DNSSDFlagsOption_ReturnIntermediates(),
	DNSSDFlagsOption_WakeOnResolve(),
	
	CLI_OPTION_GROUP( "Operation" ),
	ConnectionOptions(),
	IntegerOption( 'l', "timeLimit", &gResolve_TimeLimitSecs, "seconds", "Maximum duration of the resolve operation. Use '0' for no time limit.", false ),
	
	ConnectionSection(),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	Reconfirm Command Options
//===========================================================================================================================

static const char *		gReconfirmRecord_Name	= NULL;
static const char *		gReconfirmRecord_Type	= NULL;
static const char *		gReconfirmRecord_Class	= NULL;
static const char *		gReconfirmRecord_Data	= NULL;

static CLIOption		kReconfirmOpts[] =
{
	InterfaceOption(),
	StringOption( 'n', "name",	&gReconfirmRecord_Name,		"record name",	"Full name of the record to reconfirm.", true ),
	StringOption( 't', "type",	&gReconfirmRecord_Type,		"record type",	"Type of the record to reconfirm.", true ),
	StringOption( 'c', "class",	&gReconfirmRecord_Class,	"record class",	"Class of the record to reconfirm. Default class is IN.", false ),
	StringOption( 'd', "data",	&gReconfirmRecord_Data,		"record data",	"The record data. See " kRecordDataSection_Name " below.", false ),
	
	CLI_OPTION_GROUP( "Flags" ),
	DNSSDFlagsOption(),
	
	RecordDataSection(),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	getaddrinfo-POSIX Command Options
//===========================================================================================================================

static const char *		gGAIPOSIX_HostName			= NULL;
static const char *		gGAIPOSIX_ServName			= NULL;
static const char *		gGAIPOSIX_Family			= NULL;
static int				gGAIPOSIXFlag_AddrConfig	= false;
static int				gGAIPOSIXFlag_All			= false;
static int				gGAIPOSIXFlag_CanonName		= false;
static int				gGAIPOSIXFlag_NumericHost	= false;
static int				gGAIPOSIXFlag_NumericServ	= false;
static int				gGAIPOSIXFlag_Passive		= false;
static int				gGAIPOSIXFlag_V4Mapped		= false;
#if( defined( AI_V4MAPPED_CFG ) )
static int				gGAIPOSIXFlag_V4MappedCFG	= false;
#endif
#if( defined( AI_DEFAULT ) )
static int				gGAIPOSIXFlag_Default		= false;
#endif
#if( defined( AI_UNUSABLE ) )
static int				gGAIPOSIXFlag_Unusable		= false;
#endif

static CLIOption		kGetAddrInfoPOSIXOpts[] =
{
	StringOption(	'n', "hostname",			&gGAIPOSIX_HostName,		"hostname", "Domain name to resolve or an IPv4 or IPv6 address.", true ),
	StringOption(	's', "servname",			&gGAIPOSIX_ServName,		"servname", "Port number in decimal or service name from services(5).", false ),
	
	CLI_OPTION_GROUP( "Hints" ),
	StringOptionEx(	'f', "family",				&gGAIPOSIX_Family,			"address family", "Address family to use for hints ai_family field.", false,
		"\n"
		"Possible address family values are 'inet' for AF_INET, 'inet6' for AF_INET6, or 'unspec' for AF_UNSPEC. If no\n"
		"address family is specified, then AF_UNSPEC is used.\n"
		"\n" ),
	BooleanOption(   0 , "flag-addrconfig",		&gGAIPOSIXFlag_AddrConfig,	"In hints ai_flags field, set AI_ADDRCONFIG." ),
	BooleanOption(   0 , "flag-all",			&gGAIPOSIXFlag_All,			"In hints ai_flags field, set AI_ALL." ),
	BooleanOption(   0 , "flag-canonname",		&gGAIPOSIXFlag_CanonName,	"In hints ai_flags field, set AI_CANONNAME." ),
	BooleanOption(   0 , "flag-numerichost",	&gGAIPOSIXFlag_NumericHost,	"In hints ai_flags field, set AI_NUMERICHOST." ),
	BooleanOption(   0 , "flag-numericserv",	&gGAIPOSIXFlag_NumericServ,	"In hints ai_flags field, set AI_NUMERICSERV." ),
	BooleanOption(   0 , "flag-passive",		&gGAIPOSIXFlag_Passive,		"In hints ai_flags field, set AI_PASSIVE." ),
	BooleanOption(   0 , "flag-v4mapped",		&gGAIPOSIXFlag_V4Mapped,	"In hints ai_flags field, set AI_V4MAPPED." ),
#if( defined( AI_V4MAPPED_CFG ) )
	BooleanOption(   0 , "flag-v4mappedcfg",	&gGAIPOSIXFlag_V4MappedCFG,	"In hints ai_flags field, set AI_V4MAPPED_CFG." ),
#endif
#if( defined( AI_DEFAULT ) )
	BooleanOption(   0 , "flag-default",		&gGAIPOSIXFlag_Default,		"In hints ai_flags field, set AI_DEFAULT." ),
#endif
#if( defined( AI_UNUSABLE ) )
	BooleanOption(   0 , "flag-unusable",		&gGAIPOSIXFlag_Unusable,	"In hints ai_flags field, set AI_UNUSABLE." ),
#endif
	
#if( MDNSRESPONDER_PROJECT )
	FallbackDNSServiceGroup(),
	FallbackDNSServiceOption(),
#endif
	CLI_SECTION( "Notes", "See getaddrinfo(3) man page for more details.\n" ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	ReverseLookup Command Options
//===========================================================================================================================

static const char *		gReverseLookup_IPAddr			= NULL;
static int				gReverseLookup_OneShot			= false;
static int				gReverseLookup_TimeLimitSecs	= 0;

static CLIOption		kReverseLookupOpts[] =
{
	InterfaceOption(),
	StringOption( 'a', "address", &gReverseLookup_IPAddr, "IP address", "IPv4 or IPv6 address for which to perform a reverse IP lookup.", true ),
	
	CLI_OPTION_GROUP( "Flags" ),
	DNSSDFlagsOption(),
	DNSSDFlagsOption_ForceMulticast(),
	DNSSDFlagsOption_ReturnIntermediates(),
	DNSSDFlagsOption_SuppressUnusable(),
	
	CLI_OPTION_GROUP( "Operation" ),
	ConnectionOptions(),
	BooleanOption( 'o', "oneshot",		&gReverseLookup_OneShot,		"Finish after first set of results." ),
	IntegerOption( 'l', "timeLimit",	&gReverseLookup_TimeLimitSecs,	"seconds", "Specifies the max duration of the query record operation. Use '0' for no time limit.", false ),
	
	ConnectionSection(),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	PortMapping Command Options
//===========================================================================================================================

static int		gPortMapping_ProtocolTCP	= false;
static int		gPortMapping_ProtocolUDP	= false;
static int		gPortMapping_InternalPort	= 0;
static int		gPortMapping_ExternalPort	= 0;
static int		gPortMapping_TTL			= 0;

static CLIOption		kPortMappingOpts[] =
{
	InterfaceOption(),
	BooleanOption( 0, "tcp",			&gPortMapping_ProtocolTCP,	"Use kDNSServiceProtocol_TCP." ),
	BooleanOption( 0, "udp",			&gPortMapping_ProtocolUDP,	"Use kDNSServiceProtocol_UDP." ),
	IntegerOption( 0, "internalPort",	&gPortMapping_InternalPort,	"port number", "Internal port.", false ),
	IntegerOption( 0, "externalPort",	&gPortMapping_ExternalPort,	"port number", "Requested external port. Use '0' for any external port.", false ),
	IntegerOption( 0, "ttl",			&gPortMapping_TTL,			"seconds", "Requested TTL (renewal period) in seconds. Use '0' for a default value.", false ),
	
	CLI_OPTION_GROUP( "Flags" ),
	DNSSDFlagsOption(),
	
	CLI_OPTION_GROUP( "Operation" ),
	ConnectionOptions(),
	
	ConnectionSection(),
	CLI_OPTION_END()
};

#if( TARGET_OS_DARWIN )
//===========================================================================================================================
//	RegisterKA Command Options
//===========================================================================================================================

static const char *		gRegisterKA_LocalAddress	= NULL;
static const char *		gRegisterKA_RemoteAddress	= NULL;
static int				gRegisterKA_Timeout			= 0;

static CLIOption		kRegisterKA_Opts[] =
{
	DNSSDFlagsOption(),
	StringOption(  'l', "local",   &gRegisterKA_LocalAddress,  "IP addr+port", "TCP connection's local IPv4 or IPv6 address and port pair.", true ),
	StringOption(  'r', "remote",  &gRegisterKA_RemoteAddress, "IP addr+port", "TCP connection's remote IPv4 or IPv6 address and port pair.", true ),
	IntegerOption( 't', "timeout", &gRegisterKA_Timeout,       "timeout", "Keepalive record's timeout value, i.e., its 't=' value.", false ),
	CLI_OPTION_END()
};

static void	RegisterKACmd( void );
#endif

//===========================================================================================================================
//	BrowseAll Command Options
//===========================================================================================================================

static const char *		gBrowseAll_Domain				= NULL;
static const char **	gBrowseAll_ServiceTypes			= NULL;
static size_t			gBrowseAll_ServiceTypesCount	= 0;
static int				gBrowseAll_BrowseTimeSecs		= 5;
static int				gBrowseAll_ConnectTimeout		= 0;
static int 				gBrowseAll_UseNewGAI		 	= false;
static int 				gBrowseAll_ValidateResults		= false;

static CLIOption		kBrowseAllOpts[] =
{
	InterfaceOption(),
	StringOption(      'd', "domain",    &gBrowseAll_Domain,          "domain", "Domain in which to browse for the service.", false ),
	MultiStringOption( 't', "type",      &gBrowseAll_ServiceTypes, &gBrowseAll_ServiceTypesCount, "service type", "Service type(s), e.g., \"_ssh._tcp\". All services are browsed for if none is specified.", false ),
	BooleanOption(      0 , "useNewGAI", &gBrowseAll_UseNewGAI,       "Use dnssd_getaddrinfo_* instead of DNSServiceGetAddrInfo()." ),
	BooleanOption(     'v', "validate",  &gBrowseAll_ValidateResults, "Validate results." ),
	
	CLI_OPTION_GROUP( "Flags" ),
	DNSSDFlagsOption_IncludeAWDL(),
	
	CLI_OPTION_GROUP( "Operation" ),
	IntegerOption( 'b', "browseTime",     &gBrowseAll_BrowseTimeSecs, "seconds", "Amount of time to spend browsing in seconds. (default: 5)", false ),
	IntegerOption( 'c', "connectTimeout", &gBrowseAll_ConnectTimeout, "seconds", "Timeout for connection attempts. If <= 0, no connections are attempted. (default: 0)", false ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	GetNameInfo Command Options
//===========================================================================================================================

static void	GetNameInfoCmd( void );

static char *		gGetNameInfo_IPAddress			= NULL;
static int			gGetNameInfoFlag_DGram			= false;
static int			gGetNameInfoFlag_NameReqd		= false;
static int			gGetNameInfoFlag_NoFQDN			= false;
static int			gGetNameInfoFlag_NumericHost	= false;
static int			gGetNameInfoFlag_NumericScope	= false;
static int			gGetNameInfoFlag_NumericServ	= false;

static CLIOption		kGetNameInfoOpts[] =
{
	StringOption( 'a', "address",           &gGetNameInfo_IPAddress,        "IP address", "IPv4 or IPv6 address to use in sockaddr structure.", true ),
	
	CLI_OPTION_GROUP( "Flags" ),
	BooleanOption( 0 , "flag-dgram",        &gGetNameInfoFlag_DGram,        "Use NI_DGRAM flag." ),
	BooleanOption( 0 , "flag-namereqd",     &gGetNameInfoFlag_NameReqd,     "Use NI_NAMEREQD flag." ),
	BooleanOption( 0 , "flag-nofqdn",       &gGetNameInfoFlag_NoFQDN,       "Use NI_NOFQDN flag." ),
	BooleanOption( 0 , "flag-numerichost",  &gGetNameInfoFlag_NumericHost,  "Use NI_NUMERICHOST flag." ),
	BooleanOption( 0 , "flag-numericscope", &gGetNameInfoFlag_NumericScope, "Use NI_NUMERICSCOPE flag." ),
	BooleanOption( 0 , "flag-numericserv",  &gGetNameInfoFlag_NumericServ,  "Use NI_NUMERICSERV flag." ),
	
	CLI_SECTION( "Notes", "See getnameinfo(3) man page for more details.\n" ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	GetAddrInfoStress Command Options
//===========================================================================================================================

static int		gGAIStress_TestDurationSecs	= 0;
static int		gGAIStress_ConnectionCount	= 0;
static int		gGAIStress_DurationMinMs	= 0;
static int		gGAIStress_DurationMaxMs	= 0;
static int		gGAIStress_RequestCountMax	= 0;

static CLIOption		kGetAddrInfoStressOpts[] =
{
	InterfaceOption(),
	
	CLI_OPTION_GROUP( "Flags" ),
	DNSSDFlagsOption_ReturnIntermediates(),
	DNSSDFlagsOption_SuppressUnusable(),
	
	CLI_OPTION_GROUP( "Operation" ),
	IntegerOption( 0, "testDuration",			&gGAIStress_TestDurationSecs,	"seconds",	"Stress test duration in seconds. Use '0' for forever.", false ),
	IntegerOption( 0, "connectionCount",		&gGAIStress_ConnectionCount,	"integer",	"Number of simultaneous DNS-SD connections.", true ),
	IntegerOption( 0, "requestDurationMin",		&gGAIStress_DurationMinMs,		"ms",		"Minimum duration of DNSServiceGetAddrInfo() request in milliseconds.", true ),
	IntegerOption( 0, "requestDurationMax",		&gGAIStress_DurationMaxMs,		"ms",		"Maximum duration of DNSServiceGetAddrInfo() request in milliseconds.", true ),
	IntegerOption( 0, "consecutiveRequestMax",	&gGAIStress_RequestCountMax,	"integer",	"Maximum number of requests on a connection before restarting it.", true ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	DNSQuery Command Options
//===========================================================================================================================

static char *		gDNSQuery_Name				= NULL;
static char *		gDNSQuery_Type				= "A";
static char *		gDNSQuery_Server			= NULL;
static int			gDNSQuery_TimeLimitSecs		= 5;
static int			gDNSQuery_UseTCP			= false;
static int			gDNSQuery_Flags				= kDNSHeaderFlag_RecursionDesired;
static int			gDNSQuery_DNSSEC			= false;
static int			gDNSQuery_CheckingDisabled	= false;
static int			gDNSQuery_RawRData			= false;
static int			gDNSQuery_Verbose			= false;

#if( TARGET_OS_DARWIN )
	#define kDNSQueryServerOptionIsRequired		false
#else
	#define kDNSQueryServerOptionIsRequired		true
#endif

static CLIOption		kDNSQueryOpts[] =
{
	StringOption(   'n', "name",             &gDNSQuery_Name,             "name", "Question name (QNAME) to put in DNS query message.", true ),
	StringOption(   't', "type",             &gDNSQuery_Type,             "type", "Question type (QTYPE) to put in DNS query message. (default: A)", false ),
	StringOptionEx( 's', "server",           &gDNSQuery_Server,           "IP address[+port]", "DNS server's IPv4 or IPv6 address and optional port.", kDNSQueryServerOptionIsRequired,
		"The following exemplify the notations that are supported:\n"
		"\n"
		"    IPv4 address without port:     192.0.2.1\n"
		"    IPv4 address with port 50001:  192.0.2.1:50001\n"
		"    IPv6 address without port:     2001:db8::1\n"
		"    IPv6 address with port 50001:  [2001:db8::1]:50001\n"
		"\n"
		"If no port is specified, then the default DNS port of 53 is assumed.\n"
		"\n"
	),
	IntegerOption( 'l', "timeLimit",        &gDNSQuery_TimeLimitSecs,    "seconds", "Specifies query time limit. Use '-1' for no limit and '0' to exit immediately after sending.", false ),
	BooleanOption(  0 , "tcp",              &gDNSQuery_UseTCP,           "Send the DNS query via TCP instead of UDP." ),
	IntegerOption( 'f', "flags",            &gDNSQuery_Flags,            "flags", "16-bit value for DNS header flags/codes field. (default: 0x0100 [Recursion Desired])", false ),
	BooleanOption(  0 , "dnssec",           &gDNSQuery_DNSSEC,           "Set the AD bit and include OPT record with DO extended flag bit set." ),
	BooleanOption(  0 , "checkingDisabled", &gDNSQuery_CheckingDisabled, "Set the Checking Disabled (CD) bit." ),
	BooleanOption(  0 , "raw",              &gDNSQuery_RawRData,         "Present record data as a hexdump." ),
	BooleanOption( 'v', "verbose",          &gDNSQuery_Verbose,          "Prints the DNS message to be sent to the server." ),
	CLI_OPTION_END()
};

#if( DNSSDUTIL_INCLUDE_DNSCRYPT )
//===========================================================================================================================
//	DNSCrypt Command Options
//===========================================================================================================================

static char *		gDNSCrypt_ProviderName	= NULL;
static char *		gDNSCrypt_ProviderKey	= NULL;
static char *		gDNSCrypt_Name			= NULL;
static char *		gDNSCrypt_Type			= NULL;
static char *		gDNSCrypt_Server		= NULL;
static int			gDNSCrypt_TimeLimitSecs	= 5;
static int			gDNSCrypt_RawRData		= false;
static int			gDNSCrypt_Verbose		= false;

static CLIOption		kDNSCryptOpts[] =
{
	StringOption(  'p', "providerName",	&gDNSCrypt_ProviderName,	"name", "The DNSCrypt provider name.", true ),
	StringOption(  'k', "providerKey",	&gDNSCrypt_ProviderKey,		"hex string", "The DNSCrypt provider's public signing key.", true ),
	StringOption(  'n', "name",			&gDNSCrypt_Name,			"name",	"Question name (QNAME) to put in DNS query message.", true ),
	StringOption(  't', "type",			&gDNSCrypt_Type,			"type",	"Question type (QTYPE) to put in DNS query message.", true ),
	StringOption(  's', "server",		&gDNSCrypt_Server,			"IP address", "DNS server's IPv4 or IPv6 address.", true ),
	IntegerOption( 'l', "timeLimit",	&gDNSCrypt_TimeLimitSecs,	"seconds", "Specifies query time limit. Use '-1' for no time limit and '0' to exit immediately after sending.", false ),
	BooleanOption(  0 , "raw",			&gDNSCrypt_RawRData,		"Present record data as a hexdump." ),
	BooleanOption( 'v', "verbose",		&gDNSCrypt_Verbose,			"Prints the DNS message to be sent to the server." ),
	CLI_OPTION_END()
};
#endif

//===========================================================================================================================
//	MDNSQuery Command Options
//===========================================================================================================================

static char *		gMDNSQuery_Name			= NULL;
static char *		gMDNSQuery_Type			= NULL;
static int			gMDNSQuery_SourcePort	= 0;
static int			gMDNSQuery_IsQU			= false;
static int			gMDNSQuery_RawRData		= false;
static int			gMDNSQuery_UseIPv4		= false;
static int			gMDNSQuery_UseIPv6		= false;
static int			gMDNSQuery_AllResponses	= false;
static int			gMDNSQuery_ReceiveSecs	= 1;

static CLIOption		kMDNSQueryOpts[] =
{
	StringOption(  'i', "interface",	&gInterface,				"name or index", "Network interface by name or index.", true ),
	StringOption(  'n', "name",			&gMDNSQuery_Name,			"name", "Question name (QNAME) to put in mDNS message.", true ),
	StringOption(  't', "type",			&gMDNSQuery_Type,			"type", "Question type (QTYPE) to put in mDNS message.", true ),
	IntegerOption( 'p', "sourcePort",	&gMDNSQuery_SourcePort,		"port number", "UDP source port to use when sending mDNS messages. Default is 5353 for QM questions.", false ),
	BooleanOption( 'u', "QU",			&gMDNSQuery_IsQU,			"Set the unicast-response bit, i.e., send a QU question." ),
	BooleanOption(  0 , "raw",			&gMDNSQuery_RawRData,		"Present record data as a hexdump." ),
	BooleanOption(  0 , "ipv4",			&gMDNSQuery_UseIPv4,		"Use IPv4." ),
	BooleanOption(  0 , "ipv6",			&gMDNSQuery_UseIPv6,		"Use IPv6." ),
	BooleanOption( 'a', "allResponses",	&gMDNSQuery_AllResponses,	"Print all received mDNS messages, not just those containing answers." ),
	IntegerOption( 'r', "receiveTime",	&gMDNSQuery_ReceiveSecs,	"seconds", "Amount of time to spend receiving messages after the query is sent. The default is one second. Use -1 for unlimited time.", false ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	MDNSCollider Command Options
//===========================================================================================================================

#define kMDNSColliderProgramSection_Intro																				\
	"Programs dictate when the collider sends out unsolicited response messages for its record and how the collider\n"	\
	"ought to react to probe queries that match its record's name, if at all.\n"										\
	"\n"																												\
	"For example, suppose that the goal is to cause a specific unique record in the verified state to be renamed.\n"	\
	"The collider should be invoked such that its record's name is equal to that of the record being targeted. Also,\n"	\
	"the record's type and data should be such that no record with that name, type, and data combination currently\n"	\
	"exists. If the mDNS responder that owns the record follows sections 8.1 and 9 of RFC 6762, then the goal can be\n"	\
	"accomplished with the following program:\n"																		\
	"\n"																												\
	"    probes 3r; send; wait 5000\n"																					\
	"\n"																												\
	"The first command, 'probes 3r', tells the collider to respond to the next three probe queries that match its\n"	\
	"record's name. The second command, makes the collider send an unsolicited response message that contains its\n"	\
	"record in the answer section. The third command makes the collider wait for five seconds before exiting, which\n"	\
	"is more than enough time for the collider to respond to probe queries.\n"											\
	"\n"																												\
	"The send command will cause the targeted record to go into the probing state per section 9 since the collider's\n"	\
	"record conflicts with target record. Per the probes command, the subsequent probe query sent during the probing\n"	\
	"state will be answered by the collider, which will cause the record to be renamed per section 8.1.\n"

#define kMDNSColliderProgramSection_Probes																				\
	"The probes command defines how the collider ought to react to probe queries that match its record's name.\n"		\
	"\n"																												\
	"Usage: probes [<action-string>]\n"																					\
	"\n"																												\
	"The syntax for an action-string is\n"																				\
	"\n"																												\
	"    <action-string> ::= <action> | <action-string> \"-\" <action>\n"												\
	"    <action>        ::= [<repeat-count>] <action-code>\n"															\
	"    <repeat-count>  ::= \"1\" | \"2\" | ... | \"10\"\n"															\
	"    <action-code>   ::= \"n\" | \"r\" | \"u\" | \"m\" | \"p\"\n"													\
	"\n"																												\
	"An expanded action-string is defined as\n"																			\
	"\n"																												\
	"    <expanded-action-string> ::= <action-code> | <expanded-action-string> \"-\" <action-code>\n"					\
	"\n"																												\
	"The action-string argument is converted into an expanded-action-string by expanding each action with a\n"			\
	"repeat-count into an expanded-action-string consisting of exactly <repeat-count> <action-code>s. For example,\n"	\
	"2n-r expands to n-n-r. Action-strings that expand to expanded-action-strings with more than 10 action-codes\n"		\
	"are not allowed.\n"																								\
	"\n"																												\
	"When the probes command is executed, it does two things. Firstly, it resets to zero the collider's count of\n"		\
	"probe queries that match its record's name. Secondly, it defines how the collider ought to react to such probe\n"	\
	"queries based on the action-string argument. Specifically, the nth action-code in the expanded version of the\n"	\
	"action-string argument defines how the collider ought to react to the nth received probe query:\n"					\
	"\n"																												\
	"    Code  Action\n"																								\
	"    ----  ------\n"																								\
	"    n     Do nothing.\n"																							\
	"    r     Respond to the probe query.\n"																			\
	"    u     Respond to the probe query via unicast.\n"																\
	"    m     Respond to the probe query via multicast.\n"																\
	"    p     Multicast own probe query. (Useful for causing simultaneous probe scenarios.)\n"							\
	"\n"																												\
	"Note: If no action is defined for a received probe query, then the collider does nothing, i.e., it doesn't send\n"	\
	"a response nor does it multicast its own probe query.\n"

#define kMDNSColliderProgramSection_Send																				\
	"The send command multicasts an unsolicited mDNS response containing the collider's record in the answer\n"			\
	"section, which can be used to force unique records with the same record name into the probing state.\n"			\
	"\n"																												\
	"Usage: send\n"

#define kMDNSColliderProgramSection_Wait																				\
	"The wait command pauses program execution for the interval of time specified by its argument.\n"					\
	"\n"																												\
	"Usage: wait <milliseconds>\n"

#define kMDNSColliderProgramSection_Loop																				\
	"The loop command starts a counting loop. The done statement marks the end of the loop body. The loop command's\n"	\
	"argument specifies the number of loop iterations. Note: Loop nesting is supported up to a depth of 16.\n"			\
	"\n"																												\
	"Usage: loop <non-zero count>; ... ; done\n"																		\
	"\n"																												\
	"For example, the following program sends three unsolicited responses at an approximate rate of one per second:\n"	\
	"\n"																												\
	"    loop 3; wait 1000; send; done"

#define ConnectionSection()		CLI_SECTION( kConnectionSection_Name, kConnectionSection_Text )

static const char *		gMDNSCollider_Name			= NULL;
static const char *		gMDNSCollider_Type			= NULL;
static const char *		gMDNSCollider_RecordData	= NULL;
static int				gMDNSCollider_UseIPv4		= false;
static int				gMDNSCollider_UseIPv6		= false;
static const char *		gMDNSCollider_Program		= NULL;

static CLIOption		kMDNSColliderOpts[] =
{
	StringOption(  'i', "interface", &gInterface,               "name or index", "Network interface by name or index.", true ),
	StringOption(  'n', "name",      &gMDNSCollider_Name,       "name", "Collider's record name.", true ),
	StringOption(  't', "type",      &gMDNSCollider_Type,       "type", "Collider's record type.", true ),
	StringOption(  'd', "data",      &gMDNSCollider_RecordData, "record data", "Collider's record data. See " kRecordDataSection_Name " below.", true ),
	StringOption(  'p', "program",   &gMDNSCollider_Program,    "program", "Program to execute. See Program section below.", true ),
	BooleanOption(  0 , "ipv4",      &gMDNSCollider_UseIPv4,    "Use IPv4." ),
	BooleanOption(  0 , "ipv6",      &gMDNSCollider_UseIPv6,    "Use IPv6." ),
	
	RecordDataSection(),
	CLI_SECTION( "Program",					kMDNSColliderProgramSection_Intro ),
	CLI_SECTION( "Program Command: probes",	kMDNSColliderProgramSection_Probes ),
	CLI_SECTION( "Program Command: send",	kMDNSColliderProgramSection_Send ),
	CLI_SECTION( "Program Command: wait",	kMDNSColliderProgramSection_Wait ),
	CLI_SECTION( "Program Command: loop",	kMDNSColliderProgramSection_Loop ),
	CLI_OPTION_END()
};

static void	MDNSColliderCmd( void );

#if( TARGET_OS_DARWIN )
//===========================================================================================================================
//	PIDToUUID Command Options
//===========================================================================================================================

static int		gPIDToUUID_PID = 0;

static CLIOption		kPIDToUUIDOpts[] =
{
	IntegerOption( 'p', "pid", &gPIDToUUID_PID, "PID", "Process ID.", true ),
	CLI_OPTION_END()
};
#endif

//===========================================================================================================================
//	DNSServer Command Options
//===========================================================================================================================

static const char		kDNSServerInfoText_Intro[] =
	"The DNS server answers certain queries in the d.test. domain. Responses are dynamically generated based on the\n"
	"presence of special labels in the query's QNAME. There are currently nine types of special labels that can be\n"
	"used to generate specific responses: Alias labels, Alias-TTL labels, Count labels, Tag labels, TTL labels, the\n"
	"IPv4 label, the IPv6 label, Index labels, and SRV labels.\n"
	"\n"
	"Note: Sub-strings representing integers in domain name labels are in decimal notation and without leading zeros.\n";

static const char		kDNSServerInfoText_NameExistence[] =
	"A name is considered to exist if it's an Address name or an SRV name.\n"
	"\n"
	"An Address name is defined as a name that ends with d.test., and the other labels, if any, and in no particular\n"
	"order, unless otherwise noted, consist of\n"
	"\n"
	"    1. at most one Alias or Alias-TTL label as the first label;\n"
	"    2. at most one Count label;\n"
	"    3. zero or more Tag labels;\n"
	"    4. at most one TTL label; and\n"
	"    5. at most one IPv4 or IPv6 label.\n"
	"    6. at most one Index label.\n"
	"\n"
	"An SRV name is defined as a name with the following form:\n"
	"\n"
	" _<service>._<proto>[.<parent domain>][.<SRV label 1>[.<target 1>][.<SRV label 2>[.<target 2>][...]]].d.test.\n"
	"\n"
	"See \"SRV Names\" for details.\n";

static const char		kDNSServerInfoText_ResourceRecords[] =
	"Currently, the server only supports CNAME, A, AAAA, and SRV records.\n"
	"\n"
	"Address names that begin with an Alias or Alias-TTL label are aliases of canonical names, i.e., they're the\n"
	"names of CNAME records. See \"Alias Labels\" and \"Alias-TTL Labels\" for details.\n"
	"\n"
	"A canonical Address name can exclusively be the name of one or more A records, can exclusively be the name or\n"
	"one or more AAAA records, or can be the name of both A and AAAA records. Address names that contain an IPv4\n"
	"label have at least one A record, but no AAAA records. Address names that contain an IPv6 label, have at least\n"
	"one AAAA record, but no A records. All other Address names have at least one A record and at least one AAAA\n"
	"record. See \"Count Labels\" for how the number of address records for a given Address name is determined.\n"
	"\n"
	"A records contain IPv4 addresses in the 203.0.113.0/24 block, while AAAA records contain IPv6 addresses in the\n"
	"2001:db8:1::/120 block. Both of these address blocks are reserved for documentation. See\n"
	"<https://tools.ietf.org/html/rfc5737> and <https://tools.ietf.org/html/rfc3849>.\n"
	"\n"
	"SRV names are names of SRV records.\n"
	"\n"
	"Unless otherwise specified, all resource records will use a default TTL. The default TTL can be set with the\n"
	"--defaultTTL option. See \"Alias-TTL Labels\" and \"TTL Labels\" for details on how to query for CNAME, A, and\n"
	"AAAA records with specific TTL values.\n";

static const char		kDNSServerInfoText_AliasLabel[] =
	"Alias labels are of the form \"alias\" or \"alias-N\", where N is an integer in [2, 2^31 - 1].\n"
	"\n"
	"If QNAME is an Address name and its first label is Alias label \"alias-N\", then the response will contain\n"
	"exactly N CNAME records:\n"
	"\n"
	"    1. For each i in [3, N], the response will contain a CNAME record whose name is identical to QNAME, except\n"
	"       that the first label is \"alias-i\" instead, and whose RDATA is the name of the other CNAME record whose\n"
	"       name has \"alias-(i - 1)\" as its first label.\n"
	"\n"
	"    2. The response will contain a CNAME record whose name is identical to QNAME, except that the first label\n"
	"       is \"alias-2\" instead, and whose RDATA is the name identical to QNAME, except that the first label is\n"
	"       \"alias\" instead.\n"
	"\n"
	"    3. The response will contain a CNAME record whose name is identical to QNAME, except that the first label\n"
	"       is \"alias\" instead, and whose RDATA is the name identical to QNAME minus its first label.\n"
	"\n"
	"If QNAME is an Address name and its first label is Alias label \"alias\", then the response will contain a\n"
	"single CNAME record. The CNAME record's name will be equal to QNAME and its RDATA will be the name identical to\n"
	"QNAME minus its first label.\n"
	"\n"
	"Example. A response to a query with a QNAME of alias-3.count-5.d.test will contain the following CNAME\n"
	"records:\n"
	"\n"
	"    alias-4.count-5.d.test.                        60    IN CNAME alias-3.count-5.d.test.\n"
	"    alias-3.count-5.d.test.                        60    IN CNAME alias-2.count-5.d.test.\n"
	"    alias-2.count-5.d.test.                        60    IN CNAME alias.count-5.d.test.\n"
	"    alias.count-5.d.test.                          60    IN CNAME count-5.d.test.\n";

static const char		kDNSServerInfoText_AliasTTLLabel[] =
	"Alias-TTL labels are of the form \"alias-ttl-T_1[-T_2[...-T_N]]\", where each T_i is an integer in\n"
	"[0, 2^31 - 1] and N is a positive integer bounded by the size of the maximum legal label length (63 octets).\n"
	"\n"
	"If QNAME is an Address name and its first label is Alias-TTL label \"alias-ttl-T_1...-T_N\", then the response\n"
	"will contain exactly N CNAME records:\n"
	"\n"
	"    1. For each i in [1, N - 1], the response will contain a CNAME record whose name is identical to QNAME,\n"
	"       except that the first label is \"alias-ttl-T_i...-T_N\" instead, whose TTL value is T_i, and whose RDATA\n"
	"       is the name of the other CNAME record whose name has \"alias-ttl-T_(i+1)...-T_N\" as its first label.\n"
	"\n"
	"    2. The response will contain a CNAME record whose name is identical to QNAME, except that the first label\n"
	"       is \"alias-ttl-T_N\", whose TTL is T_N, and whose RDATA is identical to QNAME stripped of its first\n"
	"       label.\n"
	"\n"
	"Example. A response to a query with a QNAME of alias-ttl-20-40-80.count-5.d.test will contain the following\n"
	"CNAME records:\n"
	"\n"
	"    alias-ttl-20-40-80.count-5.d.test.             20    IN CNAME alias-ttl-40-80.count-5.d.test.\n"
	"    alias-ttl-40-80.count-5.d.test.                40    IN CNAME alias-ttl-80.count-5.d.test.\n"
	"    alias-ttl-80.count-5.d.test.                   80    IN CNAME count-5.d.test.\n";

static const char		kDNSServerInfoText_CountLabel[] =
	"Count labels are of the form \"count-N_1\" or \"count-N_1-N_2\", where N_1 is an integer in [0, 255] and N_2 is\n"
	"an integer in [N_1, 255].\n"
	"\n"
	"If QNAME is an Address name, contains Count label \"count-N\", and has the type of address records specified by\n"
	"QTYPE, then the response will contain exactly N address records:\n"
	"\n"
	"    1. For i in [1, N], the response will contain an address record of type QTYPE whose name is equal to QNAME\n"
	"       and whose RDATA is an address equal to a constant base address + i.\n"
	"\n"
	"    2. The address records will be ordered by the address contained in RDATA in ascending order.\n"
	"\n"
	"Example. A response to an A record query with a QNAME of alias.count-3.d.test will contain the following A\n"
	"records:\n"
	"\n"
	"    count-3.d.test.                                60    IN A     203.0.113.1\n"
	"    count-3.d.test.                                60    IN A     203.0.113.2\n"
	"    count-3.d.test.                                60    IN A     203.0.113.3\n"
	"\n"
	"If QNAME is an Address name, contains Count label \"count-N_1-N_2\", and has the type of address records\n"
	"specified by QTYPE, then the response will contain exactly N_1 address records:\n"
	"\n"
	"    1. Each of the address records will be of type QTYPE, have name equal to QNAME, and have as its RDATA a\n"
	"       unique address equal to a constant base address + i, where i is a randomly chosen integer in [1, N_2].\n"
	"\n"
	"    2. The order of the address records will be random.\n"
	"\n"
	"Example. A response to a AAAA record query with a QNAME of count-3-100.ttl-20.d.test could contain the\n"
	"following AAAA records:\n"
	"\n"
	"    count-3-100.ttl-20.d.test.                     20    IN AAAA  2001:db8:1::c\n"
	"    count-3-100.ttl-20.d.test.                     20    IN AAAA  2001:db8:1::3a\n"
	"    count-3-100.ttl-20.d.test.                     20    IN AAAA  2001:db8:1::4f\n"
	"\n"
	"If QNAME is an Address name, but doesn't have the type of address records specified by QTYPE, then the response\n"
	"will contain no address records, regardless of whether it contains a Count label.\n"
	"\n"
	"Address names that don't have a Count label are treated as though they contain a count label equal to\n"
	"count-1\".\n";

static const char		kDNSServerInfoText_TagLabel[] =
	"Tag labels are labels prefixed with \"tag-\" and contain zero or more arbitrary octets after the prefix.\n"
	"\n"
	"This type of label exists to allow testers to \"uniquify\" domain names. Tag labels can also serve as padding\n"
	"to increase the sizes of domain names.\n";

static const char		kDNSServerInfoText_TTLLabel[] =
	"TTL labels are of the form \"ttl-T\", where T is an integer in [0, 2^31 - 1].\n"
	"\n"
	"If QNAME is an Address name and contains TTL label \"ttl-T\", then all non-CNAME records contained in the\n"
	"response will have a TTL value equal to T.\n";

static const char		kDNSServerInfoText_IPv4Label[] =
	"The IPv4 label is \"ipv4\". See \"Resource Records\" for the affect of this label.\n";

static const char		kDNSServerInfoText_IPv6Label[] =
	"The IPv6 label is \"ipv6\". See \"Resource Records\" for the affect of this label.\n";

static const char		kDNSServerInfoText_IndexLabel[] =
	"Index labels are of the form \"index-N\", where N is an integer in [1, 2^31 - 1].\n"
	"\n"
	"When the server runs in loopback-only mode, each of the server's addresses is assigned a sequential index value\n"
	"starting from 1. For example, if the server is running in loopback-only mode and listening exclusively on IPv6\n"
	"with two extra IPv6 addresses, then address ::1 would be assigned index 1, the first extra IPv6 address would be\n"
	"assigned index 2, and the second extra IPv6 address would be assigned index 3.\n"
	"\n"
	"If QNAME is an Address name and has an index label, then the query will be ignored unless the query was received\n"
	"on an address whose index value equals that of the index label. This is useful for simulating unresponsive servers.\n";

static const char		kDNSServerInfoText_SRVNames[] =
	"SRV labels are of the form \"srv-R-W-P\", where R, W, and P are integers in [0, 2^16 - 1].\n"
	"\n"
	"After the first two labels, i.e., the service and protocol labels, the sequence of labels, which may be empty,\n"
	"leading up to the the first SRV label, if one exists, or the d.test. labels will be used as a parent domain for\n"
	"the target hostname of each of the SRV name's SRV records.\n"
	"\n"
	"If QNAME is an SRV name and QTYPE is SRV, then for each SRV label, the response will contain an SRV record with\n"
	"priority R, weight W, port P, and target hostname <target>[.<parent domain>]., where <target> is the sequence\n"
	"of labels, which may be empty, that follows the SRV label leading up to either the next SRV label or the\n"
	"d.test. labels, whichever comes first.\n"
	"\n"
	"Example. A response to an SRV record query with a QNAME of\n"
	"_http._tcp.example.com.srv-0-0-80.www.srv-1-0-8080.www.d.test. will contain the following SRV records:\n"
	"\n"
	"_http._tcp.example.com.srv-0-0-80.www.srv-1-0-8080.www.d.test.     60    IN SRV   0 0 80 www.example.com.\n"
	"_http._tcp.example.com.srv-0-0-80.www.srv-1-0-8080.www.d.test.     60    IN SRV   1 0 8080 www.example.com.\n";

static const char		kDNSServerInfoText_BadUDPMode[] =
	"The purpose of Bad UDP mode is to test mDNSResponder's TCP fallback mechanism by which mDNSResponder reissues a\n"
	"UDP query as a TCP query if the UDP response contains the expected QNAME, QTYPE, and QCLASS, but a message ID\n"
	"that's not equal to the query's message ID.\n"
	"\n"
	"This mode is identical to the normal mode except that all responses sent via UDP have a message ID equal to the\n"
	"query's message ID plus one. Also, in this mode, to aid in debugging, A records in responses sent via UDP have\n"
	"IPv4 addresses in the 0.0.0.0/24 block instead of the 203.0.113.0/24 block, i.e., 0.0.0.0 is used as the IPv4\n"
	"base address, and AAAA records in responses sent via UDP have IPv6 addresses in the ::ffff:0:0/120 block\n"
	"instead of the 2001:db8:1::/120 block, i.e., ::ffff:0:0 is used as the IPv6 base address.\n";

typedef enum
{
	kDNSProtocol_Do53	= 0,
	kDNSProtocol_DoT	= 1,
	kDNSProtocol_DoH	= 2
	
}	DNSProtocol;

#define kDNSProtocolStr_Do53		"Do53"
#define kDNSProtocolStr_DoT			"DoT"
#define kDNSProtocolStr_DoH			"DoH"

static int				gDNSServer_LoopbackOnly			= false;
static int				gDNSServer_Foreground			= false;
static int				gDNSServer_ResponseDelayMs		= 0;
static int				gDNSServer_DefaultTTL			= 60;
static int				gDNSServer_Port					= -1;
static const char *		gDNSServer_DomainOverride		= NULL;
static char **			gDNSServer_IgnoredQTypes		= NULL;
static size_t			gDNSServer_IgnoredQTypesCount	= 0;
static int				gDNSServer_ListenOnV4			= false;
static int				gDNSServer_ListenOnV6			= false;
static int				gDNSServer_BadUDPMode			= false;
static const char *		gDNSServer_FollowPID			= NULL;
static int				gDNSServer_ExtraV6Count			= 0;
static const char *		gDNSServer_Protocol				= kDNSProtocolStr_Do53;
static int				gDNSServer_RegisterWithSC		= false;
static int				gDNSServer_MatchAllDomains		= false;

static CLIOption		kDNSServerOpts[] =
{
	BooleanOption(     'l', "loopback",      &gDNSServer_LoopbackOnly,    "Bind only to the loopback interface." ),
	BooleanOption(     'f', "foreground",    &gDNSServer_Foreground,      "Direct log output to stdout instead of system logging." ),
	IntegerOption(     'd', "responseDelay", &gDNSServer_ResponseDelayMs, "ms", "The amount of additional delay in milliseconds to apply to responses. (default: 0)", false ),
	IntegerOption(      0 , "defaultTTL",    &gDNSServer_DefaultTTL,      "seconds", "Resource record TTL value to use when unspecified. (default: 60)", false ),
	IntegerOption(     'p', "port",          &gDNSServer_Port,            "port number", "UDP/TCP listening port. Use 0 for an ephemeral port. (default: Do53 → 53, DoT → 853, DoH → 443)", false ),
	StringOption(       0 , "domain",        &gDNSServer_DomainOverride,  "domain", "Use to override 'd.test.' as the server's domain.", false ),
	MultiStringOption( 'i', "ignoreQType",	 &gDNSServer_IgnoredQTypes, &gDNSServer_IgnoredQTypesCount, "qtype", "A QTYPE to ignore. This option can be specified more than once.", false ),
	BooleanOption(      0 , "ipv4",          &gDNSServer_ListenOnV4,      "Listen on IPv4. Will listen on both IPv4 and IPv6 if neither --ipv4 nor --ipv6 is used." ),
	BooleanOption(      0 , "ipv6",          &gDNSServer_ListenOnV6,      "Listen on IPv6. Will listen on both IPv4 and IPv6 if neither --ipv4 nor --ipv6 is used." ),
#if( TARGET_OS_DARWIN )
	StringOption(       0 , "follow",        &gDNSServer_FollowPID,       "pid", "Exit when the process, usually the parent process, specified by PID exits.", false ),
#endif
	BooleanOption(      0 , "badUDPMode",    &gDNSServer_BadUDPMode,      "Run in Bad UDP mode to trigger mDNSResponder's TCP fallback mechanism." ),
	StringOptionEx(    'P', "protocol",      &gDNSServer_Protocol,        "protocol", "The DNS protocol to use. (default: Do53)", false,
		"\n"
		"Use '" kDNSProtocolStr_Do53 "' for DNS over UDP and TCP (Do53).\n"
		"Use '" kDNSProtocolStr_DoT  "' for DNS over TLS (DoT).\n"
		"Use '" kDNSProtocolStr_DoH  "' for DNS over HTTPS (DoH).\n"
 	),
	BooleanOption(     's', "registerSC",    &gDNSServer_RegisterWithSC,  "Register Do53 service with SystemConfiguration instead of mrc_dns_service_registration_*." ),
	BooleanOption(      0 , "default",       &gDNSServer_MatchAllDomains, "If registering Do53 service with SystemConfiguration, include '.' as a match domain." ),
#if( TARGET_OS_DARWIN )
	CLI_OPTION_GROUP( "Loopback-Only Mode Options" ),
	IntegerOptionEx( 0 , "extraIPv6",     &gDNSServer_ExtraV6Count,    "count", "The number of extra IPv6 addresses to listen on. (default: 0)", false,
		"\n"
		"This option will add extra IPv6 addresses from the fded:f035:55e4::/64 address block to the loopback interface.\n"
		"The server will then bind to those addresses in addition to the standard loopback IP addresses, i.e., 127.0.0.1.\n"
		"and/or ::1, depending on the specified IP protocol options.\n"
		"\n"
		"This option is useful for setting up a DNS configuration with multiple server addresses, e.g., one for the\n"
		"primary server, one for the secondary server, etc. The Index label can then be used to simulate unresponsive\n"
		"servers.\n"
		"\n"
		"Note: This option is ignored unless the server is in loopback only mode and listening on IPv6.\n"
		"Note: This option currently requires root privileges.\n"
	),
#endif
	CLI_SECTION( "Intro",				kDNSServerInfoText_Intro ),
	CLI_SECTION( "Name Existence",		kDNSServerInfoText_NameExistence ),
	CLI_SECTION( "Resource Records",	kDNSServerInfoText_ResourceRecords ),
	CLI_SECTION( "Alias Labels",		kDNSServerInfoText_AliasLabel ),
	CLI_SECTION( "Alias-TTL Labels",	kDNSServerInfoText_AliasTTLLabel ),
	CLI_SECTION( "Count Labels",		kDNSServerInfoText_CountLabel ),
	CLI_SECTION( "Tag Labels",			kDNSServerInfoText_TagLabel ),
	CLI_SECTION( "TTL Labels",			kDNSServerInfoText_TTLLabel ),
	CLI_SECTION( "IPv4 Label",			kDNSServerInfoText_IPv4Label ),
	CLI_SECTION( "IPv6 Label",			kDNSServerInfoText_IPv6Label ),
	CLI_SECTION( "Index Labels",		kDNSServerInfoText_IndexLabel ),
	CLI_SECTION( "SRV Names",			kDNSServerInfoText_SRVNames ),
	CLI_SECTION( "Bad UDP Mode",		kDNSServerInfoText_BadUDPMode ),
	CLI_OPTION_END()
};

static void	DNSServerCommand( void );

//===========================================================================================================================
//	MDNSReplier Command Options
//===========================================================================================================================

#define kMDNSReplierPortBase		50000

static const char		kMDNSReplierInfoText_Intro[] =
	"The mDNS replier answers mDNS queries for its authoritative records. These records are of class IN and of types\n"
	"PTR, SRV, TXT, A, and AAAA as described below.\n"
	"\n"
	"Note: Sub-strings representing integers in domain name labels are in decimal notation and without leading zeros.\n";

static const char		kMDNSReplierInfoText_Parameters[] =
	"There are five parameters that control the replier's set of authoritative records.\n"
	"\n"
	"    1. <hostname> is the base name used for service instance names and the names of A and AAAA records. This\n"
	"       parameter is specified with the --hostname option.\n"
	"    2. <tag> is an arbitrary string used to uniquify service types. This parameter is specified with the --tag\n"
	"       option.\n"
	"    3. N_max in an integer in [1, 65535] and limits service types to those that have no more than N_max\n"
	"       instances. It also limits the number of hostnames to N_max, i.e., <hostname>.local.,\n"
	"       <hostname>-1.local., ..., <hostname>-N_max.local. This parameter is specified with the\n"
	"       --maxInstanceCount option.\n"
	"    4. N_a is an integer in [1, 255] and the number of A records per hostname. This parameter is specified\n"
	"       with the --countA option.\n"
	"    5. N_aaaa is an integer in [1, 255] and the number of AAAA records per hostname. This parameter is\n"
	"       specified with the --countAAAA option.\n";

static const char		kMDNSReplierInfoText_PTR[] =
	"The replier's authoritative PTR records have names of the form _t-<tag>-<L>-<N>._tcp.local., where L is an\n"
	"integer in [1, 65535], and N is an integer in [1, N_max].\n"
	"\n"
	"For a given L and N, the replier has exactly N authoritative PTR records:\n"
	"\n"
	"    1. The first PTR record is defined as\n"
	"\n"
	"        NAME:  _t-<tag>-<L>-<N>._tcp.local.\n"
	"        TYPE:  PTR\n"
	"        CLASS: IN\n"
	"        TTL:   4500\n"
	"        RDATA: <hostname>._t-<tag>-<L>-<N>._tcp.local.\n"
	"\n"
	"    2. For each i in [2, N], there is one PTR record defined as\n"
	"\n"
	"        NAME:  _t-<tag>-<L>-<N>._tcp.local.\n"
	"        TYPE:  PTR\n"
	"        CLASS: IN\n"
	"        TTL:   4500\n"
	"        RDATA: \"<hostname> (<i>)._t-<tag>-<L>-<N>._tcp.local.\"\n";

static const char		kMDNSReplierInfoText_SRV[] =
	"The replier's authoritative SRV records have names of the form <instance name>._t-<tag>-<L>-<N>._tcp.local.,\n"
	"where L is an integer in [1, 65535], N is an integer in [1, N_max], and <instance name> is <hostname> or\n"
	"\"<hostname> (<i>)\", where i is in [2, N].\n"
	"\n"
	"For a given L and N, the replier has exactly N authoritative SRV records:\n"
	"\n"
	"    1. The first SRV record is defined as\n"
	"\n"
	"        NAME:  <hostname>._t-<tag>-<L>-<N>._tcp.local.\n"
	"        TYPE:  SRV\n"
	"        CLASS: IN\n"
	"        TTL:   120\n"
	"        RDATA:\n"
	"            Priority: 0\n"
	"            Weight:   0\n"
	"            Port:     (50000 + L) mod 2^16\n"
	"            Target:   <hostname>.local.\n"
	"\n"
	"    2. For each i in [2, N], there is one SRV record defined as:\n"
	"\n"
	"        NAME:  \"<hostname> (<i>)._t-<tag>-<L>-<N>._tcp.local.\"\n"
	"        TYPE:  SRV\n"
	"        CLASS: IN\n"
	"        TTL:   120\n"
	"        RDATA:\n"
	"            Priority: 0\n"
	"            Weight:   0\n"
	"            Port:     (50000 + L) mod 2^16\n"
	"            Target:   <hostname>-<i>.local.\n";

static const char		kMDNSReplierInfoText_TXT[] =
	"The replier's authoritative TXT records have names of the form <instance name>._t-<tag>-<L>-<N>._tcp.local.,\n"
	"where L is an integer in [1, 65535], N is an integer in [1, N_max], and <instance name> is <hostname> or\n"
	"\"<hostname> (<i>)\", where i is in [2, N].\n"
	"\n"
	"For a given L and N, the replier has exactly N authoritative TXT records:\n"
	"\n"
	"    1. The first TXT record is defined as\n"
	"\n"
	"        NAME:     <hostname>._t-<tag>-<L>-<N>._tcp.local.\n"
	"        TYPE:     TXT\n"
	"        CLASS:    IN\n"
	"        TTL:      4500\n"
	"        RDLENGTH: L\n"
	"        RDATA:    <one or more strings with an aggregate length of L octets>\n"
	"\n"
	"    2. For each i in [2, N], there is one TXT record:\n"
	"\n"
	"        NAME:     \"<hostname> (<i>)._t-<tag>-<L>-<N>._tcp.local.\"\n"
	"        TYPE:     TXT\n"
	"        CLASS:    IN\n"
	"        TTL:      4500\n"
	"        RDLENGTH: L\n"
	"        RDATA:    <one or more strings with an aggregate length of L octets>\n"
	"\n"
	"The RDATA of each TXT record is exactly L octets and consists of a repeating series of the 15-byte string\n"
	"\"hash=0x<32-bit FNV-1 hash of the record name as an 8-character hexadecimal string>\". The last instance of\n"
	"the string may be truncated to satisfy the TXT record data's size requirement.\n";

static const char		kMDNSReplierInfoText_A[] =
	"The replier has exactly N_max ✕ N_a authoritative A records:\n"
	"\n"
	"    For each i in [1, N_max], for each j in [1, N_a], an A record is defined as\n"
	"\n"
	"        NAME:     \"<hostname>.local.\" if i = 1, otherwise \"<hostname>-<i>.local.\"\n"
	"        TYPE:     A\n"
	"        CLASS:    IN\n"
	"        TTL:      120\n"
	"        RDLENGTH: 4\n"
	"        RDATA:    0.<⌊i / 256⌋>.<i mod 256>.<j>\n";

static const char		kMDNSReplierInfoText_AAAA[] =
	"The replier has exactly N_max ✕ N_aaaa authoritative AAAA records:\n"
	"\n"
	"    1. For each j in [1, N_aaaa], a AAAA record is defined as\n"
	"\n"
	"        NAME:     <hostname>.local.\n"
	"        TYPE:     AAAA\n"
	"        CLASS:    IN\n"
	"        TTL:      120\n"
	"        RDLENGTH: 16\n"
	"        RDATA:    fe80::1:<j>\n"
	"\n"
	"    2. For each i in [2, N_max], for each j in [1, N_aaaa], a AAAA record is defined as\n"
	"\n"
	"        NAME:     <hostname>-<i>.local.\n"
	"        TYPE:     AAAA\n"
	"        CLASS:    IN\n"
	"        TTL:      120\n"
	"        RDLENGTH: 16\n"
	"        RDATA:    2001:db8:2::<i>:<j>\n";

static const char		kMDNSReplierInfoText_Responses[] =
	"When generating answers for a query message, any two records pertaining to the same hostname will be grouped\n"
	"together in the same response message, and any two records pertaining to different hostnames will be in\n"
	"separate response messages.\n";

static const char *		gMDNSReplier_Hostname				= NULL;
static const char *		gMDNSReplier_ServiceTypeTag			= NULL;
static int				gMDNSReplier_MaxInstanceCount		= 1000;
static int				gMDNSReplier_NoAdditionals			= false;
static int				gMDNSReplier_RecordCountA			= 1;
static int				gMDNSReplier_RecordCountAAAA		= 1;
static double			gMDNSReplier_UnicastDropRate		= 0.0;
static double			gMDNSReplier_MulticastDropRate		= 0.0;
static int				gMDNSReplier_MaxDropCount			= 0;
static int				gMDNSReplier_UseIPv4				= false;
static int				gMDNSReplier_UseIPv6				= false;
static int				gMDNSReplier_Foreground				= false;
#if( TARGET_OS_POSIX )
static const char *		gMDNSReplier_FollowPID				= NULL;
#endif

static CLIOption		kMDNSReplierOpts[] =
{
	StringOption(  'i', "interface",        &gInterface,                     "name or index", "Network interface by name or index.", true ),
	StringOption(  'n', "hostname",         &gMDNSReplier_Hostname,          "string", "Base name to use for hostnames and service instance names.", true ),
	StringOption(  't', "tag",              &gMDNSReplier_ServiceTypeTag,    "string", "Tag to use for service types, e.g., _t-<tag>-<TXT size>-<count>._tcp.", true ),
	IntegerOption( 'c', "maxInstanceCount", &gMDNSReplier_MaxInstanceCount,  "count", "Maximum number of service instances. (default: 1000)", false ),
	BooleanOption(  0 , "noAdditionals",    &gMDNSReplier_NoAdditionals,     "When answering queries, don't include any additional records." ),
	IntegerOption(  0 , "countA",           &gMDNSReplier_RecordCountA,      "count", "Number of A records per hostname. (default: 1)", false ),
	IntegerOption(  0 , "countAAAA",        &gMDNSReplier_RecordCountAAAA,   "count", "Number of AAAA records per hostname. (default: 1)", false ),
	DoubleOption(   0 , "udrop",            &gMDNSReplier_UnicastDropRate,   "probability", "Probability of dropping a unicast response. (default: 0.0)", false ),
	DoubleOption(   0 , "mdrop",            &gMDNSReplier_MulticastDropRate, "probability", "Probability of dropping a multicast query or response. (default: 0.0)", false ),
	IntegerOption(  0 , "maxDropCount",     &gMDNSReplier_MaxDropCount,      "count", "If > 0, drop probabilities are limted to first <count> responses from each instance. (default: 0)", false ),
	BooleanOption(  0 , "ipv4",             &gMDNSReplier_UseIPv4,           "Use IPv4." ),
	BooleanOption(  0 , "ipv6",             &gMDNSReplier_UseIPv6,           "Use IPv6." ),
	BooleanOption( 'f', "foreground",       &gMDNSReplier_Foreground,        "Direct log output to stdout instead of system logging." ),
#if( TARGET_OS_POSIX )
	StringOption(   0 , "follow",           &gMDNSReplier_FollowPID,         "pid", "Exit when the process, usually the parent process, specified by PID exits.", false ),
#endif
	
	CLI_SECTION( "Intro",							kMDNSReplierInfoText_Intro ),
	CLI_SECTION( "Authoritative Record Parameters",	kMDNSReplierInfoText_Parameters ),
	CLI_SECTION( "Authoritative PTR Records",		kMDNSReplierInfoText_PTR ),
	CLI_SECTION( "Authoritative SRV Records",		kMDNSReplierInfoText_SRV ),
	CLI_SECTION( "Authoritative TXT Records",		kMDNSReplierInfoText_TXT ),
	CLI_SECTION( "Authoritative A Records",			kMDNSReplierInfoText_A ),
	CLI_SECTION( "Authoritative AAAA Records",		kMDNSReplierInfoText_AAAA ),
	CLI_SECTION( "Responses",						kMDNSReplierInfoText_Responses ),
	CLI_OPTION_END()
};

static void	MDNSReplierCmd( void );

//===========================================================================================================================
//	Test Command Options
//===========================================================================================================================

#define kTestExitStatusSection_Name		"Exit Status"
#define kTestExitStatusSection_Text																						\
	"This test command can exit with one of three status codes:\n"														\
	"\n"																												\
	"0 - The test ran to completion and passed.\n"																		\
	"1 - A fatal error prevented the test from completing.\n"															\
	"2 - The test ran to completion, but it or a subtest failed. See test output for details.\n"						\
	"\n"																												\
	"Note: The pass/fail status applies to the correctness or results. It does not necessarily imply anything about\n"	\
	"performance.\n"

#define TestExitStatusSection()		CLI_SECTION( kTestExitStatusSection_Name, kTestExitStatusSection_Text )

#define kGAIPerfTestSuiteName_Basic			"basic"
#define kGAIPerfTestSuiteName_Advanced		"advanced"

static const char *		gGAIPerf_TestSuite				= NULL;
static int				gGAIPerf_CallDelayMs			= 10;
static int				gGAIPerf_ServerDelayMs			= 10;
static int				gGAIPerf_SkipPathEvalulation	= false;
static int				gGAIPerf_BadUDPMode				= false;
static int				gGAIPerf_IterationCount			= 100;
static int				gGAIPerf_IterationTimeLimitMs	= 100;
static const char *		gGAIPerf_OutputFilePath			= NULL;
static const char *		gGAIPerf_OutputFormat			= kOutputFormatStr_JSON;
static int				gGAIPerf_OutputAppendNewline	= false;
static const char *		gGAIPerf_Protocol				= kDNSProtocolStr_Do53;

static void	GAIPerfCmd( void );

#define kGAIPerfSectionText_TestSuiteBasic																					\
	"This test suite consists of the following three test cases:\n"															\
	"\n"																													\
	"Test Case #1: Resolve a domain name with\n"																			\
	"\n"																													\
	"    2 CNAME records, 4 A records, and 4 AAAA records\n"																\
	"\n"																													\
	"to its IPv4 and IPv6 addresses. Each iteration resolves a unique instance of such a domain name, which requires\n"		\
	"server queries.\n"																										\
	"\n"																													\
	"Test Case #2: Resolve a domain name with\n"																			\
	"\n"																													\
	"    2 CNAME records, 4 A records, and 4 AAAA records\n"																\
	"\n"																													\
	"to its IPv4 and IPv6 addresses. A preliminary iteration resolves a unique instance of such a domain name, which\n"		\
	"requires server queries. Each subsequent iteration resolves the same domain name as the preliminary iteration,\n"		\
	"which should ideally require no additional server queries, i.e., the results should come from the cache.\n"			\
	"\n"																													\
	"Unlike the preceding test case, this test case is concerned with DNSServiceGetAddrInfo() performance when the\n"		\
	"records of the domain name being resolved are already in the cache. Therefore, the time required to resolve the\n"		\
	"domain name in the preliminary iteration isn't counted in the performance stats.\n"									\
	"\n"																													\
	"Test Case #3: Each iteration resolves localhost to its IPv4 and IPv6 addresses.\n"

#define kGAIPerfSectionText_TestSuiteAdvanced																				\
	"This test suite consists of 33 test cases. Test cases 1 through 32 can be described in the following way\n"			\
	"\n"																													\
	"Test Case #N (where N is in [1, 32] and odd): Resolve a domain name with\n"											\
	"\n"																													\
	"    N_c CNAME records, N_a A records, and N_a AAAA records\n"															\
	"\n"																													\
	"to its IPv4 and IPv6 addresses. Each iteration resolves a unique instance of such a domain name, which requires\n"		\
	"server queries.\n"																										\
	"\n"																													\
	"Test Case #N (where N is in [1, 32] and even): Resolve a domain name with\n"											\
	"\n"																													\
	"    N_c CNAME records, N_a A records, and N_a AAAA records\n"															\
	"\n"																													\
	"to its IPv4 and IPv6 addresses. A preliminary iteration resolves a unique instance of such a domain name, which\n"		\
	"requires server queries. Each subsequent iteration resolves the same domain name as the preliminary iteration,\n"		\
	"which should ideally require no additional server queries, i.e., the results should come from the cache.\n"			\
	"\n"																													\
	"Unlike the preceding test case, this test case is concerned with DNSServiceGetAddrInfo() performance when the\n"		\
	"records of the domain name being resolved are already in the cache. Therefore, the time required to resolve the\n"		\
	"domain name in the preliminary iteration isn't counted in the performance stats.\n"									\
	"\n"																													\
	"N_c and N_a take on the following values, depending on the value of N:\n"												\
	"\n"																													\
	"    N_c is 0 if N is in [1, 8].\n"																						\
	"    N_c is 1 if N is in [9, 16].\n"																					\
	"    N_c is 2 if N is in [17, 24].\n"																					\
	"    N_c is 4 if N is in [25, 32].\n"																					\
	"\n"																													\
	"    N_a is 1 if N mod 8 is 1 or 2.\n"																					\
	"    N_a is 2 if N mod 8 is 3 or 4.\n"																					\
	"    N_a is 4 if N mod 8 is 5 or 6.\n"																					\
	"    N_a is 8 if N mod 8 is 7 or 0.\n"																					\
	"\n"																													\
	"Finally,\n"																											\
	"\n"																													\
	"Test Case #33: Each iteration resolves localhost to its IPv4 and IPv6 addresses.\n"

static CLIOption		kGAIPerfOpts[] =
{
	StringOptionEx( 's', "suite",         &gGAIPerf_TestSuite,            "name", "Name of the predefined test suite to run.", true,
		"\n"
		"There are currently two predefined test suites, '" kGAIPerfTestSuiteName_Basic "' and '" kGAIPerfTestSuiteName_Advanced "', which are described below.\n"
		"\n"
	),
	StringOption(   'o', "output",        &gGAIPerf_OutputFilePath,       "path", "Path of the file to write test results to instead of standard output (stdout).", false ),
	FormatOption(   'f', "format",        &gGAIPerf_OutputFormat,         "Specifies the test results output format. (default: " kOutputFormatStr_JSON ")", false ),
	BooleanOption(  'n', "appendNewline", &gGAIPerf_OutputAppendNewline,  "If the output format is JSON, output a trailing newline character." ),
	IntegerOption(  'i', "iterations",    &gGAIPerf_IterationCount,       "count", "The number of iterations per test case. (default: 100)", false ),
	IntegerOption(  'l', "timeLimit",     &gGAIPerf_IterationTimeLimitMs, "ms", "Time limit for each DNSServiceGetAddrInfo() operation in milliseconds. (default: 100)", false ),
	IntegerOption(   0 , "callDelay",     &gGAIPerf_CallDelayMs,          "ms", "Time to wait before calling DNSServiceGetAddrInfo() in milliseconds. (default: 10)", false ),
	BooleanOption(   0 , "skipPathEval",  &gGAIPerf_SkipPathEvalulation,  "Use kDNSServiceFlagsPathEvaluationDone when calling DNSServiceGetAddrInfo()." ),
	
	CLI_OPTION_GROUP( "DNS Server Options" ),
	IntegerOption(   0 , "responseDelay", &gGAIPerf_ServerDelayMs,        "ms", "Additional delay in milliseconds to have the server apply to responses. (default: 10)", false ),
	BooleanOption(   0 , "badUDPMode",    &gGAIPerf_BadUDPMode,           "Run server in Bad UDP mode to trigger mDNSResponder's TCP fallback mechanism." ),
	StringOptionEx( 'P', "protocol",      &gGAIPerf_Protocol,             "protocol", "The DNS protocol to use. (default: Do53)", false,
		"\n"
		"Use '" kDNSProtocolStr_Do53 "' for DNS over UDP and TCP (Do53).\n"
		"Use '" kDNSProtocolStr_DoT  "' for DNS over TLS (DoT).\n"
		"Use '" kDNSProtocolStr_DoH  "' for DNS over HTTPS (DoH).\n"
 	),
	CLI_SECTION( "Test Suite \"Basic\"",	kGAIPerfSectionText_TestSuiteBasic ),
	CLI_SECTION( "Test Suite \"Advanced\"",	kGAIPerfSectionText_TestSuiteAdvanced ),
	TestExitStatusSection(),
	CLI_OPTION_END()
};

static void	MDNSDiscoveryTestCmd( void );

static int				gMDNSDiscoveryTest_InstanceCount		= 100;
static int				gMDNSDiscoveryTest_TXTSize				= 100;
static int				gMDNSDiscoveryTest_BrowseTimeSecs		= 2;
static int				gMDNSDiscoveryTest_FlushCache			= false;
static char *			gMDNSDiscoveryTest_Interface			= NULL;
static int				gMDNSDiscoveryTest_NoAdditionals		= false;
static int				gMDNSDiscoveryTest_RecordCountA			= 1;
static int				gMDNSDiscoveryTest_RecordCountAAAA		= 1;
static double			gMDNSDiscoveryTest_UnicastDropRate		= 0.0;
static double			gMDNSDiscoveryTest_MulticastDropRate	= 0.0;
static int				gMDNSDiscoveryTest_MaxDropCount			= 0;
static int				gMDNSDiscoveryTest_UseIPv4				= false;
static int				gMDNSDiscoveryTest_UseIPv6				= false;
static int				gMDNSDiscoveryTest_UseNewGAI			= false;
static const char *		gMDNSDiscoveryTest_HeapBytesLimit		= NULL;
static const char *		gMDNSDiscoveryTest_OutputFormat			= kOutputFormatStr_JSON;
static int				gMDNSDiscoveryTest_OutputAppendNewline	= false;
static const char *		gMDNSDiscoveryTest_OutputFilePath		= NULL;

static CLIOption		kMDNSDiscoveryTestOpts[] =
{
	IntegerOption( 'c', "instanceCount",  &gMDNSDiscoveryTest_InstanceCount,       "count", "Number of service instances to discover. (default: 100)", false ),
	IntegerOption( 's', "txtSize",        &gMDNSDiscoveryTest_TXTSize,             "bytes", "Desired size of each service instance's TXT record data. (default: 100)", false ),
	IntegerOption( 'b', "browseTime",     &gMDNSDiscoveryTest_BrowseTimeSecs,      "seconds", "Amount of time to spend browsing in seconds. (default: 2)", false ),
	BooleanOption(  0 , "flushCache",     &gMDNSDiscoveryTest_FlushCache,          "Flush mDNSResponder's record cache before browsing. Requires root privileges." ),
	
	CLI_OPTION_GROUP( "mDNS Replier Parameters" ),
	StringOption(  'i', "interface",      &gMDNSDiscoveryTest_Interface,           "name or index", "Network interface. If unspecified, any available mDNS-capable interface will be used.", false ),
	BooleanOption(  0 , "noAdditionals",  &gMDNSDiscoveryTest_NoAdditionals,       "When answering queries, don't include any additional records." ),
	IntegerOption(  0 , "countA",         &gMDNSDiscoveryTest_RecordCountA,        "count", "Number of A records per hostname. (default: 1)", false ),
	IntegerOption(  0 , "countAAAA",      &gMDNSDiscoveryTest_RecordCountAAAA,     "count", "Number of AAAA records per hostname. (default: 1)", false ),
	DoubleOption(   0 , "udrop",          &gMDNSDiscoveryTest_UnicastDropRate,     "probability", "Probability of dropping a unicast response. (default: 0.0)", false ),
	DoubleOption(   0 , "mdrop",          &gMDNSDiscoveryTest_MulticastDropRate,   "probability", "Probability of dropping a multicast query or response. (default: 0.0)", false ),
	IntegerOption(  0 , "maxDropCount",   &gMDNSDiscoveryTest_MaxDropCount,        "count", "If > 0, drop probabilities are limted to first <count> responses from each instance. (default: 0)", false ),
	BooleanOption(  0 , "ipv4",           &gMDNSDiscoveryTest_UseIPv4,             "Use IPv4." ),
	BooleanOption(  0 , "ipv6",           &gMDNSDiscoveryTest_UseIPv6,             "Use IPv6." ),
	BooleanOption(  0 , "useNewGAI",      &gMDNSDiscoveryTest_UseNewGAI,           "Use dnssd_getaddrinfo_* instead of DNSServiceGetAddrInfo()." ),
	StringOption(  'm', "memoryLimit",    &gMDNSDiscoveryTest_HeapBytesLimit,      "byte count", "If > 0, test fails if mDNSResponder's heap memory usage exceeds limit. (default: 0)", false ),
	
	CLI_OPTION_GROUP( "Results" ),
	FormatOption(   'f', "format",        &gMDNSDiscoveryTest_OutputFormat,        "Specifies the test results output format. (default: " kOutputFormatStr_JSON ")", false ),
	StringOption(   'o', "output",        &gMDNSDiscoveryTest_OutputFilePath,      "path", "Path of the file to write test results to instead of standard output (stdout).", false ),
	
	TestExitStatusSection(),
	CLI_OPTION_END()
};

static void	DotLocalTestCmd( void );

static const char *		gDotLocalTest_Interface			= NULL;
static const char *		gDotLocalTest_OutputFormat		= kOutputFormatStr_JSON;
static const char *		gDotLocalTest_OutputFilePath	= NULL;

#define kDotLocalTestSubtestDesc_GAIMDNSOnly	"GAI for a dotlocal name that has only MDNS A and AAAA records."
#define kDotLocalTestSubtestDesc_GAIDNSOnly		"GAI for a dotlocal name that has only DNS A and AAAA records."
#define kDotLocalTestSubtestDesc_GAIBoth		"GAI for a dotlocal name that has both mDNS and DNS A and AAAA records."
#define kDotLocalTestSubtestDesc_GAINeither		"GAI for a dotlocal name that has no A or AAAA records."
#define kDotLocalTestSubtestDesc_GAINoSuchRecord \
	"GAI for a dotlocal name that has no A or AAAA records, but is a subdomain name of a search domain."
#define kDotLocalTestSubtestDesc_QuerySRV		"SRV query for a dotlocal name that has only a DNS SRV record."

#define kDotLocalTestSectionText_Description																				\
	"The goal of the dotlocal test is to verify that mDNSResponder properly handles queries for domain names in the\n"		\
	"local domain when a local SOA record exists. As part of the test setup, a test DNS server and an mdnsreplier are\n"	\
	"spawned, and a dummy local SOA record is registered with DNSServiceRegisterRecord(). The server is invoked such\n"		\
	"that its domain is a second-level subdomain of the local domain, i.e., <some label>.local, while the mdnsreplier is\n"	\
	"invoked such that its base hostname is equal to the server's domain, e.g., if the server's domain is test.local.,\n"	\
	"then the mdnsreplier's base hostname is test.local.\n"																	\
	"\n"																													\
	"The dotlocal test consists of six subtests that perform either a DNSServiceGetAddrInfo (GAI) operation for a\n"		\
	"hostname in the local domain or a DNSServiceQueryRecord operation to query for an SRV record in the local domain:\n"	\
	"\n"																													\
	"1. " kDotLocalTestSubtestDesc_GAIMDNSOnly		"\n"																	\
	"2. " kDotLocalTestSubtestDesc_GAIDNSOnly		"\n"																	\
	"3. " kDotLocalTestSubtestDesc_GAIBoth			"\n"																	\
	"4. " kDotLocalTestSubtestDesc_GAINeither		"\n"																	\
	"5. " kDotLocalTestSubtestDesc_GAINoSuchRecord	"\n"																	\
	"6. " kDotLocalTestSubtestDesc_QuerySRV			"\n"																	\
	"\n"																													\
	"Each subtest runs for five seconds.\n"

static CLIOption		kDotLocalTestOpts[] =
{
	StringOption(  'i', "interface",     &gDotLocalTest_Interface,           "name or index", "mdnsreplier's network interface. If not set, any mDNS-capable interface will be used.", false ),
	
	CLI_OPTION_GROUP( "Results" ),
	FormatOption(  'f', "format",        &gDotLocalTest_OutputFormat,        "Specifies the test results output format. (default: " kOutputFormatStr_JSON ")", false ),
	StringOption(  'o', "output",        &gDotLocalTest_OutputFilePath,      "path", "Path of the file to write test results to instead of standard output (stdout).", false ),
	
	CLI_SECTION( "Description", kDotLocalTestSectionText_Description ),
	TestExitStatusSection(),
	CLI_OPTION_END()
};

static void	ProbeConflictTestCmd( void );

static const char *		gProbeConflictTest_Interface		= NULL;
static int				gProbeConflictTest_UseComputerName	= false;
static int				gProbeConflictTest_UseIPv4			= false;
static int				gProbeConflictTest_UseIPv6			= false;
static int				gProbeConflictTest_RegisterOnAny	= false;
static int				gProbeConflictTest_ExtraWaitMs		= 0;
static const char *		gProbeConflictTest_OutputFormat		= kOutputFormatStr_JSON;
static const char *		gProbeConflictTest_OutputFilePath	= NULL;

static CLIOption		kProbeConflictTestOpts[] =
{
	StringOption(    'i', "interface",       &gProbeConflictTest_Interface,       "name or index", "mdnsreplier's network interface. If not set, any mDNS-capable interface will be used.", false ),
	BooleanOption(   'c', "useComputerName", &gProbeConflictTest_UseComputerName, "Use the device's \"computer name\" for the test service's name." ),
	BooleanOption(    0 , "ipv4",            &gProbeConflictTest_UseIPv4,         "Use IPv4 instead of IPv6. (Default behavior.)" ),
	BooleanOption(    0 , "ipv6",            &gProbeConflictTest_UseIPv6,         "Use IPv6 instead of IPv4." ),
	BooleanOption(   'r', "registerOnAny",   &gProbeConflictTest_RegisterOnAny,   "Register test service on kDNSServiceInterfaceIndexAny instead of specific interface." ),
	IntegerOptionEx(  0 , "extraWait",       &gProbeConflictTest_ExtraWaitMs,     "ms", "Extra time in milliseconds to wait after a probe conflict. (default: 0)", false,
		"\n"
		"After each probe conflict, there is a minimum amount of time that the test will wait to allow for probing and\n"
		"renames to take place. The minimum wait time was chosen for relatively tranquil environments.\n"
		"\n"
		"If the test environment is such that mDNSResponder is expected to be busier than usual, then use this option to\n"
		"allow extra time for renames, which may be delayed by events outside of mDNSResponder's control, such as network\n"
		"changes.\n"
	),
	
	CLI_OPTION_GROUP( "Results" ),
	FormatOption( 'f', "format", &gProbeConflictTest_OutputFormat,   "Specifies the test report output format. (default: " kOutputFormatStr_JSON ")", false ),
	StringOption( 'o', "output", &gProbeConflictTest_OutputFilePath, "path", "Path of the file to write test report to instead of standard output (stdout).", false ),
	
	TestExitStatusSection(),
	CLI_OPTION_END()
};

static void	RegistrationTestCmd( void );

static int				gRegistrationTest_BATSEnvironment	= false;
static const char *		gRegistrationTest_OutputFormat		= kOutputFormatStr_JSON;
static const char *		gRegistrationTest_OutputFilePath	= NULL;

static CLIOption		kRegistrationTestOpts[] =
{
	CLI_OPTION_BOOLEAN( 0, "bats", &gRegistrationTest_BATSEnvironment, "Informs the test that it's running in a BATS environment.",
		"\n"
		"This option allows the test to take special measures while running in a BATS environment. Currently, this option\n"
		"only has an effect on watchOS. Because it has been observed that the Wi-Fi interface sometimes goes down during\n"
		"watchOS BATS testing, for watchOS, when a service is registered using kDNSServiceInterfaceIndexAny,\n"
		"\n"
		"    1. missing browse and query \"add\" results for Wi-Fi interfaces aren't enough for a subtest to fail; and\n"
		"    2. unexpected browse and query results for Wi-Fi interfaces are ignored.\n"
	),
	CLI_OPTION_GROUP( "Results" ),
	FormatOption( 'f', "format", &gRegistrationTest_OutputFormat,   "Specifies the test results output format. (default: " kOutputFormatStr_JSON ")", false ),
	StringOption( 'o', "output", &gRegistrationTest_OutputFilePath, "path", "Path of the file to write test results to instead of standard output (stdout).", false ),
	
	TestExitStatusSection(),
	CLI_OPTION_END()
};

#if( MDNSRESPONDER_PROJECT )
static void	FallbackTestCmd( void );

static int				gFallbackTest_UseRefused		= false;
static const char *		gFallbackTest_OutputFormat		= kOutputFormatStr_JSON;
static const char *		gFallbackTest_OutputFilePath	= NULL;

static CLIOption		kFallbackTestOpts[] =
{
	BooleanOption( 0 , "useRefused", &gFallbackTest_UseRefused,     "Have the server use the Refused RCODE in responses when a query is not allowed to be answered." ),
	CLI_OPTION_GROUP( "Results" ),
	FormatOption( 'f', "format",     &gFallbackTest_OutputFormat,   "Specifies the test results output format. (default: " kOutputFormatStr_JSON ")", false ),
	StringOption( 'o', "output",     &gFallbackTest_OutputFilePath, "path", "Path of the file to write test results to instead of standard output (stdout).", false ),
	
	TestExitStatusSection(),
	CLI_OPTION_END()
};

static void ExpensiveConstrainedTestCmd( void );

static const char *     gExpensiveConstrainedTest_Interface                 = NULL;
static const char *     gExpensiveConstrainedTest_Name                      = NULL;
static Boolean          gExpensiveConstrainedTest_DenyExpensive             = false;
static Boolean          gExpensiveConstrainedTest_DenyConstrained           = false;
static Boolean          gExpensiveConstrainedTest_StartFromExpensive        = false;
static int              gExpensiveConstrainedTest_ProtocolIPv4              = false;
static int              gExpensiveConstrainedTest_ProtocolIPv6              = false;
static const char *     gExpensiveConstrainedTest_OutputFormat              = kOutputFormatStr_JSON;
static const char *     gExpensiveConstrainedTest_OutputFilePath            = NULL;

static CLIOption        kExpensiveConstrainedTestOpts[] =
{
    CLI_OPTION_GROUP( "Results" ),
    FormatOption( 'f', "format", &gExpensiveConstrainedTest_OutputFormat,              "Specifies the test results output format. (default: " kOutputFormatStr_JSON ")", false ),
    StringOption( 'o', "output", &gExpensiveConstrainedTest_OutputFilePath, "path",    "Path of the file to write test results to instead of standard output (stdout).", false ),

    TestExitStatusSection(),
    CLI_OPTION_END()
};

static void	DNSProxyTestCmd( void );

static const char *		gDNSProxyTest_OutputFormat		= kOutputFormatStr_JSON;
static const char *		gDNSProxyTest_OutputFilePath	= NULL;

static CLIOption		kDNSProxyTestOpts[] =
{
	CLI_OPTION_GROUP( "Results" ),
	FormatOption(  'f', "format", &gDNSProxyTest_OutputFormat,      "Specifies the test results output format. (default: " kOutputFormatStr_JSON ")", false ),
	StringOption(  'o', "output", &gDNSProxyTest_OutputFilePath,    "path", "Path of the file to write test results to instead of standard output (stdout).", false ),
	
	TestExitStatusSection(),
	CLI_OPTION_END()
};

static void	RCodeTestCmd( void );

static const char *		gRCodeTest_OutputFormat		= kOutputFormatStr_JSON;
static const char *		gRCodeTest_OutputFilePath	= NULL;

static CLIOption		kRCodeTestOpts[] =
{
	CLI_OPTION_GROUP( "Results" ),
	FormatOption( 'f', "format", &gRCodeTest_OutputFormat,   "Specifies the test results output format. (default: " kOutputFormatStr_JSON ")", false ),
	StringOption( 'o', "output", &gRCodeTest_OutputFilePath, "path", "Path of the file to write test results to instead of standard output (stdout).", false ),
	
	TestExitStatusSection(),
	CLI_OPTION_END()
};

static void	DNSQueryTestCmd( void );

static const char *		gDNSQueryTest_OutputFormat		= kOutputFormatStr_JSON;
static const char *		gDNSQueryTest_OutputFilePath	= NULL;

static CLIOption		kDNSQueryTestOpts[] =
{
	CLI_OPTION_GROUP( "Results" ),
	FormatOption( 'f', "format", &gDNSQueryTest_OutputFormat,   "Specifies the test results output format. (default: " kOutputFormatStr_JSON ")", false ),
	StringOption( 'o', "output", &gDNSQueryTest_OutputFilePath, "path", "Path of the file to write test results to instead of standard output (stdout).", false ),
	
	TestExitStatusSection(),
	CLI_OPTION_END()
};

static void	FastRecoveryTestCmd( void );

static const char *		gFastRecoveryTest_OutputFormat		= kOutputFormatStr_JSON;
static const char *		gFastRecoveryTest_OutputFilePath	= NULL;

static CLIOption		kFastRecoveryTestOpts[] =
{
	CLI_OPTION_GROUP( "Results" ),
	FormatOption( 'f', "format", &gFastRecoveryTest_OutputFormat,   "Specifies the test results output format. (default: " kOutputFormatStr_JSON ")", false ),
	StringOption( 'o', "output", &gFastRecoveryTest_OutputFilePath, "path", "Path of the file to write test results to instead of standard output (stdout).", false ),
	
	TestExitStatusSection(),
	CLI_OPTION_END()
};

static void XCTestCmd( void );

static const char *     gXCTest_Classname        = NULL;

static CLIOption        kXCTestOpts[] =
{
    StringOption(      'c', "class", &gXCTest_Classname, "classname", "The classname of the XCTest to run (from /AppleInternal/XCTests/com.apple.mDNSResponder/Tests.xctest)", true ),
    CLI_OPTION_END()
};

static void MultiConnectTestCmd( void );

static int    			gMultiConnectTest_ConnectionCount = 4; // default to 4

static CLIOption        kMultiConnectTestOpts[] =
{
	IntegerOption( 0, "connections", &gMultiConnectTest_ConnectionCount,	"count", "Number of simultanious connections. (default: 4)", false ),
    CLI_OPTION_END()
};
#endif	// MDNSRESPONDER_PROJECT

#if( TARGET_OS_DARWIN )
static void	KeepAliveTestCmd( void );

static const char *		gKeepAliveTest_OutputFormat		= kOutputFormatStr_JSON;
static const char *		gKeepAliveTest_OutputFilePath	= NULL;

static CLIOption		kKeepAliveTestOpts[] =
{
	CLI_OPTION_GROUP( "Results" ),
	FormatOption( 'f', "format", &gKeepAliveTest_OutputFormat,   "Specifies the test results output format. (default: " kOutputFormatStr_JSON ")", false ),
	StringOption( 'o', "output", &gKeepAliveTest_OutputFilePath, "path", "Path of the file to write test results to instead of standard output (stdout).", false ),
	
	TestExitStatusSection(),
	CLI_OPTION_END()
};
#endif	// TARGET_OS_DARWIN

static void DNSSECTestCmd( void );

static const char * gDNSSECTest_TestCaseName	= NULL;
#if ( ENABLE_DNSSDUTIL_DNSSEC_TEST == 1 )
static const char * gDNSSECTest_OutputFormat	= kOutputFormatStr_JSON;
static const char * gDNSSECTest_OutputFilePath	= NULL;
#endif

static CLIOption	kDNSSECTestOpts[] =
{
	StringOption( 'n', "testCaseName", &gDNSSECTest_TestCaseName,                  "Specifies the DNSSEC test that the user intends to run", "test name", true ),

	CLI_OPTION_GROUP( "Results" ),
	FormatOption( 'f', "format",       &gExpensiveConstrainedTest_OutputFormat,    "Specifies the test results output format. (default: " kOutputFormatStr_JSON ")", false ),
	StringOption( 'o', "output",       &gExpensiveConstrainedTest_OutputFilePath,  "path",    "Path of the file to write test results to instead of standard output (stdout).", false ),

	TestExitStatusSection(),
	CLI_OPTION_END()
};

static void	OptimisticDNSTestCommand( void );

static int		gOptimisticDNSTest_FullTest = false;

static CLIOption		kOptimisticDNSTestOpts[] =
{
	BooleanOption( 'f', "full", &gOptimisticDNSTest_FullTest, "Proceed with the full version of the test, including subtests that require mDNSResponder to be killed." ),
	CLI_SECTION( "Exit Status", "This command exits with a status code of 0 if the test passed and a non-zero status code if it fails.\n" ),
	CLI_OPTION_END()
};

static void	RecordRegistrationTestCommand( void );

static int		gRecordRegistrationTest_RRSetChangeIntervalMs = 0;

static CLIOption		kRecordRegistrationTestOpts[] =
{
	IntegerOption( 'n', "interval", &gRecordRegistrationTest_RRSetChangeIntervalMs, "ms", "Interval between bulk RRSet changes in milliseconds. Use 0 for a default interval. (default: 0)", false ),
	CLI_SECTION( "Exit Status", "This command exits with a status code of 0 if the test passed and a non-zero status code if it fails.\n" ),
	CLI_OPTION_END()
};

static void	RecordCacheFlushTestCommand( void );

static CLIOption		kRecordCacheFlushTestOpts[] =
{
	CLI_SECTION( "Exit Status", "This command exits with a status code of 0 if the test passed and a non-zero status code if it fails.\n" ),
	CLI_OPTION_END()
};

static void	ResolverOverrideTestCommand( void );

static CLIOption		kResolverOverrideTestOpts[] =
{
	CLI_SECTION( "Exit Status", "This command exits with a status code of 0 if the test passed and a non-zero status code if it fails.\n" ),
	CLI_OPTION_END()
};

static CLIOption		kTestOpts[] =
{
	Command( "gaiperf",                       GAIPerfCmd,                    kGAIPerfOpts,                  "Runs DNSServiceGetAddrInfo() performance tests.", false ),
	Command( "mdnsdiscovery",                 MDNSDiscoveryTestCmd,          kMDNSDiscoveryTestOpts,        "Tests mDNS service discovery for correctness.", false ),
	Command( "dotlocal",                      DotLocalTestCmd,               kDotLocalTestOpts,             "Tests DNS and mDNS queries for domain names in the local domain.", false ),
	Command( "probeconflicts",                ProbeConflictTestCmd,          kProbeConflictTestOpts,        "Tests various probing conflict scenarios.", false ),
	Command( "registration",                  RegistrationTestCmd,           kRegistrationTestOpts,         "Tests service registrations.", false ),
#if( MDNSRESPONDER_PROJECT )
	Command( "fallback",                      FallbackTestCmd,               kFallbackTestOpts,             "Tests DNS server fallback.", false ),
	Command( "expensive_constrained_updates", ExpensiveConstrainedTestCmd,   kExpensiveConstrainedTestOpts, "Tests if the mDNSResponder can handle expensive and constrained property change correctly", false ),
	Command( "dnsproxy",                      DNSProxyTestCmd,               kDNSProxyTestOpts,             "Tests mDNSResponder's DNS proxy.", false ),
	Command( "rcodes",                        RCodeTestCmd,                  kRCodeTestOpts,                "Tests handling of all DNS RCODEs.", false ),
	Command( "dnsquery",                      DNSQueryTestCmd,               kDNSQueryTestOpts,             "Tests mDNSResponder's DNS queries.", false ),
	Command( "fastrecovery",                  FastRecoveryTestCmd,           kFastRecoveryTestOpts,         "Tests mDNSResponder's fast querier recovery.", false ),
	Command( "xctest",                        XCTestCmd,                     kXCTestOpts,                   "Run a XCTest from /AppleInternal/XCTests/com.apple.mDNSResponder/Tests.xctest.", true ),
	Command( "multiconnect",                  MultiConnectTestCmd,           kMultiConnectTestOpts,         "Tests multiple simultanious connections.", false ),
#endif
#if( TARGET_OS_DARWIN )
	Command( "keepalive",                     KeepAliveTestCmd,              kKeepAliveTestOpts,            "Tests keepalive record registrations.", false ),
#endif
	Command( "dnssec",                        DNSSECTestCmd,                 kDNSSECTestOpts,               "Tests mDNSResponder's DNSSEC validation", false),
	Command( "optimisticDNS",                 OptimisticDNSTestCommand,      kOptimisticDNSTestOpts,        "Tests mDNSResponder's Optimistic DNS functionality.", false ),
	Command( "record-registration",           RecordRegistrationTestCommand, kRecordRegistrationTestOpts,   "Tests the registration and deregistration of records.", false ),
	Command( "record-cache-flush",            RecordCacheFlushTestCommand,   kRecordCacheFlushTestOpts,     "Tests the mrc_record_cache_flush SPI.", false ),
	Command( "resolver-override",             ResolverOverrideTestCommand,   kResolverOverrideTestOpts,     "Tests use of DNSServiceQueryRecordWithAttribute() with a resolver override attribute.", false ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	SSDP Command Options
//===========================================================================================================================

static int				gSSDPDiscover_MX			= 1;
static const char *		gSSDPDiscover_ST			= "ssdp:all";
static int				gSSDPDiscover_ReceiveSecs	= 1;
static int				gSSDPDiscover_UseIPv4		= false;
static int				gSSDPDiscover_UseIPv6		= false;
static int				gSSDPDiscover_Verbose		= false;

static CLIOption		kSSDPDiscoverOpts[] =
{
	StringOption(  'i', "interface",	&gInterface,				"name or index", "Network interface by name or index.", true ),
	IntegerOption( 'm', "mx",			&gSSDPDiscover_MX,			"seconds", "MX value in search request, i.e., max response delay in seconds. (Default: 1 second)", false ),
	StringOption(  's', "st",			&gSSDPDiscover_ST,			"string", "ST value in search request, i.e., the search target. (Default: \"ssdp:all\")", false ),
	IntegerOption( 'r', "receiveTime",	&gSSDPDiscover_ReceiveSecs,	"seconds", "Amount of time to spend receiving responses. -1 means unlimited. (Default: 1 second)", false ),
	BooleanOption(  0 , "ipv4",			&gSSDPDiscover_UseIPv4,		"Use IPv4, i.e., multicast to 239.255.255.250:1900." ),
	BooleanOption(  0 , "ipv6",			&gSSDPDiscover_UseIPv6,		"Use IPv6, i.e., multicast to [ff02::c]:1900" ),
	BooleanOption( 'v', "verbose",		&gSSDPDiscover_Verbose,		"Prints the search request(s) that were sent." ),
	CLI_OPTION_END()
};

static void	SSDPDiscoverCmd( void );

static CLIOption		kSSDPOpts[] =
{
	Command( "discover", SSDPDiscoverCmd, kSSDPDiscoverOpts, "Crafts and multicasts an SSDP search message.", false ),
	CLI_OPTION_END()
};

#if( TARGET_OS_DARWIN )
//===========================================================================================================================
//	res_query Command Options
//===========================================================================================================================

static void	ResQueryCmd( void );

static const char *		gResQuery_Name			= NULL;
static const char *		gResQuery_Type			= NULL;
static const char *		gResQuery_Class			= NULL;
static int				gResQuery_UseLibInfo	= false;

static CLIOption		kResQueryOpts[] =
{
	StringOption( 'n', "name",		&gResQuery_Name,		"domain name",	"Full domain name of record to query.", true ),
	StringOption( 't', "type",		&gResQuery_Type,		"record type",	"Record type by name (e.g., TXT, SRV, etc.) or number.", true ),
	StringOption( 'c', "class",		&gResQuery_Class,		"record class",	"Record class by name or number. Default class is IN.", false ),
	BooleanOption( 0 , "libinfo",	&gResQuery_UseLibInfo,	"Use res_query from libinfo instead of libresolv." ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	dns_query Command Options
//===========================================================================================================================

static void ResolvDNSQueryCmd( void );

static const char *		gResolvDNSQuery_Name	= NULL;
static const char *		gResolvDNSQuery_Type	= NULL;
static const char *		gResolvDNSQuery_Class	= NULL;
static const char *		gResolvDNSQuery_Path	= NULL;

static CLIOption		kResolvDNSQueryOpts[] =
{
	StringOption( 'n', "name",	&gResolvDNSQuery_Name,	"domain name",	"Full domain name of record to query.", true ),
	StringOption( 't', "type",	&gResolvDNSQuery_Type,	"record type",	"Record type by name (e.g., TXT, SRV, etc.) or number.", true ),
	StringOption( 'c', "class",	&gResolvDNSQuery_Class,	"record class",	"Record class by name or number. Default class is IN.", false ),
	StringOption( 'p', "path",	&gResolvDNSQuery_Path,	"file path",	"The path argument to pass to dns_open() before calling dns_query(). Default value is NULL.", false ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	CFHost Command Options
//===========================================================================================================================

static void	CFHostCmd( void );

static const char *		gCFHost_Name		= NULL;
static int				gCFHost_WaitSecs	= 0;

static CLIOption		kCFHostOpts[] =
{
	StringOption(  'n', "name", &gCFHost_Name,     "hostname", "Hostname to resolve.", true ),
	IntegerOption( 'w', "wait", &gCFHost_WaitSecs, "seconds",  "Time in seconds to wait before a normal exit. (default: 0)", false ),
	CLI_OPTION_END()
};

static CLIOption		kLegacyOpts[] =
{
	Command( "res_query", ResQueryCmd,       kResQueryOpts,       "Uses res_query() from either libresolv or libinfo to query for a record.", true ),
	Command( "dns_query", ResolvDNSQueryCmd, kResolvDNSQueryOpts, "Uses dns_query() from libresolv to query for a record.", true ),
	Command( "cfhost",    CFHostCmd,         kCFHostOpts,         "Uses CFHost to resolve a hostname.", true ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	DNSConfigAdd Command Options
//===========================================================================================================================

static void	DNSConfigAddCmd( void );

static CFStringRef		gDNSConfigAdd_ID			= NULL;
static char **			gDNSConfigAdd_IPAddrArray	= NULL;
static size_t			gDNSConfigAdd_IPAddrCount	= 0;
static char **			gDNSConfigAdd_DomainArray	= NULL;
static size_t			gDNSConfigAdd_DomainCount	= 0;
static const char *		gDNSConfigAdd_Interface		= NULL;
static int				gDNSConfigAdd_SearchOrder	= -1;

static CLIOption		kDNSConfigAddOpts[] =
{
	CFStringOption(     0 , "id",          &gDNSConfigAdd_ID,                                      "ID", "Arbitrary ID to use for resolver entry.", true ),
	MultiStringOption( 'a', "address",     &gDNSConfigAdd_IPAddrArray, &gDNSConfigAdd_IPAddrCount, "IP address", "DNS server IP address(es). Can be specified more than once.", true ),
	MultiStringOption( 'd', "domain",      &gDNSConfigAdd_DomainArray, &gDNSConfigAdd_DomainCount, "domain", "Specific domain(s) for the resolver entry. Can be specified more than once.", false ),
	StringOption(      'i', "interface",   &gDNSConfigAdd_Interface,                               "interface name", "Specific interface for the resolver entry.", false ),
	IntegerOption(     'o', "searchOrder", &gDNSConfigAdd_SearchOrder,                             "integer", "Resolver entry's search order. Will only be set for values >= 0. (default: -1)", false ),
	
	CLI_SECTION( "Notes", "Run 'scutil -d -v --dns' to see the current DNS configuration. See scutil(8) man page for more details.\n" ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	DNSConfigRemove Command Options
//===========================================================================================================================

static void	DNSConfigRemoveCmd( void );

static CFStringRef		gDNSConfigRemove_ID = NULL;

static CLIOption		kDNSConfigRemoveOpts[] =
{
	CFStringOption( 0, "id", &gDNSConfigRemove_ID, "ID", "ID of resolver entry to remove.", true ),
	
	CLI_SECTION( "Notes", "Run 'scutil -d -v --dns' to see the current DNS configuration. See scutil(8) man page for more details.\n" ),
	CLI_OPTION_END()
};

static CLIOption		kDNSConfigOpts[] =
{
	Command( "add",    DNSConfigAddCmd,    kDNSConfigAddOpts,    "Add a supplemental resolver entry to the system's DNS configuration.", true ),
	Command( "remove", DNSConfigRemoveCmd, kDNSConfigRemoveOpts, "Remove a supplemental resolver entry from the system's DNS configuration.", true ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	XPCSend
//===========================================================================================================================

static void	XPCSendCommand( void );

static const char *		gXPCSend_ServiceName		= NULL;
static const char *		gXPCSend_MessageStr			= NULL;
static int				gXPCSend_NoReply			= false;
static int				gXPCSend_CancelDelaySecs	= 0;

static const char		kXPCSendMessageSection_Name[] = "Message Argument";
static const char		kXPCSendMessageSection_Text[] =
	"XPC messages are described as a string using the following syntax.\n"
	"\n"
	"With the exception of the top-most XPC message dictionary, dictionaries begin with a '{' and end with a '}'.\n"
	"Key-value pairs are of the form <key>=<value>, where <key> is a string and <value> is a value of any of the\n"
	"currently supported XPC types.\n"
	"\n"
	"Arrays begin with a '[' and end with a ']'.\n"
	"\n"
	"The following non-container XPC types are supported:\n"
	"\n"
	"Type                              Syntax                      Example\n"
	"bool                              bool:<string>               bool:true (or yes/y/on/1), bool:false (or no/n/off/0)\n"
	"data                              data:<hex string>           data:C0000201\n"
	"int64  (signed 64-bit integer)    int:<integer>               int:10, int:-1\n"
	"string                            string:<string>             string:example, string:escaped\\ white\\ space\n"
	"uint64 (unsigned 64-bit integer)  uint:<non-neg. integer>     uint:1024 or uint:0x400\n"
	"UUID                              uuid:<UUID>                 uuid:dab10183-84b5-4859-9de6-4bee287cfea3\n"
	"\n"
	"For convenience, the following type prefix abbreviations are accepted:\n"
	"\n"
	"    bool:   ↔ b:\n"
	"    data:   ↔ d:\n"
	"    int:    ↔ i:\n"
	"    string: ↔ s:\n"
	"    uint:   ↔ u:\n"
	"\n"
	"Here are some message string examples:\n"
	"\n"
	"    1. 'cmd=s:add make=s:Apple model=s:Macintosh aliases=[s:Mac s:Macintosh\\ 128K]'\n"
	"    2. 'cmd=s:search features={portable=b:yes solar=b:no} priceMin=u:100 priceMax=u:200'\n";

static CLIOption		kXPCSendOpts[] =
{
	StringOption(  's', "service", &gXPCSend_ServiceName,     "service name", "XPC service name.", true ),
	StringOption(  'm', "message", &gXPCSend_MessageStr,      "message",      "XPC message as a string.", false ),
	BooleanOption( 'n', "noReply", &gXPCSend_NoReply,         "No reply is expected." ),
	IntegerOption( 'd', "delay",   &gXPCSend_CancelDelaySecs, "seconds",      "Time to delay the XPC connection's cancellation in seconds. (default: 0)", false ),
	
	CLI_SECTION( kXPCSendMessageSection_Name, kXPCSendMessageSection_Text ),
	CLI_OPTION_END()
};
#endif	// TARGET_OS_DARWIN

#if( MDNSRESPONDER_PROJECT )
//===========================================================================================================================
//	InterfaceMonitor Command Options
//===========================================================================================================================

static void InterfaceMonitorCmd( void );

static CLIOption		kInterfaceMonitorOpts[] =
{
	StringOption( 'i', "interface", &gInterface, "name or index", "Network interface by name or index.", true ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	Querier Command Options
//===========================================================================================================================

#define kMDNSResolverTypeStr_Normal		"normal"
#define kMDNSResolverTypeStr_TCPOnly	"tcp"
#define kMDNSResolverTypeStr_TLS		"tls"
#define kMDNSResolverTypeStr_HTTPS		"https"

static const char *		gQuerier_Name				= NULL;
static const char *		gQuerier_Type				= "A";
static const char *		gQuerier_Class				= "IN";
static const char *		gQuerier_Delegator			= NULL;
static int				gQuerier_DNSSECOK			= false;
static int				gQuerier_CheckingDisabled	= false;
static int				gQuerier_SensitiveLogging	= false;
static const char *		gQuerier_ResolverType		= NULL;
static char **			gQuerier_ServerAddrs		= NULL;
static size_t			gQuerier_ServerAddrCount	= 0;
static const char *		gQuerier_ProviderName		= NULL;
static const char *		gQuerier_ConnectionHostname	= NULL;
static const char *		gQuerier_URLPath			= NULL;
static const char *		gQuerier_ODoHConfig			= NULL;
static const char *		gQuerier_IdentityReference	= NULL;
static int				gQuerier_NoConnectionReuse	= false;
static int				gQuerier_SquashCNAMEs		= false;
static char **			gQuerier_Domains			= NULL;
static size_t			gQuerier_DomainCount		= 0;
static const char *		gQuerier_StartLeewayMs		= NULL;

static CLIOption		kQuerierOpts[] =
{
	StringOption( 'i', "interface",        &gInterface,                "name or index", "If specified, network traffic is scoped to this interface.", false ),
	StringOption( 'n', "name",             &gQuerier_Name,             "name", "Question name (QNAME).", true ),
	StringOption( 't', "type",             &gQuerier_Type,             "type", "Question type (QTYPE). (default: A)", false ),
	StringOption( 'c', "class",            &gQuerier_Class,            "class", "Question class (QCLASS). (default: IN)", false ),
	StringOption(  0 , "delegator",        &gQuerier_Delegator,        "PID|UUID", "Delegator's PID or UUID.", false ),
	BooleanOption( 0 , "dnssec",           &gQuerier_DNSSECOK,         "Have queries include an OPT record with the DNSSEC OK (DO) bit set." ),
	BooleanOption( 0 , "checkingDisabled", &gQuerier_CheckingDisabled, "Set the Checking Disabled (CD) bit in queries." ),
	BooleanOption( 0 , "sensitiveLogging", &gQuerier_SensitiveLogging, "Enable sensitive logging for the query." ),
	StringOption(  0 , "startLeeway",      &gQuerier_StartLeewayMs,    "ms", "Start time leeway in milliseconds. Negative values mean infinite leeway.", false ),
	
	CLI_OPTION_GROUP( "DNS Service Options" ),
	StringOptionEx( 'r', "resolverType", &gQuerier_ResolverType, "resolver type", "Specifies the type of resolver to use.", false,
		"\n"
		"Use '" kMDNSResolverTypeStr_Normal  "' for DNS over UDP and TCP (Do53).\n"
		"Use '" kMDNSResolverTypeStr_TCPOnly "' for DNS over TCP.\n"
		"Use '" kMDNSResolverTypeStr_TLS     "' for DNS over TLS (DoT).\n"
		"Use '" kMDNSResolverTypeStr_HTTPS   "' for DNS over HTTPS (DoH or ODoH).\n"
		"\n"
		"If no resolver type is specified, an mdns_dns_service_manager will be used to determine which DNS service to use.\n"
		"How the mdns_dns_service_manager's services are populated depends on whether the server and domain options are\n"
		"specified. If either the server or domain, or both, options are specified, then an mdns_dns_service_definition\n"
		"will be used to define a Do53 DNS service using the interface, server, and domain options. If neither the server\n"
		"nor the domain options are specified, then the mdns_dns_service_manager will be populated with the system's DNS\n"
		"configuration. In both of these cases, the other resolver-specific options will be ignored.\n"
		"\n"
	),
	MultiStringOptionEx( 's', "server", &gQuerier_ServerAddrs, &gQuerier_ServerAddrCount, "IP address", "Server's IPv4 or IPv6 address with optionally-specified port.", false,
		"\n"
		"Use this option one or more times to specify a DNS service's server(s) by IP address.\n"
		"\n"
		"If no server IP addresses are specified for DNS over TLS/HTTPS resolvers, then connections to the DNS service\n"
		"will use the specified provider name as the DNS service's hostname.\n"
		"\n"
	),
	
	MultiStringOptionEx( 'd', "domain", &gQuerier_Domains, &gQuerier_DomainCount, "domain name", "DNS service's domains.", false,
		"\n"
		"Use this option one or more times to specify a DNS service's domains. This option is ignored if a resolver type\n"
		"is specified.\n"
		"\n"
	),
	StringOption( 'p', "providerName",       &gQuerier_ProviderName,       "domain name", "Provider's domain name for DNS over TLS/HTTPS.", false ),
	StringOption(  0 , "connectionHostname", &gQuerier_ConnectionHostname, "hostname",    "Overrides hostname used for transport layer connection for DNS over TLS/HTTPS.", false ),
	StringOption( 'q', "urlPath",            &gQuerier_URLPath,            "path", "URL path for DNS over HTTPS.", false ),
	StringOption( 'o', "odohConfig",         &gQuerier_ODoHConfig,         "odoh config", "Config for Oblivious DNS over HTTPS.", false ),
	StringOption(  0 , "identityReference",  &gQuerier_IdentityReference,  "hex string", "Persistent keychain reference for a client certificate.", false ),
	BooleanOption( 0 , "noConnectionReuse",  &gQuerier_NoConnectionReuse,  "Disable connection reuse." ),
	BooleanOption( 0 , "squashCNAMEs",       &gQuerier_SquashCNAMEs,       "Squash CNAME chains in responses." ),
	CLI_OPTION_END()
};

static void QuerierCommand( void );

//===========================================================================================================================
//	DNSProxy Command Options
//===========================================================================================================================

static void DNSProxyCmd( void );

static char **			gDNSProxy_InputInterfaces		= NULL;
static size_t			gDNSProxy_InputInterfaceCount	= 0;
static const char *		gDNSProxy_OutputInterface		= NULL;
static const char *		gDNSProxy_DNS64IPv6Prefix		= NULL;
static int				gDNSProxy_ForceAAAASynthesis	= false;

static CLIOption		kDNSProxyOpts[] =
{
	MultiStringOption( 'i', "inputInterface",  &gDNSProxy_InputInterfaces, &gDNSProxy_InputInterfaceCount, "name or index", "Interface to accept queries on. Can be specified more than once.", true ),
	StringOption(      'o', "outputInterface", &gDNSProxy_OutputInterface,    "name or index", "Interface to forward queries over. Use '0' for primary interface. (default: 0)", false ),
	StringOption(      'p', "dns64Prefix",     &gDNSProxy_DNS64IPv6Prefix,    "IPv6 prefix", "IPv6 prefix to use for DNS64 AAAA record synthesis.", false ),
	BooleanOption(     'f', "forceAAAASynth",  &gDNSProxy_ForceAAAASynthesis, "Force AAAA synthesis for DNS64." ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	GetAddrInfoNew Command Options
//===========================================================================================================================

static const char *		gGAINew_Hostname				= NULL;
static const char *		gGAINew_DelegatorID				= NULL;
static const char *		gGAINew_ServiceScheme			= NULL;
static const char *		gGAINew_AccountID				= NULL;
static int				gGAINew_ProtocolIPv4			= false;
static int				gGAINew_ProtocolIPv6			= false;
static int				gGAINew_ShowTracker				= false;
static int				gGAINew_UseFailover				= false;
static int				gGAINew_ProhibitEncryptedDNS	= false;
static int				gGAINew_OneShot					= false;
static int				gGAINew_TimeLimitSecs			= 0;
static const char *		gGAINew_QoS						= NULL;
static const char *		gGAINew_ResolverUUID			= NULL;
static int				gGAINew_PrivateLogging			= false;

#define kQoSTypeStr_Unspecified			"unspecified"
#define kQoSTypeStr_Background			"background"
#define kQoSTypeStr_Utility				"utility"
#define kQoSTypeStr_Default				"default"
#define kQoSTypeStr_UserInitiated		"userInitiated"
#define kQoSTypeStr_UserInteractive		"userInteractive"

#define kQoSArgShortName		"QoS class"

static CLIOption		kGetAddrInfoNewOpts[] =
{
	InterfaceOption(),
	StringOption(  'n', "name",                 &gGAINew_Hostname,             "domain name", "Hostname to resolve.", true ),
	StringOption(  'd', "delegate",             &gGAINew_DelegatorID,          "PID|UUID", "Delegator's PID or UUID. If PID p < 0, the audit token of PID |p| will be used.", false ),
	StringOption(   0,  "accountID",            &gGAINew_AccountID,            "account ID", "Account ID string.", false ),
	StringOption(   0,  "serviceScheme",        &gGAINew_ServiceScheme,        "scheme", "Service scheme such as '_443._https'.", false ),
	BooleanOption(  0 , "ipv4",                 &gGAINew_ProtocolIPv4,         "Use kDNSServiceProtocol_IPv4." ),
	BooleanOption(  0 , "ipv6",                 &gGAINew_ProtocolIPv6,         "Use kDNSServiceProtocol_IPv6." ),
	BooleanOption( 't', "showTracker",          &gGAINew_ShowTracker,          "Display tracker hostnames." ),
	BooleanOption(  0,  "useFailover",          &gGAINew_UseFailover,          "Use DNS service failover if necessary and applicable." ),
	BooleanOption(  0,  "prohibitEncryptedDNS", &gGAINew_ProhibitEncryptedDNS, "Prohibit use of encrypted DNS protocols such as DoT, DoH, ODoH, etc." ),
	StringOption(   0,  "resolverUUID",         &gGAINew_ResolverUUID,         "UUID", "UUID of libnetwork DNS resolver configuration to use.", false ),
	BooleanOption(  0,  "privateLogging",		&gGAINew_PrivateLogging,       "Use dnssd_log_privacy_level_private logging privacy level." ),
	
	CLI_OPTION_GROUP( "Flags" ),
	DNSSDFlagsOption(),
	DNSSDFlagsOption_AllowExpiredAnswers(),
	DNSSDFlagsOption_DenyCellular(),
	DNSSDFlagsOption_DenyConstrained(),
	DNSSDFlagsOption_DenyExpensive(),
	DNSSDFlagsOption_IncludeAWDL(),
	DNSSDFlagsOption_PathEvalDone(),
	DNSSDFlagsOption_ReturnIntermediates(),
	DNSSDFlagsOption_SuppressUnusable(),
	DNSSDFlagsOption_Timeout(),
	
	CLI_OPTION_GROUP( "Operation" ),
	ConnectionOptions(),
	BooleanOption(  'o', "oneshot",   &gGAINew_OneShot,       "Finish after first set of results." ),
	IntegerOption(  'l', "timeLimit", &gGAINew_TimeLimitSecs, "seconds", "Time limit for dnssd_getaddrinfo operation. Use '0' for no limit. (default: 0)", false ),
	StringOptionEx( 'q', "qos",       &gGAINew_QoS,           kQoSArgShortName, "Specifies the QoS of the queue used for the dnssd_getaddrinfo object.", false,
		"\n"
		"Use '" kQoSTypeStr_Unspecified     "' for QOS_CLASS_UNSPECIFIED.\n"
		"Use '" kQoSTypeStr_Background      "' for QOS_CLASS_BACKGROUND.\n"
		"Use '" kQoSTypeStr_Utility         "' for QOS_CLASS_UTILITY.\n"
		"Use '" kQoSTypeStr_Default         "' for QOS_CLASS_DEFAULT.\n"
		"Use '" kQoSTypeStr_UserInitiated   "' for QOS_CLASS_USER_INITIATED.\n"
		"Use '" kQoSTypeStr_UserInteractive "' for QOS_CLASS_USER_INTERACTIVE.\n"
		"\n"
	),
	CLI_OPTION_END()
};

static void	GetAddrInfoNewCommand( void );

//===========================================================================================================================
//	TCPInfo Command Options
//===========================================================================================================================

static const char *		gTCPInfo_LocalAddrStr	= NULL;
static const char *		gTCPInfo_RemoteAddrStr	= NULL;

static CLIOption		kTCPInfoOpts[] =
{
	StringOption( 'l', "local",  &gTCPInfo_LocalAddrStr,  "IP address+port", "TCP connection's local IPv4 or IPv6 address and port number.", true ),
	StringOption( 'r', "remote", &gTCPInfo_RemoteAddrStr, "IP address+port", "TCP connection's remote IPv4 or IPv6 address and port number.", true ),
	CLI_OPTION_END()
};

static void	TCPInfoCommand( void );

#endif	// MDNSRESPONDER_PROJECT

//===========================================================================================================================
//	PF
//===========================================================================================================================

static const char *		gThreadPFNAT64_IPv6Prefix	= NULL;
static const char *		gThreadPFNAT64_IPv4Address	= NULL;

static CLIOption		kThreadPFNAT64Opts[] =
{
	StringOption( 'p', "prefix",  &gThreadPFNAT64_IPv6Prefix,  "IPv6 prefix", "NAT64's IPv6 prefix.", true ),
	StringOption( 'a', "address", &gThreadPFNAT64_IPv4Address, "IPv4 address", "NAT64's IPv4 address.", true ),
	CLI_OPTION_END()
};

static void	ThreadPFNAT64Command( void );
static void	ThreadPFDeleteCommand( void );

static CLIOption		kPFOpts[] =
{
	Command( "nat64",  ThreadPFNAT64Command,  kThreadPFNAT64Opts, "Set Thread border router NAT64 PF rules.", true ),
	Command( "delete", ThreadPFDeleteCommand, NULL,               "Delete Thread border router PF rules.", true ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	IPv4 Forwarding
//===========================================================================================================================

static void	IPv4FwdEnableCommand( void );
static void	IPv4FwdDisableCommand( void );

static CLIOption		kIPv4FwdOpts[] =
{
	Command( "enable",  IPv4FwdEnableCommand,  NULL, "Enable IPv4 forwarding between network interfaces.", true ),
	Command( "disable", IPv4FwdDisableCommand, NULL, "Disable IPv4 forwarding between network interfaces.", true ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	IPv6 Forwarding
//===========================================================================================================================

static void	IPv6FwdEnableCommand( void );
static void	IPv6FwdDisableCommand( void );

static CLIOption		kIPv6FwdOpts[] =
{
	Command( "enable",  IPv6FwdEnableCommand,  NULL, "Enable IPv6 forwarding between network interfaces.", true ),
	Command( "disable", IPv6FwdDisableCommand, NULL, "Disable IPv6 forwarding between network interfaces.", true ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	Print
//===========================================================================================================================

static void	PrintCommand( void );

static CLIOption		kPrintOpts[] =
{
	CLI_OPTIONAL_ARGUMENT( "file", "Path to the file containing the DNS message.",
		"\n"
		"If this argument isn't specified or the argument is '-' (a single hyphen), then the DNS message is read from\n"
		"standard input.\n"
	),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	WiFi
//===========================================================================================================================

static void	WiFiOnCommand( void );
static void	WiFiOffCommand( void );

static CLIOption		kWiFiOpts[] =
{
	Command( "on",  WiFiOnCommand,  NULL, "Turn Wi-Fi power on.", true ),
	Command( "off", WiFiOffCommand, NULL, "Turn Wi-Fi power off.", true ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	Discovery Proxy
//===========================================================================================================================

static void	DiscoveryProxyCommand( void );

static char **		gDiscoveryProxy_ServerAddrs			= NULL;
static size_t		gDiscoveryProxy_ServerAddrCount		= 0;
static char **		gDiscoveryProxy_MatchDomains		= NULL;
static size_t		gDiscoveryProxy_MatchDomainCount	= 0;
static char **		gDiscoveryProxy_Certificates		= NULL;
static size_t		gDiscoveryProxy_CertificateCount	= 0;

static CLIOption		kDiscoveryProxyOpts[] =
{
	InterfaceOption(),
	MultiStringOption( 's', "server",		&gDiscoveryProxy_ServerAddrs, &gDiscoveryProxy_ServerAddrCount,		"IP address",	"Server's IPv4 or IPv6 address with optionally-specified port. Can be specified more than once.", true ),
	MultiStringOption( 'd', "domain",		&gDiscoveryProxy_MatchDomains, &gDiscoveryProxy_MatchDomainCount,	"domain name",	"Domain to match. Can be specified more than once.", true ),
	MultiStringOption( 'c', "certificate",	&gDiscoveryProxy_Certificates, &gDiscoveryProxy_CertificateCount,	"hex string",	"Server certificate. Can be specified more than once.", true ),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	Command Table
//===========================================================================================================================

static OSStatus	VersionOptionCallback( CLIOption *inOption, const char *inArg, int inUnset );

static void	BrowseCmd( void );
static void	GetAddrInfoCmd( void );
static void	QueryRecordCmd( void );
static void	RegisterCmd( void );
static void	RegisterRecordCmd( void );
static void	ResolveCmd( void );
static void	ReconfirmCmd( void );
static void	GetAddrInfoPOSIXCmd( void );
static void	ReverseLookupCmd( void );
static void	PortMappingCmd( void );
static void	BrowseAllCmd( void );
static void	GetAddrInfoStressCmd( void );
static void	DNSQueryCmd( void );
#if( DNSSDUTIL_INCLUDE_DNSCRYPT )
static void	DNSCryptCmd( void );
#endif
static void	MDNSQueryCmd( void );
#if( TARGET_OS_DARWIN )
static void	PIDToUUIDCmd( void );
#endif
static void	DNSProxyStateCmd( void );
static void	CachedLocalRecordsCommand( void );
static void	DaemonVersionCmd( void );

static CLIOption		kGlobalOpts[] =
{
	CLI_OPTION_CALLBACK_EX( 'V', "version", VersionOptionCallback, NULL, NULL,
		kCLIOptionFlags_NoArgument | kCLIOptionFlags_GlobalOnly, "Displays the version of this tool.", NULL ),
	CLI_OPTION_HELP(),
	
	// Common commands.
	
	Command( "browse",				BrowseCmd,				kBrowseOpts,			"Uses DNSServiceBrowse() to browse for one or more service types.", false ),
	Command( "getAddrInfo",			GetAddrInfoCmd,			kGetAddrInfoOpts,		"Uses DNSServiceGetAddrInfo() to resolve a hostname to IP addresses.", false ),
	Command( "queryRecord",			QueryRecordCmd,			kQueryRecordOpts,		"Uses DNSServiceQueryRecord() to query for an arbitrary DNS record.", false ),
	Command( "register",			RegisterCmd,			kRegisterOpts,			"Uses DNSServiceRegister() to register a service.", false ),
	Command( "registerRecord",		RegisterRecordCmd,		kRegisterRecordOpts,	"Uses DNSServiceRegisterRecord() to register a record.", false ),
	Command( "resolve",				ResolveCmd,				kResolveOpts,			"Uses DNSServiceResolve() to resolve a service.", false ),
	Command( "reconfirm",			ReconfirmCmd,			kReconfirmOpts,			"Uses DNSServiceReconfirmRecord() to reconfirm a record.", false ),
	Command( "getaddrinfo-posix",	GetAddrInfoPOSIXCmd,	kGetAddrInfoPOSIXOpts,	"Uses getaddrinfo() to resolve a hostname to IP addresses.", false ),
	Command( "reverseLookup",		ReverseLookupCmd,		kReverseLookupOpts,		"Uses DNSServiceQueryRecord() to perform a reverse IP address lookup.", false ),
	Command( "portMapping",			PortMappingCmd,			kPortMappingOpts,		"Uses DNSServiceNATPortMappingCreate() to create a port mapping.", false ),
#if( TARGET_OS_DARWIN )
	Command( "registerKA",			RegisterKACmd,			kRegisterKA_Opts,		"Uses DNSServiceSleepKeepalive_sockaddr() to register a keep alive record.", false ),
#endif
	Command( "browseAll",			BrowseAllCmd,			kBrowseAllOpts,			"Browse and resolve all (or specific) services and, optionally, attempt connections.", false ),
	
	// Uncommon commands.
	
	Command( "getnameinfo",				GetNameInfoCmd,				kGetNameInfoOpts,		"Calls getnameinfo() and prints results.", true ),
	Command( "getAddrInfoStress",		GetAddrInfoStressCmd,		kGetAddrInfoStressOpts,	"Runs DNSServiceGetAddrInfo() stress testing.", true ),
	Command( "DNSQuery",				DNSQueryCmd,				kDNSQueryOpts,			"Crafts and sends a DNS query.", true ),
#if( DNSSDUTIL_INCLUDE_DNSCRYPT )
	Command( "DNSCrypt",				DNSCryptCmd,				kDNSCryptOpts,			"Crafts and sends a DNSCrypt query.", true ),
#endif
	Command( "mdnsquery",				MDNSQueryCmd,				kMDNSQueryOpts,			"Crafts and sends an mDNS query over the specified interface.", true ),
	Command( "mdnscollider",			MDNSColliderCmd,			kMDNSColliderOpts,		"Creates record name collision scenarios.", true ),
#if( TARGET_OS_DARWIN )
	Command( "pid2uuid",				PIDToUUIDCmd,				kPIDToUUIDOpts,			"Prints the UUID of a process.", true ),
#endif
	Command( "server",					DNSServerCommand,			kDNSServerOpts,			"DNS server for testing.", true ),
	Command( "mdnsreplier",				MDNSReplierCmd,				kMDNSReplierOpts,		"Responds to mDNS queries for a set of authoritative resource records.", true ),
	Command( "test",					NULL,						kTestOpts,				"Commands for testing DNS-SD.", true ),
	Command( "ssdp",					NULL,						kSSDPOpts,				"Simple Service Discovery Protocol (SSDP).", true ),
#if( TARGET_OS_DARWIN )
	Command( "legacy",					NULL,						kLegacyOpts,			"Legacy DNS API.", true ),
	Command( "dnsconfig",				NULL,						kDNSConfigOpts,			"Add/remove a supplemental resolver entry to/from the system's DNS configuration.", true ),
	Command( "xpcsend",					XPCSendCommand,				kXPCSendOpts,			"Sends a message to an XPC service.", true ),
#endif
#if( MDNSRESPONDER_PROJECT )
	Command( "interfaceMonitor",		InterfaceMonitorCmd,		kInterfaceMonitorOpts,	"Instantiates an mdns_interface_monitor.", true ),
	Command( "querier",					QuerierCommand,				kQuerierOpts,			"Sends a DNS query using mdns_querier.", true ),
	Command( "dnsproxy",				DNSProxyCmd,				kDNSProxyOpts,			"Enables mDNSResponder's DNS proxy.", true ),
	Command( "dnsproxy-state",			DNSProxyStateCmd,			NULL,					"Gets mDNSResponder's DNS proxy state dump.", true ),
	Command( "getaddrinfo-new",			GetAddrInfoNewCommand,		kGetAddrInfoNewOpts,	"Uses dnssd_getaddrinfo to resolve a hostname to IP addresses.", false ),
	Command( "tcpinfo",					TCPInfoCommand,				kTCPInfoOpts,			"Uses mdns_tcpinfo_* to get TCP info.", true ),
#endif
	Command( "pf",						NULL,						kPFOpts,				"Packet filter commands.", true ),
	Command( "ipv4fwd",					NULL,						kIPv4FwdOpts,			"IPv4 forwarding commands.", true ),
	Command( "ipv6fwd",					NULL,						kIPv6FwdOpts,			"IPv6 forwarding commands.", true ),
	Command( "print",					PrintCommand,				kPrintOpts,				"Reads a DNS message in wire format and writes it to stdout in a human-readable form.", true ),
	Command( "wifi",					NULL,						kWiFiOpts,				"Wi-Fi commands.", true ),
	Command( "discovery-proxy",			DiscoveryProxyCommand,		kDiscoveryProxyOpts,	"Enables mDNSResponder's discovery proxy.", true ),
	Command( "cached-local-records",	CachedLocalRecordsCommand,	NULL,					"Uses mrc_cached_local_records_inquiry to inquire about cached .local records.", true ),
	Command( "daemonVersion",			DaemonVersionCmd,			NULL,					"Prints the version of the DNS-SD daemon.", true ),
	
	CLI_COMMAND_HELP(),
	CLI_OPTION_END()
};

//===========================================================================================================================
//	Helper Prototypes
//===========================================================================================================================

#define kExitReason_OneShotDone				"one-shot done"
#define kExitReason_ReceivedResponse		"received response"
#define kExitReason_SIGINT					"interrupt signal"
#define kExitReason_Timeout					"timeout"
#define kExitReason_TimeLimit				"time limit"

static void	Exit( void *inContext ) ATTRIBUTE_NORETURN;

static DNSServiceFlags	GetDNSSDFlagsFromOpts( void );

typedef enum
{
	kConnectionType_None			= 0,
	kConnectionType_Normal			= 1,
	kConnectionType_DelegatePID		= 2,
	kConnectionType_DelegateUUID	= 3
	
}	ConnectionType;

typedef struct
{
	ConnectionType		type;
	union
	{
		int32_t			pid;
		uint8_t			uuid[ 16 ];
		
	}	delegate;
	
}	ConnectionDesc;

static OSStatus
	CreateConnectionFromArgString(
		const char *			inString,
		dispatch_queue_t		inQueue,
		DNSServiceRef *			outSDRef,
		ConnectionDesc *		outDesc );
static OSStatus			InterfaceIndexFromArgString( const char *inString, uint32_t *outIndex );
static OSStatus			RecordDataFromArgString( const char *inString, uint8_t **outDataPtr, size_t *outDataLen );
static OSStatus			RecordTypeFromArgString( const char *inString, uint16_t *outValue );
static OSStatus			RecordClassFromArgString( const char *inString, uint16_t *outValue );
static OSStatus			SockAddrFromArgString( const char *inString, const char *inArgName, sockaddr_ip *outSA );

#define kInterfaceNameBufLen		( Max( IF_NAMESIZE, 16 ) + 1 )

static char *			InterfaceIndexToName( uint32_t inIfIndex, char inNameBuf[ kInterfaceNameBufLen ] );
static const char *		RecordTypeToString( int inValue );
#if( MDNSRESPONDER_PROJECT )
static const char *		RecordClassToString( int inValue );
#endif

static OSStatus
	WriteDNSQueryMessage(
		uint8_t			inMsg[ kDNSQueryMessageMaxLen ],
		uint16_t		inMsgID,
		uint16_t		inFlags,
		const char *	inQName,
		uint16_t		inQType,
		uint16_t		inQClass,
		size_t *		outMsgLen );

// Dispatch helpers

typedef void ( *DispatchHandler )( void *inContext );

static OSStatus
	DispatchSignalSourceCreate(
		int					inSignal,
		dispatch_queue_t	inQueue,
		DispatchHandler		inEventHandler,
		void *				inContext,
		dispatch_source_t *	outSource );
static OSStatus
	DispatchSocketSourceCreate(
		SocketRef				inSock,
		dispatch_source_type_t	inType,
		dispatch_queue_t		inQueue,
		DispatchHandler			inEventHandler,
		DispatchHandler			inCancelHandler,
		void *					inContext,
		dispatch_source_t *		outSource );

#define DispatchReadSourceCreate( SOCK, QUEUE, EVENT_HANDLER, CANCEL_HANDLER, CONTEXT, OUT_SOURCE ) \
	DispatchSocketSourceCreate( SOCK, DISPATCH_SOURCE_TYPE_READ, QUEUE, EVENT_HANDLER, CANCEL_HANDLER, CONTEXT, OUT_SOURCE )

#define DispatchWriteSourceCreate( SOCK, QUEUE, EVENT_HANDLER, CANCEL_HANDLER, CONTEXT, OUT_SOURCE ) \
	DispatchSocketSourceCreate( SOCK, DISPATCH_SOURCE_TYPE_WRITE, QUEUE, EVENT_HANDLER, CANCEL_HANDLER, CONTEXT, OUT_SOURCE )

static OSStatus
	DispatchTimerCreate(
		dispatch_time_t		inStart,
		uint64_t			inIntervalNs,
		uint64_t			inLeewayNs,
		dispatch_queue_t	inQueue,
		DispatchHandler		inEventHandler,
		DispatchHandler		inCancelHandler,
		void *				inContext,
		dispatch_source_t *	outTimer );

#define DispatchTimerOneShotCreate( IN_START, IN_LEEWAY, IN_QUEUE, IN_EVENT_HANDLER, IN_CONTEXT, OUT_TIMER )	\
	DispatchTimerCreate( IN_START, DISPATCH_TIME_FOREVER, IN_LEEWAY, IN_QUEUE, IN_EVENT_HANDLER, NULL, IN_CONTEXT, OUT_TIMER )

#if( TARGET_OS_DARWIN )
static OSStatus
	DispatchProcessMonitorCreate(
		pid_t				inPID,
		unsigned long		inFlags,
		dispatch_queue_t	inQueue,
		DispatchHandler		inEventHandler,
		DispatchHandler		inCancelHandler,
		void *				inContext,
		dispatch_source_t *	outMonitor );
#endif

static const char *	ServiceTypeDescription( const char *inName );

typedef void ( *SocketContextFinalizer_f )( void *inUserCtx );

typedef struct
{
	SocketRef						sock;			// Socket.
	int32_t							refCount;		// Reference count.
	void *							userContext;	// User's context.
	SocketContextFinalizer_f		userFinalizer;	// User's finalizer.
	
}	SocketContext;

static SocketContext *	SocketContextCreate( SocketRef inSock, void *inUserContext, OSStatus *outError );
static SocketContext *
	SocketContextCreateEx(
		SocketRef					inSock,
		void *						inUserContext,
		SocketContextFinalizer_f	inUserFinalizer,
		OSStatus *					outError );
static SocketContext *	SocketContextRetain( SocketContext *inContext );
static void				SocketContextRelease( SocketContext *inContext );
static void				SocketContextCancelHandler( void *inContext );
static void				SocketContextFinalizerCF( void *inUserCtx );

#define ForgetSocketContext( X )	ForgetCustom( X, SocketContextRelease )

static OSStatus		StringToInt32( const char *inString, int32_t *outValue );
static OSStatus		StringToUInt32( const char *inString, uint32_t *outValue );
#if( TARGET_OS_DARWIN )
static int64_t		_StringToInt64( const char *inString, OSStatus *outError );
static uint64_t		_StringToUInt64( const char *inString, OSStatus *outError );
static pid_t		_StringToPID( const char *inString, OSStatus *outError );
static OSStatus
	_ParseEscapedString(
		const char *	inSrc,
		const char *	inEnd,
		const char *	inDelimiters,
		char *			inBufPtr,
		size_t			inBufLen,
		size_t *		outCopiedLen,
		size_t *		outActualLen,
		const char **	outPtr );
static OSStatus
	_ParseEscapedStringWithCopy(
		const char *	inSrc,
		const char *	inEnd,
		const char *	inDelimiters,
		char *			inBufPtr,
		size_t			inBufLen,
		const char **	outString,
		char **			outMemory,
		const char **	outPtr );
#endif
static OSStatus		StringToARecordData( const char *inString, uint8_t **outPtr, size_t *outLen );
static OSStatus		StringToAAAARecordData( const char *inString, uint8_t **outPtr, size_t *outLen );
static OSStatus		StringToDomainName( const char *inString, uint8_t **outPtr, size_t *outLen );
#if( TARGET_OS_DARWIN )
static OSStatus		GetDefaultDNSServer( sockaddr_ip *outAddr );
#endif
static OSStatus
	_ServerSocketOpenEx2( 
		int				inFamily, 
		int				inType, 
		int				inProtocol, 
		const void *	inAddr, 
		int				inPort, 
		int *			outPort, 
		int				inRcvBufSize, 
		Boolean			inNoPortReuse,
		SocketRef *		outSock );

static const struct sockaddr *	GetMDNSMulticastAddrV4( void );
static const struct sockaddr *	GetMDNSMulticastAddrV6( void );

static OSStatus
	CreateMulticastSocket(
		const struct sockaddr *	inAddr,
		int						inPort,
		const char *			inIfName,
		uint32_t				inIfIndex,
		Boolean					inJoin,
		int *					outPort,
		SocketRef *				outSock );

static OSStatus	DecimalTextToUInt32( const char *inSrc, const char *inEnd, uint32_t *outValue, const char **outPtr );
static OSStatus	CheckIntegerArgument( int inArgValue, const char *inArgName, int inMin, int inMax );
static OSStatus	CheckDoubleArgument( double inArgValue, const char *inArgName, double inMin, double inMax );
static OSStatus	CheckRootUser( void );
#if( TARGET_OS_POSIX )
static OSStatus
	_SpawnCommand(
		pid_t *			outPID,
		const char *	inStdOutRedirect,
		const char *	inStdErrRedirect,
		const char *	inFormat,
		... );
#endif
static OSStatus	OutputFormatFromArgString( const char *inArgString, OutputFormatType *outFormat );
static OSStatus	OutputPropertyList( CFPropertyListRef inPList, OutputFormatType inType, const char *inOutputFilePath );
static OSStatus	CreateSRVRecordDataFromString( const char *inString, uint8_t **outPtr, size_t *outLen );
static OSStatus	CreateTXTRecordDataFromString( const char *inString, int inDelimiter, uint8_t **outPtr, size_t *outLen );
static OSStatus
	CreateNSECRecordData(
		const uint8_t *	inNextDomainName,
		uint8_t **		outPtr,
		size_t *		outLen,
		unsigned int	inTypeCount,
		... );
static OSStatus
	AppendSOARecord(
		DataBuffer *	inDB,
		const uint8_t *	inNamePtr,
		size_t			inNameLen,
		uint16_t		inType,
		uint16_t		inClass,
		uint32_t		inTTL,
		const uint8_t *	inMName,
		const uint8_t *	inRName,
		uint32_t		inSerial,
		uint32_t		inRefresh,
		uint32_t		inRetry,
		uint32_t		inExpire,
		uint32_t		inMinimumTTL,
		size_t *		outLen );
static OSStatus
	CreateSOARecordData(
		const uint8_t *	inMName,
		const uint8_t *	inRName,
		uint32_t		inSerial,
		uint32_t		inRefresh,
		uint32_t		inRetry,
		uint32_t		inExpire,
		uint32_t		inMinimumTTL,
		uint8_t **		outPtr,
		size_t *		outLen );
static OSStatus
	_DataBuffer_AppendDNSQuestion(
		DataBuffer *	inDB,
		const uint8_t *	inNamePtr,
		size_t			inNameLen,
		uint16_t		inType,
		uint16_t		inClass );
static OSStatus
	_DataBuffer_AppendDNSRecord(
		DataBuffer *	inDB,
		const uint8_t *	inNamePtr,
		size_t			inNameLen,
		uint16_t		inType,
		uint16_t		inClass,
		uint32_t		inTTL,
		const uint8_t *	inRDataPtr,
		size_t			inRDataLen );
static char *	_NanoTime64ToTimestamp( NanoTime64 inTime, char *inBuf, size_t inMaxLen );

typedef struct MDNSInterfaceItem		MDNSInterfaceItem;
struct MDNSInterfaceItem
{
	MDNSInterfaceItem *		next;
	char *					ifName;
	uint32_t				ifIndex;
	Boolean					hasIPv4;
	Boolean					hasIPv6;
	Boolean					isAWDL;
	Boolean					isWiFi;
};

typedef enum
{
	kMDNSInterfaceSubset_All		= 0,	// All mDNS-capable interfaces.
	kMDNSInterfaceSubset_AWDL		= 1,	// All mDNS-capable AWDL interfaces.
	kMDNSInterfaceSubset_NonAWDL	= 2		// All mDNS-capable non-AWDL iterfaces.
	
}	MDNSInterfaceSubset;

static OSStatus	_MDNSInterfaceListCreate( MDNSInterfaceSubset inSubset, size_t inItemSize, MDNSInterfaceItem **outList );
static void		_MDNSInterfaceListFree( MDNSInterfaceItem *inList );
#define _MDNSInterfaceListForget( X )		ForgetCustom( X, _MDNSInterfaceListFree )
static OSStatus _MDNSInterfaceGetAny( MDNSInterfaceSubset inSubset, char inNameBuf[ IF_NAMESIZE + 1 ], uint32_t *outIndex );

static OSStatus	_SetComputerName( CFStringRef inComputerName, CFStringEncoding inEncoding );
static OSStatus	_SetComputerNameWithUTF8CString( const char *inComputerName );
static OSStatus	_SetLocalHostName( CFStringRef inLocalHostName );
static OSStatus	_SetLocalHostNameWithUTF8CString( const char *inLocalHostName );
#if( TARGET_OS_DARWIN )
static OSStatus	_InterfaceIPv6AddressAdd( const char *inIfName, uint8_t inAddr[ STATIC_PARAM 16 ], int inMaskBitLen );
static OSStatus	_InterfaceIPv6AddressRemove( const char *inIfName, const uint8_t inAddr[ STATIC_PARAM 16 ] );
#endif
static int64_t	_TicksDiff( uint64_t inT1, uint64_t inT2 );
static void		_SockAddrInitIPv4( struct sockaddr_in *inSA, uint32_t inIPv4, uint16_t inPort );
static void
	_SockAddrInitIPv6(
		struct sockaddr_in6 *	inSA,
		const uint8_t			inIPv6[ STATIC_PARAM 16 ],
		uint32_t				inScope,
		uint16_t				inPort );

#define kIP6ArpaDomainStr					"ip6.arpa."
#define kReverseIPv6DomainNameBufLen		( ( 4 * 16 ) + sizeof_string( kIP6ArpaDomainStr ) + 1 )

static void
	_WriteReverseIPv6DomainNameString(
		const uint8_t	inIPv6Addr[ STATIC_PARAM 16 ],
		char			outBuffer[ STATIC_PARAM kReverseIPv6DomainNameBufLen ] );

#define kInAddrArpaDomainStr				"in-addr.arpa."
#define kReverseIPv4DomainNameBufLen		( ( 4 * 4 ) + sizeof_string( kInAddrArpaDomainStr ) + 1 )

static void
	_WriteReverseIPv4DomainNameString(
		uint32_t	inIPv4Addr,
		char		outBuffer[ STATIC_PARAM kReverseIPv4DomainNameBufLen ] );

#if( MDNSRESPONDER_PROJECT )
static OSStatus	_SetDefaultFallbackDNSService( const char *inFallbackDNSServiceStr );
#endif

static OSStatus
	_StringToIPv4Address(
		const char *			inStr,
		StringToIPAddressFlags	inFlags,
		uint32_t *				outIP,
		int *					outPort,
		uint32_t *				outSubnet,
		uint32_t *				outRouter,
		const char **			outStr );
static OSStatus
	_StringToIPv6Address(
		const char *			inStr,
		StringToIPAddressFlags	inFlags,
		uint8_t					outIPv6[ 16 ],
		uint32_t *				outScope,
		int *					outPort,
		int *					outPrefix,
		const char **			outStr );
static Boolean
	_ParseQuotedEscapedString(
		const char *	inSrc,
		const char *	inEnd,
		const char *	inDelimiters,
		char *			inBuf,
		size_t			inMaxLen,
		size_t *		outCopiedLen,
		size_t *		outTotalLen,
		const char **	outSrc );
static void *	_memdup( const void *inPtr, size_t inLen );
static int		_memicmp( const void *inP1, const void *inP2, size_t inLen );
static uint32_t	_FNV1( const void *inData, size_t inSize );
static OSStatus	_UInt32FromArgString( const char *inArgStr, const char *inArgName, uint32_t *outValue );
static char *	_UnixTimeToDateAndTimeString( int64_t inTimeSecs, char *inBufPtr, size_t inBufLen );
static char *	_DNSSDSourceVersionToCString( uint32_t inVersion, char *inBufPtr, size_t inBufLen );
static Boolean	_StdOutIsTTY( void );
#if( TARGET_OS_IOS )
static Boolean	_StdErrIsTTY( void );
#endif
static void		_PrintValidatedToStdOut( const char *inPrefix, Boolean inValidated, const char *inSuffix );

static Boolean		_DNSProtocolIsSecure( DNSProtocol inProtocol );
static const char *	_DNSProtocolToString( DNSProtocol inProtocol );

#define Unused( X )		(void)(X)

//===========================================================================================================================
//	MDNSCollider
//===========================================================================================================================

typedef struct MDNSColliderPrivate *		MDNSColliderRef;

typedef uint32_t		MDNSColliderProtocols;
#define kMDNSColliderProtocol_None		0
#define kMDNSColliderProtocol_IPv4		( 1 << 0 )
#define kMDNSColliderProtocol_IPv6		( 1 << 1 )

typedef void ( *MDNSColliderStopHandler_f )( void *inContext, OSStatus inError );

static OSStatus	MDNSColliderCreate( dispatch_queue_t inQueue, MDNSColliderRef *outCollider );
static OSStatus	MDNSColliderStart( MDNSColliderRef inCollider );
static void		MDNSColliderStop( MDNSColliderRef inCollider );
static void		MDNSColliderSetProtocols( MDNSColliderRef inCollider, MDNSColliderProtocols inProtocols );
static void		MDNSColliderSetInterfaceIndex( MDNSColliderRef inCollider, uint32_t inInterfaceIndex );
static OSStatus	MDNSColliderSetProgram( MDNSColliderRef inCollider, const char *inProgramStr );
static void
	MDNSColliderSetStopHandler(
		MDNSColliderRef				inCollider,
		MDNSColliderStopHandler_f	inStopHandler,
		void *						inStopContext );
static OSStatus
	MDNSColliderSetRecord(
		MDNSColliderRef	inCollider,
		const uint8_t *	inName,
		uint16_t		inType,
		const void *	inRDataPtr,
		size_t			inRDataLen );
static CFTypeID	MDNSColliderGetTypeID( void );

#define MDNSColliderForget( X )		ForgetCustomEx( X, MDNSColliderStop, CFRelease )

//===========================================================================================================================
//	ServiceBrowser
//===========================================================================================================================

typedef struct ServiceBrowserPrivate *		ServiceBrowserRef;
typedef struct ServiceBrowserResults		ServiceBrowserResults;
typedef struct SBRDomain					SBRDomain;
typedef struct SBRServiceType				SBRServiceType;
typedef struct SBRServiceInstance			SBRServiceInstance;
typedef struct SBRIPAddress					SBRIPAddress;

typedef void ( *ServiceBrowserCallback_f )( ServiceBrowserResults *inResults, OSStatus inError, void *inContext );

struct ServiceBrowserResults
{
	SBRDomain *		domainList;	// List of domains in which services were found.
};

struct SBRDomain
{
	SBRDomain *				next;		// Next domain in list.
	char *					name;		// Name of domain represented by this object.
	SBRServiceType *		typeList;	// List of service types in this domain.
};

struct SBRServiceType
{
	SBRServiceType *			next;			// Next service type in list.
	char *						name;			// Name of service type represented by this object.
	SBRServiceInstance *		instanceList;	// List of service instances of this service type.
};

struct SBRServiceInstance
{
	SBRServiceInstance *		next;			// Next service instance in list.
	char *						name;			// Name of service instance represented by this object.
	char *						hostname;		// Target from service instance's SRV record.
	uint32_t					ifIndex;		// Index of interface over which this service instance was discovered.
	uint16_t					port;			// Port from service instance's SRV record.
	uint8_t *					txtPtr;			// Service instance's TXT record data.
	size_t						txtLen;			// Service instance's TXT record data length.
	SBRIPAddress *				ipaddrList;		// List of IP addresses that the hostname resolved to.
	uint64_t					discoverTimeUs;	// Time it took to discover this service instance in microseconds.
	uint64_t					resolveTimeUs;	// Time it took to resolve this service instance in microseconds.
};

struct SBRIPAddress
{
	SBRIPAddress *		next;			// Next IP address in list.
	sockaddr_ip			sip;			// IPv4 or IPv6 address.
	uint64_t			resolveTimeUs;	// Time it took to resolve this IP address in microseconds.
	Boolean				validated;		// True if IP address is validated.
};

static CFTypeID	ServiceBrowserGetTypeID( void );
static OSStatus
	ServiceBrowserCreate(
		dispatch_queue_t	inQueue,
		uint32_t			inInterfaceIndex,
		const char *		inDomain,
		unsigned int		inBrowseTimeSecs,
		Boolean				inIncludeAWDL,
		ServiceBrowserRef *	outBrowser );
static void		ServiceBrowserSetUseNewGAI( ServiceBrowserRef inBrowser, Boolean inUseNewGAI );
static void		ServiceBrowserSetValidateResults( ServiceBrowserRef inBrowser, Boolean inValidateResults );
static void		ServiceBrowserStart( ServiceBrowserRef inBrowser );
static OSStatus	ServiceBrowserAddServiceType( ServiceBrowserRef inBrowser, const char *inServiceType );
static void
	ServiceBrowserSetCallback(
		ServiceBrowserRef			inBrowser,
		ServiceBrowserCallback_f	inCallback,
		void *						inContext );
static void		ServiceBrowserResultsRetain( ServiceBrowserResults *inResults );
static void		ServiceBrowserResultsRelease( ServiceBrowserResults *inResults );

#define ForgetServiceBrowserResults( X )		ForgetCustom( X, ServiceBrowserResultsRelease )

//===========================================================================================================================
//	DNSServer
//===========================================================================================================================

typedef struct DNSServerPrivate *		DNSServerRef;

typedef void ( *DNSServerStartHandler_f )( uint16_t inActualPort, void *inCtx );
typedef void ( *DNSServerStopHandler_f )( OSStatus inError, void *inCtx );

static CFTypeID	DNSServerGetTypeID( void );
static OSStatus
	_DNSServerCreate(
		dispatch_queue_t		inQueue,
		DNSServerStartHandler_f	inStartHandler,
		DNSServerStopHandler_f	inStopHandler,
		void *					inUserContext,
		unsigned int			inResponseDelayMs,
		uint32_t				inDefaultTTL,
		const sockaddr_ip *		inServerArray,
		size_t					inServerCount,
		const char *			inDomain,
		Boolean					inBadUDPMode,
		DNSServerRef *			outServer );
static OSStatus	_DNSServerSetIgnoredQType( DNSServerRef inServer, int inQType );
static void		_DNSServerSetPort( DNSServerRef inServer, uint16_t inPort );
static void		_DNSServerStart( DNSServerRef inServer );
static void		_DNSServerStop( DNSServerRef inServer );

//===========================================================================================================================
//	main
//===========================================================================================================================

#define _PRINTF_EXTENSION_HANDLER_DECLARE( NAME )	\
	static int										\
		_PrintFExtensionHandler_ ## NAME (			\
			PrintFContext *	inContext,				\
			PrintFFormat *	inFormat,				\
			PrintFVAList *	inArgs,					\
			void *			inUserContext )

_PRINTF_EXTENSION_HANDLER_DECLARE( Timestamp );
_PRINTF_EXTENSION_HANDLER_DECLARE( DNSMessage );
_PRINTF_EXTENSION_HANDLER_DECLARE( RawDNSMessage );
_PRINTF_EXTENSION_HANDLER_DECLARE( CallbackFlags );
_PRINTF_EXTENSION_HANDLER_DECLARE( DNSRecordData );
_PRINTF_EXTENSION_HANDLER_DECLARE( DomainName );

#ifdef FUZZING
#define main main_dnssdutil
#endif

int	main( int argc, const char **argv )
{
	OSStatus		err;
	
	// Route DebugServices logging output to stderr.
	
	dlog_control( "DebugServices:output=file;stderr" );
	
	PrintFRegisterExtension( "du:time",		_PrintFExtensionHandler_Timestamp,		NULL );
	PrintFRegisterExtension( "du:dnsmsg",	_PrintFExtensionHandler_DNSMessage,		NULL );
	PrintFRegisterExtension( "du:rdnsmsg",	_PrintFExtensionHandler_RawDNSMessage,	NULL );
	PrintFRegisterExtension( "du:cbflags",	_PrintFExtensionHandler_CallbackFlags,	NULL );
	PrintFRegisterExtension( "du:rdata",	_PrintFExtensionHandler_DNSRecordData,	NULL );
	PrintFRegisterExtension( "du:dname",	_PrintFExtensionHandler_DomainName,		NULL );
	CLIInit( argc, argv );
	err = CLIParse( kGlobalOpts, kCLIFlags_None );
	if( err ) gExitCode = 1;
	
	return( gExitCode );
}

//===========================================================================================================================
//	VersionOptionCallback
//===========================================================================================================================

static OSStatus	VersionOptionCallback( CLIOption *inOption, const char *inArg, int inUnset )
{
	const char *		srcVers;
#if( MDNSRESPONDER_PROJECT )
	char				srcStr[ 16 ];
#endif
	
	Unused( inOption );
	Unused( inArg );
	Unused( inUnset );
	
#if( MDNSRESPONDER_PROJECT )
	srcVers = _DNSSDSourceVersionToCString( _DNS_SD_H, srcStr, sizeof( srcStr ) );
#else
	srcVers = DNSSDUTIL_SOURCE_VERSION;
#endif
	FPrintF( stdout, "%s version %v (%s)\n", gProgramName, kDNSSDUtilNumVersion, srcVers );
	
	return( kEndingErr );
}

//===========================================================================================================================
//	BrowseCmd
//===========================================================================================================================

typedef struct BrowseResolveOp		BrowseResolveOp;

struct BrowseResolveOp
{
	BrowseResolveOp *		next;			// Next resolve operation in list.
	DNSServiceRef			sdRef;			// sdRef of the DNSServiceResolve or DNSServiceQueryRecord operation.
	char *					fullName;		// Full name of the service to resolve.
	uint32_t				interfaceIndex;	// Interface index of the DNSServiceResolve or DNSServiceQueryRecord operation.
};

typedef struct
{
	DNSServiceRef			mainRef;			// Main sdRef for shared connection.
	DNSServiceRef *			opRefs;				// Array of sdRefs for individual Browse operarions.
	size_t					opRefsCount;		// Count of array of sdRefs for non-shared connections.
	const char *			domain;				// Domain for DNSServiceBrowse operation(s).
	DNSServiceFlags			flags;				// Flags for DNSServiceBrowse operation(s).
	char **					serviceTypes;		// Array of service types to browse for.
	size_t					serviceTypesCount;	// Count of array of service types to browse for.
	int						timeLimitSecs;		// Time limit of DNSServiceBrowse operation in seconds.
	BrowseResolveOp *		resolveList;		// List of resolve and/or TXT record query operations.
	uint32_t				ifIndex;			// Interface index of DNSServiceBrowse operation(s).
	useconds_t				resolveDelayUs;		// Amount of time to wait before a resolve or TXT query in microseconds.
	Boolean					printedHeader;		// True if results header has been printed.
	Boolean					doResolve;			// True if service instances are to be resolved.
	Boolean					doResolveTXTOnly;	// True if TXT records of service instances are to be queried.
	Boolean					validateResults;	// True if signed results are requested and validated.
	
}	BrowseContext;

#define _DNSServiceAttrForget( X )		ForgetCustom( X, DNSServiceAttributeDeallocate )

static void		BrowsePrintPrologue( const BrowseContext *inContext );
static void		BrowseContextFree( BrowseContext *inContext );
static OSStatus	BrowseResolveOpCreate( const char *inFullName, uint32_t inInterfaceIndex, BrowseResolveOp **outOp );
static void		BrowseResolveOpFree( BrowseResolveOp *inOp );
static void DNSSD_API
	BrowseCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		uint32_t			inInterfaceIndex,
		DNSServiceErrorType	inError,
		const char *		inName,
		const char *		inRegType,
		const char *		inDomain,
		void *				inContext );
static void DNSSD_API
	BrowseResolveCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		const char *			inHostname,
		uint16_t				inPort,
		uint16_t				inTXTLen,
		const unsigned char *	inTXTPtr,
		void *					inContext );
static void DNSSD_API
	BrowseQueryRecordCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext );

ulog_define_ex( kDNSSDUtilIdentifier, Browse, kLogLevelTrace, kLogFlags_None, "Browse", NULL );
#define bc_ulog( LEVEL, ... )		ulog( &log_category_from_name( Browse ), (LEVEL), __VA_ARGS__ )

static void	BrowseCmd( void )
{
	OSStatus				err;
	size_t					i;
	BrowseContext *			context			= NULL;
	dispatch_source_t		signalSource	= NULL;
	int						useMainConnection;
	
	// Set up SIGINT handler.
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), Exit, kExitReason_SIGINT, &signalSource );
	require_noerr( err, exit );
	dispatch_resume( signalSource );
	
	// Create context.
	
	context = (BrowseContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	context->opRefs = (DNSServiceRef *) calloc( gBrowse_ServiceTypesCount, sizeof( DNSServiceRef ) );
	require_action( context->opRefs, exit, err = kNoMemoryErr );
	context->opRefsCount = gBrowse_ServiceTypesCount;
	
	// Check command parameters.
	
	if( gBrowse_TimeLimitSecs < 0 )
	{
		FPrintF( stderr, "Invalid time limit: %d seconds.\n", gBrowse_TimeLimitSecs );
		err = kParamErr;
		goto exit;
	}
	
	// Create main connection.
	
	if( gConnectionOpt )
	{
		err = CreateConnectionFromArgString( gConnectionOpt, dispatch_get_main_queue(), &context->mainRef, NULL );
		require_noerr_quiet( err, exit );
		useMainConnection = true;
	}
	else
	{
		useMainConnection = false;
	}
	
	// Get flags.
	
	context->flags = GetDNSSDFlagsFromOpts();
	if( useMainConnection ) context->flags |= kDNSServiceFlagsShareConnection;
	
	// Get interface.
	
	err = InterfaceIndexFromArgString( gInterface, &context->ifIndex );
	require_noerr_quiet( err, exit );
	
	// Set remaining parameters.
	
	context->serviceTypes		= gBrowse_ServiceTypes;
	context->serviceTypesCount	= gBrowse_ServiceTypesCount;
	context->domain				= gBrowse_Domain;
	context->doResolve			= gBrowse_DoResolve			? true : false;
	context->timeLimitSecs		= gBrowse_TimeLimitSecs;
	context->doResolveTXTOnly	= gBrowse_QueryTXT			? true : false;
	context->validateResults	= gBrowse_ValidateResults	? true : false;
	
	if( gBrowse_ResolveDelayMs > 0 )
	{
		const int		maxDelayMs = ( (useconds_t) -1 ) / kMicrosecondsPerMillisecond;
		
		err = CheckIntegerArgument( gBrowse_ResolveDelayMs, "resolveDelay", INT_MIN, maxDelayMs );
		require_noerr_quiet( err, exit );
		
		context->resolveDelayUs = (useconds_t)( gBrowse_ResolveDelayMs * kMicrosecondsPerMillisecond );
	}
#if( TARGET_OS_IOS )
	// Check for potential issues.
	
	if( context->validateResults && context->doResolve && os_feature_enabled( mDNSResponder, revoke_media_sessions ) )
	{
		FPrintF( stderr,
			"%s"
			"Warning: --validate and --resolve are not compatible when media revocation is enabled.\n"
			"         Use 'ffctl mDNSResponder/revoke_media_sessions=off' to disable.\n"
			"%s",
			_StdErrIsTTY() ? kANSIRed : "", _StdErrIsTTY() ? kANSINormal : "" );
	}
#endif
	// Print prologue.
	
	BrowsePrintPrologue( context );
	
	// Start operation(s).
	
	for( i = 0; i < context->serviceTypesCount; ++i )
	{
		DNSServiceRef		sdRef;
		
		sdRef = useMainConnection ? context->mainRef : kBadDNSServiceRef;
		if( context->validateResults )
		{
			err = DNSServiceBrowseEx( &sdRef, context->flags, context->ifIndex, context->serviceTypes[ i ],
				context->domain, &kDNSServiceAttrValidationRequired, BrowseCallback, context );
			require_noerr( err, exit );
		}
		else
		{
			err = DNSServiceBrowse( &sdRef, context->flags, context->ifIndex, context->serviceTypes[ i ],
				context->domain, BrowseCallback, context );
			require_noerr( err, exit );
		}
		context->opRefs[ i ] = sdRef;
		if( !useMainConnection )
		{
			err = DNSServiceSetDispatchQueue( context->opRefs[ i ], dispatch_get_main_queue() );
			require_noerr( err, exit );
		}
	}
	
	// Set time limit.
	
	if( context->timeLimitSecs > 0 )
	{
		dispatch_after_f( dispatch_time_seconds( context->timeLimitSecs ), dispatch_get_main_queue(),
			kExitReason_TimeLimit, Exit );
	}
	dispatch_main();
	
exit:
	dispatch_source_forget( &signalSource );
	if( context ) BrowseContextFree( context );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	BrowsePrintPrologue
//===========================================================================================================================

static void	BrowsePrintPrologue( const BrowseContext *inContext )
{
	const int						timeLimitSecs	= inContext->timeLimitSecs;
	const char * const *			ptr				= (const char **) inContext->serviceTypes;
	const char * const * const		end				= (const char **) inContext->serviceTypes + inContext->serviceTypesCount;
	char							ifName[ kInterfaceNameBufLen ];
	
	InterfaceIndexToName( inContext->ifIndex, ifName );
	
	FPrintF( stdout, "Flags:         %#{flags}\n",	inContext->flags, kDNSServiceFlagsDescriptors );
	FPrintF( stdout, "Interface:     %d (%s)\n",	(int32_t) inContext->ifIndex, ifName );
	FPrintF( stdout, "Service types: %s",			*ptr++ );
	while( ptr < end ) FPrintF( stdout, ", %s",		*ptr++ );
	FPrintF( stdout, "\n" );
	FPrintF( stdout, "Domain:        %s\n",	inContext->domain ? inContext->domain : "<NULL> (default domains)" );
	FPrintF( stdout, "Time limit:    " );
	if( timeLimitSecs > 0 )	FPrintF( stdout, "%d second%?c\n", timeLimitSecs, timeLimitSecs != 1, 's' );
	else					FPrintF( stdout, "∞\n" );
	FPrintF( stdout, "Start time:    %{du:time}\n", NULL );
	FPrintF( stdout, "---\n" );
}

//===========================================================================================================================
//	BrowseContextFree
//===========================================================================================================================

static void	BrowseContextFree( BrowseContext *inContext )
{
	size_t		i;
	
	for( i = 0; i < inContext->opRefsCount; ++i )
	{
		DNSServiceForget( &inContext->opRefs[ i ] );
	}
	if( inContext->serviceTypes )
	{
		StringArray_Free( inContext->serviceTypes, inContext->serviceTypesCount );
		inContext->serviceTypes			= NULL;
		inContext->serviceTypesCount	= 0;
	}
	DNSServiceForget( &inContext->mainRef );
	free( inContext );
}

//===========================================================================================================================
//	BrowseResolveOpCreate
//===========================================================================================================================

static OSStatus	BrowseResolveOpCreate( const char *inFullName, uint32_t inInterfaceIndex, BrowseResolveOp **outOp )
{
	OSStatus				err;
	BrowseResolveOp *		resolveOp;
	
	resolveOp = (BrowseResolveOp *) calloc( 1, sizeof( *resolveOp ) );
	require_action( resolveOp, exit, err = kNoMemoryErr );
	
	resolveOp->fullName = strdup( inFullName );
	require_action( resolveOp->fullName, exit, err = kNoMemoryErr );
	
	resolveOp->interfaceIndex = inInterfaceIndex;
	
	*outOp = resolveOp;
	resolveOp = NULL;
	err = kNoErr;
	
exit:
	if( resolveOp ) BrowseResolveOpFree( resolveOp );
	return( err );
}

//===========================================================================================================================
//	BrowseResolveOpFree
//===========================================================================================================================

static void	BrowseResolveOpFree( BrowseResolveOp *inOp )
{
	DNSServiceForget( &inOp->sdRef );
	ForgetMem( &inOp->fullName );
	free( inOp );
}

//===========================================================================================================================
//	BrowseCallback
//===========================================================================================================================

static void DNSSD_API
	BrowseCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		uint32_t			inInterfaceIndex,
		DNSServiceErrorType	inError,
		const char *		inName,
		const char *		inRegType,
		const char *		inDomain,
		void *				inContext )
{
	BrowseContext * const			context = (BrowseContext *) inContext;
	OSStatus						err;
	BrowseResolveOp *				newOp			= NULL;
	BrowseResolveOp **				p;
	mdns_signed_browse_result_t		signedResult	= NULL;
	DNSServiceAttributeRef			attr			= NULL;
	struct timeval					now;
	Boolean 						browseOnly, resultValidated;
	char							fullName[ kDNSServiceMaxDomainName ];
	
	gettimeofday( &now, NULL );
	
	err = inError;
	require_noerr( err, exit );
	
	if( !context->printedHeader )
	{
		FPrintF( stdout, "%-26s  %-17s IF %-20s %-20s Instance Name\n", "Timestamp", "Flags", "Domain", "Service Type" );
		context->printedHeader = true;
	}
	browseOnly		= !context->doResolve && !context->doResolveTXTOnly;
	resultValidated	= false;
	if( context->validateResults )
	{
		const uint8_t *		dataPtr;
		size_t				dataLen;
		
		dataPtr = DNSServiceGetValidationData( inSDRef, &dataLen );
		bc_ulog( kLogLevelTrace, "Got %zu bytes of validation data for TXT query result\n", dataLen );
		if( dataPtr )
		{
			if( browseOnly )
			{
				signedResult = mdns_signed_browse_result_create_from_data( dataPtr, dataLen, &err );
				bc_ulog( kLogLevelTrace, "Signed browse result -- %@", signedResult );
				if( signedResult )
				{
					uint8_t		instanceName[ kDomainNameLengthMax ];
					
					err = DNSServiceConstructFullName( fullName, inName, inRegType, inDomain );
					require_noerr( err, exit );
					
					err = DomainNameFromString( instanceName, fullName, NULL );
					require_noerr( err, exit );
					
					if( mdns_signed_browse_result_contains( signedResult, instanceName, inInterfaceIndex ) )
					{
						resultValidated = true;
					}
					else
					{
						bc_ulog( kLogLevelError, "Signed browse result doesn't contain instance %s interface %d\n",
							fullName, inInterfaceIndex );
					}
					mdns_forget( &signedResult );
				}
				else
				{
					bc_ulog( kLogLevelError, "mdns_signed_browse_result_create_from_data() failed: %#m\n", err );
				}
			}
			else
			{
				attr = DNSServiceAttributeCreate();
				require( attr, exit );
				
				err = DNSServiceAttrSetValidationData( attr, dataPtr, dataLen );
				require_noerr( err, exit );
			}
		}
	}
	FPrintF( stdout, "%{du:time}  %{du:cbflags} %2d %-20s %-20s %s",
		&now, inFlags, (int32_t) inInterfaceIndex, inDomain, inRegType, inName );
	if( browseOnly && context->validateResults ) _PrintValidatedToStdOut( " (", resultValidated, ")" );
	FPrintF( stdout, "\n" );
	if( browseOnly ) goto exit;
	
	err = DNSServiceConstructFullName( fullName, inName, inRegType, inDomain );
	require_noerr( err, exit );
	
	if( inFlags & kDNSServiceFlagsAdd )
	{
		DNSServiceRef		sdRef;
		DNSServiceFlags		flags;
		
		err = BrowseResolveOpCreate( fullName, inInterfaceIndex, &newOp );
		require_noerr( err, exit );
		
		if( context->mainRef )
		{
			sdRef = context->mainRef;
			flags = kDNSServiceFlagsShareConnection;
		}
		else
		{
			flags = 0;
		}
		if( context->resolveDelayUs > 0 ) usleep( context->resolveDelayUs );
		if( context->doResolve )
		{
			if( attr )
			{
				err = DNSServiceResolveEx( &sdRef, flags, inInterfaceIndex, inName, inRegType, inDomain, attr,
					BrowseResolveCallback, context );
				require_noerr( err, exit );
			}
			else
			{
				err = DNSServiceResolve( &sdRef, flags, inInterfaceIndex, inName, inRegType, inDomain,
					BrowseResolveCallback, context );
				require_noerr( err, exit );
			}
		}
		else
		{
			if( attr )
			{
				err = DNSServiceQueryRecordWithAttribute( &sdRef, flags, inInterfaceIndex, fullName,
					kDNSServiceType_TXT, kDNSServiceClass_IN, attr, BrowseQueryRecordCallback, context );
				require_noerr( err, exit );
			}
			else
			{
				err = DNSServiceQueryRecord( &sdRef, flags, inInterfaceIndex, fullName, kDNSServiceType_TXT,
					kDNSServiceClass_IN, BrowseQueryRecordCallback, context );
				require_noerr( err, exit );
			}
		}
		
		newOp->sdRef = sdRef;
		if( !context->mainRef )
		{
			err = DNSServiceSetDispatchQueue( newOp->sdRef, dispatch_get_main_queue() );
			require_noerr( err, exit );
		}
		for( p = &context->resolveList; *p; p = &( *p )->next ) {}
		*p = newOp;
		newOp = NULL;
	}
	else
	{
		BrowseResolveOp *		resolveOp;
		
		for( p = &context->resolveList; ( resolveOp = *p ) != NULL; p = &resolveOp->next )
		{
			if( ( resolveOp->interfaceIndex == inInterfaceIndex ) && ( strcasecmp( resolveOp->fullName, fullName ) == 0 ) )
			{
				break;
			}
		}
		if( resolveOp )
		{
			*p = resolveOp->next;
			BrowseResolveOpFree( resolveOp );
		}
	}
	
exit:
	mdns_forget( &signedResult );
	_DNSServiceAttrForget( &attr );
	if( newOp ) BrowseResolveOpFree( newOp );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	BrowseQueryRecordCallback
//===========================================================================================================================

static void DNSSD_API
	BrowseQueryRecordCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext )
{
	OSStatus				err;
	BrowseContext * const	context = (BrowseContext *) inContext;
	struct timeval			now;
	Boolean					txtValidated = false;
	
	Unused( inClass );
	Unused( inTTL );
	
	gettimeofday( &now, NULL );
	
	err = inError;
	require_noerr( err, exit );
	require_action( inType == kDNSServiceType_TXT, exit, err = kTypeErr );
	
	if( context->validateResults )
	{
		const uint8_t *		dataPtr;
		size_t				dataLen;
		
		dataPtr = DNSServiceGetValidationData( inSDRef, &dataLen );
		bc_ulog( kLogLevelTrace, "Got %zu bytes of validation data for TXT query result\n", dataLen );
		if( dataPtr )
		{
			mdns_signed_browse_result_t		signedResult;
			
			signedResult = mdns_signed_browse_result_create_from_data( dataPtr, dataLen, &err );
			bc_ulog( kLogLevelTrace, "Signed browse result -- %@", signedResult );
			if( signedResult )
			{
				if( mdns_signed_browse_result_covers_txt_rdata( signedResult, inRDataPtr, inRDataLen ) )
				{
					txtValidated = true;
				}
				else
				{
					bc_ulog( kLogLevelError, "Signed resolve result doesn't cover TXT record data\n" );
				}
				mdns_forget( &signedResult );
			}
			else
			{
				bc_ulog( kLogLevelError, "mdns_signed_resolve_result_create_from_data() failed: %#m\n", err );
			}
		}
	}
	FPrintF( stdout, "%{du:time}  %s %s TXT on interface %d\n    TXT: %#{txt}",
		&now, DNSServiceFlagsToAddRmvStr( inFlags ), inFullName, (int32_t) inInterfaceIndex, inRDataPtr,
		(size_t) inRDataLen );
	if( context->validateResults ) _PrintValidatedToStdOut( " (", txtValidated, ")" );
	FPrintF( stdout, "\n" );
	
exit:
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	BrowseResolveCallback
//===========================================================================================================================

static void DNSSD_API
	BrowseResolveCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		const char *			inHostname,
		uint16_t				inPort,
		uint16_t				inTXTLen,
		const unsigned char *	inTXTPtr,
		void *					inContext )
{
	BrowseContext * const	context = (BrowseContext *) inContext;
	struct timeval			now;
	char					errorStr[ 64 ];
	Boolean					txtValidated = false;
	
	Unused( inFlags );
	
	gettimeofday( &now, NULL );
	
	if( inError ) SNPrintF( errorStr, sizeof( errorStr ), " error %#m", inError );
	
	if( context->validateResults )
	{
		OSStatus	err;
		const uint8_t *		dataPtr;
		size_t				dataLen;
		
		dataPtr = DNSServiceGetValidationData( inSDRef, &dataLen );
		bc_ulog( kLogLevelTrace, "Got %zu bytes of validation data for resolve result\n", dataLen );
		if( dataPtr )
		{
			mdns_signed_resolve_result_t		signedResult;
			
			signedResult = mdns_signed_resolve_result_create_from_data( dataPtr, dataLen, &err );
			bc_ulog( kLogLevelTrace, "Signed resolve result -- %@", signedResult );
			if( signedResult )
			{
				if( mdns_signed_resolve_result_covers_txt_rdata( signedResult, inTXTPtr, inTXTLen ) )
				{
					txtValidated = true;
				}
				else
				{
					bc_ulog( kLogLevelError, "Signed resolve result doesn't cover TXT record data\n" );
				}
				mdns_forget( &signedResult );
			}
			else
			{
				bc_ulog( kLogLevelError, "mdns_signed_resolve_result_create_from_data() failed: %#m\n", err );
			}
		}
	}
	
	FPrintF( stdout, "%{du:time}  %s can be reached at %s:%u (interface %d)%?s\n",
		&now, inFullName, inHostname, ntohs( inPort ), (int32_t) inInterfaceIndex, inError, errorStr );
	if( inTXTLen == 1 )
	{
		FPrintF( stdout, " TXT record: %#H", inTXTPtr, (int) inTXTLen, INT_MAX );
	}
	else
	{
		FPrintF( stdout, " TXT record: %#{txt}", inTXTPtr, (size_t) inTXTLen );
	}
	if( context->validateResults ) _PrintValidatedToStdOut( " (", txtValidated, ")" );
	FPrintF( stdout, "\n" );
}

//===========================================================================================================================
//	GetAddrInfoCmd
//===========================================================================================================================

typedef struct
{
	DNSServiceRef			mainRef;		// Main sdRef for shared connection.
	DNSServiceRef			opRef;			// sdRef for the DNSServiceGetAddrInfo operation.
	const char *			name;			// Hostname to resolve.
	DNSServiceFlags			flags;			// Flags argument for DNSServiceGetAddrInfo().
	DNSServiceProtocol		protocols;		// Protocols argument for DNSServiceGetAddrInfo().
	uint32_t				ifIndex;		// Interface index argument for DNSServiceGetAddrInfo().
	int						timeLimitSecs;	// Time limit for the DNSServiceGetAddrInfo() operation in seconds.
	Boolean					printedHeader;	// True if the results header has been printed.
	Boolean					oneShotMode;	// True if command is done after the first set of results (one-shot mode).
	Boolean					needIPv4;		// True if in one-shot mode and an IPv4 result is needed.
	Boolean					needIPv6;		// True if in one-shot mode and an IPv6 result is needed.
	
}	GetAddrInfoContext;

static void	GetAddrInfoPrintPrologue( const GetAddrInfoContext *inContext );
static void	GetAddrInfoContextFree( GetAddrInfoContext *inContext );
static void DNSSD_API
	GetAddrInfoCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext );

static void	GetAddrInfoCmd( void )
{
	OSStatus					err;
	DNSServiceRef				sdRef;
	GetAddrInfoContext *		context			= NULL;
	dispatch_source_t			signalSource	= NULL;
	int							useMainConnection;
	
	// Set up SIGINT handler.
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), Exit, kExitReason_SIGINT, &signalSource );
	require_noerr( err, exit );
	dispatch_resume( signalSource );
	
	// Check command parameters.
	
	if( gGetAddrInfo_TimeLimitSecs < 0 )
	{
		FPrintF( stderr, "Invalid time limit: %d s.\n", gGetAddrInfo_TimeLimitSecs );
		err = kParamErr;
		goto exit;
	}
	
#if( MDNSRESPONDER_PROJECT )
	if( gFallbackDNSService )
	{
		err = _SetDefaultFallbackDNSService( gFallbackDNSService );
		require_noerr_quiet( err, exit );
	}
#endif
	// Create context.
	
	context = (GetAddrInfoContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	// Create main connection.
	
	if( gConnectionOpt )
	{
		err = CreateConnectionFromArgString( gConnectionOpt, dispatch_get_main_queue(), &context->mainRef, NULL );
		require_noerr_quiet( err, exit );
		useMainConnection = true;
	}
	else
	{
		useMainConnection = false;
	}
	
	// Get flags.
	
	context->flags = GetDNSSDFlagsFromOpts();
	if( useMainConnection ) context->flags |= kDNSServiceFlagsShareConnection;
	
	// Get interface.
	
	err = InterfaceIndexFromArgString( gInterface, &context->ifIndex );
	require_noerr_quiet( err, exit );
	
	// Set remaining parameters.
	
	context->name			= gGetAddrInfo_Name;
	context->timeLimitSecs	= gGetAddrInfo_TimeLimitSecs;
	if( gGetAddrInfo_ProtocolIPv4 ) context->protocols |= kDNSServiceProtocol_IPv4;
	if( gGetAddrInfo_ProtocolIPv6 ) context->protocols |= kDNSServiceProtocol_IPv6;
	if( gGetAddrInfo_OneShot )
	{
		context->oneShotMode	= true;
		context->needIPv4		= ( gGetAddrInfo_ProtocolIPv4 || !gGetAddrInfo_ProtocolIPv6 ) ? true : false;
		context->needIPv6		= ( gGetAddrInfo_ProtocolIPv6 || !gGetAddrInfo_ProtocolIPv4 ) ? true : false;
	}
	
	// Print prologue.
	
	GetAddrInfoPrintPrologue( context );
	
	// Start operation.
	
	sdRef = useMainConnection ? context->mainRef : kBadDNSServiceRef;
	err = DNSServiceGetAddrInfo( &sdRef, context->flags, context->ifIndex, context->protocols, context->name,
		GetAddrInfoCallback, context );
	require_noerr( err, exit );
	
	context->opRef = sdRef;
	if( !useMainConnection )
	{
		err = DNSServiceSetDispatchQueue( context->opRef, dispatch_get_main_queue() );
		require_noerr( err, exit );
	}
	
	// Set time limit.
	
	if( context->timeLimitSecs > 0 )
	{
		dispatch_after_f( dispatch_time_seconds( context->timeLimitSecs ), dispatch_get_main_queue(),
			kExitReason_TimeLimit, Exit );
	}
	dispatch_main();
	
exit:
	dispatch_source_forget( &signalSource );
	if( context ) GetAddrInfoContextFree( context );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	GetAddrInfoPrintPrologue
//===========================================================================================================================

static void	GetAddrInfoPrintPrologue( const GetAddrInfoContext *inContext )
{
	const int		timeLimitSecs = inContext->timeLimitSecs;
	char			ifName[ kInterfaceNameBufLen ];
	
	InterfaceIndexToName( inContext->ifIndex, ifName );
	
	FPrintF( stdout, "Flags:      %#{flags}\n",		inContext->flags, kDNSServiceFlagsDescriptors );
	FPrintF( stdout, "Interface:  %d (%s)\n",		(int32_t) inContext->ifIndex, ifName );
	FPrintF( stdout, "Protocols:  %#{flags}\n",		inContext->protocols, kDNSServiceProtocolDescriptors );
	FPrintF( stdout, "Name:       %s\n",			inContext->name );
	FPrintF( stdout, "Mode:       %s\n",			inContext->oneShotMode ? "one-shot" : "continuous" );
	FPrintF( stdout, "Time limit: " );
	if( timeLimitSecs > 0 )	FPrintF( stdout, "%d second%?c\n", timeLimitSecs, timeLimitSecs != 1, 's' );
	else					FPrintF( stdout, "∞\n" );
	FPrintF( stdout, "Start time: %{du:time}\n",	NULL );
	FPrintF( stdout, "---\n" );
}

//===========================================================================================================================
//	GetAddrInfoContextFree
//===========================================================================================================================

static void	GetAddrInfoContextFree( GetAddrInfoContext *inContext )
{
	DNSServiceForget( &inContext->opRef );
	DNSServiceForget( &inContext->mainRef );
	free( inContext );
}

//===========================================================================================================================
//	GetAddrInfoCallback
//===========================================================================================================================

static void DNSSD_API
	GetAddrInfoCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext )
{
	GetAddrInfoContext * const		context = (GetAddrInfoContext *) inContext;
	struct timeval					now;
	OSStatus						err;
	const char *					addrStr;
	char							addrStrBuf[ kSockAddrStringMaxSize ];
	
	Unused( inSDRef );
	
	gettimeofday( &now, NULL );
	
	switch( inError )
	{
		case kDNSServiceErr_NoError:
		case kDNSServiceErr_NoSuchRecord:
			err = kNoErr;
			break;
		
		case kDNSServiceErr_Timeout:
			Exit( kExitReason_Timeout );
		
		default:
			err = inError;
			goto exit;
	}
	
	if( ( inSockAddr->sa_family != AF_INET ) && ( inSockAddr->sa_family != AF_INET6 ) )
	{
		dlogassert( "Unexpected address family: %d", inSockAddr->sa_family );
		err = kTypeErr;
		goto exit;
	}
	
	if( !inError )
	{
		err = SockAddrToString( inSockAddr, kSockAddrStringFlagsNone, addrStrBuf );
		require_noerr( err, exit );
		addrStr = addrStrBuf;
	}
	else
	{
		addrStr = ( inSockAddr->sa_family == AF_INET ) ? kNoSuchRecordAStr : kNoSuchRecordAAAAStr;
	}
	
	if( !context->printedHeader )
	{
		FPrintF( stdout, "%-26s  %-17s IF %-30s %-34s %6s\n", "Timestamp", "Flags", "Hostname", "Address", "TTL" );
		context->printedHeader = true;
	}
	FPrintF( stdout, "%{du:time}  %{du:cbflags} %2d %-30s %-34s %6u\n",
		&now, inFlags, (int32_t) inInterfaceIndex, inHostname, addrStr, inTTL );
	
	if( context->oneShotMode )
	{
		if( inFlags & kDNSServiceFlagsAdd )
		{
			if( inSockAddr->sa_family == AF_INET )	context->needIPv4 = false;
			else									context->needIPv6 = false;
		}
		if( !( inFlags & kDNSServiceFlagsMoreComing ) && !context->needIPv4 && !context->needIPv6 )
		{
			Exit( kExitReason_OneShotDone );
		}
	}
	
exit:
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	QueryRecordCmd
//===========================================================================================================================

typedef struct
{
	DNSServiceRef		mainRef;			// Main sdRef for shared connection.
	DNSServiceRef		opRef;				// sdRef for the DNSServiceQueryRecord operation.
	const char *		recordName;			// Resource record name argument for DNSServiceQueryRecord().
	uuid_t *			resolverOverride;	// UUID of libnetwork DNS resolver configuration to use for resolver override.
	DNSServiceFlags		flags;				// Flags argument for DNSServiceQueryRecord().
	uint32_t			ifIndex;			// Interface index argument for DNSServiceQueryRecord().
	int					timeLimitSecs;		// Time limit for the DNSServiceQueryRecord() operation in seconds.
	uint16_t			recordType;			// Resource record type argument for DNSServiceQueryRecord().
	Boolean				useAAAAFallback;	// True if query for AAAA should fallback to A if AAAA doesn't exist.
	Boolean				useFailover;		// True if DNS service failover should be used if necessary and applicable.
	Boolean				printedHeader;		// True if the results header was printed.
	Boolean				oneShotMode;		// True if command is done after the first set of results (one-shot mode).
	Boolean				gotRecord;			// True if in one-shot mode and received at least one record of the desired type.
	Boolean				printRawRData;		// True if RDATA results are not to be formatted when printed.
	
}	QueryRecordContext;

static void	QueryRecordPrintPrologue( const QueryRecordContext *inContext );
static void	QueryRecordContextFree( QueryRecordContext *inContext );
static void DNSSD_API
	QueryRecordCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext );

static void	QueryRecordCmd( void )
{
	OSStatus					err;
	DNSServiceRef				sdRef;
	DNSServiceAttributeRef		attr			= NULL;
	QueryRecordContext *		context			= NULL;
	dispatch_source_t			signalSource	= NULL;
	int							useMainConnection;
	
	// Set up SIGINT handler.
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), Exit, kExitReason_SIGINT, &signalSource );
	require_noerr( err, exit );
	dispatch_resume( signalSource );
	
	// Create context.
	
	context = (QueryRecordContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	// Check command parameters.
	
	if( gQueryRecord_TimeLimitSecs < 0 )
	{
		FPrintF( stderr, "Invalid time limit: %d seconds.\n", gQueryRecord_TimeLimitSecs );
		err = kParamErr;
		goto exit;
	}
	
#if( MDNSRESPONDER_PROJECT )
	if( gFallbackDNSService )
	{
		err = _SetDefaultFallbackDNSService( gFallbackDNSService );
		require_noerr_quiet( err, exit );
	}
#endif
	// Create main connection.
	
	if( gConnectionOpt )
	{
		err = CreateConnectionFromArgString( gConnectionOpt, dispatch_get_main_queue(), &context->mainRef, NULL );
		require_noerr_quiet( err, exit );
		useMainConnection = true;
	}
	else
	{
		useMainConnection = false;
	}
	
	// Get flags.
	
	context->flags = GetDNSSDFlagsFromOpts();
	if( useMainConnection ) context->flags |= kDNSServiceFlagsShareConnection;
	
	// Get interface.
	
	err = InterfaceIndexFromArgString( gInterface, &context->ifIndex );
	require_noerr_quiet( err, exit );
	
	// Get record type.
	
	err = RecordTypeFromArgString( gQueryRecord_Type, &context->recordType );
	require_noerr( err, exit );
	
	// Get resolver override UUID.
	
	if( gQueryRecord_ResolverOverride )
	{
		uuid_t		uuid;
		
		err = uuid_parse( gQueryRecord_ResolverOverride, uuid );
		if( err )
		{
			FPrintF( stderr, "Invalid resolver UUID: %s\n", gQueryRecord_ResolverOverride );
			err = kParamErr;
			goto exit;
		}
		context->resolverOverride = (uuid_t *) _memdup( uuid, sizeof( uuid ) );
		require_action( context->resolverOverride, exit, err = kNoMemoryErr );
	}
	
	// Set remaining parameters.
	
	context->recordName			= gQueryRecord_Name;
	context->timeLimitSecs		= gQueryRecord_TimeLimitSecs;
	context->useAAAAFallback	= gQueryRecord_AAAAFallback ? true : false;
	context->useFailover		= gQueryRecord_UseFailover  ? true : false;
	context->oneShotMode		= gQueryRecord_OneShot      ? true : false;
	context->printRawRData		= gQueryRecord_RawRData     ? true : false;
	
	// Print prologue.
	
	QueryRecordPrintPrologue( context );
	
	// Start operation.
	
	if( context->useAAAAFallback || context->useFailover || context->resolverOverride )
	{
		attr = DNSServiceAttributeCreate();
		require_action( attr, exit, err = kNoResourcesErr );
		if( context->useAAAAFallback )
		{
			err = DNSServiceAttributeSetAAAAPolicy( attr, kDNSServiceAAAAPolicyFallback );
			require_noerr( err, exit );
		}
		if( context->useFailover )
		{
			err = DNSServiceAttrSetFailoverPolicy( attr, kDNSServiceFailoverPolicyAllow );
			require_noerr( err, exit );
		}
		if( context->resolverOverride )
		{
			if( __builtin_available( macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, * ) )
			{
				err = DNSServiceAttributeSetResolverOverride( attr, *context->resolverOverride );
				require_noerr( err, exit );
			}
			else
			{
				FPrintF( stderr, "DNSServiceAttributeSetResolverOverride() is not available on this OS build." );
				err = kUnsupportedErr;
				goto exit;
			}
		}
	}
	sdRef = useMainConnection ? context->mainRef : kBadDNSServiceRef;
	if( attr )
	{

		err = DNSServiceQueryRecordWithAttribute( &sdRef, context->flags, context->ifIndex, context->recordName,
			context->recordType, kDNSServiceClass_IN, attr, QueryRecordCallback, context );
		require_noerr( err, exit );
		_DNSServiceAttrForget( &attr );
	}
	else
	{
		err = DNSServiceQueryRecord( &sdRef, context->flags, context->ifIndex, context->recordName, context->recordType,
			kDNSServiceClass_IN, QueryRecordCallback, context );
		require_noerr( err, exit );
	}
	
	context->opRef = sdRef;
	if( !useMainConnection )
	{
		err = DNSServiceSetDispatchQueue( context->opRef, dispatch_get_main_queue() );
		require_noerr( err, exit );
	}
	
	// Set time limit.
	
	if( context->timeLimitSecs > 0 )
	{
		dispatch_after_f( dispatch_time_seconds( context->timeLimitSecs ), dispatch_get_main_queue(), kExitReason_TimeLimit,
			Exit );
	}
	dispatch_main();
	
exit:
	_DNSServiceAttrForget( &attr );
	dispatch_source_forget( &signalSource );
	if( context ) QueryRecordContextFree( context );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	QueryRecordContextFree
//===========================================================================================================================

static void	QueryRecordContextFree( QueryRecordContext *inContext )
{
	DNSServiceForget( &inContext->opRef );
	DNSServiceForget( &inContext->mainRef );
	ForgetMem( &inContext->resolverOverride );
	free( inContext );
}

//===========================================================================================================================
//	QueryRecordPrintPrologue
//===========================================================================================================================

static void	QueryRecordPrintPrologue( const QueryRecordContext *inContext )
{
	const int		timeLimitSecs = inContext->timeLimitSecs;
	char			ifName[ kInterfaceNameBufLen ];
	
	InterfaceIndexToName( inContext->ifIndex, ifName );
	const char *resolverOverrideStr = "n/a";
	uuid_string_t resolverOverrideUUIDStr;
	if( inContext->resolverOverride )
	{
		uuid_unparse_upper( *inContext->resolverOverride, resolverOverrideUUIDStr );
		resolverOverrideStr = resolverOverrideUUIDStr;
	}
	FPrintF( stdout, "Flags:              %#{flags}\n",	inContext->flags, kDNSServiceFlagsDescriptors );
	FPrintF( stdout, "Interface:          %d (%s)\n",	(int32_t) inContext->ifIndex, ifName );
	FPrintF( stdout, "Name:               %s\n",		inContext->recordName );
	FPrintF( stdout, "Type:               %s (%u)\n",	RecordTypeToString( inContext->recordType ), inContext->recordType );
	FPrintF( stdout, "AAAA Fallback:      %s\n",		YesNoStr( inContext->useAAAAFallback ) );
	FPrintF( stdout, "Resolver Override:  %s\n",		resolverOverrideStr );
	FPrintF( stdout, "Mode:               %s\n",		inContext->oneShotMode ? "one-shot" : "continuous" );
	FPrintF( stdout, "Time limit:         " );
	if( timeLimitSecs > 0 )	FPrintF( stdout, "%d second%?c\n", timeLimitSecs, timeLimitSecs != 1, 's' );
	else					FPrintF( stdout, "∞\n" );
	FPrintF( stdout, "Start time:         %{du:time}\n", NULL );
	FPrintF( stdout, "---\n" );
	
}

//===========================================================================================================================
//	QueryRecordCallback
//===========================================================================================================================

static void DNSSD_API
	QueryRecordCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext )
{
	QueryRecordContext * const		context		= (QueryRecordContext *) inContext;
	struct timeval					now;
	OSStatus						err;
	char *							rdataStrMem	= NULL;
	const char *					rdataStr;
	
	Unused( inSDRef );
	
	gettimeofday( &now, NULL );
	
	switch( inError )
	{
		case kDNSServiceErr_NoError:
			if( !context->printRawRData ) DNSRecordDataToString( inRDataPtr, inRDataLen, inType, &rdataStrMem );
			if( !rdataStrMem )
			{
				ASPrintF( &rdataStrMem, "%#H", inRDataPtr, (int) inRDataLen, INT_MAX );
				require_action( rdataStrMem, exit, err = kNoMemoryErr );
			}
			rdataStr = rdataStrMem;
			break;
		
		case kDNSServiceErr_NoSuchRecord:
			rdataStr = kNoSuchRecordStr;
			break;
		
		case kDNSServiceErr_NoSuchName:
			rdataStr = kNoSuchNameStr;
			break;
		
		case kDNSServiceErr_Timeout:
			Exit( kExitReason_Timeout );
		
		default:
			err = inError;
			goto exit;
	}
	if( !context->printedHeader )
	{
		FPrintF( stdout, "%-26s  %-17s IF %-32s %-5s %-5s %6s RData\n",
			"Timestamp", "Flags", "Name", "Type", "Class", "TTL" );
		context->printedHeader = true;
	}
	FPrintF( stdout, "%{du:time}  %{du:cbflags} %2d %-32s %-5s %?-5s%?5u %6u %s\n",
		&now, inFlags, (int32_t) inInterfaceIndex, inFullName, RecordTypeToString( inType ),
		( inClass == kDNSServiceClass_IN ), "IN", ( inClass != kDNSServiceClass_IN ), inClass, inTTL, rdataStr );
	
	if( context->oneShotMode )
	{
		if( ( inFlags & kDNSServiceFlagsAdd ) &&
			( ( context->recordType == kDNSServiceType_ANY ) || ( context->recordType == inType ) ) )
		{
			context->gotRecord = true;
		}
		if( !( inFlags & kDNSServiceFlagsMoreComing ) && context->gotRecord ) Exit( kExitReason_OneShotDone );
	}
	err = kNoErr;
	
exit:
	ForgetMem( &rdataStrMem );
	if( err ) ErrQuit( 1, "error: %#m\n", err );
}

//===========================================================================================================================
//	RegisterCmd
//===========================================================================================================================

typedef struct RecordUpdate		RecordUpdate;
struct RecordUpdate
{
	RecordUpdate *		next;		// Next record update in list.
	uint8_t *			dataPtr;	// Record data.
	size_t				dataLen;	// Record data length.
	uint32_t			ttl;		// Record TTL value.
	uint32_t			delayMs;	// Update delay in milliseconds.
};

typedef struct
{
	DNSRecordRef		recordRef;	// Reference returned by DNSServiceAddRecord().
	uint8_t *			dataPtr;	// Record data.
	size_t				dataLen;	// Record data length.
	uint32_t			ttl;		// Record TTL value.
	uint16_t			type;		// Record type.
	
}	ExtraRecord;

typedef struct
{
	DNSServiceRef			opRef;				// sdRef for DNSServiceRegister operation.
	const char *			name;				// Service name argument for DNSServiceRegister().
	const char *			type;				// Service type argument for DNSServiceRegister().
	const char *			domain;				// Domain in which advertise the service.
	uint8_t *				txtPtr;				// Service TXT record data. (malloc'd)
	size_t					txtLen;				// Service TXT record data len.
	ExtraRecord *			extraRecords;		// Array of extra records to add to registered service.
	size_t					extraRecordsCount;	// Number of extra records.
	RecordUpdate *			txtUpdates;			// List of TXT record updates. (malloc'd)
	dispatch_source_t		updateTimer;		// Timer for the next delayed TXT record update.
	dispatch_source_t		lifetimeTimer;		// Timer for the registration's lifetime.
	DNSServiceFlags			flags;				// Flags argument for DNSServiceRegister().
	uint32_t				ifIndex;			// Interface index argument for DNSServiceRegister().
	int						lifetimeMs;			// Lifetime of the record registration in milliseconds.
	uint32_t				timestamp;			// Timestamp in seconds since epoch time to indicate when the service is registered.
	uint32_t				hostKeyHash;		// Host key hash value.
	uint16_t				port;				// Service instance's port number.
	Boolean					setTimestamp;		// True if the timestamp attribute needs to be set.
	Boolean					setHostKeyHash;		// True if the host key hash attribute needs to be set.
	Boolean					printedHeader;		// True if results header was printed.
	Boolean					didRegister;		// True if service was registered.
}	RegisterContext;

static void	RegisterPrintPrologue( const RegisterContext *inContext );
static void	RegisterContextFree( RegisterContext *inContext );
static void DNSSD_API
	RegisterCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		DNSServiceErrorType	inError,
		const char *		inName,
		const char *		inType,
		const char *		inDomain,
		void *				inContext );
static OSStatus	_RegisterHandleTXTRecordUpdates( RegisterContext *inContext );
static void		_RegisterUpdateTimerHandler( void *inContext );
static void		_RecordUpdateFree( RecordUpdate *inUpdate );
#define _RecordUpdateForget( X )		ForgetCustom( X, _RecordUpdateFree )

static void	RegisterCmd( void )
{
	OSStatus					err;
	RegisterContext *			context			= NULL;
	dispatch_source_t			signalSource	= NULL;
	DNSServiceAttributeRef		attr			= NULL;
	
	// Set up SIGINT handler.
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), Exit, kExitReason_SIGINT, &signalSource );
	require_noerr( err, exit );
	dispatch_resume( signalSource );
	
	// Create context.
	
	context = (RegisterContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	// Check command parameters.
	
	err = CheckIntegerArgument( gRegister_Port, "port", 0, UINT16_MAX );
	require_noerr_quiet( err, exit );
	require_action_quiet( ( gAddRecord_TypesCount == gAddRecord_DataCount ) &&
		( ( gAddRecord_TTLsCount == 0 ) || ( gAddRecord_TTLsCount == gAddRecord_TypesCount ) ), exit,
		FPrintF( stderr, "There are missing additional record parameters.\n" ); err = kParamErr );
	require_action_quiet( ( gUpdateRecord_DataCount == gUpdateRecord_DelayCount ) &&
		( ( gUpdateRecord_TTLCount == 0 ) || ( gUpdateRecord_TTLCount == gUpdateRecord_DataCount ) ), exit,
		FPrintF( stderr, "There are missing update record parameters.\n" ); err = kParamErr );

	// Get flags.
	
	context->flags = GetDNSSDFlagsFromOpts();
	
	// Get interface index.
	
	err = InterfaceIndexFromArgString( gInterface, &context->ifIndex );
	require_noerr_quiet( err, exit );
	
	// Get TXT record data.
	
	if( gRegister_TXT )
	{
		err = RecordDataFromArgString( gRegister_TXT, &context->txtPtr, &context->txtLen );
		require_noerr_quiet( err, exit );
	}
	
	// Set remaining parameters.
	
	context->name		= gRegister_Name;
	context->type		= gRegister_Type;
	context->domain		= gRegister_Domain;
	context->port		= (uint16_t) gRegister_Port;
	context->lifetimeMs	= gRegister_LifetimeMs;
	if ( gRegister_TimeOfReceipt )
	{
		err = _UInt32FromArgString( gRegister_TimeOfReceipt, "timestamp", &context->timestamp );
		require_noerr_quiet( err, exit );

		context->setTimestamp = true;
	}
	if( gRegister_HostKeyHash )
	{
		err = _UInt32FromArgString( gRegister_HostKeyHash, "hostKeyHash", &context->hostKeyHash );
		require_noerr_quiet( err, exit );

		context->setHostKeyHash = true;
	}
	if( gAddRecord_TypesCount > 0 )
	{
		size_t		i;
		
		context->extraRecords = (ExtraRecord *) calloc( gAddRecord_TypesCount, sizeof( *context->extraRecords ) );
		require_action( context->extraRecords, exit, err = kNoMemoryErr );
		context->extraRecordsCount = gAddRecord_TypesCount;
		
		for( i = 0; i < gAddRecord_TypesCount; ++i )
		{
			ExtraRecord * const		extraRecord = &context->extraRecords[ i ];
			
			err = RecordTypeFromArgString( gAddRecord_Types[ i ], &extraRecord->type );
			require_noerr( err, exit );
			
			err = RecordDataFromArgString( gAddRecord_Data[ i ], &extraRecord->dataPtr, &extraRecord->dataLen );
			require_noerr_quiet( err, exit );
			
			if( gAddRecord_TTLsCount > 0 )
			{
				err = StringToUInt32( gAddRecord_TTLs[ i ], &extraRecord->ttl );
				require_noerr_action_quiet( err, exit,
					FPrintF( stderr, "Invalid TTL value: '%s'\n", gAddRecord_TTLs[ i ] ); err = kParamErr );
			}
			else
			{
				extraRecord->ttl = 0;
			}
		}
	}
	
	if( gUpdateRecord_DataCount > 0 )
	{
		size_t				i;
		RecordUpdate **		ptr = &context->txtUpdates;
		
		for( i = 0; i < gUpdateRecord_DataCount; ++i )
		{
			RecordUpdate *		update;
			
			update = (RecordUpdate *) calloc( 1, sizeof( *update ) );
			require_action( update, exit, err = kNoMemoryErr );
			*ptr = update;
			ptr = &update->next;
			
			err = RecordDataFromArgString( gUpdateRecord_Datas[ i ], &update->dataPtr, &update->dataLen );
			require_noerr_quiet( err, exit );
			
			err = StringToUInt32( gUpdateRecord_DelaysMs[ i ], &update->delayMs );
			require_noerr_action_quiet( err, exit,
				FPrintF( stderr, "Invalid delay value: '%s'\n", gUpdateRecord_DelaysMs[ i ] ); err = kParamErr );
			
			if( gUpdateRecord_TTLCount > 0 )
			{
				err = StringToUInt32( gUpdateRecord_TTLs[ i ], &update->ttl );
				require_noerr_action_quiet( err, exit,
					FPrintF( stderr, "Invalid TTL value: '%s'\n", gUpdateRecord_TTLs[ i ] ); err = kParamErr );
			}
			else
			{
				update->ttl = 0;
			}
		}
	}
	
	// Print prologue.
	
	RegisterPrintPrologue( context );
	
	// Start operation.
	
	if( context->setTimestamp || context->setHostKeyHash )
	{
		attr = DNSServiceAttributeCreate();
		require_action( attr, exit, err = kNoResourcesErr );
		
		if( context->setTimestamp )
		{
			err = DNSServiceAttributeSetTimestamp( attr, context->timestamp );
			require_noerr( err, exit );
		}
		if( context->setHostKeyHash )
		{
			if( __builtin_available( macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, * ) )
			{
				err = DNSServiceAttributeSetHostKeyHash( attr, context->hostKeyHash );
				require_noerr( err, exit );
			}
			else
			{
				FPrintF( stderr, "DNSServiceAttributeSetHostKeyHash() is not available on this OS build." );
				err = kUnsupportedErr;
				goto exit;
			}
		}
	}
	if( attr )
	{
		err = DNSServiceRegisterWithAttribute( &context->opRef, context->flags, context->ifIndex, context->name,
			context->type, context->domain, NULL, htons( context->port ), (uint16_t) context->txtLen, context->txtPtr,
			attr, RegisterCallback, context );
		require_noerr( err, exit );
		_DNSServiceAttrForget( &attr );
	}
	else
	{
		err = DNSServiceRegister( &context->opRef, context->flags, context->ifIndex, context->name, context->type,
			context->domain, NULL, htons( context->port ), (uint16_t) context->txtLen, context->txtPtr,
			RegisterCallback, context );
		ForgetMem( &context->txtPtr );
		require_noerr( err, exit );
	}
	err = DNSServiceSetDispatchQueue( context->opRef, dispatch_get_main_queue() );
	require_noerr( err, exit );
	
	dispatch_main();
	
exit:
	_DNSServiceAttrForget( &attr );
	dispatch_source_forget( &signalSource );
	if( context ) RegisterContextFree( context );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	RegisterPrintPrologue
//===========================================================================================================================

static void	RegisterPrintPrologue( const RegisterContext *inContext )
{
	char					ifName[ kInterfaceNameBufLen ];
	const char * const		defaultTTLStr = " (system will use a default value.)";
	Boolean					printedRecords;
	
	InterfaceIndexToName( inContext->ifIndex, ifName );
	
	FPrintF( stdout, "Flags:          %#{flags}\n",	inContext->flags, kDNSServiceFlagsDescriptors );
	FPrintF( stdout, "Interface:      %d (%s)\n",	(int32_t) inContext->ifIndex, ifName );
	FPrintF( stdout, "Name:           %s\n",		inContext->name ? inContext->name : "<NULL>" );
	FPrintF( stdout, "Type:           %s\n",		inContext->type );
	FPrintF( stdout, "Domain:         %s\n",		inContext->domain ? inContext->domain : "<NULL> (default domains)" );
	FPrintF( stdout, "Port:           %u\n",		inContext->port );
	FPrintF( stdout, "TXT data:       %#{txt}\n",	inContext->txtPtr, inContext->txtLen );
	if( inContext->setTimestamp )
	{
		const char *		dateTimeStr;
		char				dateTimeBuf[ 32 ];
		
		FPrintF( stdout, "Timestamp:      %u", inContext->timestamp );
		dateTimeStr = _UnixTimeToDateAndTimeString( inContext->timestamp, dateTimeBuf, sizeof( dateTimeBuf ) );
		if( dateTimeStr ) FPrintF( stdout, " (%s)", dateTimeStr );
		FPrintF( stdout, "\n" );
	}
	if( inContext->setHostKeyHash )
	{
		FPrintF( stdout, "Host Key Hash:  0x%08X (%u)\n", inContext->hostKeyHash, inContext->hostKeyHash );
	}
	if( inContext->lifetimeMs < 0 )	FPrintF( stdout, "Lifetime:       ∞ ms\n" );
	else							FPrintF( stdout, "Lifetime:       %d ms\n", inContext->lifetimeMs );
	
	printedRecords = false;
	if( inContext->extraRecordsCount > 0 )
	{
		size_t		i;
		
		for( i = 0; i < inContext->extraRecordsCount; ++i )
		{
			const ExtraRecord *		record = &inContext->extraRecords[ i ];
			
			FPrintF( stdout, "\nExtra record #%zu:\n",		i + 1 );
			FPrintF( stdout, "    Type:  %s (%u)\n",		RecordTypeToString( record->type ), record->type );
			FPrintF( stdout, "    RData: %{du:rdata}\n",	record->type, record->dataPtr, (unsigned int) record->dataLen );
			FPrintF( stdout, "    TTL:   %u%?s\n",			record->ttl, record->ttl == 0, defaultTTLStr );
		}
		printedRecords = true;
	}
	if( inContext->txtUpdates )
	{
		const RecordUpdate *		update;
		size_t						count = 0;
		
		for( update = inContext->txtUpdates; update; update = update->next )
		{
			++count;
			FPrintF( stdout, "\nTXT record update #%zu:\n",	count );
			FPrintF( stdout, "    Delay:    %u ms\n",		( update->delayMs > 0 ) ? update->delayMs : 0 );
			FPrintF( stdout, "    TXT data: %#{txt}\n",		update->dataPtr, update->dataLen );
			FPrintF( stdout, "    TTL:      %u%?s\n",		update->ttl, update->ttl == 0, defaultTTLStr );
		}
		printedRecords = true;
	}
	FPrintF( stdout, "%sStart time:     %{du:time}\n", printedRecords ? "\n" : "", NULL );
	FPrintF( stdout, "---\n" );
}

//===========================================================================================================================
//	RegisterContextFree
//===========================================================================================================================

static void	RegisterContextFree( RegisterContext *inContext )
{
	ExtraRecord *					record;
	const ExtraRecord * const		end = inContext->extraRecords + inContext->extraRecordsCount;
	
	DNSServiceForget( &inContext->opRef );
	ForgetMem( &inContext->txtPtr );
	for( record = inContext->extraRecords; record < end; ++record )
	{
		check( !record->recordRef );
		ForgetMem( &record->dataPtr );
	}
	ForgetMem( &inContext->extraRecords );
	while( inContext->txtUpdates )
	{
		RecordUpdate *		update = inContext->txtUpdates;
		
		inContext->txtUpdates = update->next;
		_RecordUpdateForget( &update );
	}
	dispatch_source_forget( &inContext->updateTimer );
	dispatch_source_forget( &inContext->lifetimeTimer );
	free( inContext );
}

//===========================================================================================================================
//	RegisterCallback
//===========================================================================================================================

static void DNSSD_API
	RegisterCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		DNSServiceErrorType	inError,
		const char *		inName,
		const char *		inType,
		const char *		inDomain,
		void *				inContext )
{
	RegisterContext * const		context = (RegisterContext *) inContext;
	OSStatus					err;
	struct timeval				now;
	
	Unused( inSDRef );
	
	gettimeofday( &now, NULL );
	
	if( !context->printedHeader )
	{
		FPrintF( stdout, "%-26s  %-17s Service\n", "Timestamp", "Flags" );
		context->printedHeader = true;
	}
	FPrintF( stdout, "%{du:time}  %{du:cbflags} %s.%s%s %?#m\n", &now, inFlags, inName, inType, inDomain, inError, inError );
	
	require_noerr_action_quiet( inError, exit, err = inError );
	
	if( !context->didRegister && ( inFlags & kDNSServiceFlagsAdd ) )
	{
		context->didRegister = true;
		if( context->extraRecordsCount > 0 )
		{
			ExtraRecord *					record;
			const ExtraRecord * const		end = context->extraRecords + context->extraRecordsCount;
			
			for( record = context->extraRecords; record < end; ++record )
			{
				err = DNSServiceAddRecord( context->opRef, &record->recordRef, 0, record->type,
					(uint16_t) record->dataLen, record->dataPtr, record->ttl );
				require_noerr( err, exit );
			}
		}
		if( context->lifetimeMs == 0 )
		{
			Exit( kExitReason_TimeLimit );
		}
		else if( context->lifetimeMs > 0 )
		{
			check( !context->lifetimeTimer );
			err = DispatchTimerOneShotCreate( dispatch_time_milliseconds( context->lifetimeMs ), 0,
				dispatch_get_main_queue(), Exit, kExitReason_TimeLimit, &context->lifetimeTimer );
			require_noerr( err, exit );
			dispatch_activate( context->lifetimeTimer );
		}
		err = _RegisterHandleTXTRecordUpdates( context );
		require_noerr( err, exit );
	}
	err = kNoErr;
	
exit:
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	_RegisterHandleTXTRecordUpdates
//===========================================================================================================================

static OSStatus	_RegisterHandleTXTRecordUpdates( RegisterContext *inContext )
{
	OSStatus		err;
	
	while( inContext->txtUpdates )
	{
		RecordUpdate *		update = inContext->txtUpdates;
		
		if( update->delayMs > 0 )
		{
			check( !inContext->updateTimer );
			err = DispatchTimerOneShotCreate( dispatch_time_milliseconds( update->delayMs ), 0, dispatch_get_main_queue(),
				_RegisterUpdateTimerHandler, inContext, &inContext->updateTimer );
			require_noerr( err, exit );
			dispatch_activate( inContext->updateTimer );
			break;
		}
		err = DNSServiceUpdateRecord( inContext->opRef, NULL, 0, (uint16_t) update->dataLen, update->dataPtr, update->ttl );
		require_noerr( err, exit );
		inContext->txtUpdates = update->next;
		_RecordUpdateForget( &update );
	}
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_RegisterUpdateTimerHandler
//===========================================================================================================================

static void	_RegisterUpdateTimerHandler( void *inContext )
{
	OSStatus					err;
	RegisterContext * const		context	= (RegisterContext *) inContext;
	RecordUpdate * const		update	= context->txtUpdates;
	
	dispatch_source_forget( &context->updateTimer );
	if( update ) update->delayMs = 0;
	err = _RegisterHandleTXTRecordUpdates( context );
	require_noerr( err, exit );
	
exit:
	if( err ) ErrQuit( 1, "error: %#m\n", err );
}

//===========================================================================================================================
//	_RecordUpdateFree
//===========================================================================================================================

static void	_RecordUpdateFree( RecordUpdate *inUpdate )
{
	inUpdate->next = NULL;
	ForgetMem( &inUpdate->dataPtr );
	free( inUpdate );
}

//===========================================================================================================================
//	RegisterRecordCmd
//===========================================================================================================================

typedef struct
{
	DNSServiceRef		conRef;			// sdRef to be initialized by DNSServiceCreateConnection().
	DNSRecordRef		recordRef;		// Registered record reference.
	const char *		recordName;		// Name of resource record.
	uint8_t *			dataPtr;		// Pointer to resource record data.
	size_t				dataLen;		// Length of resource record data.
	uint32_t			ttl;			// TTL value of resource record in seconds.
	uint32_t			ifIndex;		// Interface index argument for DNSServiceRegisterRecord().
	DNSServiceFlags		flags;			// Flags argument for DNSServiceRegisterRecord().
	int					lifetimeMs;		// Lifetime of the record registration in milliseconds.
	uint16_t			recordType;		// Resource record type.
	uint8_t *			updateDataPtr;	// Pointer to data for record update. (malloc'd)
	size_t				updateDataLen;	// Length of data for record update.
	uint32_t			updateTTL;		// TTL for updated record.
	int					updateDelayMs;	// Post-registration record update delay in milliseconds.
	uint32_t			timestamp;		// Timestamp in seconds since epoch time to indicate when the record is registered.
	uint32_t			hostKeyHash;	// Host key hash value.
	Boolean				setTimestamp;	// True if the timestamp attribute needs to be set.
	Boolean				setHostKeyHash;	// True if the host key hash attribute needs to be set.
	Boolean				didRegister;	// True if the record was registered.

	
}	RegisterRecordContext;

static void	RegisterRecordPrintPrologue( const RegisterRecordContext *inContext );
static void	RegisterRecordContextFree( RegisterRecordContext *inContext );
static void DNSSD_API
	RegisterRecordCallback(
		DNSServiceRef		inSDRef,
		DNSRecordRef		inRecordRef,
		DNSServiceFlags		inFlags,
		DNSServiceErrorType	inError,
		void *				inContext );
static void	RegisterRecordUpdate( void *inContext );

static void	RegisterRecordCmd( void )
{
	OSStatus					err;
	RegisterRecordContext *		context			= NULL;
	dispatch_source_t			signalSource	= NULL;
	DNSServiceAttributeRef		attr			= NULL;
	
	// Set up SIGINT handler.
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), Exit, kExitReason_SIGINT, &signalSource );
	require_noerr( err, exit );
	dispatch_resume( signalSource );

	// Create context.
	
	context = (RegisterRecordContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	// Create connection.
	
	err = CreateConnectionFromArgString( gConnectionOpt, dispatch_get_main_queue(), &context->conRef, NULL );
	require_noerr_quiet( err, exit );
	
	// Get flags.
	
	context->flags = GetDNSSDFlagsFromOpts();
	
	// Get interface.
	
	err = InterfaceIndexFromArgString( gInterface, &context->ifIndex );
	require_noerr_quiet( err, exit );
	
	// Get record type.
	
	err = RecordTypeFromArgString( gRegisterRecord_Type, &context->recordType );
	require_noerr( err, exit );
	
	// Get record data.
	
	if( gRegisterRecord_Data )
	{
		err = RecordDataFromArgString( gRegisterRecord_Data, &context->dataPtr, &context->dataLen );
		require_noerr_quiet( err, exit );
	}
	
	// Set remaining parameters.
	
	context->recordName	= gRegisterRecord_Name;
	context->ttl		= (uint32_t) gRegisterRecord_TTL;
	context->lifetimeMs	= gRegisterRecord_LifetimeMs;
	if( gRegisterRecord_TimeOfReceipt )
	{
		err = _UInt32FromArgString( gRegisterRecord_TimeOfReceipt, "timestamp", &context->timestamp );
		require_noerr_quiet( err, exit );

		context->setTimestamp = true;
	}
	if( gRegisterRecord_HostKeyHash )
	{
		err = _UInt32FromArgString( gRegisterRecord_HostKeyHash, "hostKeyHash", &context->hostKeyHash );
		require_noerr_quiet( err, exit );

		context->setHostKeyHash = true;
	}

	// Get update data.
	
	if( gRegisterRecord_UpdateData )
	{
		err = RecordDataFromArgString( gRegisterRecord_UpdateData, &context->updateDataPtr, &context->updateDataLen );
		require_noerr_quiet( err, exit );
		
		context->updateTTL		= (uint32_t) gRegisterRecord_UpdateTTL;
		context->updateDelayMs	= gRegisterRecord_UpdateDelayMs;
	}
	
	// Print prologue.
	
	RegisterRecordPrintPrologue( context );
	
	// Start operation.
	
	// Only call DNSServiceAttributeSetTimestamp when the option is set.
	if( context->setTimestamp || context->setHostKeyHash )
	{
		attr = DNSServiceAttributeCreate();
		require_action( attr, exit, err = kNoResourcesErr );
		
		if( context->setTimestamp )
		{
			err = DNSServiceAttributeSetTimestamp( attr, context->timestamp );
			require_noerr( err, exit );
		}
		if( context->setHostKeyHash )
		{
			if( __builtin_available( macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, * ) )
			{
				err = DNSServiceAttributeSetHostKeyHash( attr, context->hostKeyHash );
				require_noerr( err, exit );
			}
			else
			{
				FPrintF( stderr, "DNSServiceAttributeSetHostKeyHash() is not available on this OS build." );
				err = kUnsupportedErr;
				goto exit;
			}
		}
	}
	if( attr )
	{
		err = DNSServiceRegisterRecordWithAttribute( context->conRef, &context->recordRef, context->flags,
			context->ifIndex, context->recordName, context->recordType, kDNSServiceClass_IN,
			(uint16_t) context->dataLen, context->dataPtr, context->ttl, attr, RegisterRecordCallback, context );
		require_noerr( err, exit );
		_DNSServiceAttrForget( &attr );
	}
	else
	{
		err = DNSServiceRegisterRecord( context->conRef, &context->recordRef, context->flags, context->ifIndex,
			context->recordName, context->recordType, kDNSServiceClass_IN, (uint16_t) context->dataLen, context->dataPtr,
			context->ttl, RegisterRecordCallback, context );
		if( err )
		{
			FPrintF( stderr, "DNSServiceRegisterRecord() returned %#m\n", err );
			goto exit;
		}
	}

	dispatch_main();
	
exit:
	_DNSServiceAttrForget( &attr );
	dispatch_source_forget( &signalSource );
	if( context ) RegisterRecordContextFree( context );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	RegisterRecordPrintPrologue
//===========================================================================================================================

static void	RegisterRecordPrintPrologue( const RegisterRecordContext *inContext )
{
	int			infinite;
	char		ifName[ kInterfaceNameBufLen ];
	
	InterfaceIndexToName( inContext->ifIndex, ifName );
	
	FPrintF( stdout, "Flags:          %#{flags}\n",	inContext->flags, kDNSServiceFlagsDescriptors );
	FPrintF( stdout, "Interface:      %d (%s)\n",		(int32_t) inContext->ifIndex, ifName );
	FPrintF( stdout, "Name:           %s\n",			inContext->recordName );
	FPrintF( stdout, "Type:           %s (%u)\n",		RecordTypeToString( inContext->recordType ), inContext->recordType );
	FPrintF( stdout, "TTL:            %u\n",			inContext->ttl );
	FPrintF( stdout, "Data:           %#H\n",			inContext->dataPtr, (int) inContext->dataLen, INT_MAX );
	if( inContext->setTimestamp )
	{
		const char *		dateTimeStr;
		char				dateTimeBuf[ 32 ];
		
		FPrintF( stdout, "Timestamp:      %u", inContext->timestamp );
		dateTimeStr = _UnixTimeToDateAndTimeString( inContext->timestamp, dateTimeBuf, sizeof( dateTimeBuf ) );
		if( dateTimeStr ) FPrintF( stdout, " (%s)", dateTimeStr );
		FPrintF( stdout, "\n" );
	}
	if( inContext->setHostKeyHash )
	{
		FPrintF( stdout, "Host Key Hash:  0x%08X (%u)\n", inContext->hostKeyHash, inContext->hostKeyHash );
	}
	infinite = ( inContext->lifetimeMs < 0 ) ? true : false;
	FPrintF( stdout, "Lifetime:       %?s%?d ms\n",	infinite, "∞", !infinite, inContext->lifetimeMs );
	if( inContext->updateDataPtr )
	{
		FPrintF( stdout, "\nUpdate record:\n" );
		FPrintF( stdout, "    Delay:    %d ms\n",	( inContext->updateDelayMs >= 0 ) ? inContext->updateDelayMs : 0 );
		FPrintF( stdout, "    TTL:      %u%?s\n",
			inContext->updateTTL, inContext->updateTTL == 0, " (system will use a default value.)" );
		FPrintF( stdout, "    RData:    %#H\n",		inContext->updateDataPtr, (int) inContext->updateDataLen, INT_MAX );
	}
	FPrintF( stdout, "Start time:     %{du:time}\n", NULL );
	FPrintF( stdout, "---\n" );
}

//===========================================================================================================================
//	RegisterRecordContextFree
//===========================================================================================================================

static void	RegisterRecordContextFree( RegisterRecordContext *inContext )
{
	DNSServiceForget( &inContext->conRef );
	ForgetMem( &inContext->dataPtr );
	ForgetMem( &inContext->updateDataPtr );
	free( inContext );
}

//===========================================================================================================================
//	RegisterRecordCallback
//===========================================================================================================================

static void
	RegisterRecordCallback(
		DNSServiceRef		inSDRef,
		DNSRecordRef		inRecordRef,
		DNSServiceFlags		inFlags,
		DNSServiceErrorType	inError,
		void *				inContext )
{
	RegisterRecordContext *		context = (RegisterRecordContext *) inContext;
	struct timeval				now;
	
	Unused( inSDRef );
	Unused( inRecordRef );
	Unused( inFlags );
	Unused( context );
	
	gettimeofday( &now, NULL );
	FPrintF( stdout, "%{du:time} Record registration result (error %#m)\n", &now, inError );
	
	if( !context->didRegister && !inError )
	{
		context->didRegister = true;
		if( context->updateDataPtr )
		{
			if( context->updateDelayMs > 0 )
			{
				dispatch_after_f( dispatch_time_milliseconds( context->updateDelayMs ), dispatch_get_main_queue(),
					context, RegisterRecordUpdate );
			}
			else
			{
				RegisterRecordUpdate( context );
			}
		}
		if( context->lifetimeMs == 0 )
		{
			Exit( kExitReason_TimeLimit );
		}
		else if( context->lifetimeMs > 0 )
		{
			dispatch_after_f( dispatch_time_milliseconds( context->lifetimeMs ), dispatch_get_main_queue(),
				kExitReason_TimeLimit, Exit );
		}
	}
}

//===========================================================================================================================
//	RegisterRecordUpdate
//===========================================================================================================================

static void	RegisterRecordUpdate( void *inContext )
{
	OSStatus							err;
	RegisterRecordContext * const		context = (RegisterRecordContext *) inContext;
	
	err = DNSServiceUpdateRecord( context->conRef, context->recordRef, 0, (uint16_t) context->updateDataLen,
		context->updateDataPtr, context->updateTTL );
	require_noerr( err, exit );
	
exit:
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	ResolveCmd
//===========================================================================================================================

typedef struct
{
	DNSServiceRef		mainRef;		// Main sdRef for shared connections.
	DNSServiceRef		opRef;			// sdRef for the DNSServiceResolve operation.
	DNSServiceFlags		flags;			// Flags argument for DNSServiceResolve().
	const char *		name;			// Service name argument for DNSServiceResolve().
	const char *		type;			// Service type argument for DNSServiceResolve().
	const char *		domain;			// Domain argument for DNSServiceResolve().
	uint32_t			ifIndex;		// Interface index argument for DNSServiceResolve().
	int					timeLimitSecs;	// Time limit for the DNSServiceResolve operation in seconds.
	
}	ResolveContext;

static void	ResolvePrintPrologue( const ResolveContext *inContext );
static void	ResolveContextFree( ResolveContext *inContext );
static void DNSSD_API
	ResolveCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		const char *			inHostname,
		uint16_t				inPort,
		uint16_t				inTXTLen,
		const unsigned char *	inTXTPtr,
		void *					inContext );

static void	ResolveCmd( void )
{
	OSStatus				err;
	DNSServiceRef			sdRef;
	ResolveContext *		context			= NULL;
	dispatch_source_t		signalSource	= NULL;
	int						useMainConnection;
	
	// Set up SIGINT handler.
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), Exit, kExitReason_SIGINT, &signalSource );
	require_noerr( err, exit );
	dispatch_resume( signalSource );
	
	// Create context.
	
	context = (ResolveContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	// Check command parameters.
	
	if( gResolve_TimeLimitSecs < 0 )
	{
		FPrintF( stderr, "Invalid time limit: %d seconds.\n", gResolve_TimeLimitSecs );
		err = kParamErr;
		goto exit;
	}
	
	// Create main connection.
	
	if( gConnectionOpt )
	{
		err = CreateConnectionFromArgString( gConnectionOpt, dispatch_get_main_queue(), &context->mainRef, NULL );
		require_noerr_quiet( err, exit );
		useMainConnection = true;
	}
	else
	{
		useMainConnection = false;
	}
	
	// Get flags.
	
	context->flags = GetDNSSDFlagsFromOpts();
	if( useMainConnection ) context->flags |= kDNSServiceFlagsShareConnection;
	
	// Get interface index.
	
	err = InterfaceIndexFromArgString( gInterface, &context->ifIndex );
	require_noerr_quiet( err, exit );
	
	// Set remaining parameters.
	
	context->name			= gResolve_Name;
	context->type			= gResolve_Type;
	context->domain			= gResolve_Domain;
	context->timeLimitSecs	= gResolve_TimeLimitSecs;
	
	// Print prologue.
	
	ResolvePrintPrologue( context );
	
	// Start operation.
	
	sdRef = useMainConnection ? context->mainRef : kBadDNSServiceRef;
	err = DNSServiceResolve( &sdRef, context->flags, context->ifIndex, context->name, context->type, context->domain,
		ResolveCallback, NULL );
	require_noerr( err, exit );
	
	context->opRef = sdRef;
	if( !useMainConnection )
	{
		err = DNSServiceSetDispatchQueue( context->opRef, dispatch_get_main_queue() );
		require_noerr( err, exit );
	}
	
	// Set time limit.
	
	if( context->timeLimitSecs > 0 )
	{
		dispatch_after_f( dispatch_time_seconds( context->timeLimitSecs ), dispatch_get_main_queue(),
			kExitReason_TimeLimit, Exit );
	}
	dispatch_main();
	
exit:
	dispatch_source_forget( &signalSource );
	if( context ) ResolveContextFree( context );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	ReconfirmCmd
//===========================================================================================================================

static void	ReconfirmCmd( void )
{
	OSStatus			err;
	uint8_t *			rdataPtr = NULL;
	size_t				rdataLen = 0;
	DNSServiceFlags		flags;
	uint32_t			ifIndex;
	uint16_t			type, class;
	char				ifName[ kInterfaceNameBufLen ];
	
	// Get flags.
	
	flags = GetDNSSDFlagsFromOpts();
	
	// Get interface index.
	
	err = InterfaceIndexFromArgString( gInterface, &ifIndex );
	require_noerr_quiet( err, exit );
	
	// Get record type.
	
	err = RecordTypeFromArgString( gReconfirmRecord_Type, &type );
	require_noerr( err, exit );
	
	// Get record data.
	
	if( gReconfirmRecord_Data )
	{
		err = RecordDataFromArgString( gReconfirmRecord_Data, &rdataPtr, &rdataLen );
		require_noerr_quiet( err, exit );
	}
	
	// Get record class.
	
	if( gReconfirmRecord_Class )
	{
		err = RecordClassFromArgString( gReconfirmRecord_Class, &class );
		require_noerr( err, exit );
	}
	else
	{
		class = kDNSServiceClass_IN;
	}
	
	// Print prologue.
	
	FPrintF( stdout, "Flags:     %#{flags}\n",	flags, kDNSServiceFlagsDescriptors );
	FPrintF( stdout, "Interface: %d (%s)\n",	(int32_t) ifIndex, InterfaceIndexToName( ifIndex, ifName ) );
	FPrintF( stdout, "Name:      %s\n",			gReconfirmRecord_Name );
	FPrintF( stdout, "Type:      %s (%u)\n",	RecordTypeToString( type ), type );
	FPrintF( stdout, "Class:     %s (%u)\n",	( class == kDNSServiceClass_IN ) ? "IN" : "???", class );
	FPrintF( stdout, "Data:      %#H\n",		rdataPtr, (int) rdataLen, INT_MAX );
	FPrintF( stdout, "---\n" );
	
	err = DNSServiceReconfirmRecord( flags, ifIndex, gReconfirmRecord_Name, type, class, (uint16_t) rdataLen, rdataPtr );
	FPrintF( stdout, "Error:     %#m\n", err );
	
exit:
	FreeNullSafe( rdataPtr );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	ResolvePrintPrologue
//===========================================================================================================================

static void	ResolvePrintPrologue( const ResolveContext *inContext )
{
	const int		timeLimitSecs = inContext->timeLimitSecs;
	char			ifName[ kInterfaceNameBufLen ];
	
	InterfaceIndexToName( inContext->ifIndex, ifName );
	
	FPrintF( stdout, "Flags:      %#{flags}\n",		inContext->flags, kDNSServiceFlagsDescriptors );
	FPrintF( stdout, "Interface:  %d (%s)\n",		(int32_t) inContext->ifIndex, ifName );
	FPrintF( stdout, "Name:       %s\n",			inContext->name );
	FPrintF( stdout, "Type:       %s\n",			inContext->type );
	FPrintF( stdout, "Domain:     %s\n",			inContext->domain );
	FPrintF( stdout, "Time limit: " );
	if( timeLimitSecs > 0 )	FPrintF( stdout, "%d second%?c\n", timeLimitSecs, timeLimitSecs != 1, 's' );
	else					FPrintF( stdout, "∞\n" );
	FPrintF( stdout, "Start time: %{du:time}\n",	NULL );
	FPrintF( stdout, "---\n" );
}

//===========================================================================================================================
//	ResolveContextFree
//===========================================================================================================================

static void	ResolveContextFree( ResolveContext *inContext )
{
	DNSServiceForget( &inContext->opRef );
	DNSServiceForget( &inContext->mainRef );
	free( inContext );
}

//===========================================================================================================================
//	ResolveCallback
//===========================================================================================================================

static void DNSSD_API
	ResolveCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		const char *			inHostname,
		uint16_t				inPort,
		uint16_t				inTXTLen,
		const unsigned char *	inTXTPtr,
		void *					inContext )
{
	struct timeval		now;
	char				errorStr[ 64 ];
	
	Unused( inSDRef );
	Unused( inFlags );
	Unused( inContext );
	
	gettimeofday( &now, NULL );
	
	if( inError ) SNPrintF( errorStr, sizeof( errorStr ), " error %#m", inError );
	
	FPrintF( stdout, "%{du:time}: %s can be reached at %s:%u (interface %d)%?s\n",
		&now, inFullName, inHostname, ntohs( inPort ), (int32_t) inInterfaceIndex, inError, errorStr );
	if( inTXTLen == 1 )
	{
		FPrintF( stdout, " TXT record: %#H\n", inTXTPtr, (int) inTXTLen, INT_MAX );
	}
	else
	{
		FPrintF( stdout, " TXT record: %#{txt}\n", inTXTPtr, (size_t) inTXTLen );
	}
}

//===========================================================================================================================
//	GetAddrInfoPOSIXCmd
//===========================================================================================================================

#define AddressFamilyStr( X ) (				\
	( (X) == AF_INET )		? "inet"	:	\
	( (X) == AF_INET6 )		? "inet6"	:	\
	( (X) == AF_UNSPEC )	? "unspec"	:	\
							  "???" )

typedef struct
{
    unsigned int		flag;
    const char *        str;

}   FlagStringPair;

#define CaseFlagStringify( X )		{ (X), # X }

const FlagStringPair		kGAIPOSIXFlagStringPairs[] =
{
#if( defined( AI_UNUSABLE ) )
	CaseFlagStringify( AI_UNUSABLE ),
#endif
	CaseFlagStringify( AI_NUMERICSERV ),
	CaseFlagStringify( AI_V4MAPPED ),
	CaseFlagStringify( AI_ADDRCONFIG ),
#if( defined( AI_V4MAPPED_CFG ) )
	CaseFlagStringify( AI_V4MAPPED_CFG ),
#endif
	CaseFlagStringify( AI_ALL ),
	CaseFlagStringify( AI_NUMERICHOST ),
	CaseFlagStringify( AI_CANONNAME ),
	CaseFlagStringify( AI_PASSIVE ),
	{ 0, NULL }
};

static void	GetAddrInfoPOSIXCmd( void )
{
	OSStatus					err;
	struct addrinfo				hints;
	struct timeval				now;
	const struct addrinfo *		addrInfo;
	struct addrinfo *			addrInfoList = NULL;
	const FlagStringPair *		pair;
	
	memset( &hints, 0, sizeof( hints ) );
	hints.ai_socktype = SOCK_STREAM;
	
	// Set hints address family.
	
	if( !gGAIPOSIX_Family )										hints.ai_family = AF_UNSPEC;
	else if( strcasecmp( gGAIPOSIX_Family, "inet" ) == 0 )		hints.ai_family = AF_INET;
	else if( strcasecmp( gGAIPOSIX_Family, "inet6" ) == 0 )		hints.ai_family = AF_INET6;
	else if( strcasecmp( gGAIPOSIX_Family, "unspec" ) == 0 )	hints.ai_family = AF_UNSPEC;
	else
	{
		FPrintF( stderr, "Invalid address family: %s.\n", gGAIPOSIX_Family );
		err = kParamErr;
		goto exit;
	}
	
	// Set hints flags.
	
	if( gGAIPOSIXFlag_AddrConfig )	hints.ai_flags |= AI_ADDRCONFIG;
	if( gGAIPOSIXFlag_All )			hints.ai_flags |= AI_ALL;
	if( gGAIPOSIXFlag_CanonName )	hints.ai_flags |= AI_CANONNAME;
	if( gGAIPOSIXFlag_NumericHost )	hints.ai_flags |= AI_NUMERICHOST;
	if( gGAIPOSIXFlag_NumericServ )	hints.ai_flags |= AI_NUMERICSERV;
	if( gGAIPOSIXFlag_Passive )		hints.ai_flags |= AI_PASSIVE;
	if( gGAIPOSIXFlag_V4Mapped )	hints.ai_flags |= AI_V4MAPPED;
#if( defined( AI_V4MAPPED_CFG ) )
	if( gGAIPOSIXFlag_V4MappedCFG )	hints.ai_flags |= AI_V4MAPPED_CFG;
#endif
#if( defined( AI_DEFAULT ) )
	if( gGAIPOSIXFlag_Default )		hints.ai_flags |= AI_DEFAULT;
#endif
#if( defined( AI_UNUSABLE ) )
	if( gGAIPOSIXFlag_Unusable )	hints.ai_flags |= AI_UNUSABLE;
#endif
	
#if( MDNSRESPONDER_PROJECT )
	if( gFallbackDNSService )
	{
		err = _SetDefaultFallbackDNSService( gFallbackDNSService );
		require_noerr_quiet( err, exit );
	}
#endif
	// Print prologue.
	
	FPrintF( stdout, "Hostname:       %s\n",	gGAIPOSIX_HostName );
	FPrintF( stdout, "Servname:       %s\n",	gGAIPOSIX_ServName );
	FPrintF( stdout, "Address family: %s\n",	AddressFamilyStr( hints.ai_family ) );
	FPrintF( stdout, "Flags:          0x%X < ",	hints.ai_flags );
	for( pair = kGAIPOSIXFlagStringPairs; pair->str != NULL; ++pair )
	{
		if( ( (unsigned int) hints.ai_flags ) & pair->flag ) FPrintF( stdout, "%s ", pair->str );
	}
	FPrintF( stdout, ">\n" );
	FPrintF( stdout, "Start time:     %{du:time}\n", NULL );
	FPrintF( stdout, "---\n" );
	
	// Call getaddrinfo().
	
	err = getaddrinfo( gGAIPOSIX_HostName, gGAIPOSIX_ServName, &hints, &addrInfoList );
	gettimeofday( &now, NULL );
	if( err )
	{
		FPrintF( stderr, "Error %d: %s.\n", err, gai_strerror( err ) );
	}
	else
	{
		int		addrCount = 0;
		
		for( addrInfo = addrInfoList; addrInfo; addrInfo = addrInfo->ai_next ) { ++addrCount; }
		
		FPrintF( stdout, "Addresses (%d total):\n", addrCount );
		for( addrInfo = addrInfoList; addrInfo; addrInfo = addrInfo->ai_next )
		{
			FPrintF( stdout, "%##a\n", addrInfo->ai_addr );
		}
	}
	FPrintF( stdout, "---\n" );
	FPrintF( stdout, "End time:       %{du:time}\n", &now );
	
exit:
	if( addrInfoList ) freeaddrinfo( addrInfoList );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	ReverseLookupCmd
//===========================================================================================================================

static void	ReverseLookupCmd( void )
{
	OSStatus					err;
	QueryRecordContext *		context			= NULL;
	DNSServiceRef				sdRef;
	dispatch_source_t			signalSource	= NULL;
	uint32_t					ipv4Addr;
	uint8_t						ipv6Addr[ 16 ];
	char						recordName[ kReverseIPv6DomainNameBufLen ];
	int							useMainConnection;
	const char *				endPtr;
	
	// Set up SIGINT handler.
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), Exit, kExitReason_SIGINT, &signalSource );
	require_noerr( err, exit );
	dispatch_resume( signalSource );
	
	// Create context.
	
	context = (QueryRecordContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	// Check command parameters.
	
	if( gReverseLookup_TimeLimitSecs < 0 )
	{
		FPrintF( stderr, "Invalid time limit: %d s.\n", gReverseLookup_TimeLimitSecs );
		err = kParamErr;
		goto exit;
	}
	
	// Create main connection.
	
	if( gConnectionOpt )
	{
		err = CreateConnectionFromArgString( gConnectionOpt, dispatch_get_main_queue(), &context->mainRef, NULL );
		require_noerr_quiet( err, exit );
		useMainConnection = true;
	}
	else
	{
		useMainConnection = false;
	}
	
	// Get flags.
	
	context->flags = GetDNSSDFlagsFromOpts();
	if( useMainConnection ) context->flags |= kDNSServiceFlagsShareConnection;
	
	// Get interface index.
	
	err = InterfaceIndexFromArgString( gInterface, &context->ifIndex );
	require_noerr_quiet( err, exit );
	
	// Create reverse lookup record name.
	
	err = _StringToIPv4Address( gReverseLookup_IPAddr, kStringToIPAddressFlagsNoPort | kStringToIPAddressFlagsNoPrefix,
		&ipv4Addr, NULL, NULL, NULL, &endPtr );
	if( err || ( *endPtr != '\0' ) )
	{
		err = _StringToIPv6Address( gReverseLookup_IPAddr,
			kStringToIPAddressFlagsNoPort | kStringToIPAddressFlagsNoPrefix | kStringToIPAddressFlagsNoScope,
			ipv6Addr, NULL, NULL, NULL, &endPtr );
		if( err || ( *endPtr != '\0' ) )
		{
			FPrintF( stderr, "Invalid IP address: \"%s\".\n", gReverseLookup_IPAddr );
			err = kParamErr;
			goto exit;
		}
		_WriteReverseIPv6DomainNameString( ipv6Addr, recordName );
	}
	else
	{
		_WriteReverseIPv4DomainNameString( ipv4Addr, recordName );
	}
	
	// Set remaining parameters.
	
	context->recordName		= recordName;
	context->recordType		= kDNSServiceType_PTR;
	context->timeLimitSecs	= gReverseLookup_TimeLimitSecs;
	context->oneShotMode	= gReverseLookup_OneShot ? true : false;
	
	// Print prologue.
	
	QueryRecordPrintPrologue( context );
	
	// Start operation.
	
	sdRef = useMainConnection ? context->mainRef : kBadDNSServiceRef;
	err = DNSServiceQueryRecord( &sdRef, context->flags, context->ifIndex, context->recordName, context->recordType,
		kDNSServiceClass_IN, QueryRecordCallback, context );
	require_noerr( err, exit );
	
	context->opRef = sdRef;
	if( !useMainConnection )
	{
		err = DNSServiceSetDispatchQueue( context->opRef, dispatch_get_main_queue() );
		require_noerr( err, exit );
	}
	
	// Set time limit.
	
	if( context->timeLimitSecs > 0 )
	{
		dispatch_after_f( dispatch_time_seconds( context->timeLimitSecs ), dispatch_get_main_queue(),
			kExitReason_TimeLimit, Exit );
	}
	dispatch_main();
	
exit:
	dispatch_source_forget( &signalSource );
	if( context ) QueryRecordContextFree( context );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	PortMappingCmd
//===========================================================================================================================

typedef struct
{
	DNSServiceRef			mainRef;		// Main sdRef for shared connection.
	DNSServiceRef			opRef;			// sdRef for the DNSServiceNATPortMappingCreate operation.
	DNSServiceFlags			flags;			// Flags for DNSServiceNATPortMappingCreate operation.
	uint32_t				ifIndex;		// Interface index argument for DNSServiceNATPortMappingCreate operation.
	DNSServiceProtocol		protocols;		// Protocols argument for DNSServiceNATPortMappingCreate operation.
	uint32_t				ttl;			// TTL argument for DNSServiceNATPortMappingCreate operation.
	uint16_t				internalPort;	// Internal port argument for DNSServiceNATPortMappingCreate operation.
	uint16_t				externalPort;	// External port argument for DNSServiceNATPortMappingCreate operation.
	Boolean					printedHeader;	// True if results header was printed.
	
}	PortMappingContext;

static void	PortMappingPrintPrologue( const PortMappingContext *inContext );
static void	PortMappingContextFree( PortMappingContext *inContext );
static void DNSSD_API
	PortMappingCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		uint32_t			inInterfaceIndex,
		DNSServiceErrorType	inError,
		uint32_t			inExternalIPv4Address,
		DNSServiceProtocol	inProtocol,
		uint16_t			inInternalPort,
		uint16_t			inExternalPort,
		uint32_t			inTTL,
		void *				inContext );

static void	PortMappingCmd( void )
{
	OSStatus					err;
	PortMappingContext *		context			= NULL;
	DNSServiceRef				sdRef;
	dispatch_source_t			signalSource	= NULL;
	int							useMainConnection;
	
	// Set up SIGINT handler.
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), Exit, kExitReason_SIGINT, &signalSource );
	require_noerr( err, exit );
	dispatch_resume( signalSource );
	
	// Create context.
	
	context = (PortMappingContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	// Check command parameters.
	
	if( ( gPortMapping_InternalPort < 0 ) || ( gPortMapping_InternalPort > UINT16_MAX ) )
	{
		FPrintF( stderr, "Internal port number %d is out-of-range.\n", gPortMapping_InternalPort );
		err = kParamErr;
		goto exit;
	}
	
	if( ( gPortMapping_ExternalPort < 0 ) || ( gPortMapping_ExternalPort > UINT16_MAX ) )
	{
		FPrintF( stderr, "External port number %d is out-of-range.\n", gPortMapping_ExternalPort );
		err = kParamErr;
		goto exit;
	}
	
	// Create main connection.
	
	if( gConnectionOpt )
	{
		err = CreateConnectionFromArgString( gConnectionOpt, dispatch_get_main_queue(), &context->mainRef, NULL );
		require_noerr_quiet( err, exit );
		useMainConnection = true;
	}
	else
	{
		useMainConnection = false;
	}
	
	// Get flags.
	
	context->flags = GetDNSSDFlagsFromOpts();
	if( useMainConnection ) context->flags |= kDNSServiceFlagsShareConnection;
	
	// Get interface index.
	
	err = InterfaceIndexFromArgString( gInterface, &context->ifIndex );
	require_noerr_quiet( err, exit );
	
	// Set remaining parameters.
	
	if( gPortMapping_ProtocolTCP ) context->protocols |= kDNSServiceProtocol_TCP;
	if( gPortMapping_ProtocolUDP ) context->protocols |= kDNSServiceProtocol_UDP;
	context->ttl			= (uint32_t) gPortMapping_TTL;
	context->internalPort	= (uint16_t) gPortMapping_InternalPort;
	context->externalPort	= (uint16_t) gPortMapping_ExternalPort;
	
	// Print prologue.
	
	PortMappingPrintPrologue( context );
	
	// Start operation.
	
	sdRef = useMainConnection ? context->mainRef : kBadDNSServiceRef;
	err = DNSServiceNATPortMappingCreate( &sdRef, context->flags, context->ifIndex, context->protocols,
		htons( context->internalPort ), htons( context->externalPort ), context->ttl, PortMappingCallback, context );
	require_noerr( err, exit );
	
	context->opRef = sdRef;
	if( !useMainConnection )
	{
		err = DNSServiceSetDispatchQueue( context->opRef, dispatch_get_main_queue() );
		require_noerr( err, exit );
	}
	
	dispatch_main();
	
exit:
	dispatch_source_forget( &signalSource );
	if( context ) PortMappingContextFree( context );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	PortMappingPrintPrologue
//===========================================================================================================================

static void	PortMappingPrintPrologue( const PortMappingContext *inContext )
{
	char		ifName[ kInterfaceNameBufLen ];
	
	InterfaceIndexToName( inContext->ifIndex, ifName );
	
	FPrintF( stdout, "Flags:         %#{flags}\n",		inContext->flags, kDNSServiceFlagsDescriptors );
	FPrintF( stdout, "Interface:     %d (%s)\n",		(int32_t) inContext->ifIndex, ifName );
	FPrintF( stdout, "Protocols:     %#{flags}\n",		inContext->protocols, kDNSServiceProtocolDescriptors );
	FPrintF( stdout, "Internal Port: %u\n",				inContext->internalPort );
	FPrintF( stdout, "External Port: %u\n",				inContext->externalPort );
	FPrintF( stdout, "TTL:           %u%?s\n",			inContext->ttl, !inContext->ttl,
		" (system will use a default value.)" );
	FPrintF( stdout, "Start time:    %{du:time}\n",	NULL );
	FPrintF( stdout, "---\n" );
	
}

//===========================================================================================================================
//	PortMappingContextFree
//===========================================================================================================================

static void	PortMappingContextFree( PortMappingContext *inContext )
{
	DNSServiceForget( &inContext->opRef );
	DNSServiceForget( &inContext->mainRef );
	free( inContext );
}

//===========================================================================================================================
//	PortMappingCallback
//===========================================================================================================================

static void DNSSD_API
	PortMappingCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		uint32_t			inInterfaceIndex,
		DNSServiceErrorType	inError,
		uint32_t			inExternalIPv4Address,
		DNSServiceProtocol	inProtocol,
		uint16_t			inInternalPort,
		uint16_t			inExternalPort,
		uint32_t			inTTL,
		void *				inContext )
{
	PortMappingContext * const		context = (PortMappingContext *) inContext;
	struct timeval					now;
	char							errorStr[ 128 ];
	
	Unused( inSDRef );
	Unused( inFlags );
	
	gettimeofday( &now, NULL );
	
	if( inError ) SNPrintF( errorStr, sizeof( errorStr ), " (error: %#m)", inError );
	if( !context->printedHeader )
	{
		FPrintF( stdout, "%-26s  IF %7s %15s %7s %6s Protocol\n", "Timestamp", "IntPort", "ExtAddr", "ExtPort", "TTL" );
		context->printedHeader = true;
	}
	FPrintF( stdout, "%{du:time}  %2u %7u %15.4a %7u %6u %#{flags}%?s\n",
		&now, inInterfaceIndex, ntohs( inInternalPort), &inExternalIPv4Address, ntohs( inExternalPort ), inTTL,
		inProtocol, kDNSServiceProtocolDescriptors, inError, errorStr );
}

#if( TARGET_OS_DARWIN )
//===========================================================================================================================
//	RegisterKACmd
//===========================================================================================================================

typedef struct
{
	dispatch_queue_t			queue;			// Serial queue for command's events.
	dispatch_semaphore_t		doneSem;		// Semaphore to signal when underlying command operation is done.
	sockaddr_ip					local;			// Connection's local IP address and port.
	sockaddr_ip					remote;			// Connection's remote IP address and port.
	DNSServiceFlags				flags;			// Flags to pass to DNSServiceSleepKeepalive_sockaddr().
	unsigned int				timeout;		// Timeout to pass to DNSServiceSleepKeepalive_sockaddr().
	DNSServiceRef				keepalive;		// DNSServiceSleepKeepalive_sockaddr operation.
	dispatch_source_t			sourceSigInt;	// Dispatch source for SIGINT.
	dispatch_source_t			sourceSigTerm;	// Dispatch source for SIGTERM.
	OSStatus					error;			// Command's error.
	
}	RegisterKACmdContext;

static void	_RegisterKACmdFree( RegisterKACmdContext *inCmd );
static void	_RegisterKACmdStart( void *inContext );

static void	RegisterKACmd( void )
{
	OSStatus					err;
	RegisterKACmdContext *		cmd = NULL;
	
	cmd = (RegisterKACmdContext *) calloc( 1, sizeof( *cmd ) );
	require_action( cmd, exit, err = kNoMemoryErr );
	
	err = SockAddrFromArgString( gRegisterKA_LocalAddress, "local IP address", &cmd->local );
	require_noerr_quiet( err, exit );
	
	err = SockAddrFromArgString( gRegisterKA_RemoteAddress, "remote IP address", &cmd->remote );
	require_noerr_quiet( err, exit );
	
	err = CheckIntegerArgument( gRegisterKA_Timeout, "timeout", 0, INT_MAX );
	require_noerr_quiet( err, exit );
	
	cmd->flags		= GetDNSSDFlagsFromOpts();
	cmd->timeout	= (unsigned int) gRegisterKA_Timeout;
	
	// Start command.
	
	cmd->queue = dispatch_queue_create( "com.apple.dnssdutil.registerka-command", DISPATCH_QUEUE_SERIAL );
	require_action( cmd->queue, exit, err = kNoResourcesErr );
	
	cmd->doneSem = dispatch_semaphore_create( 0 );
	require_action( cmd->doneSem, exit, err = kNoResourcesErr );
	
	dispatch_async_f( cmd->queue, cmd, _RegisterKACmdStart );
    dispatch_semaphore_wait( cmd->doneSem, DISPATCH_TIME_FOREVER );
	if( cmd->error ) err = cmd->error;
	
	FPrintF( stdout, "---\n" );
	FPrintF( stdout, "End time:   %{du:time}\n", NULL );
	
exit:
	if( cmd ) _RegisterKACmdFree( cmd );
	gExitCode = err ? 1 : 0;
}

//===========================================================================================================================

static void	_RegisterKACmdFree( RegisterKACmdContext *inCmd )
{
	check( !inCmd->keepalive );
	check( !inCmd->sourceSigInt );
	check( !inCmd->sourceSigTerm );
	dispatch_forget( &inCmd->queue );
	dispatch_forget( &inCmd->doneSem );
	free( inCmd );
}

//===========================================================================================================================

static void				_RegisterKACmdStop( RegisterKACmdContext *inCmd, OSStatus inError );
static void				_RegisterKACmdSignalHandler( void *inContext );
static void DNSSD_API	_RegisterKACmdKeepaliveCallback( DNSServiceRef inSDRef, DNSServiceErrorType inError, void *inCtx );

static void	_RegisterKACmdStart( void *inContext )
{
	OSStatus							err;
	RegisterKACmdContext * const		cmd = (RegisterKACmdContext *) inContext;
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, cmd->queue, _RegisterKACmdSignalHandler, cmd, &cmd->sourceSigInt );
	require_noerr( err, exit );
	dispatch_resume( cmd->sourceSigInt );
	
	signal( SIGTERM, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGTERM, cmd->queue, _RegisterKACmdSignalHandler, cmd, &cmd->sourceSigTerm );
	require_noerr( err, exit );
	dispatch_resume( cmd->sourceSigTerm );
	
	FPrintF( stdout, "Flags:      %#{flags}\n",		cmd->flags, kDNSServiceFlagsDescriptors );
	FPrintF( stdout, "Local:      %##a\n",			&cmd->local.sa );
	FPrintF( stdout, "Remote:     %##a\n",			&cmd->remote.sa );
	FPrintF( stdout, "Timeout:    %u\n",			cmd->timeout );
	FPrintF( stdout, "Start time: %{du:time}\n",	NULL );
	FPrintF( stdout, "---\n" );
	
	err = DNSServiceSleepKeepalive_sockaddr( &cmd->keepalive, cmd->flags, &cmd->local.sa, &cmd->remote.sa, cmd->timeout,
		_RegisterKACmdKeepaliveCallback, cmd );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( cmd->keepalive, cmd->queue );
	require_noerr( err, exit );
	
exit:
	if( err ) _RegisterKACmdStop( cmd, err );
}

//===========================================================================================================================

static void	_RegisterKACmdStop( RegisterKACmdContext *inCmd, OSStatus inError )
{
	if( !inCmd->error ) inCmd->error = inError;
	DNSServiceForget( &inCmd->keepalive );
	dispatch_source_forget( &inCmd->sourceSigInt );
	dispatch_source_forget( &inCmd->sourceSigTerm );
	dispatch_semaphore_signal( inCmd->doneSem );
}

//===========================================================================================================================

static void	_RegisterKACmdSignalHandler( void *inContext )
{
	RegisterKACmdContext * const		cmd = (RegisterKACmdContext *) inContext;
	
	_RegisterKACmdStop( cmd, kNoErr );
}

//===========================================================================================================================

static void DNSSD_API	_RegisterKACmdKeepaliveCallback( DNSServiceRef inSDRef, DNSServiceErrorType inError, void *inCtx )
{
	RegisterKACmdContext * const		cmd = (RegisterKACmdContext *) inCtx;
	
	Unused( inSDRef );
	
	FPrintF( stdout, "%{du:time} Record registration result: %#m\n", NULL, inError );
	if( !cmd->error ) cmd->error = inError;
}
#endif	// TARGET_OS_DARWIN

//===========================================================================================================================
//	BrowseAllCmd
//===========================================================================================================================

typedef struct BrowseAllConnection		BrowseAllConnection;

typedef struct
{
	ServiceBrowserRef			browser;				// Service browser.
	ServiceBrowserResults *		results;				// Results from the service browser.
	BrowseAllConnection *		connectionList;			// List of connections.
	dispatch_source_t			connectionTimer;		// Timer for connection timeout.
	int							connectionPendingCount;	// Number of pending connections.
	int							connectionTimeoutSecs;	// Timeout value for connections in seconds.
	Boolean						validateResults;		// Validate results.
	
}	BrowseAllContext;

struct BrowseAllConnection
{
	BrowseAllConnection *		next;				// Next connection object in list.
	sockaddr_ip					sip;				// IPv4 or IPv6 address to connect to.
	uint16_t					port;				// TCP port to connect to.
	AsyncConnectionRef			asyncCnx;			// AsyncConnection object to handle the actual connection.
	OSStatus					status;				// Status of connection. NoErr means connection succeeded.
	CFTimeInterval				connectTimeSecs;	// Time it took to connect in seconds.
	int32_t						refCount;			// This object's reference count.
	BrowseAllContext *			context;			// Back pointer to parent context.
};

static void	_BrowseAllContextFree( BrowseAllContext *inContext );
static void	_BrowseAllServiceBrowserCallback( ServiceBrowserResults *inResults, OSStatus inError, void *inContext );
static OSStatus
	_BrowseAllConnectionCreate(
		const struct sockaddr *	inSockAddr,
		uint16_t				inPort,
		BrowseAllContext *		inContext,
		BrowseAllConnection **	outConnection );
static void _BrowseAllConnectionRetain( BrowseAllConnection *inConnection );
static void	_BrowseAllConnectionRelease( BrowseAllConnection *inConnection );
static void	_BrowseAllConnectionProgress( int inPhase, const void *inDetails, void *inArg );
static void	_BrowseAllConnectionHandler( SocketRef inSock, OSStatus inError, void *inArg );
static void	_BrowseAllExit( void *inContext );

static Boolean	_IsServiceTypeTCP( const char *inServiceType );

static void	BrowseAllCmd( void )
{
	OSStatus				err;
	BrowseAllContext *		context = NULL;
	size_t					i;
	uint32_t				ifIndex;
	char					ifName[ kInterfaceNameBufLen ];
	
	// Check parameters.
	
	if( gBrowseAll_BrowseTimeSecs <= 0 )
	{
		FPrintF( stdout, "Invalid browse time: %d seconds.\n", gBrowseAll_BrowseTimeSecs );
		err = kParamErr;
		goto exit;
	}
	
	context = (BrowseAllContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	context->connectionTimeoutSecs	= gBrowseAll_ConnectTimeout;
	context->validateResults		= gBrowseAll_ValidateResults ? true : false;
#if( TARGET_OS_POSIX )
	// Increase the open file descriptor limit for connection sockets.
	
	if( context->connectionTimeoutSecs > 0 )
	{
		struct rlimit		fdLimits;
		
		err = getrlimit( RLIMIT_NOFILE, &fdLimits );
		err = map_global_noerr_errno( err );
		require_noerr( err, exit );
		
		if( fdLimits.rlim_cur < 4096 )
		{
			fdLimits.rlim_cur = 4096;
			err = setrlimit( RLIMIT_NOFILE, &fdLimits );
			err = map_global_noerr_errno( err );
			require_noerr( err, exit );
		}
	}
#endif
	
	// Get interface index.
	
	err = InterfaceIndexFromArgString( gInterface, &ifIndex );
	require_noerr_quiet( err, exit );
	
#if( TARGET_OS_IOS )
	// Check for potential issues.
	
	if( context->validateResults && os_feature_enabled( mDNSResponder, revoke_media_sessions ) )
	{
		FPrintF( stderr,
			"%s"
			"Warning: --validate may not work as expected when media revocation is enabled.\n"
			"         Use 'ffctl mDNSResponder/revoke_media_sessions=off' to disable.\n"
			"%s",
			_StdErrIsTTY() ? kANSIRed : "", _StdErrIsTTY() ? kANSINormal : "" );
	}
#endif
	// Print prologue.
	
	FPrintF( stdout, "Interface:        %d (%s)\n", (int32_t) ifIndex, InterfaceIndexToName( ifIndex, ifName ) );
	FPrintF( stdout, "Service types:    ");
	if( gBrowseAll_ServiceTypesCount > 0 )
	{
		FPrintF( stdout, "%s", gBrowseAll_ServiceTypes[ 0 ] );
		for( i = 1; i < gBrowseAll_ServiceTypesCount; ++i )
		{
			FPrintF( stdout, ", %s", gBrowseAll_ServiceTypes[ i ] );
		}
		FPrintF( stdout, "\n" );
	}
	else
	{
		FPrintF( stdout, "all services\n" );
	}
	FPrintF( stdout, "Domain:           %s\n", gBrowseAll_Domain ? gBrowseAll_Domain : "default domains" );
	FPrintF( stdout, "Browse time:      %d second%?c\n", gBrowseAll_BrowseTimeSecs, gBrowseAll_BrowseTimeSecs != 1, 's' );
	FPrintF( stdout, "Connect timeout:  %d second%?c\n",
		context->connectionTimeoutSecs, context->connectionTimeoutSecs != 1, 's' );
	FPrintF( stdout, "IncludeAWDL:      %s\n", YesNoStr( gDNSSDFlag_IncludeAWDL ) );
	FPrintF( stdout, "New GAI:          %s\n", YesNoStr( gBrowseAll_UseNewGAI ) );
	FPrintF( stdout, "Validate results: %s\n", YesNoStr( context->validateResults ) );
	FPrintF( stdout, "Start time:       %{du:time}\n", NULL );
	FPrintF( stdout, "---\n" );
	err = ServiceBrowserCreate( dispatch_get_main_queue(), ifIndex, gBrowseAll_Domain,
		(unsigned int) gBrowseAll_BrowseTimeSecs, gDNSSDFlag_IncludeAWDL ? true : false,
		&context->browser );
	require_noerr( err, exit );
	
	ServiceBrowserSetUseNewGAI( context->browser, gBrowseAll_UseNewGAI ? true : false );
	ServiceBrowserSetValidateResults( context->browser, context->validateResults );
	
	for( i = 0; i < gBrowseAll_ServiceTypesCount; ++i )
	{
		err = ServiceBrowserAddServiceType( context->browser, gBrowseAll_ServiceTypes[ i ] );
		require_noerr( err, exit );
	}
	ServiceBrowserSetCallback( context->browser, _BrowseAllServiceBrowserCallback, context );
	ServiceBrowserStart( context->browser );
	dispatch_main();
	
exit:
	if( context ) _BrowseAllContextFree( context );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	_BrowseAllContextFree
//===========================================================================================================================

static void	_BrowseAllContextFree( BrowseAllContext *inContext )
{
	check( !inContext->browser );
	check( !inContext->connectionTimer );
	check( !inContext->connectionList );
	ForgetServiceBrowserResults( &inContext->results );
	free( inContext );
}

//===========================================================================================================================
//	_BrowseAllServiceBrowserCallback
//===========================================================================================================================

#define kDiscardProtocolPort		9

static void	_BrowseAllServiceBrowserCallback( ServiceBrowserResults *inResults, OSStatus inError, void *inContext )
{
	OSStatus						err;
	BrowseAllContext * const		context = (BrowseAllContext *) inContext;
	SBRDomain *						domain;
	SBRServiceType *				type;
	SBRServiceInstance *			instance;
	SBRIPAddress *					ipaddr;
	
	Unused( inError );
	
	require_action( inResults, exit, err = kUnexpectedErr );
	
	check( !context->results );
	context->results = inResults;
	ServiceBrowserResultsRetain( context->results );
	
	check( context->connectionPendingCount == 0 );
	if( context->connectionTimeoutSecs > 0 )
	{
		BrowseAllConnection *			connection;
		BrowseAllConnection **			connectionPtr = &context->connectionList;
		char							destination[ kSockAddrStringMaxSize ];
		
		for( domain = context->results->domainList; domain; domain = domain->next )
		{
			for( type = domain->typeList; type; type = type->next )
			{
				if( !_IsServiceTypeTCP( type->name ) ) continue;
				for( instance = type->instanceList; instance; instance = instance->next )
				{
					if( instance->port == kDiscardProtocolPort ) continue;
					for( ipaddr = instance->ipaddrList; ipaddr; ipaddr = ipaddr->next )
					{
						err = _BrowseAllConnectionCreate( &ipaddr->sip.sa, instance->port, context, &connection );
						require_noerr( err, exit );
						
						*connectionPtr = connection;
						 connectionPtr = &connection->next;
						
						err = SockAddrToString( &ipaddr->sip, kSockAddrStringFlagsNoPort, destination );
						check_noerr( err );
						if( !err )
						{
							err = AsyncConnection_Connect( &connection->asyncCnx, destination, -instance->port,
								kAsyncConnectionFlag_P2P, kAsyncConnectionNoTimeout,
								kSocketBufferSize_DontSet, kSocketBufferSize_DontSet,
								_BrowseAllConnectionProgress, connection, _BrowseAllConnectionHandler, connection,
								dispatch_get_main_queue() );
							check_noerr( err );
						}
						if( !err )
						{
							_BrowseAllConnectionRetain( connection );
							connection->status = kInProgressErr;
							++context->connectionPendingCount;
						}
						else
						{
							connection->status = err;
						}
					}
				}
			}
		}
	}
	
	if( context->connectionPendingCount > 0 )
	{
		check( !context->connectionTimer );
		err = DispatchTimerCreate( dispatch_time_seconds( context->connectionTimeoutSecs ), DISPATCH_TIME_FOREVER,
			100 * kNanosecondsPerMillisecond, NULL, _BrowseAllExit, NULL, context, &context->connectionTimer );
		require_noerr( err, exit );
		dispatch_resume( context->connectionTimer );
	}
	else
	{
		dispatch_async_f( dispatch_get_main_queue(), context, _BrowseAllExit );
	}
	err = kNoErr;
	
exit:
	ForgetCF( &context->browser );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	_BrowseAllConnectionCreate
//===========================================================================================================================

static OSStatus
	_BrowseAllConnectionCreate(
		const struct sockaddr *	inSockAddr,
		uint16_t				inPort,
		BrowseAllContext *		inContext,
		BrowseAllConnection **	outConnection )
{
	OSStatus					err;
	BrowseAllConnection *		obj;
	
	obj = (BrowseAllConnection *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->refCount	= 1;
	SockAddrCopy( inSockAddr, &obj->sip );
	obj->port		= inPort;
	obj->context	= inContext;
	
	*outConnection = obj;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_BrowseAllConnectionRetain
//===========================================================================================================================

static void _BrowseAllConnectionRetain( BrowseAllConnection *inConnection )
{
	++inConnection->refCount;
}

//===========================================================================================================================
//	_BrowseAllConnectionRelease
//===========================================================================================================================

static void	_BrowseAllConnectionRelease( BrowseAllConnection *inConnection )
{
	if( --inConnection->refCount == 0 ) free( inConnection );
}

//===========================================================================================================================
//	_BrowseAllConnectionProgress
//===========================================================================================================================

static void	_BrowseAllConnectionProgress( int inPhase, const void *inDetails, void *inArg )
{
	BrowseAllConnection * const		connection = (BrowseAllConnection *) inArg;
	
	if( inPhase == kAsyncConnectionPhase_Connected )
	{
		const AsyncConnectedInfo * const		info = (AsyncConnectedInfo *) inDetails;
		
		connection->connectTimeSecs = info->connectSecs;
	}
}

//===========================================================================================================================
//	_BrowseAllConnectionHandler
//===========================================================================================================================

static void	_BrowseAllConnectionHandler( SocketRef inSock, OSStatus inError, void *inArg )
{
	BrowseAllConnection * const		connection	= (BrowseAllConnection *) inArg;
	BrowseAllContext * const		context		= connection->context;
	
	connection->status = inError;
	ForgetSocket( &inSock );
	if( context )
	{
		check( context->connectionPendingCount > 0 );
		if( ( --context->connectionPendingCount == 0 ) && context->connectionTimer )
		{
			dispatch_source_forget( &context->connectionTimer );
			dispatch_async_f( dispatch_get_main_queue(), context, _BrowseAllExit );
		}
	}
	_BrowseAllConnectionRelease( connection );
}

//===========================================================================================================================
//	_BrowseAllExit
//===========================================================================================================================

#define Indent( X )		( (X) * 4 ), ""

static void	_BrowseAllExit( void *inContext )
{
	BrowseAllContext * const		context					= (BrowseAllContext *) inContext;
	SBRDomain *						domain;
	SBRServiceType *				type;
	SBRServiceInstance *			instance;
	SBRIPAddress *					ipaddr;
	char							textBuf[ 512 ];
	size_t							serviceInstanceCount	= 0;
	size_t							serviceResolveCount		= 0;
	uint64_t						totalDiscoveryTimeUs	= 0;
	uint64_t						totalResolveTimeUs		= 0;
	
	dispatch_source_forget( &context->connectionTimer );
	
	for( domain = context->results->domainList; domain; domain = domain->next )
	{
		FPrintF( stdout, "%s\n\n", domain->name );
		
		for( type = domain->typeList; type; type = type->next )
		{
			const char *		description;
			const Boolean		serviceTypeIsTCP = _IsServiceTypeTCP( type->name );
			
			description = ServiceTypeDescription( type->name );
			if( description )	FPrintF( stdout, "%*s" "%s (%s)\n\n",	Indent( 1 ), description, type->name );
			else				FPrintF( stdout, "%*s" "%s\n\n",		Indent( 1 ), type->name );
			
			for( instance = type->instanceList; instance; instance = instance->next )
			{
				char *				dst = textBuf;
				char * const		lim = &textBuf[ countof( textBuf ) ];
				char				ifname[ IF_NAMESIZE + 1 ];
				
				++serviceInstanceCount;
				totalDiscoveryTimeUs += instance->discoverTimeUs;
				SNPrintF_Add( &dst, lim, "%s via ", instance->name );
				if( instance->ifIndex == 0 )
				{
					SNPrintF_Add( &dst, lim, "the Internet" );
				}
				else if( if_indextoname( instance->ifIndex, ifname ) )
				{
					NetTransportType		netType;
					
					SocketGetInterfaceInfo( kInvalidSocketRef, ifname, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &netType );
					SNPrintF_Add( &dst, lim, "%s (%s)",
						( netType == kNetTransportType_Ethernet ) ? "Ethernet" : NetTransportTypeToString( netType ),
						ifname );
				}
				else
				{
					SNPrintF_Add( &dst, lim, "interface index %u", instance->ifIndex );
				}
				FPrintF( stdout, "%*s" "%-55s %4llu.%03llu ms\n\n",
					Indent( 2 ), textBuf, instance->discoverTimeUs / 1000, instance->discoverTimeUs % 1000 );
				
				if( instance->hostname )
				{
					++serviceResolveCount;
					totalResolveTimeUs += instance->resolveTimeUs;
					SNPrintF( textBuf, sizeof( textBuf ), "%s:%u", instance->hostname, instance->port );
					FPrintF( stdout, "%*s" "%-51s %4llu.%03llu ms\n",
						Indent( 3 ), textBuf, instance->resolveTimeUs / 1000, instance->resolveTimeUs % 1000 );
				}
				else
				{
					FPrintF( stdout, "%*s" "%s:%u\n", Indent( 3 ), instance->hostname, instance->port );
				}
				
				for( ipaddr = instance->ipaddrList; ipaddr; ipaddr = ipaddr->next )
				{
					BrowseAllConnection *		conn;
					BrowseAllConnection **		connPtr;
					const char * const			colorEnd = _StdOutIsTTY() ? kANSINormal : "";
					
					FPrintF( stdout, "%*s" "%-##47a %4llu.%03llu ms",
						Indent( 4 ), &ipaddr->sip.sa, ipaddr->resolveTimeUs / 1000, ipaddr->resolveTimeUs % 1000 );
					
					conn = NULL;
					if( serviceTypeIsTCP && ( instance->port != kDiscardProtocolPort ) )
					{
						for( connPtr = &context->connectionList; ( conn = *connPtr ) != NULL; connPtr = &conn->next )
						{
							if( ( conn->port == instance->port ) &&
								( SockAddrCompareAddr( &conn->sip, &ipaddr->sip ) == 0 ) ) break;
						}
						if( conn )
						{
							if( conn->status == kInProgressErr ) conn->status = kTimeoutErr;
							*connPtr = conn->next;
							conn->context = NULL;
							AsyncConnection_Forget( &conn->asyncCnx );
						}
					}
					FPrintF( stdout, " (");
					if( context->validateResults ) _PrintValidatedToStdOut( "", ipaddr->validated , ", " );
					if( conn )
					{
						if( conn->status == kNoErr )
						{
							FPrintF( stdout, "%sconnected%s in %.3f ms",
								_StdOutIsTTY() ? kANSIGreen : "", colorEnd, conn->connectTimeSecs * 1000 );
						}
						else
						{
							FPrintF( stdout, "%scould not connect%s: %m",
								_StdOutIsTTY() ? kANSIRed : "", colorEnd, conn->status );
						}
						_BrowseAllConnectionRelease( conn );
					}
					else
					{
						FPrintF( stdout, "no connection attempted" );
					}
					FPrintF( stdout, ")\n" );
				}
				
				FPrintF( stdout, "\n" );
				if( instance->txtLen == 0 ) continue;
				
				FPrintF( stdout, "%*s" "TXT record (%zu byte%?c):\n",
					Indent( 3 ), instance->txtLen, instance->txtLen != 1, 's' );
				if( instance->txtLen > 1 )
				{
					FPrintF( stdout, "%3{txt}", instance->txtPtr, instance->txtLen );
				}
				else
				{
					FPrintF( stdout, "%*s" "%#H\n", Indent( 3 ), instance->txtPtr, (int) instance->txtLen, INT_MAX );
				}
				FPrintF( stdout, "\n" );
			}
			FPrintF( stdout, "\n" );
		}
	}
	
	_BrowseAllContextFree( context );
	
	// Additional Stats
	
	FPrintF( stdout, "---\n" );
	FPrintF( stdout, "Service instance count: %zu\n", serviceInstanceCount );
	FPrintF( stdout, "Service resolve count:  %zu\n", serviceResolveCount );
	if( serviceInstanceCount > 0 )
	{
		const uint64_t averageDiscoveryTimeUs = totalDiscoveryTimeUs / serviceInstanceCount;
		FPrintF( stdout,
			"Average discovery time: %llu.%03llu ms\n", averageDiscoveryTimeUs / 1000, averageDiscoveryTimeUs % 1000 );
	}
	if( serviceResolveCount > 0 )
	{
		const uint64_t averageResolveTimeUs = totalResolveTimeUs / serviceResolveCount;
		FPrintF( stdout,
			"Average resolve time:   %llu.%03llu ms\n", averageResolveTimeUs / 1000, averageResolveTimeUs % 1000 );
	}
	Exit( NULL );
}

//===========================================================================================================================
//	_IsServiceTypeTCP
//===========================================================================================================================

static Boolean	_IsServiceTypeTCP( const char *inServiceType )
{
	OSStatus			err;
	const uint8_t *		secondLabel;
	uint8_t				name[ kDomainNameLengthMax ];
	
	err = DomainNameFromString( name, inServiceType, NULL );
	if( !err )
	{
		secondLabel = DomainNameGetNextLabel( name );
		if( secondLabel && DomainNameEqual( secondLabel, (const uint8_t *) "\x04" "_tcp" ) ) return( true );
	}
	return( false );
}

//===========================================================================================================================
//	GetNameInfoCmd
//===========================================================================================================================

const FlagStringPair		kGetNameInfoFlagStringPairs[] =
{
	CaseFlagStringify( NI_NUMERICSCOPE ),
	CaseFlagStringify( NI_DGRAM ),
	CaseFlagStringify( NI_NUMERICSERV ),
	CaseFlagStringify( NI_NAMEREQD ),
	CaseFlagStringify( NI_NUMERICHOST ),
	CaseFlagStringify( NI_NOFQDN ),
	{ 0, NULL }
};

static void	GetNameInfoCmd( void )
{
	OSStatus					err;
	sockaddr_ip					sip;
	size_t						sockAddrLen;
	unsigned int				flags;
	const FlagStringPair *		pair;
	struct timeval				now;
	char						host[ NI_MAXHOST ];
	char						serv[ NI_MAXSERV ];
	
	err = StringToSockAddr( gGetNameInfo_IPAddress, &sip, sizeof( sip ), &sockAddrLen );
	check_noerr( err );
	if( err )
	{
		FPrintF( stderr, "Failed to convert \"%s\" to a sockaddr.\n", gGetNameInfo_IPAddress );
		goto exit;
	}
	
	flags = 0;
	if( gGetNameInfoFlag_DGram )		flags |= NI_DGRAM;
	if( gGetNameInfoFlag_NameReqd )		flags |= NI_NAMEREQD;
	if( gGetNameInfoFlag_NoFQDN )		flags |= NI_NOFQDN;
	if( gGetNameInfoFlag_NumericHost )	flags |= NI_NUMERICHOST;
	if( gGetNameInfoFlag_NumericScope )	flags |= NI_NUMERICSCOPE;
	if( gGetNameInfoFlag_NumericServ )	flags |= NI_NUMERICSERV;
	
	// Print prologue.
	
	FPrintF( stdout, "SockAddr:   %##a\n",	&sip.sa );
	FPrintF( stdout, "Flags:      0x%X < ",	flags );
	for( pair = kGetNameInfoFlagStringPairs; pair->str != NULL; ++pair )
	{
		if( flags & pair->flag ) FPrintF( stdout, "%s ", pair->str );
	}
	FPrintF( stdout, ">\n" );
	FPrintF( stdout, "Start time: %{du:time}\n", NULL );
	FPrintF( stdout, "---\n" );
	
	// Call getnameinfo().
	
	err = getnameinfo( &sip.sa, (socklen_t) sockAddrLen, host, (socklen_t) sizeof( host ), serv, (socklen_t) sizeof( serv ),
		(int) flags );
	gettimeofday( &now, NULL );
	if( err )
	{
		FPrintF( stderr, "Error %d: %s.\n", err, gai_strerror( err ) );
	}
	else
	{
		FPrintF( stdout, "host: %s\n", host );
		FPrintF( stdout, "serv: %s\n", serv );
	}
	FPrintF( stdout, "---\n" );
	FPrintF( stdout, "End time:   %{du:time}\n", &now );
	
exit:
	gExitCode = err ? 1 : 0;
}

//===========================================================================================================================
//	GetAddrInfoStressCmd
//===========================================================================================================================

typedef struct
{
	DNSServiceRef			mainRef;
	DNSServiceRef			sdRef;
	DNSServiceFlags			flags;
	unsigned int			interfaceIndex;
	unsigned int			connectionNumber;
	unsigned int			requestCount;
	unsigned int			requestCountMax;
	unsigned int			requestCountLimit;
	unsigned int			durationMinMs;
	unsigned int			durationMaxMs;
	
}	GAIStressContext;

static void	GetAddrInfoStressEvent( void *inContext );
static void	DNSSD_API
	GetAddrInfoStressCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext );

static void	GetAddrInfoStressCmd( void )
{
	OSStatus				err;
	GAIStressContext *		context = NULL;
	int						i;
	DNSServiceFlags			flags;
	uint32_t				ifIndex;
	char					ifName[ kInterfaceNameBufLen ];
	
	if( gGAIStress_TestDurationSecs < 0 )
	{
		FPrintF( stdout, "Invalid test duration: %d s.\n", gGAIStress_TestDurationSecs );
		err = kParamErr;
		goto exit;
	}
	if( gGAIStress_ConnectionCount <= 0 )
	{
		FPrintF( stdout, "Invalid simultaneous connection count: %d.\n", gGAIStress_ConnectionCount );
		err = kParamErr;
		goto exit;
	}
	if( gGAIStress_DurationMinMs <= 0 )
	{
		FPrintF( stdout, "Invalid minimum DNSServiceGetAddrInfo() duration: %d ms.\n", gGAIStress_DurationMinMs );
		err = kParamErr;
		goto exit;
	}
	if( gGAIStress_DurationMaxMs <= 0 )
	{
		FPrintF( stdout, "Invalid maximum DNSServiceGetAddrInfo() duration: %d ms.\n", gGAIStress_DurationMaxMs );
		err = kParamErr;
		goto exit;
	}
	if( gGAIStress_DurationMinMs > gGAIStress_DurationMaxMs )
	{
		FPrintF( stdout, "Invalid minimum and maximum DNSServiceGetAddrInfo() durations: %d ms and %d ms.\n",
			gGAIStress_DurationMinMs, gGAIStress_DurationMaxMs );
		err = kParamErr;
		goto exit;
	}
	if( gGAIStress_RequestCountMax <= 0 )
	{
		FPrintF( stdout, "Invalid maximum request count: %d.\n", gGAIStress_RequestCountMax );
		err = kParamErr;
		goto exit;
	}
	
	// Set flags.
	
	flags = GetDNSSDFlagsFromOpts();
	
	// Set interface index.
	
	err = InterfaceIndexFromArgString( gInterface, &ifIndex );
	require_noerr_quiet( err, exit );
	
	for( i = 0; i < gGAIStress_ConnectionCount; ++i )
	{
		context = (GAIStressContext *) calloc( 1, sizeof( *context ) );
		require_action( context, exit, err = kNoMemoryErr );
		
		context->flags				= flags;
		context->interfaceIndex		= ifIndex;
		context->connectionNumber	= (unsigned int)( i + 1 );
		context->requestCountMax	= (unsigned int) gGAIStress_RequestCountMax;
		context->durationMinMs		= (unsigned int) gGAIStress_DurationMinMs;
		context->durationMaxMs		= (unsigned int) gGAIStress_DurationMaxMs;
		
		dispatch_async_f( dispatch_get_main_queue(), context, GetAddrInfoStressEvent );
		context = NULL;
	}
	
	if( gGAIStress_TestDurationSecs > 0 )
	{
		dispatch_after_f( dispatch_time_seconds( gGAIStress_TestDurationSecs ), dispatch_get_main_queue(), NULL, Exit );
	}
	
	FPrintF( stdout, "Flags:                %#{flags}\n",	flags, kDNSServiceFlagsDescriptors );
	FPrintF( stdout, "Interface:            %d (%s)\n",		(int32_t) ifIndex, InterfaceIndexToName( ifIndex, ifName ) );
	FPrintF( stdout, "Test duration:        " );
	if( gGAIStress_TestDurationSecs == 0 )
	{
		FPrintF( stdout, "∞\n" );
	}
	else
	{
		FPrintF( stdout, "%d s\n", gGAIStress_TestDurationSecs );
	}
	FPrintF( stdout, "Connection count:     %d\n",			gGAIStress_ConnectionCount );
	FPrintF( stdout, "Request duration min: %d ms\n",		gGAIStress_DurationMinMs );
	FPrintF( stdout, "Request duration max: %d ms\n",		gGAIStress_DurationMaxMs );
	FPrintF( stdout, "Request count max:    %d\n",			gGAIStress_RequestCountMax );
	FPrintF( stdout, "Start time:           %{du:time}\n",	NULL);
	FPrintF( stdout, "---\n" );
	
	dispatch_main();
	
exit:
	FreeNullSafe( context );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	GetAddrInfoStressEvent
//===========================================================================================================================

#define kStressRandStrLen		5

#define kLowercaseAlphaCharSet		"abcdefghijklmnopqrstuvwxyz"

static void	GetAddrInfoStressEvent( void *inContext )
{
	GAIStressContext * const		context = (GAIStressContext *) inContext;
	OSStatus						err;
	DNSServiceRef					sdRef;
	unsigned int					nextMs;
	char							randomStr[ kStressRandStrLen + 1 ];
	char							hostname[ kStressRandStrLen + 4 + 1 ];
	Boolean							isConnectionNew	= false;
	static Boolean					printedHeader	= false;
	
	if( !context->mainRef || ( context->requestCount >= context->requestCountLimit ) )
	{
		DNSServiceForget( &context->mainRef );
		context->sdRef				= NULL;
		context->requestCount		= 0;
		context->requestCountLimit	= RandomRange( 1, context->requestCountMax );
		
		err = DNSServiceCreateConnection( &context->mainRef );
		require_noerr( err, exit );
		
		err = DNSServiceSetDispatchQueue( context->mainRef, dispatch_get_main_queue() );
		require_noerr( err, exit );
		
		isConnectionNew = true;
	}
	
	RandomString( kLowercaseAlphaCharSet, sizeof_string( kLowercaseAlphaCharSet ), 2, kStressRandStrLen, randomStr );
	SNPrintF( hostname, sizeof( hostname ), "%s.com", randomStr );
	
	nextMs = RandomRange( context->durationMinMs, context->durationMaxMs );
	
	if( !printedHeader )
	{
		FPrintF( stdout, "%-26s Conn  Hostname Dur (ms)\n", "Timestamp" );
		printedHeader = true;
	}
	FPrintF( stdout, "%{du:time} %3u%c %9s %8u\n",
		NULL, context->connectionNumber, isConnectionNew ? '*': ' ', hostname, nextMs );
	
	DNSServiceForget( &context->sdRef );
	sdRef = context->mainRef;
	err = DNSServiceGetAddrInfo( &sdRef, context->flags | kDNSServiceFlagsShareConnection, context->interfaceIndex,
		kDNSServiceProtocol_IPv4 | kDNSServiceProtocol_IPv6, hostname, GetAddrInfoStressCallback, NULL );
	require_noerr( err, exit );
	context->sdRef = sdRef;
	
	context->requestCount++;
	
	dispatch_after_f( dispatch_time_milliseconds( nextMs ), dispatch_get_main_queue(), context, GetAddrInfoStressEvent );
	
exit:
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	GetAddrInfoStressCallback
//===========================================================================================================================

static void DNSSD_API
	GetAddrInfoStressCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext )
{
	Unused( inSDRef );
	Unused( inFlags );
	Unused( inInterfaceIndex );
	Unused( inError );
	Unused( inHostname );
	Unused( inSockAddr );
	Unused( inTTL );
	Unused( inContext );
}

//===========================================================================================================================
//	DNSQueryCmd
//===========================================================================================================================

typedef struct
{
	sockaddr_ip				serverAddr;
	uint64_t				sendTicks;
	uint8_t *				msgPtr;
	size_t					msgLen;
	size_t					msgOffset;
	const char *			name;
	dispatch_source_t		readSource;
	SocketRef				sock;
	int						timeLimitSecs;
	uint16_t				queryID;
	uint16_t				type;
	Boolean					haveTCPLen;
	Boolean					useTCP;
	Boolean					printRawRData;	// True if RDATA results are not to be formatted.
	uint8_t					msgBuf[ 512 ];
	
}	DNSQueryContext;

static void	DNSQueryPrintPrologue( const DNSQueryContext *inContext );
static void	DNSQueryReadHandler( void *inContext );
static void	DNSQueryCancelHandler( void *inContext );

static void	DNSQueryCmd( void )
{
	OSStatus				err;
	DNSQueryContext *		context = NULL;
	uint8_t *				msgPtr;
	size_t					msgLen, sendLen;
	
	// Check command parameters.
	
	if( gDNSQuery_TimeLimitSecs < -1 )
	{
		FPrintF( stdout, "Invalid time limit: %d seconds.\n", gDNSQuery_TimeLimitSecs );
		err = kParamErr;
		goto exit;
	}
	if( ( gDNSQuery_Flags < INT16_MIN ) || ( gDNSQuery_Flags > UINT16_MAX ) )
	{
		FPrintF( stdout, "DNS flags-and-codes value is out of the unsigned 16-bit range: 0x%08X.\n", gDNSQuery_Flags );
		err = kParamErr;
		goto exit;
	}
	
	// Create context.
	
	context = (DNSQueryContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	context->name			= gDNSQuery_Name;
	context->sock			= kInvalidSocketRef;
	context->timeLimitSecs	= gDNSQuery_TimeLimitSecs;
	context->queryID		= (uint16_t) Random32();
	context->useTCP			= gDNSQuery_UseTCP	 ? true : false;
	context->printRawRData	= gDNSQuery_RawRData ? true : false;
	
#if( TARGET_OS_DARWIN )
	if( gDNSQuery_Server )
#endif
	{
		err = StringToSockAddr( gDNSQuery_Server, &context->serverAddr, sizeof( context->serverAddr ), NULL );
		require_noerr( err, exit );
	}
#if( TARGET_OS_DARWIN )
	else
	{
		err = GetDefaultDNSServer( &context->serverAddr );
		require_noerr( err, exit );
	}
#endif
	if( SockAddrGetPort( &context->serverAddr ) == 0 ) SockAddrSetPort( &context->serverAddr, kDNSPort_Do53 );
	
	err = RecordTypeFromArgString( gDNSQuery_Type, &context->type );
	require_noerr( err, exit );
	
	// Write query message.
	
	check_compile_time_code( sizeof( context->msgBuf ) >= ( 2 + kDNSQueryMessageMaxLen + sizeof( dns_fixed_fields_opt ) ) );
	
	msgPtr = context->useTCP ? &context->msgBuf[ 2 ] : context->msgBuf;
	err = WriteDNSQueryMessage( msgPtr, context->queryID, (uint16_t) gDNSQuery_Flags, context->name, context->type,
		kDNSServiceClass_IN, &msgLen );
	require_noerr( err, exit );
	check( msgLen <= UINT16_MAX );
	
	if( gDNSQuery_DNSSEC )
	{
		DNSHeader * const					hdr = (DNSHeader *) msgPtr;
		dns_fixed_fields_opt * const		opt = (dns_fixed_fields_opt *) &msgPtr[ msgLen ];
		unsigned int						flags;
		
		memset( opt, 0, sizeof( *opt ) );
		dns_fixed_fields_opt_set_type( opt, kDNSServiceType_OPT );
		dns_fixed_fields_opt_set_udp_payload_size( opt, 512 );
		dns_fixed_fields_opt_set_extended_flags( opt, kDNSExtendedFlag_DNSSECOK );
		
		flags = DNSHeaderGetFlags( hdr ) | kDNSHeaderFlag_AuthenticData;
		DNSHeaderSetFlags( hdr, flags );
		DNSHeaderSetAdditionalCount( hdr, 1 );
		msgLen += sizeof( dns_fixed_fields_opt );
	}
	if( gDNSQuery_CheckingDisabled )
	{
		DNSHeader * const		hdr = (DNSHeader *) msgPtr;
		unsigned int			flags;
		
		flags = DNSHeaderGetFlags( hdr ) | kDNSHeaderFlag_CheckingDisabled;
		DNSHeaderSetFlags( hdr, flags );
	}
	if( context->useTCP )
	{
		WriteBig16Typed( context->msgBuf, (uint16_t) msgLen );
		sendLen = 2 + msgLen;
	}
	else
	{
		sendLen = msgLen;
	}
	
	DNSQueryPrintPrologue( context );
	
	if( gDNSQuery_Verbose )
	{
		FPrintF( stdout, "DNS message to send:\n\n%{du:dnsmsg}\n", msgPtr, msgLen );
		FPrintF( stdout, "---\n" );
	}
	
	if( context->useTCP )
	{
		// Create TCP socket.
		
		context->sock = socket( context->serverAddr.sa.sa_family, SOCK_STREAM, IPPROTO_TCP );
		err = map_socket_creation_errno( context->sock );
		require_noerr( err, exit );
		
		err = SocketConnect( context->sock, &context->serverAddr, 5 );
		require_noerr( err, exit );
	}
	else
	{
		// Create UDP socket.
		
		err = UDPClientSocketOpen( AF_UNSPEC, &context->serverAddr, 0, -1, NULL, &context->sock );
		require_noerr( err, exit );
	}
	
	context->sendTicks = UpTicks();
	err = SocketWriteAll( context->sock, context->msgBuf, sendLen, 5 );
	require_noerr( err, exit );
	
	if( context->timeLimitSecs == 0 ) goto exit;
	
	err = DispatchReadSourceCreate( context->sock, NULL, DNSQueryReadHandler, DNSQueryCancelHandler, context,
		&context->readSource );
	require_noerr( err, exit );
	dispatch_resume( context->readSource );
	
	if( context->timeLimitSecs > 0 )
	{
		dispatch_after_f( dispatch_time_seconds( context->timeLimitSecs ), dispatch_get_main_queue(), kExitReason_Timeout,
			Exit );
	}
	dispatch_main();
	
exit:
	if( context )
	{
		dispatch_source_forget( &context->readSource );
		ForgetSocket( &context->sock );
		free( context );
	}
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	DNSQueryPrintPrologue
//===========================================================================================================================

static void	DNSQueryPrintPrologue( const DNSQueryContext *inContext )
{
	const int		timeLimitSecs = inContext->timeLimitSecs;
	
	FPrintF( stdout, "Name:        %s\n",		inContext->name );
	FPrintF( stdout, "Type:        %s (%u)\n",	RecordTypeToString( inContext->type ), inContext->type );
	FPrintF( stdout, "Server:      %##a\n",		&inContext->serverAddr );
	FPrintF( stdout, "Transport:   %s\n",		inContext->useTCP ? "TCP" : "UDP" );
	FPrintF( stdout, "Time limit:  " );
	if( timeLimitSecs >= 0 )	FPrintF( stdout, "%d second%?c\n", timeLimitSecs, timeLimitSecs != 1, 's' );
	else						FPrintF( stdout, "∞\n" );
	FPrintF( stdout, "Start time:  %{du:time}\n", NULL );
	FPrintF( stdout, "---\n" );
}

//===========================================================================================================================
//	DNSQueryReadHandler
//===========================================================================================================================

static void	DNSQueryReadHandler( void *inContext )
{
	OSStatus					err;
	struct timeval				now;
	const uint64_t				nowTicks	= UpTicks();
	DNSQueryContext * const		context		= (DNSQueryContext *) inContext;
	
	gettimeofday( &now, NULL );
	
	if( context->useTCP )
	{
		if( !context->haveTCPLen )
		{
			err = SocketReadData( context->sock, &context->msgBuf, 2, &context->msgOffset );
			if( err == EWOULDBLOCK ) { err = kNoErr; goto exit; }
			require_noerr( err, exit );
			
			context->msgOffset	= 0;
			context->msgLen		= ReadBig16( context->msgBuf );
			context->haveTCPLen	= true;
			if( context->msgLen <= sizeof( context->msgBuf ) )
			{
				context->msgPtr = context->msgBuf;
			}
			else
			{
				context->msgPtr = (uint8_t *) malloc( context->msgLen );
				require_action( context->msgPtr, exit, err = kNoMemoryErr );
			}
		}
		
		err = SocketReadData( context->sock, context->msgPtr, context->msgLen, &context->msgOffset );
		if( err == EWOULDBLOCK ) { err = kNoErr; goto exit; }
		require_noerr( err, exit );
		context->msgOffset	= 0;
		context->haveTCPLen	= false;
	}
	else
	{
		sockaddr_ip		fromAddr;
		
		context->msgPtr = context->msgBuf;
		err = SocketRecvFrom( context->sock, context->msgPtr, sizeof( context->msgBuf ), &context->msgLen, &fromAddr,
			sizeof( fromAddr ), NULL, NULL, NULL, NULL );
		require_noerr( err, exit );
		
		check( SockAddrCompareAddr( &fromAddr, &context->serverAddr ) == 0 );
	}
	
	FPrintF( stdout, "Receive time: %{du:time}\n",	&now );
	FPrintF( stdout, "Source:       %##a\n",		&context->serverAddr );
	FPrintF( stdout, "Message size: %zu\n",			context->msgLen );
	FPrintF( stdout, "RTT:          %llu ms\n\n",	UpTicksToMilliseconds( nowTicks - context->sendTicks ) );
	if( context->printRawRData )	FPrintF( stdout, "%{du:rdnsmsg}\n", context->msgPtr, context->msgLen );
	else							FPrintF( stdout, "%{du:dnsmsg}\n",  context->msgPtr, context->msgLen );
	
	if( ( context->msgLen >= kDNSHeaderLength ) && ( DNSHeaderGetID( (DNSHeader *) context->msgPtr ) == context->queryID ) )
	{
		Exit( kExitReason_ReceivedResponse );
	}
	
exit:
	if( err ) dispatch_source_forget( &context->readSource );
}

//===========================================================================================================================
//	DNSQueryCancelHandler
//===========================================================================================================================

static void	DNSQueryCancelHandler( void *inContext )
{
	DNSQueryContext * const		context = (DNSQueryContext *) inContext;
	
	check( !context->readSource );
	ForgetSocket( &context->sock );
	if( context->msgPtr != context->msgBuf ) ForgetMem( &context->msgPtr );
	free( context );
	dispatch_async_f( dispatch_get_main_queue(), NULL, Exit );
}

#if( DNSSDUTIL_INCLUDE_DNSCRYPT )
//===========================================================================================================================
//	DNSCryptCmd
//===========================================================================================================================

#define kDNSCryptPort		443

#define kDNSCryptMinPadLength				8
#define kDNSCryptMaxPadLength				256
#define kDNSCryptBlockSize					64
#define kDNSCryptCertMinimumLength			124
#define kDNSCryptClientMagicLength			8
#define kDNSCryptResolverMagicLength		8
#define kDNSCryptHalfNonceLength			12
#define kDNSCryptCertMagicLength			4

check_compile_time( ( kDNSCryptHalfNonceLength * 2 ) == crypto_box_NONCEBYTES );

static const uint8_t		kDNSCryptCertMagic[ kDNSCryptCertMagicLength ] = { 'D', 'N', 'S', 'C' };
static const uint8_t		kDNSCryptResolverMagic[ kDNSCryptResolverMagicLength ] =
{
	0x72, 0x36, 0x66, 0x6e, 0x76, 0x57, 0x6a, 0x38
};

typedef struct
{
	uint8_t		certMagic[ kDNSCryptCertMagicLength ];
	uint8_t		esVersion[ 2 ];
	uint8_t		minorVersion[ 2 ];
	uint8_t		signature[ crypto_sign_BYTES ];
	uint8_t		publicKey[ crypto_box_PUBLICKEYBYTES ];
	uint8_t		clientMagic[ kDNSCryptClientMagicLength ];
	uint8_t		serial[ 4 ];
	uint8_t		startTime[ 4 ];
	uint8_t		endTime[ 4 ];
	uint8_t		extensions[ 1 ];	// Variably-sized extension data.
	
}	DNSCryptCert;

check_compile_time( offsetof( DNSCryptCert, extensions ) == kDNSCryptCertMinimumLength );

typedef struct
{
	uint8_t		clientMagic[ kDNSCryptClientMagicLength ];
	uint8_t		clientPublicKey[ crypto_box_PUBLICKEYBYTES ];
	uint8_t		clientNonce[ kDNSCryptHalfNonceLength ];
	uint8_t		poly1305MAC[ 16 ];
	
}	DNSCryptQueryHeader;

check_compile_time( sizeof( DNSCryptQueryHeader ) == 68 );
check_compile_time( sizeof( DNSCryptQueryHeader ) >= crypto_box_ZEROBYTES );
check_compile_time( ( sizeof( DNSCryptQueryHeader ) - crypto_box_ZEROBYTES + crypto_box_BOXZEROBYTES ) ==
	offsetof( DNSCryptQueryHeader, poly1305MAC ) );

typedef struct
{
	uint8_t		resolverMagic[ kDNSCryptResolverMagicLength ];
	uint8_t		clientNonce[ kDNSCryptHalfNonceLength ];
	uint8_t		resolverNonce[ kDNSCryptHalfNonceLength ];
	uint8_t		poly1305MAC[ 16 ];
	
}	DNSCryptResponseHeader;

check_compile_time( sizeof( DNSCryptResponseHeader ) == 48 );
check_compile_time( offsetof( DNSCryptResponseHeader, poly1305MAC ) >= crypto_box_BOXZEROBYTES );
check_compile_time( ( offsetof( DNSCryptResponseHeader, poly1305MAC ) - crypto_box_BOXZEROBYTES + crypto_box_ZEROBYTES ) ==
	sizeof( DNSCryptResponseHeader ) );

typedef struct
{
	sockaddr_ip				serverAddr;
	uint64_t				sendTicks;
	const char *			providerName;
	const char *			qname;
	const uint8_t *			certPtr;
	size_t					certLen;
	dispatch_source_t		readSource;
	size_t					msgLen;
	int						timeLimitSecs;
	uint16_t				queryID;
	uint16_t				qtype;
	Boolean					printRawRData;
	uint8_t					serverPublicSignKey[ crypto_sign_PUBLICKEYBYTES ];
	uint8_t					serverPublicKey[ crypto_box_PUBLICKEYBYTES ];
	uint8_t					clientPublicKey[ crypto_box_PUBLICKEYBYTES ];
	uint8_t					clientSecretKey[ crypto_box_SECRETKEYBYTES ];
	uint8_t					clientMagic[ kDNSCryptClientMagicLength ];
	uint8_t					clientNonce[ kDNSCryptHalfNonceLength ];
	uint8_t					nmKey[ crypto_box_BEFORENMBYTES ];
	uint8_t					msgBuf[ 512 ];
	
}	DNSCryptContext;

static void		DNSCryptReceiveCertHandler( void *inContext );
static void		DNSCryptReceiveResponseHandler( void *inContext );
static void		DNSCryptProceed( void *inContext );
static OSStatus	DNSCryptProcessCert( DNSCryptContext *inContext );
static OSStatus	DNSCryptBuildQuery( DNSCryptContext *inContext );
static OSStatus	DNSCryptSendQuery( DNSCryptContext *inContext );
static void		DNSCryptPrintCertificate( const DNSCryptCert *inCert, size_t inLen );

static void	DNSCryptCmd( void )
{
	OSStatus				err;
	DNSCryptContext *		context		= NULL;
	size_t					writtenBytes;
	size_t					totalBytes;
	SocketContext *			sockCtx;
	SocketRef				sock		= kInvalidSocketRef;
	const char *			ptr;
	
	// Check command parameters.
	
	if( gDNSCrypt_TimeLimitSecs < -1 )
	{
		FPrintF( stdout, "Invalid time limit: %d seconds.\n", gDNSCrypt_TimeLimitSecs );
		err = kParamErr;
		goto exit;
	}
	
	// Create context.
	
	context = (DNSCryptContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	context->providerName	= gDNSCrypt_ProviderName;
	context->qname			= gDNSCrypt_Name;
	context->timeLimitSecs	= gDNSCrypt_TimeLimitSecs;
	context->printRawRData	= gDNSCrypt_RawRData ? true : false;
	
	err = crypto_box_keypair( context->clientPublicKey, context->clientSecretKey );
	require_noerr( err, exit );
	
	err = HexToData( gDNSCrypt_ProviderKey, kSizeCString, kHexToData_DefaultFlags,
		context->serverPublicSignKey, sizeof( context->serverPublicSignKey ), &writtenBytes, &totalBytes, &ptr );
	if( err || ( *ptr != '\0' ) )
	{
		FPrintF( stderr, "Failed to parse public signing key hex string (%s).\n", gDNSCrypt_ProviderKey );
		goto exit;
	}
	else if( totalBytes != sizeof( context->serverPublicSignKey ) )
	{
		FPrintF( stderr, "Public signing key contains incorrect number of hex bytes (%zu != %zu)\n",
			totalBytes, sizeof( context->serverPublicSignKey ) );
		err = kSizeErr;
		goto exit;
	}
	check( writtenBytes == totalBytes );
	
	err = StringToSockAddr( gDNSCrypt_Server, &context->serverAddr, sizeof( context->serverAddr ), NULL );
	require_noerr( err, exit );
	if( SockAddrGetPort( &context->serverAddr ) == 0 ) SockAddrSetPort( &context->serverAddr, kDNSCryptPort );
	
	err = RecordTypeFromArgString( gDNSCrypt_Type, &context->qtype );
	require_noerr( err, exit );
	
	// Write query message.
	
	context->queryID = (uint16_t) Random32();
	err = WriteDNSQueryMessage( context->msgBuf, context->queryID, kDNSHeaderFlag_RecursionDesired, context->providerName,
		kDNSServiceType_TXT, kDNSServiceClass_IN, &context->msgLen );
	require_noerr( err, exit );
	
	// Create UDP socket.
	
	err = UDPClientSocketOpen( AF_UNSPEC, &context->serverAddr, 0, -1, NULL, &sock );
	require_noerr( err, exit );
	
	// Send DNS query.
	
	context->sendTicks = UpTicks();
	err = SocketWriteAll( sock, context->msgBuf, context->msgLen, 5 );
	require_noerr( err, exit );
	
	sockCtx = SocketContextCreate( sock, context, &err );
	require_noerr( err, exit );
	sock = kInvalidSocketRef;
	
	err = DispatchReadSourceCreate( sockCtx->sock, NULL, DNSCryptReceiveCertHandler, SocketContextCancelHandler, sockCtx,
		&context->readSource );
	if( err ) ForgetSocketContext( &sockCtx );
	require_noerr( err, exit );
	
	dispatch_resume( context->readSource );
	
	if( context->timeLimitSecs > 0 )
	{
		dispatch_after_f( dispatch_time_seconds( context->timeLimitSecs ), dispatch_get_main_queue(), kExitReason_Timeout,
			Exit );
	}
	dispatch_main();
	
exit:
	if( context ) free( context );
	ForgetSocket( &sock );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	DNSCryptReceiveCertHandler
//===========================================================================================================================

static void	DNSCryptReceiveCertHandler( void *inContext )
{
	OSStatus					err;
	struct timeval				now;
	const uint64_t				nowTicks	= UpTicks();
	SocketContext * const		sockCtx		= (SocketContext *) inContext;
	DNSCryptContext * const		context		= (DNSCryptContext *) sockCtx->userContext;
	const DNSHeader *			hdr;
	sockaddr_ip					fromAddr;
	const uint8_t *				ptr;
	const uint8_t *				txtPtr;
	size_t						txtLen;
	unsigned int				answerCount, i;
	uint8_t						targetName[ kDomainNameLengthMax ];
	
	gettimeofday( &now, NULL );
	
	dispatch_source_forget( &context->readSource );
	
	err = SocketRecvFrom( sockCtx->sock, context->msgBuf, sizeof( context->msgBuf ), &context->msgLen,
		&fromAddr, sizeof( fromAddr ), NULL, NULL, NULL, NULL );
	require_noerr( err, exit );
	check( SockAddrCompareAddr( &fromAddr, &context->serverAddr ) == 0 );
	
	FPrintF( stdout, "Receive time: %{du:time}\n",	&now );
	FPrintF( stdout, "Source:       %##a\n",		&context->serverAddr );
	FPrintF( stdout, "Message size: %zu\n",			context->msgLen );
	FPrintF( stdout, "RTT:          %llu ms\n\n",	UpTicksToMilliseconds( nowTicks - context->sendTicks ) );
	if( context->printRawRData )	FPrintF( stdout, "%{du:rdnsmsg}\n", context->msgBuf, context->msgLen );
	else							FPrintF( stdout, "%{du:dnsmsg}\n",  context->msgBuf, context->msgLen );
	
	require_action_quiet( context->msgLen >= kDNSHeaderLength, exit, err = kSizeErr );
	
	hdr = (DNSHeader *) context->msgBuf;
	require_action_quiet( DNSHeaderGetID( hdr ) == context->queryID, exit, err = kMismatchErr );
	
	err = DNSMessageGetAnswerSection( context->msgBuf, context->msgLen, &ptr );
	require_noerr( err, exit );
	
	err = DomainNameFromString( targetName, context->providerName, NULL );
	require_noerr( err, exit );
	
	answerCount = DNSHeaderGetAnswerCount( hdr );
	for( i = 0; i < answerCount; ++i )
	{
		uint16_t		type;
		uint16_t		class;
		uint8_t			name[ kDomainNameLengthMax ];
		
		err = DNSMessageExtractRecord( context->msgBuf, context->msgLen, ptr, name, &type, &class, NULL, &txtPtr, &txtLen,
			&ptr );
		require_noerr( err, exit );
		
		if( ( type == kDNSServiceType_TXT ) && ( class == kDNSServiceClass_IN ) && DomainNameEqual( name, targetName ) )
		{
			break;
		}
	}
	
	if( txtLen < ( 1 + kDNSCryptCertMinimumLength ) )
	{
		FPrintF( stderr, "TXT record length is too short (%u < %u)\n", txtLen, kDNSCryptCertMinimumLength + 1 );
		err = kSizeErr;
		goto exit;
	}
	if( txtPtr[ 0 ] < kDNSCryptCertMinimumLength )
	{
		FPrintF( stderr, "TXT record value length is too short (%u < %u)\n", txtPtr[ 0 ], kDNSCryptCertMinimumLength );
		err = kSizeErr;
		goto exit;
	}
	
	context->certLen = txtPtr[ 0 ];
	context->certPtr = &txtPtr[ 1 ];
	
	dispatch_async_f( dispatch_get_main_queue(), context, DNSCryptProceed );
	
exit:
	if( err ) Exit( NULL );
}

//===========================================================================================================================
//	DNSCryptReceiveResponseHandler
//===========================================================================================================================

static void	DNSCryptReceiveResponseHandler( void *inContext )
{
	OSStatus						err;
	struct timeval					now;
	const uint64_t					nowTicks	= UpTicks();
	SocketContext * const			sockCtx		= (SocketContext *) inContext;
	DNSCryptContext * const			context		= (DNSCryptContext *) sockCtx->userContext;
	sockaddr_ip						fromAddr;
	DNSCryptResponseHeader *		hdr;
	const uint8_t *					end;
	uint8_t *						ciphertext;
	uint8_t *						plaintext;
	const uint8_t *					response;
	uint8_t							nonce[ crypto_box_NONCEBYTES ];
	
	gettimeofday( &now, NULL );
	
	dispatch_source_forget( &context->readSource );
	
	err = SocketRecvFrom( sockCtx->sock, context->msgBuf, sizeof( context->msgBuf ), &context->msgLen,
		&fromAddr, sizeof( fromAddr ), NULL, NULL, NULL, NULL );
	require_noerr( err, exit );
	check( SockAddrCompareAddr( &fromAddr, &context->serverAddr ) == 0 );
	
	FPrintF( stdout, "Receive time: %{du:time}\n",	&now );
	FPrintF( stdout, "Source:       %##a\n",		&context->serverAddr );
	FPrintF( stdout, "Message size: %zu\n",			context->msgLen );
	FPrintF( stdout, "RTT:          %llu ms\n\n",	UpTicksToMilliseconds( nowTicks - context->sendTicks ) );
	
	if( context->msgLen < sizeof( DNSCryptResponseHeader ) )
	{
		FPrintF( stderr, "DNSCrypt response is too short.\n" );
		err = kSizeErr;
		goto exit;
	}
	
	hdr = (DNSCryptResponseHeader *) context->msgBuf;
	
	if( memcmp( hdr->resolverMagic, kDNSCryptResolverMagic, kDNSCryptResolverMagicLength ) != 0 )
	{
		FPrintF( stderr, "DNSCrypt response resolver magic %#H != %#H\n",
			hdr->resolverMagic,		kDNSCryptResolverMagicLength, INT_MAX,
			kDNSCryptResolverMagic, kDNSCryptResolverMagicLength, INT_MAX );
		err = kValueErr;
		goto exit;
	}
	
	if( memcmp( hdr->clientNonce, context->clientNonce, kDNSCryptHalfNonceLength ) != 0 )
	{
		FPrintF( stderr, "DNSCrypt response client nonce mismatch.\n" );
		err = kValueErr;
		goto exit;
	}
	
	memcpy( nonce, hdr->clientNonce, crypto_box_NONCEBYTES );
	
	ciphertext = hdr->poly1305MAC - crypto_box_BOXZEROBYTES;
	memset( ciphertext, 0, crypto_box_BOXZEROBYTES );
	
	plaintext = (uint8_t *)( hdr + 1 ) - crypto_box_ZEROBYTES;
	check( plaintext == ciphertext );
	
	end = context->msgBuf + context->msgLen;
	
	err = crypto_box_open_afternm( plaintext, ciphertext, (size_t)( end - ciphertext ), nonce, context->nmKey );
	require_noerr( err, exit );
	
	response = plaintext + crypto_box_ZEROBYTES;
	if( context->printRawRData )	FPrintF( stdout, "%{du:rdnsmsg}\n", response, (size_t)( end - response ) );
	else							FPrintF( stdout, "%{du:dnsmsg}\n",  response, (size_t)( end - response ) );
	Exit( kExitReason_ReceivedResponse );
	
exit:
	if( err ) Exit( NULL );
}

//===========================================================================================================================
//	DNSCryptProceed
//===========================================================================================================================

static void	DNSCryptProceed( void *inContext )
{
	OSStatus					err;
	DNSCryptContext * const		context = (DNSCryptContext *) inContext;
	
	err = DNSCryptProcessCert( context );
	require_noerr_quiet( err, exit );
	
	err = DNSCryptBuildQuery( context );
	require_noerr_quiet( err, exit );
	
	err = DNSCryptSendQuery( context );
	require_noerr_quiet( err, exit );
	
exit:
	if( err ) Exit( NULL );
}

//===========================================================================================================================
//	DNSCryptProcessCert
//===========================================================================================================================

static OSStatus	DNSCryptProcessCert( DNSCryptContext *inContext )
{
	OSStatus						err;
	const DNSCryptCert * const		cert	= (DNSCryptCert *) inContext->certPtr;
	const uint8_t * const			certEnd	= inContext->certPtr + inContext->certLen;
	struct timeval					now;
	time_t							startTimeSecs, endTimeSecs;
	size_t							signedLen;
	uint8_t *						tempBuf;
	unsigned long long				tempLen;
	
	DNSCryptPrintCertificate( cert, inContext->certLen );
	
	if( memcmp( cert->certMagic, kDNSCryptCertMagic, kDNSCryptCertMagicLength ) != 0 )
	{
		FPrintF( stderr, "DNSCrypt certificate magic %#H != %#H\n",
			cert->certMagic,	kDNSCryptCertMagicLength, INT_MAX,
			kDNSCryptCertMagic, kDNSCryptCertMagicLength, INT_MAX );
		err = kValueErr;
		goto exit;
	}
	
	startTimeSecs	= (time_t) ReadBig32( cert->startTime );
	endTimeSecs		= (time_t) ReadBig32( cert->endTime );
	
	gettimeofday( &now, NULL );
	if( now.tv_sec < startTimeSecs )
	{
		FPrintF( stderr, "DNSCrypt certificate start time is in the future.\n" );
		err = kDateErr;
		goto exit;
	}
	if( now.tv_sec >= endTimeSecs )
	{
		FPrintF( stderr, "DNSCrypt certificate has expired.\n" );
		err = kDateErr;
		goto exit;
	}
	
	signedLen = (size_t)( certEnd - cert->signature );
	tempBuf = (uint8_t *) malloc( signedLen );
	require_action( tempBuf, exit, err = kNoMemoryErr );
	err = crypto_sign_open( tempBuf, &tempLen, cert->signature, signedLen, inContext->serverPublicSignKey );
	free( tempBuf );
	if( err )
	{
		FPrintF( stderr, "DNSCrypt certificate failed verification.\n" );
		err = kAuthenticationErr;
		goto exit;
	}
	
	memcpy( inContext->serverPublicKey,	cert->publicKey,	crypto_box_PUBLICKEYBYTES );
	memcpy( inContext->clientMagic,		cert->clientMagic,	kDNSCryptClientMagicLength );
	
	err = crypto_box_beforenm( inContext->nmKey, inContext->serverPublicKey, inContext->clientSecretKey );
	require_noerr( err, exit );
	
	inContext->certPtr	= NULL;
	inContext->certLen	= 0;
	inContext->msgLen	= 0;
	
exit:
	return( err );
}

//===========================================================================================================================
//	DNSCryptBuildQuery
//===========================================================================================================================

static OSStatus	DNSCryptPadQuery( uint8_t *inMsgPtr, size_t inMsgLen, size_t inMaxLen, size_t *outPaddedLen );

static OSStatus	DNSCryptBuildQuery( DNSCryptContext *inContext )
{
	OSStatus						err;
	DNSCryptQueryHeader * const		hdr			= (DNSCryptQueryHeader *) inContext->msgBuf;
	uint8_t * const					queryPtr	= (uint8_t *)( hdr + 1 );
	size_t							queryLen;
	size_t							paddedQueryLen;
	const uint8_t * const			msgLimit	= inContext->msgBuf + sizeof( inContext->msgBuf );
	const uint8_t *					padLimit;
	uint8_t							nonce[ crypto_box_NONCEBYTES ];
	
	check_compile_time_code( sizeof( inContext->msgBuf ) >= ( sizeof( DNSCryptQueryHeader ) + kDNSQueryMessageMaxLen ) );
	
	inContext->queryID = (uint16_t) Random32();
	err = WriteDNSQueryMessage( queryPtr, inContext->queryID, kDNSHeaderFlag_RecursionDesired, inContext->qname,
		inContext->qtype, kDNSServiceClass_IN, &queryLen );
	require_noerr( err, exit );
	
	padLimit = &queryPtr[ queryLen + kDNSCryptMaxPadLength ];
	if( padLimit > msgLimit ) padLimit = msgLimit;
	
	err = DNSCryptPadQuery( queryPtr, queryLen, (size_t)( padLimit - queryPtr ), &paddedQueryLen );
	require_noerr( err, exit );
	
	memset( queryPtr - crypto_box_ZEROBYTES, 0, crypto_box_ZEROBYTES );
	RandomBytes( inContext->clientNonce, kDNSCryptHalfNonceLength );
	memcpy( nonce, inContext->clientNonce, kDNSCryptHalfNonceLength );
	memset( &nonce[ kDNSCryptHalfNonceLength ], 0, kDNSCryptHalfNonceLength );
	
	err = crypto_box_afternm( queryPtr - crypto_box_ZEROBYTES, queryPtr - crypto_box_ZEROBYTES,
		paddedQueryLen + crypto_box_ZEROBYTES, nonce, inContext->nmKey );
	require_noerr( err, exit );
	
	memcpy( hdr->clientMagic,		inContext->clientMagic,		kDNSCryptClientMagicLength );
	memcpy( hdr->clientPublicKey,	inContext->clientPublicKey,	crypto_box_PUBLICKEYBYTES );
	memcpy( hdr->clientNonce,		nonce,						kDNSCryptHalfNonceLength );
	
	inContext->msgLen = (size_t)( &queryPtr[ paddedQueryLen ] - inContext->msgBuf );
	
exit:
	return( err );
}

static OSStatus	DNSCryptPadQuery( uint8_t *inMsgPtr, size_t inMsgLen, size_t inMaxLen, size_t *outPaddedLen )
{
	OSStatus		err;
	size_t			paddedLen;
	
	require_action_quiet( ( inMsgLen + kDNSCryptMinPadLength ) <= inMaxLen, exit, err = kSizeErr );
	
	paddedLen = inMsgLen + kDNSCryptMinPadLength +
		arc4random_uniform( (uint32_t)( inMaxLen - ( inMsgLen + kDNSCryptMinPadLength ) + 1 ) );
	paddedLen += ( kDNSCryptBlockSize - ( paddedLen % kDNSCryptBlockSize ) );
	if( paddedLen > inMaxLen ) paddedLen = inMaxLen;
	
	inMsgPtr[ inMsgLen ] = 0x80;
	memset( &inMsgPtr[ inMsgLen + 1 ], 0, paddedLen - ( inMsgLen + 1 ) );
	
	if( outPaddedLen ) *outPaddedLen = paddedLen;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	DNSCryptSendQuery
//===========================================================================================================================

static OSStatus	DNSCryptSendQuery( DNSCryptContext *inContext )
{
	OSStatus			err;
	SocketContext *		sockCtx;
	SocketRef			sock = kInvalidSocketRef;
	
	check( inContext->msgLen > 0 );
	check( !inContext->readSource );
	
	err = UDPClientSocketOpen( AF_UNSPEC, &inContext->serverAddr, 0, -1, NULL, &sock );
	require_noerr( err, exit );
	
	inContext->sendTicks = UpTicks();
	err = SocketWriteAll( sock, inContext->msgBuf, inContext->msgLen, 5 );
	require_noerr( err, exit );
	
	sockCtx = SocketContextCreate( sock, inContext, &err );
	require_noerr( err, exit );
	sock = kInvalidSocketRef;
	
	err = DispatchReadSourceCreate( sockCtx->sock, NULL, DNSCryptReceiveResponseHandler, SocketContextCancelHandler, sockCtx,
		&inContext->readSource );
	if( err ) ForgetSocketContext( &sockCtx );
	require_noerr( err, exit );
	
	dispatch_resume( inContext->readSource );
	
exit:
	ForgetSocket( &sock );
	return( err );
}

//===========================================================================================================================
//	DNSCryptPrintCertificate
//===========================================================================================================================

#define kCertTimeStrBufLen		32

static char *	CertTimeStr( time_t inTime, char inBuffer[ kCertTimeStrBufLen ] );

static void	DNSCryptPrintCertificate( const DNSCryptCert *inCert, size_t inLen )
{
	time_t		startTime, endTime;
	int			extLen;
	char		timeBuf[ kCertTimeStrBufLen ];
	
	check( inLen >= kDNSCryptCertMinimumLength );
	
	startTime	= (time_t) ReadBig32( inCert->startTime );
	endTime		= (time_t) ReadBig32( inCert->endTime );
	
	FPrintF( stdout, "DNSCrypt certificate (%zu bytes):\n", inLen );
	FPrintF( stdout, "Cert Magic:    %#H\n", inCert->certMagic, kDNSCryptCertMagicLength, INT_MAX );
	FPrintF( stdout, "ES Version:    %u\n",	ReadBig16( inCert->esVersion ) );
	FPrintF( stdout, "Minor Version: %u\n",	ReadBig16( inCert->minorVersion ) );
	FPrintF( stdout, "Signature:     %H\n",	inCert->signature, crypto_sign_BYTES / 2, INT_MAX );
	FPrintF( stdout, "               %H\n",	&inCert->signature[ crypto_sign_BYTES / 2 ], crypto_sign_BYTES / 2, INT_MAX );
	FPrintF( stdout, "Public Key:    %H\n", inCert->publicKey, sizeof( inCert->publicKey ), INT_MAX );
	FPrintF( stdout, "Client Magic:  %H\n", inCert->clientMagic, kDNSCryptClientMagicLength, INT_MAX );
	FPrintF( stdout, "Serial:        %u\n",	ReadBig32( inCert->serial ) );
	FPrintF( stdout, "Start Time:    %u (%s)\n", (uint32_t) startTime, CertTimeStr( startTime, timeBuf ) );
	FPrintF( stdout, "End Time:      %u (%s)\n", (uint32_t) endTime, CertTimeStr( endTime, timeBuf ) );
	
	if( inLen > kDNSCryptCertMinimumLength )
	{
		extLen = (int)( inLen - kDNSCryptCertMinimumLength );
		FPrintF( stdout, "Extensions:    %.1H\n", inCert->extensions, extLen, extLen );
	}
	FPrintF( stdout, "\n" );
}

static char *	CertTimeStr( time_t inTime, char inBuffer[ kCertTimeStrBufLen ] )
{
	struct tm *		tm;
	
	tm = localtime( &inTime );
	if( !tm )
	{
		dlogassert( "localtime() returned a NULL pointer.\n" );
		*inBuffer = '\0';
	}
	else
	{
		strftime( inBuffer, kCertTimeStrBufLen, "%a %b %d %H:%M:%S %Z %Y", tm );
	}
	
	return( inBuffer );
}

#endif	// DNSSDUTIL_INCLUDE_DNSCRYPT

//===========================================================================================================================
//	MDNSQueryCmd
//===========================================================================================================================

typedef struct
{
	const char *			qnameStr;							// Name (QNAME) of the record being queried as a C string.
	dispatch_source_t		readSourceV4;						// Read dispatch source for IPv4 socket.
	dispatch_source_t		readSourceV6;						// Read dispatch source for IPv6 socket.
	int						localPort;							// The port number to which the sockets are bound.
	int						receiveSecs;						// After send, the amount of time to spend receiving.
	uint32_t				ifIndex;							// Index of the interface over which to send the query.
	uint16_t				qtype;								// The type (QTYPE) of the record being queried.
	Boolean					isQU;								// True if the query is QU, i.e., requests unicast responses.
	Boolean					allResponses;						// True if all mDNS messages received should be printed.
	Boolean					printRawRData;						// True if RDATA should be printed as hexdumps.
	Boolean					useIPv4;							// True if the query should be sent via IPv4 multicast.
	Boolean					useIPv6;							// True if the query should be sent via IPv6 multicast.
	char					ifName[ IF_NAMESIZE + 1 ];			// Name of the interface over which to send the query.
	uint8_t					qname[ kDomainNameLengthMax ];		// Buffer to hold the QNAME in DNS label format.
	uint8_t					msgBuf[ kMDNSMessageSizeMax ];		// mDNS message buffer.
	
}	MDNSQueryContext;

static void	MDNSQueryPrintPrologue( const MDNSQueryContext *inContext );
static void	MDNSQueryReadHandler( void *inContext );

static void	MDNSQueryCmd( void )
{
	OSStatus				err;
	MDNSQueryContext *		context;
	SocketRef				sockV4 = kInvalidSocketRef;
	SocketRef				sockV6 = kInvalidSocketRef;
	ssize_t					n;
	const char *			ifname;
	size_t					msgLen;
	unsigned int			sendCount;
	
	// Check command parameters.
	
	if( gMDNSQuery_ReceiveSecs < -1 )
	{
		FPrintF( stdout, "Invalid receive time value: %d seconds.\n", gMDNSQuery_ReceiveSecs );
		err = kParamErr;
		goto exit;
	}
	
	context = (MDNSQueryContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	context->qnameStr		= gMDNSQuery_Name;
	context->receiveSecs	= gMDNSQuery_ReceiveSecs;
	context->isQU			= gMDNSQuery_IsQU		  ? true : false;
	context->allResponses	= gMDNSQuery_AllResponses ? true : false;
	context->printRawRData	= gMDNSQuery_RawRData	  ? true : false;
	context->useIPv4		= ( gMDNSQuery_UseIPv4 || !gMDNSQuery_UseIPv6 ) ? true : false;
	context->useIPv6		= ( gMDNSQuery_UseIPv6 || !gMDNSQuery_UseIPv4 ) ? true : false;
	
	err = InterfaceIndexFromArgString( gInterface, &context->ifIndex );
	require_noerr_quiet( err, exit );
	
	ifname = if_indextoname( context->ifIndex, context->ifName );
	require_action( ifname, exit, err = kNameErr );
	
	err = RecordTypeFromArgString( gMDNSQuery_Type, &context->qtype );
	require_noerr( err, exit );
	
	// Set up IPv4 socket.
	
	if( context->useIPv4 )
	{
		err = CreateMulticastSocket( GetMDNSMulticastAddrV4(),
			gMDNSQuery_SourcePort ? gMDNSQuery_SourcePort : ( context->isQU ? context->localPort : kMDNSPort ),
			ifname, context->ifIndex, !context->isQU, &context->localPort, &sockV4 );
		require_noerr( err, exit );
	}
	
	// Set up IPv6 socket.
	
	if( context->useIPv6 )
	{
		err = CreateMulticastSocket( GetMDNSMulticastAddrV6(),
			gMDNSQuery_SourcePort ? gMDNSQuery_SourcePort : ( context->isQU ? context->localPort : kMDNSPort ),
			ifname, context->ifIndex, !context->isQU, &context->localPort, &sockV6 );
		require_noerr( err, exit );
	}
	
	// Craft mDNS query message.
	
	check_compile_time_code( sizeof( context->msgBuf ) >= kDNSQueryMessageMaxLen );
	err = WriteDNSQueryMessage( context->msgBuf, kDefaultMDNSMessageID, kDefaultMDNSQueryFlags, context->qnameStr,
		context->qtype, context->isQU ? ( kDNSServiceClass_IN | kMDNSClassUnicastResponseBit ) : kDNSServiceClass_IN,
		&msgLen );
	require_noerr( err, exit );
	
	// Print prologue.
	
	MDNSQueryPrintPrologue( context );
	
	// Send mDNS query message.
	
	sendCount = 0;
	if( IsValidSocket( sockV4 ) )
	{
		const struct sockaddr * const		mcastAddr4 = GetMDNSMulticastAddrV4();
		
		n = sendto( sockV4, context->msgBuf, msgLen, 0, mcastAddr4, SockAddrGetSize( mcastAddr4 ) );
		err = map_socket_value_errno( sockV4, n == (ssize_t) msgLen, n );
		if( err )
		{
			FPrintF( stderr, "*** Failed to send query on IPv4 socket with error %#m\n", err );
			ForgetSocket( &sockV4 );
		}
		else
		{
			++sendCount;
		}
	}
	if( IsValidSocket( sockV6 ) )
	{
		const struct sockaddr * const		mcastAddr6 = GetMDNSMulticastAddrV6();
		
		n = sendto( sockV6, context->msgBuf, msgLen, 0, mcastAddr6, SockAddrGetSize( mcastAddr6 ) );
		err = map_socket_value_errno( sockV6, n == (ssize_t) msgLen, n );
		if( err )
		{
			FPrintF( stderr, "*** Failed to send query on IPv6 socket with error %#m\n", err );
			ForgetSocket( &sockV6 );
		}
		else
		{
			++sendCount;
		}
	}
	require_action_quiet( sendCount > 0, exit, err = kUnexpectedErr );
	
	// If there's no wait period after the send, then exit.
	
	if( context->receiveSecs == 0 ) goto exit;
	
	// Create dispatch read sources for socket(s).
	
	if( IsValidSocket( sockV4 ) )
	{
		SocketContext *		sockCtx;
		
		sockCtx = SocketContextCreate( sockV4, context, &err );
		require_noerr( err, exit );
		sockV4 = kInvalidSocketRef;
		
		err = DispatchReadSourceCreate( sockCtx->sock, NULL, MDNSQueryReadHandler, SocketContextCancelHandler, sockCtx,
			&context->readSourceV4 );
		if( err ) ForgetSocketContext( &sockCtx );
		require_noerr( err, exit );
		
		dispatch_resume( context->readSourceV4 );
	}
	
	if( IsValidSocket( sockV6 ) )
	{
		SocketContext *		sockCtx;
		
		sockCtx = SocketContextCreate( sockV6, context, &err );
		require_noerr( err, exit );
		sockV6 = kInvalidSocketRef;
		
		err = DispatchReadSourceCreate( sockCtx->sock, NULL, MDNSQueryReadHandler, SocketContextCancelHandler, sockCtx,
			&context->readSourceV6 );
		if( err ) ForgetSocketContext( &sockCtx );
		require_noerr( err, exit );
		
		dispatch_resume( context->readSourceV6 );
	}
	
	if( context->receiveSecs > 0 )
	{
		dispatch_after_f( dispatch_time_seconds( context->receiveSecs ), dispatch_get_main_queue(), kExitReason_Timeout,
			Exit );
	}
	dispatch_main();
	
exit:
	ForgetSocket( &sockV4 );
	ForgetSocket( &sockV6 );
	exit( 1 );
}

//===========================================================================================================================
//	MDNSColliderCmd
//===========================================================================================================================

static void	_MDNSColliderCmdStopHandler( void *inContext, OSStatus inError );

static void	MDNSColliderCmd( void )
{
	OSStatus					err;
	MDNSColliderRef				collider = NULL;
	uint8_t *					rdataPtr = NULL;
	size_t						rdataLen = 0;
	const char *				ifname;
	uint32_t					ifIndex;
	MDNSColliderProtocols		protocols;
	uint16_t					type;
	char						ifName[ IF_NAMESIZE + 1 ];
	uint8_t						name[ kDomainNameLengthMax ];
	
	err = InterfaceIndexFromArgString( gInterface, &ifIndex );
	require_noerr_quiet( err, exit );
	
	ifname = if_indextoname( ifIndex, ifName );
	if( !ifname )
	{
		FPrintF( stderr, "error: Invalid interface name or index: %s\n", gInterface );
		err = kNameErr;
		goto exit;
	}
	
	err = DomainNameFromString( name, gMDNSCollider_Name, NULL );
	if( err )
	{
		FPrintF( stderr, "error: Invalid record name: %s\n", gMDNSCollider_Name );
		goto exit;
	}
	
	err = RecordTypeFromArgString( gMDNSCollider_Type, &type );
	require_noerr_quiet( err, exit );
	
	if( gMDNSCollider_RecordData )
	{
		err = RecordDataFromArgString( gMDNSCollider_RecordData, &rdataPtr, &rdataLen );
		require_noerr_quiet( err, exit );
	}
	
	err = MDNSColliderCreate( dispatch_get_main_queue(), &collider );
	require_noerr( err, exit );
	
	err = MDNSColliderSetProgram( collider, gMDNSCollider_Program );
	if( err )
	{
		FPrintF( stderr, "error: Failed to set program string: '%s'\n", gMDNSCollider_Program );
		goto exit;
	}
	
	err = MDNSColliderSetRecord( collider, name, type, rdataPtr, rdataLen );
	require_noerr( err, exit );
	ForgetMem( &rdataPtr );
	
	protocols = kMDNSColliderProtocol_None;
	if( gMDNSCollider_UseIPv4 || !gMDNSCollider_UseIPv6 ) protocols |= kMDNSColliderProtocol_IPv4;
	if( gMDNSCollider_UseIPv6 || !gMDNSCollider_UseIPv4 ) protocols |= kMDNSColliderProtocol_IPv6;
	MDNSColliderSetProtocols( collider, protocols );
	MDNSColliderSetInterfaceIndex( collider, ifIndex );
	MDNSColliderSetStopHandler( collider, _MDNSColliderCmdStopHandler, collider );
	
	err = MDNSColliderStart( collider );
	require_noerr( err, exit );
	
	dispatch_main();
	
exit:
	FreeNullSafe( rdataPtr );
	CFReleaseNullSafe( collider );
	if( err ) exit( 1 );
}

static void	_MDNSColliderCmdStopHandler( void *inContext, OSStatus inError )
{
	MDNSColliderRef const		collider = (MDNSColliderRef) inContext;
	
	CFRelease( collider );
	exit( inError ? 1 : 0 );
}

//===========================================================================================================================
//	MDNSQueryPrintPrologue
//===========================================================================================================================

static void	MDNSQueryPrintPrologue( const MDNSQueryContext *inContext )
{
	const int		receiveSecs = inContext->receiveSecs;
	
	FPrintF( stdout, "Interface:        %d (%s)\n",		(int32_t) inContext->ifIndex, inContext->ifName );
	FPrintF( stdout, "Name:             %s\n",			inContext->qnameStr );
	FPrintF( stdout, "Type:             %s (%u)\n",		RecordTypeToString( inContext->qtype ), inContext->qtype );
	FPrintF( stdout, "Class:            IN (%s)\n",		inContext->isQU ? "QU" : "QM" );
	FPrintF( stdout, "Local port:       %d\n",			inContext->localPort );
	FPrintF( stdout, "IP protocols:     %?s%?s%?s\n",
		inContext->useIPv4, "IPv4", ( inContext->useIPv4 && inContext->useIPv6 ), ", ", inContext->useIPv6, "IPv6" );
	FPrintF( stdout, "Receive duration: " );
	if( receiveSecs >= 0 )	FPrintF( stdout, "%d second%?c\n", receiveSecs, receiveSecs != 1, 's' );
	else					FPrintF( stdout, "∞\n" );
	FPrintF( stdout, "Start time:       %{du:time}\n",	NULL );
}

//===========================================================================================================================
//	MDNSQueryReadHandler
//===========================================================================================================================

static void	MDNSQueryReadHandler( void *inContext )
{
	OSStatus						err;
	struct timeval					now;
	SocketContext * const			sockCtx = (SocketContext *) inContext;
	MDNSQueryContext * const		context = (MDNSQueryContext *) sockCtx->userContext;
	size_t							msgLen;
	sockaddr_ip						fromAddr;
	Boolean							foundAnswer	= false;
	
	gettimeofday( &now, NULL );
	
	err = SocketRecvFrom( sockCtx->sock, context->msgBuf, sizeof( context->msgBuf ), &msgLen, &fromAddr,
		sizeof( fromAddr ), NULL, NULL, NULL, NULL );
	require_noerr( err, exit );
	
	if( !context->allResponses && ( msgLen >= kDNSHeaderLength ) )
	{
		const uint8_t *				ptr;
		const DNSHeader * const		hdr = (DNSHeader *) context->msgBuf;
		unsigned int				rrCount, i;
		uint16_t					type, class;
		uint8_t						name[ kDomainNameLengthMax ];
		
		err = DNSMessageGetAnswerSection( context->msgBuf, msgLen, &ptr );
		require_noerr( err, exit );
		
		if( context->qname[ 0 ] == 0 )
		{
			err = DomainNameAppendString( context->qname, context->qnameStr, NULL );
			require_noerr( err, exit );
		}
		
		rrCount = DNSHeaderGetAnswerCount( hdr ) + DNSHeaderGetAuthorityCount( hdr ) + DNSHeaderGetAdditionalCount( hdr );
		for( i = 0; i < rrCount; ++i )
		{
			err = DNSMessageExtractRecord( context->msgBuf, msgLen, ptr, name, &type, &class, NULL, NULL, NULL, &ptr );
			require_noerr( err, exit );
			
			if( ( ( context->qtype == kDNSServiceType_ANY ) || ( type == context->qtype ) ) &&
				DomainNameEqual( name, context->qname ) )
			{
				foundAnswer = true;
				break;
			}
		}
	}
	if( context->allResponses || foundAnswer )
	{
		FPrintF( stdout, "---\n" );
		FPrintF( stdout, "Receive time: %{du:time}\n",	&now );
		FPrintF( stdout, "Source:       %##a\n",		&fromAddr );
		FPrintF( stdout, "Message size: %zu\n\n",		msgLen );
		if( context->printRawRData )	FPrintF( stdout, "%#{du:rdnsmsg}\n", context->msgBuf, msgLen );
		else							FPrintF( stdout, "%#{du:dnsmsg}\n",  context->msgBuf, msgLen );
	}
	
exit:
	if( err ) exit( 1 );
}

#if( TARGET_OS_DARWIN )
//===========================================================================================================================
//	PIDToUUIDCmd
//===========================================================================================================================

static void	PIDToUUIDCmd( void )
{
	OSStatus		err;
	uuid_t			uuid;
	
	err = mdns_system_pid_to_uuid( gPIDToUUID_PID, uuid );
	require_noerr_quiet( err, exit );
	
	FPrintF( stdout, "%#U\n", uuid );
	
exit:
	gExitCode = err ? 1 : 0;
}
#endif

//===========================================================================================================================
//	DNSServerCommand
//===========================================================================================================================

typedef struct
{
	DNSServerRef						server;				// Reference to the DNS server.
	dispatch_queue_t					queue;				// Serial queue for server.
	sockaddr_ip *						addrArray;			// Server's addresses.
	size_t								addrCount;			// Count of server's addresses.
	dispatch_source_t					sourceSigInt;		// Dispatch source for SIGINT.
	dispatch_source_t					sourceSigTerm;		// Dispatch source for SIGTERM.
	const char *						domainOverride;		// If non-NULL, server is to use this domain instead of d.test.
	dispatch_group_t					group;				// Dispatch group to signal when command is done.
	dispatch_source_t					processMonitor;		// Process monitor source for process being followed, if any.
	nw_resolver_config_t				secureDNSConfig;	// Resolver configuration for DNS over TLS (DoT).
	mdns_network_policy_t				domainPolicy;		// Networking policy for matching domains to DoT service.
	SecIdentityRef						secIdentity;		// Security identity associated with self-signed DoT certificate.
	nw_listener_t						tlsListener;		// TLS listener.
	mrc_dns_service_registration_t		registration;		// DNS service registration for Do53.
	DNSProtocol							protocol;			// DNS protocol to use, e.g., Do53, DoT, or DoH.
	pid_t								followPID;			// PID of process being followed, if any. If it exits, we exit.
	int32_t								refCount;			// Object's reference count.
	OSStatus							error;				// Error encounted while running server.
	uint16_t							portRequested;		// The port that was requested by the user.
	Boolean								loopbackOnly;		// True if the server should be bound to the loopback interface.
	Boolean								addedResolver;		// True if a resolver entry was added to the system DNS settings.
	Boolean								registerWithSC;		// True if Do53 is to be registered with SystemConfiguration.
	Boolean								matchAllDomains;	// Include '.' as match domain if registering Do53 with SC. [1]
	Boolean								stopped;			// True if the command has stopped.
	
}	DNSServerCmd;

// Notes:
// 1. If registering a Do53 service with SystemConfiguration, include '.' (root domain) as a low-priority match domain.
//    This is useful if a test scenario where a default DNS service needs to be available for domain names for which the
//    DNS server is not authoritative. Note that this means that the server may end up getting queries for domain names
//    that it doesn't recognize.
//
//    For example, suppose that a test involves iterating search domains. Currently, a negative response from a server
//    is required to iterate to the next search domain in the search domain list. If a test device happens to not be
//    connected to any network, then it won't have a DHCP-assigned DNS service to act as a default DNS service that
//    could provide a potentially negative response.
//
//    If the test DNS server includes '.' as a low-priority match domain, then it can act as a default DNS service of
//    last resort. configd usually sets up a default DNS service from DHCP with '.' as its match domain with an order
//    value of 0, which gives that DNS service the highest level of priority when comparing it to a DNS service that
//    has '.' as a match domain, but with a greater order value (lower order value means higher priority).

static DNSServerCmd *	_DNSServerCmdCreate( OSStatus *outError );
static void				_DNSServerCmdRetain( DNSServerCmd *inCmd );
static void				_DNSServerCmdRelease( DNSServerCmd *inCmd );
static void				_DNSServerCmdStart( void *inCtx );
static void				_DNSServerCmdStop( DNSServerCmd *inCmd, OSStatus inError );
static OSStatus			_DNSServerCmdAddExtraLoopbackAddrs( sockaddr_ip *inAddrArray, size_t inAddrCount );
static void				_DNSServerCmdServerStartHandler( uint16_t inDNSServerPort, void *inCtx );
static void				_DNSServerCmdServerStopHandler( OSStatus inError, void *inCtx );
static void				_DNSServerCmdSIGINTHandler( void *inCtx );
static void				_DNSServerCmdSIGTERMHandler( void *inCtx );
static void				_DNSServerCmdShutdown( DNSServerCmd *inCtx, int inSignal );
static void				_DNSServerCmdFollowedProcessHandler( void *inCtx );
static OSStatus			_DNSServerCmdModifySystemSettings( DNSServerCmd *inCmd, uint16_t inListeningPort );
static OSStatus			_DNSServerCmdUndoSystemSettings( DNSServerCmd *inCmd );
static SecIdentityRef	_DNSServerCmdCreateSecIdentity( OSStatus *outError );
static OSStatus			_DNSServerCmdSetUpCertificate( SecIdentityRef inIdentity );
static OSStatus			_DNSServerCmdCleanUpCertificates( void );
static nw_listener_t
	_DNSServerCmdCreateTLSListener(
		sec_identity_t	inIdentity,
		uint16_t		inDesiredPort,
		Boolean			inUseHTTPS,
		OSStatus *		outError );
static OSStatus
	_DNSServerCmdHandleNewTLSConnection(
		DNSServerCmd *	inCmd,
		nw_connection_t	inConnection,
		uint16_t		inTLSListeningPort,
		uint16_t		inDNSServerPort );

ulog_define_ex( kDNSSDUtilIdentifier, DNSServer, kLogLevelInfo, kLogFlags_None, "DNSServer", NULL );
#define ds_ulog( LEVEL, ... )		ulog( &log_category_from_name( DNSServer ), (LEVEL), __VA_ARGS__ )

static void	DNSServerCommand( void )
{
	OSStatus			err;
	DNSServerCmd *		cmd = NULL;
	sockaddr_ip *		sip;
	size_t				addrCount, extraLoopbackV6Count;
	uint16_t			port;
	Boolean				listenOnV4, listenOnV6;
	
	// Check command arguments.
	
	if( gDNSServer_Foreground ) LogControl( "DNSServer:output=file;stdout,DNSServer:flags=time;prefix" );
	err = CheckIntegerArgument( gDNSServer_ResponseDelayMs, "response delay (ms)", 0, INT_MAX );
	require_noerr_quiet( err, exit );
	
	err = CheckIntegerArgument( gDNSServer_DefaultTTL, "default TTL", 0, INT32_MAX );
	require_noerr_quiet( err, exit );
	
	cmd = _DNSServerCmdCreate( &err );
	require_noerr( err, exit );
	
	cmd->followPID		= -1;
	cmd->domainOverride	= gDNSServer_DomainOverride;
	cmd->protocol = (DNSProtocol) CLIArgToValue( "protocol", gDNSServer_Protocol, &err,
		kDNSProtocolStr_Do53,	(int) kDNSProtocol_Do53,
		kDNSProtocolStr_DoT,	(int) kDNSProtocol_DoT,
		kDNSProtocolStr_DoH,	(int) kDNSProtocol_DoH,
		NULL );
	require_noerr_quiet( err, exit );
	
	if( gDNSServer_Port == -1 )
	{
		switch( cmd->protocol )
		{
			case kDNSProtocol_Do53:	port = kDNSPort_Do53;	break;
			case kDNSProtocol_DoT:	port = kDNSPort_DoT;	break;
			case kDNSProtocol_DoH:	port = kDNSPort_DoH;	break;
		}
	}
	else
	{
		err = CheckIntegerArgument( gDNSServer_Port, "port number", 0, UINT16_MAX );
		require_noerr_quiet( err, exit );
		
		port = (uint16_t) gDNSServer_Port;
	}
	cmd->portRequested = port;
	listenOnV4 = gDNSServer_ListenOnV4 || !gDNSServer_ListenOnV6;
	listenOnV6 = gDNSServer_ListenOnV6 || !gDNSServer_ListenOnV4;
	if( gDNSServer_LoopbackOnly || ( cmd->protocol == kDNSProtocol_DoT ) || ( cmd->protocol == kDNSProtocol_DoH ) )
	{
		cmd->loopbackOnly = true;
	}
	if( cmd->loopbackOnly && listenOnV6 )
	{
		err = CheckIntegerArgument( gDNSServer_ExtraV6Count, "extra IPv6", 0, 100 );
		require_noerr_quiet( err, exit );
		extraLoopbackV6Count = (size_t) gDNSServer_ExtraV6Count;
		if( extraLoopbackV6Count > 0 )
		{
			err = CheckRootUser();
			require_noerr_quiet( err, exit );
		}
	}
	else
	{
		extraLoopbackV6Count = 0;
	}
	if( gDNSServer_FollowPID )
	{
		cmd->followPID = _StringToPID( gDNSServer_FollowPID, &err );
		if( err || ( cmd->followPID < 0 ) )
		{
			FPrintF( stderr, "error: Invalid follow PID: %s\n", gDNSServer_FollowPID );
			err = kParamErr;
			goto exit;
		}
	}
	cmd->registerWithSC  = gDNSServer_RegisterWithSC  ? true : false;
	cmd->matchAllDomains = gDNSServer_MatchAllDomains ? true : false;
	
	// Set up IP addresses.
	
	if( listenOnV4 ) ++cmd->addrCount;
	if( listenOnV6 ) ++cmd->addrCount;
	cmd->addrCount += extraLoopbackV6Count;
	check( cmd->addrCount > 0 );
	cmd->addrArray = (sockaddr_ip *) calloc( cmd->addrCount, sizeof( *cmd->addrArray ) );
	require_action( cmd->addrArray, exit, err = kNoMemoryErr );
	
	addrCount = 0;
	if( listenOnV4 )
	{
		sip = &cmd->addrArray[ addrCount++ ];
		_SockAddrInitIPv4( &sip->v4, cmd->loopbackOnly ? INADDR_LOOPBACK : INADDR_ANY, 0 );
	}
	if( listenOnV6 )
	{
		const struct in6_addr * const		addr = cmd->loopbackOnly ? &in6addr_loopback : &in6addr_any;
		
		sip = &cmd->addrArray[ addrCount++ ];
		_SockAddrInitIPv6( &sip->v6, addr->s6_addr, 0, 0 );
	}
	if( extraLoopbackV6Count > 0 )
	{
		err = _DNSServerCmdAddExtraLoopbackAddrs( &cmd->addrArray[ addrCount ], extraLoopbackV6Count );
		require_noerr( err, exit );
		addrCount += extraLoopbackV6Count;
	}
	check( addrCount == cmd->addrCount );
	
	// Start command.
	
	cmd->queue = dispatch_queue_create( "com.apple.dnssdutil.server-command", DISPATCH_QUEUE_SERIAL );
	require_action( cmd->queue, exit, err = kNoResourcesErr );
	
	dispatch_group_enter( cmd->group ); // Enter for the duration of _DNSServerCmdStart().
	dispatch_async_f( cmd->queue, cmd, _DNSServerCmdStart );
    dispatch_group_wait( cmd->group, DISPATCH_TIME_FOREVER );
	
exit:
	if( err ) FPrintF( stderr, "Failed to start DNS server: %#m\n", err );
	if( cmd ) _DNSServerCmdRelease( cmd );
	gExitCode = err ? 1 : 0;
}

//===========================================================================================================================

static DNSServerCmd *	_DNSServerCmdCreate( OSStatus * const outError )
{
	OSStatus			err;
	DNSServerCmd *		obj;
	DNSServerCmd *		cmd = NULL;
	
	obj = (DNSServerCmd *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->refCount	= 1;
	obj->group		= dispatch_group_create();
	require_action( obj->group, exit, err = kNoResourcesErr );
	
	cmd = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) _DNSServerCmdRelease( obj );
	if( outError ) *outError = err;
	return( cmd );
}

//===========================================================================================================================

static void	_DNSServerCmdRetain( DNSServerCmd * const inCmd )
{
	atomic_add_32( &inCmd->refCount, 1 );
}

//===========================================================================================================================

static void	_DNSServerCmdRelease( DNSServerCmd * const inCmd )
{
	if( atomic_add_and_fetch_32( &inCmd->refCount, -1 ) == 0 )
	{
		size_t		i;
		
		check( !inCmd->server );
		check( !inCmd->sourceSigInt );
		check( !inCmd->sourceSigTerm );
		check( !inCmd->processMonitor );
		check( !inCmd->secureDNSConfig );
		check( !inCmd->domainPolicy );
		check( !inCmd->tlsListener );
		for( i = 0; i < inCmd->addrCount; ++i )
		{
			OSStatus								err;
			const struct sockaddr_in6 * const		sin6 = &inCmd->addrArray[ i ].v6;
			int										cmp;
			
			if( sin6->sin6_family != AF_INET6 ) continue;
			cmp = memcmp( sin6->sin6_addr.s6_addr, kExtraLoopbackIPv6Prefix, sizeof( kExtraLoopbackIPv6Prefix ) );
			if( cmp != 0 ) continue;
			err = _InterfaceIPv6AddressRemove( "lo0", sin6->sin6_addr.s6_addr );
			check_noerr( err );
		}
		dispatch_forget( &inCmd->queue );
		dispatch_forget( &inCmd->group );
		ForgetMem( &inCmd->addrArray );
		CFForget( &inCmd->secIdentity );
		free( inCmd );
	}
}

//===========================================================================================================================

static void	_DNSServerCmdStart( void *inCtx )
{
	OSStatus					err;
	DNSServerCmd * const		cmd = (DNSServerCmd *) inCtx;
	size_t						i;
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, cmd->queue, _DNSServerCmdSIGINTHandler, cmd, &cmd->sourceSigInt );
	require_noerr( err, exit );
	dispatch_resume( cmd->sourceSigInt );
	
	signal( SIGTERM, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGTERM, cmd->queue, _DNSServerCmdSIGTERMHandler, cmd, &cmd->sourceSigTerm );
	require_noerr( err, exit );
	dispatch_resume( cmd->sourceSigTerm );
	
	if( cmd->followPID >= 0 )
	{
		err = DispatchProcessMonitorCreate( cmd->followPID, DISPATCH_PROC_EXIT, cmd->queue,
			_DNSServerCmdFollowedProcessHandler, NULL, cmd, &cmd->processMonitor );
		require_noerr( err, exit );
		dispatch_resume( cmd->processMonitor );
	}
	
	err = _DNSServerCreate( cmd->queue, _DNSServerCmdServerStartHandler, _DNSServerCmdServerStopHandler, cmd,
		(unsigned int) gDNSServer_ResponseDelayMs, (uint32_t) gDNSServer_DefaultTTL, cmd->addrArray, cmd->addrCount,
		cmd->domainOverride, gDNSServer_BadUDPMode ? true : false, &cmd->server );
	require_noerr( err, exit );
	
	_DNSServerCmdRetain( cmd );
	dispatch_group_enter( cmd->group ); // Enter for the DNS server's lifetime.
	_DNSServerSetPort( cmd->server, _DNSProtocolIsSecure( cmd->protocol ) ? 0 : cmd->portRequested );
	for( i = 0; i < gDNSServer_IgnoredQTypesCount; ++i )
	{
		uint16_t		qtype;
		
		err = RecordTypeFromArgString( gDNSServer_IgnoredQTypes[ i ], &qtype );
		require_noerr( err, exit );
		
		err = _DNSServerSetIgnoredQType( cmd->server, qtype );
		require_noerr( err, exit );
	}
	_DNSServerStart( cmd->server );
	
exit:
	if( err ) _DNSServerCmdStop( cmd, err );
	dispatch_group_leave( cmd->group ); // Leave for _DNSServerCmdStart().
}

//===========================================================================================================================

static void	_DNSServerCmdStop( DNSServerCmd *inCmd, OSStatus inError )
{
	if( !inCmd->stopped )
	{
		inCmd->stopped = true;
		if( !inCmd->error ) inCmd->error = inError;
		if( inCmd->server )
		{
			_DNSServerStop( inCmd->server );
			CFForget( &inCmd->server );
		}
		dispatch_source_forget( &inCmd->sourceSigInt );
		dispatch_source_forget( &inCmd->sourceSigTerm );
		dispatch_source_forget( &inCmd->processMonitor );
	}
}

//===========================================================================================================================

static OSStatus	_DNSServerCmdAddExtraLoopbackAddrs( sockaddr_ip *inAddrArray, size_t inAddrCount )
{
	OSStatus		err;
	uint8_t			addrV6[ 16 ];
	size_t			i;
	
	check_compile_time_code( sizeof( kExtraLoopbackIPv6Prefix ) == 8 );
	memcpy( addrV6, kExtraLoopbackIPv6Prefix, 8 );	// 64-bit prefix
	RandomBytes( &addrV6[ 8 ], 4 );					// 32-bit random
	WriteBig32Typed( &addrV6[ 12 ], 2 );			// 16-bit base offset starting at 2
	for( i = 0; i < inAddrCount; ++i )
	{
		struct sockaddr_in6 * const		sin6 = &inAddrArray[ i ].v6;
		
		err = _InterfaceIPv6AddressAdd( "lo0", addrV6, kExtraLoopbackIPv6PrefixBitLen );
		require_noerr( err, exit );
		
		_SockAddrInitIPv6( sin6, addrV6, 0, 0 );
		BigEndianIntegerIncrement( addrV6, sizeof( addrV6 ) );
	}
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static void
_DNSServerCmdServerStartHandler( const uint16_t inDNSServerPort, void * const inCtx )
{
	OSStatus					err;
	DNSServerCmd * const		cmd			= (DNSServerCmd *) inCtx;
	sec_identity_t				secIdentity	= NULL;
	
	if( cmd->loopbackOnly )
	{
		if( _DNSProtocolIsSecure( cmd->protocol ) )
		{
			Boolean		useHTTPS;
			
			err = _DNSServerCmdCleanUpCertificates();
			require_noerr( err, exit );
			
			check( !cmd->secIdentity );
			cmd->secIdentity = _DNSServerCmdCreateSecIdentity( &err );
			require_noerr( err, exit );
			
			err = _DNSServerCmdSetUpCertificate( cmd->secIdentity );
			require_noerr( err, exit );
			
			secIdentity = sec_identity_create( cmd->secIdentity );
			require_action( secIdentity, exit, err = kNoResourcesErr );
			
			useHTTPS = ( cmd->protocol == kDNSProtocol_DoH );
			cmd->tlsListener = _DNSServerCmdCreateTLSListener( secIdentity, cmd->portRequested, useHTTPS, &err );
			require_noerr( err, exit );
			
			_DNSServerCmdRetain( cmd );
			dispatch_group_enter( cmd->group ); // Enter for the TLS listener's lifetime.
			nw_listener_set_state_changed_handler( cmd->tlsListener,
			^( const nw_listener_state_t inState, const nw_error_t inError )
			{
				if( cmd->tlsListener )
				{
					switch( inState )
					{
						case nw_listener_state_ready:
						{
							const uint16_t listeningPort = nw_listener_get_port( cmd->tlsListener );
							ds_ulog( kLogLevelInfo, "Listening for TLS connections on port %u\n", listeningPort );
							
							const OSStatus settingsErr = _DNSServerCmdModifySystemSettings( cmd, listeningPort );
							if( settingsErr )
							{
								ds_ulog( kLogLevelError, "Failed to modify system settings: %#m\n", settingsErr );
								_DNSServerCmdStop( cmd, settingsErr );
							}
							break;
						}
						case nw_listener_state_failed:
						{
							OSStatus		listenerErr;
							
							ds_ulog( kLogLevelError, "TLS listener failed: %@\n", inError );
							listenerErr = nw_error_get_error_code( inError );
							if( !listenerErr ) listenerErr = kUnknownErr;
							_DNSServerCmdStop( cmd, listenerErr );
							break;
						}
						case nw_listener_state_invalid:
						case nw_listener_state_waiting:
						case nw_listener_state_cancelled:
							break;
					}
				}
				if( inState == nw_listener_state_cancelled )
				{
					dispatch_group_leave( cmd->group ); // Leave for the TLS listener.
					_DNSServerCmdRelease( cmd );
				}
			} );
			nw_listener_set_new_connection_handler( cmd->tlsListener,
			^( const nw_connection_t inConnection )
			{
				if( cmd->tlsListener )
				{
					const uint16_t listeningPort = nw_listener_get_port( cmd->tlsListener );
					const OSStatus connectionErr = _DNSServerCmdHandleNewTLSConnection( cmd, inConnection, listeningPort,
						inDNSServerPort );
					if( connectionErr ) _DNSServerCmdStop( cmd, connectionErr );
				}
			} );
			nw_listener_set_queue( cmd->tlsListener, cmd->queue );
			nw_listener_start( cmd->tlsListener );
		}
		else
		{
			err = _DNSServerCmdModifySystemSettings( cmd, inDNSServerPort );
			require_noerr_action( err, exit, ds_ulog( kLogLevelError,
				"Failed to modify system settings: %#m\n", err ) );
		}
	}
	err = kNoErr;
	
exit:
	if( secIdentity ) sec_release( secIdentity );
	if( err ) _DNSServerCmdStop( cmd, err );
}

//===========================================================================================================================

static void	_DNSServerCmdServerStopHandler( OSStatus inError, void *inCtx )
{
	OSStatus					err;
	DNSServerCmd * const		cmd = (DNSServerCmd *) inCtx;
	
	if( inError )
	{
		ds_ulog( kLogLevelError, "The server stopped unexpectedly with error: %#m.\n", inError );
		if( !cmd->error ) cmd->error = inError;
	}
	err = _DNSServerCmdUndoSystemSettings( cmd );
	if( err )
	{
		ds_ulog( kLogLevelError, "Failed to undo system settings: %#m\n", err );
		if( !cmd->error ) cmd->error = err;
	}
	if( _DNSProtocolIsSecure( cmd->protocol ) )
	{
		err = _DNSServerCmdCleanUpCertificates();
		if( !cmd->error ) cmd->error = err;
	}
	if( cmd->tlsListener )
	{
		nw_listener_cancel( cmd->tlsListener );
		nw_forget( &cmd->tlsListener );
	}
	_DNSServerCmdStop( cmd, cmd->error );
	dispatch_group_leave( cmd->group ); // Leave for the DNS server.
	_DNSServerCmdRelease( cmd );
}

//===========================================================================================================================

static void	_DNSServerCmdSIGINTHandler( void *inCtx )
{
	_DNSServerCmdShutdown( (DNSServerCmd *) inCtx, SIGINT );
}

//===========================================================================================================================

static void	_DNSServerCmdSIGTERMHandler( void *inCtx )
{
	_DNSServerCmdShutdown( (DNSServerCmd *) inCtx, SIGTERM );
}

//===========================================================================================================================

static void	_DNSServerCmdShutdown( DNSServerCmd *inCmd, int inSignal )
{
	dispatch_source_forget( &inCmd->sourceSigInt );
	dispatch_source_forget( &inCmd->sourceSigTerm );
	dispatch_source_forget( &inCmd->processMonitor );
	if( inSignal == 0 )
	{
		ds_ulog( kLogLevelNotice, "Exiting: followed process (%lld) exited\n", (int64_t) inCmd->followPID );
	}
	else
	{
		const char *		sigName;
		
		switch( inSignal )
		{
			case SIGINT:	sigName = "SIGINT";		break;
			case SIGTERM:	sigName = "SIGTERM";	break;
			default:		sigName = "???";		break;
		}
		ds_ulog( kLogLevelNotice, "Exiting: received signal %d (%s)\n", inSignal, sigName );
	}
	_DNSServerCmdStop( inCmd, kNoErr );
}

//===========================================================================================================================

static void	_DNSServerCmdFollowedProcessHandler( void *inCtx )
{
	DNSServerCmd * const		cmd = (DNSServerCmd *) inCtx;
	
	if( dispatch_source_get_data( cmd->processMonitor ) & DISPATCH_PROC_EXIT ) _DNSServerCmdShutdown( cmd, 0 );
}
//===========================================================================================================================

#define kDNSServerHostname			"dns.apple.test"
#define kDNSServerDoHURLPath		"/dns-query"
#define kDNSServerServiceID			CFSTR( "com.apple.dnssdutil.server" )

static OSStatus	_DNSServerCmdModifySystemSettings( DNSServerCmd * const inCmd, const uint16_t inListeningPort )
{
	OSStatus						err;
	mdns_dns_configurator_t			configurator	= NULL;
	mdns_dns_service_definition_t	definition		= NULL;
	nw_resolver_config_t			secureDNSConfig	= NULL;
	nw_endpoint_t					endpoint		= NULL;
	const char *					primaryDomainStr;
	CFArrayRef						domains			= NULL;
	size_t							i;
	char							dnssecDomainStr[ kDNSServiceMaxDomainName ];
	
	require_action_quiet( inCmd->addrCount > 0, exit, err = kCountErr );
	
	err = DomainNameToString( kDNSServerDomain_DNSSEC, NULL, dnssecDomainStr, NULL );
	require_noerr( err, exit );
	
	primaryDomainStr = inCmd->domainOverride ? inCmd->domainOverride : "d.test.";
	const char * const domainStrings[] =
	{
		primaryDomainStr,
		dnssecDomainStr,
		kDNSServerReverseIPv4DomainStr,
		kDNSServerReverseIPv6DomainStr
	};
	switch( inCmd->protocol )
	{
		case kDNSProtocol_DoT:
		case kDNSProtocol_DoH:
		{
			// Try to clean up a stale Do53 resolver entry for a previous server from the system's DNS configuration in case
			// one happens to still be present. Such an entry may interfere with the system settings for the DoT server if
			// they're for the same domain, i.e., queries may be incorrectly sent to the Do53 server which no longer exists.
			
			Boolean usingDefaultPort;
			mdns_dns_configurator_deregister_configuration( kDNSServerServiceID, CFSTR( kDNSSDUtilIdentifier ) );
			if( inCmd->protocol == kDNSProtocol_DoT )
			{
				usingDefaultPort = ( inListeningPort == kDNSPort_DoT );
				const uint16_t endpointPort = usingDefaultPort ? 0 : inListeningPort;
				endpoint = nw_endpoint_create_host_with_numeric_port( kDNSServerHostname, endpointPort );
				require_action( endpoint, exit, err = kUnknownErr );
				
				secureDNSConfig = nw_resolver_config_create_tls( endpoint );
				nw_forget( &endpoint );
				require_action( secureDNSConfig, exit, err = kUnknownErr );
			}
			else
			{
				char *templateURL = NULL;
				usingDefaultPort = ( inListeningPort == kDNSPort_DoH );
				if( usingDefaultPort )
				{
					ASPrintF( &templateURL, "https://%s%s", kDNSServerHostname, kDNSServerDoHURLPath );
					require_action( templateURL, exit, err = kNoMemoryErr );
				}
				else
				{
					ASPrintF( &templateURL, "https://%s:%u%s", kDNSServerHostname, inListeningPort, kDNSServerDoHURLPath );
					require_action( templateURL, exit, err = kNoMemoryErr );
				}
				endpoint = nw_endpoint_create_url( templateURL );
				ForgetMem( &templateURL );
				require_action( endpoint, exit, err = kUnknownErr );
				
				secureDNSConfig = nw_resolver_config_create_https( endpoint );
				nw_forget( &endpoint );
				require_action( secureDNSConfig, exit, err = kUnknownErr );
			}
			domains = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
			require_action( domains, exit, err = kNoResourcesErr );
			
			for( i = 0; i < countof( domainStrings ); ++i )
			{
				const char * const		domainStr = domainStrings[ i ];
				size_t					len;
				char *					domainStrDup;
				
				len = strlen( domainStr );
				if( len <= 0 ) continue;
				
				// NECP doesn't properly handle trailing root dots at the end of FQDNs, so remove them if present.
				
				if( domainStr[ len - 1 ] == '.' ) --len;
				domainStrDup = NULL;
				ASPrintF( &domainStrDup, "%.*s", (int) len, domainStr );
				require_action( domainStrDup, exit, err = kNoMemoryErr );
				
				nw_resolver_config_add_match_domain( secureDNSConfig, domainStrDup );
				err = CFPropertyListAppendFormatted( NULL, domains, "%s", domainStrDup );
				ForgetMem( &domainStrDup );
				require_noerr( err, exit );
			}
			for( i = 0; i < inCmd->addrCount; ++i )
			{
				sockaddr_ip serverAddr;
				SockAddrCopy( &inCmd->addrArray[ i ], &serverAddr );
				SockAddrSetPort( &serverAddr.sa, usingDefaultPort ? 0 : inListeningPort );
				char serverAddrStr[ kSockAddrStringMaxSize ];
				err = SockAddrToString( &serverAddr.sa, kSockAddrStringFlagsNone, serverAddrStr );
				require_noerr( err, exit );
				
				nw_resolver_config_add_name_server( secureDNSConfig, serverAddrStr );
			}
			nw_resolver_config_set_interface_name( secureDNSConfig, "lo0" );
			const bool ok = nw_resolver_config_publish( secureDNSConfig );
			require_action( ok, exit, err = kUnknownErr );
			
			if( domains )
			{
				uuid_t		resolverConfigID;
				
				uuid_clear( resolverConfigID );
				nw_resolver_config_get_identifier( secureDNSConfig, resolverConfigID );
				check( !inCmd->domainPolicy );
				inCmd->domainPolicy = mdns_system_add_net_agent_match_domains( resolverConfigID, domains );
			}
			inCmd->secureDNSConfig = secureDNSConfig;
			secureDNSConfig = NULL;
			break;
		}
		case kDNSProtocol_Do53:
		{
			if( inCmd->registerWithSC )
			{
				configurator = mdns_dns_configurator_create_with_cfstring_id( kDNSServerServiceID, &err );
				require_noerr( err, exit );
				
				err = mdns_dns_configurator_add_domain( configurator, primaryDomainStr, 0 );
				require_noerr( err, exit );
				
				err = mdns_dns_configurator_add_domain( configurator, dnssecDomainStr, 0 );
				require_noerr( err, exit );
				
				err = mdns_dns_configurator_add_domain( configurator, kDNSServerReverseIPv4DomainStr, 0 );
				require_noerr( err, exit );
				
				err = mdns_dns_configurator_add_domain( configurator, kDNSServerReverseIPv6DomainStr, 0 );
				require_noerr( err, exit );
				
				if( inCmd->matchAllDomains )
				{
					// Use the highest possible order value to make the root match domain as low-priority as possible.
					
					err = mdns_dns_configurator_add_domain( configurator, ".", UINT32_MAX );
					require_noerr( err, exit );
				}
				for( i = 0; i < inCmd->addrCount; ++i )
				{
					const sockaddr_ip * const		sip = &inCmd->addrArray[ i ];
					char							addrStr[ kSockAddrStringMaxSize ];
					
					err = SockAddrToString( &sip->sa, kSockAddrStringFlagsNoPort, addrStr );
					require_noerr( err, exit );
					
					err = mdns_dns_configurator_add_server_address_string( configurator, addrStr );
					require_noerr( err, exit );
				}
				err = mdns_dns_configurator_set_port( configurator, inListeningPort );
				require_noerr( err, exit );
				
				err = mdns_dns_configurator_set_interface( configurator, "lo0" );
				require_noerr( err, exit );
				
				err = mdns_dns_configurator_register( configurator, CFSTR( kDNSSDUtilIdentifier ) );
				require_noerr( err, exit );
				
				inCmd->addedResolver = true;
			}
			else
			{
				definition = mdns_dns_service_definition_create();
				require_action( definition, exit, err = kNoResourcesErr );
				
				for( i = 0; i < countof( domainStrings ); ++i )
				{
					const char * const domainStr = domainStrings[ i ];
					mdns_domain_name_t domain = mdns_domain_name_create( domainStr, mdns_domain_name_create_opts_none,
						&err );
					require_noerr_action( err, exit, ds_ulog( kLogLevelError,
						"error: Failed to create domain name for '%s': %#m\n", domainStr, err ) );
					
					mdns_dns_service_definition_add_domain( definition, domain );
					mdns_forget( &domain );
				}
				for( i = 0; i < inCmd->addrCount; ++i )
				{
					sockaddr_ip sip = inCmd->addrArray[ i ];
					SockAddrSetPort( &sip, inListeningPort );
					char addrStr[ kSockAddrStringMaxSize ];
					err = SockAddrToString( &sip.sa, kSockAddrStringFlagsNone, addrStr );
					require_noerr( err, exit );
					
					mdns_address_t serverAddr = mdns_address_create_from_ip_address_string( addrStr );
					require_action( serverAddr, exit, err = kNoResourcesErr; ds_ulog( kLogLevelError,
						"error: Failed to create address for '%s'\n", addrStr ) );
					
					err = mdns_dns_service_definition_append_server_address( definition, serverAddr );
					mdns_forget( &serverAddr );
					require_noerr( err, exit );
				}
				const uint32_t ifIndex = if_nametoindex( "lo0" );
				err = map_global_value_errno( ifIndex != 0, ifIndex );
				require_noerr_action_quiet( err, exit, ds_ulog( kLogLevelError,
					"Failed to get interface index for lo0: %#m", err ) );
				
				mdns_dns_service_definition_set_interface_index( definition, ifIndex, false );
				inCmd->registration = mrc_dns_service_registration_create( definition );
				mdns_forget( &definition );
				require_action_quiet( inCmd->registration, exit, err = kNoResourcesErr );
				
				mrc_dns_service_registration_set_queue( inCmd->registration, inCmd->queue );
				mrc_dns_service_registration_set_event_handler( inCmd->registration,
				^( const mrc_dns_service_registration_event_t inEvent, const OSStatus inError )
				{
					switch( inEvent )
					{
						case mrc_dns_service_registration_event_started:
							ds_ulog( kLogLevelInfo, "DNS service registration started\n" );
							break;
						
						case mrc_dns_service_registration_event_interruption:
							ds_ulog( kLogLevelInfo, "DNS service registration interrupted\n" );
							break;
						
						case mrc_dns_service_registration_event_invalidation:
							if( inError )
							{
								ds_ulog( kLogLevelError, "DNS service registration invalidated with error: %#m\n", inError );
							}
							else
							{
								ds_ulog( kLogLevelInfo, "DNS service registration gracefully invalidated\n" );
							}
							break;

						case mrc_dns_service_registration_event_connection_error:
							ds_ulog( kLogLevelFault, "DNS service registration invalid event with error: %#m\n",
								inError );
							break;
					}
				} );
				mrc_dns_service_registration_activate( inCmd->registration );
			}
			break;
		}
		default:
			err = kInternalErr;
			goto exit;
	}
	
exit:
	mdns_forget( &configurator );
	mdns_forget( &definition );
	nw_forget( &secureDNSConfig );
	nw_forget( &endpoint );
	CFForget( &domains );
	return( err );
}

//===========================================================================================================================

static OSStatus	_DNSServerCmdUndoSystemSettings( DNSServerCmd * const inCmd )
{
	OSStatus		err;
	
	if( inCmd->secureDNSConfig )
	{
		nw_resolver_config_unpublish( inCmd->secureDNSConfig );
		nw_forget( &inCmd->secureDNSConfig );
	}
	if( inCmd->domainPolicy )
	{
		mdns_system_remove_network_policy( inCmd->domainPolicy );
		inCmd->domainPolicy = NULL;
	}
	if( inCmd->addedResolver )
	{
		err = mdns_dns_configurator_deregister_configuration( kDNSServerServiceID, CFSTR( kDNSSDUtilIdentifier ) );
		require_noerr( err, exit );
		
		inCmd->addedResolver = false;
	}
	mrc_dns_service_registration_forget( &inCmd->registration );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static SecIdentityRef	_DNSServerCmdCreateSecIdentity( OSStatus * const outError )
{
	SecIdentityRef		identity	= NULL;
	CFArrayRef			certSubject	= NULL;
	CFDictionaryRef		csrParams	= NULL;
	
	OSStatus err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &certSubject,
		"["
			"[[%O%O]]"	// kSecOidCommonName
			"[[%O%O]]"	// kSecOidOrganizationalUnit
			"[[%O%O]]"	// kSecOidOrganization
		"]",
		kSecOidCommonName,			CFSTR( kDNSServerHostname ),
		kSecOidOrganizationalUnit,	CFSTR( "Networking" ),
		kSecOidOrganization,		CFSTR( "Apple Inc." ) );
	require_noerr( err, exit );
	
	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &csrParams,
		"{"
			"%kO="			// kSecSubjectAltName
			"{"
				"%kO=%O"	// kSecSubjectAltNameDNSName
			"}"
			"%kO=%lli"		// kSecCertificateLifetime
		"}",
		kSecSubjectAltName,
		kSecSubjectAltNameDNSName,	CFSTR( kDNSServerHostname ),
		kSecCertificateLifetime,	(int64_t)( 24 * kSecondsPerHour ) );
	require_noerr( err, exit );
	
	identity = mdns_security_create_self_signed_certificate( certSubject, csrParams, kSecAttrKeyTypeECSECPrimeRandom, 256,
		&err );
	require_noerr( err, exit );
	
exit:
	CFForget( &certSubject );
	CFForget( &csrParams );
	if( outError ) *outError = err;
	return( identity );
}

//===========================================================================================================================

static OSStatus	_DNSServerCmdSetUpCertificate( const SecIdentityRef inIdentity )
{
	SecCertificateRef		cert			= NULL;
	SecPolicyRef			policySSL		= NULL;
	CFDictionaryRef			trustSettings	= NULL;
	
	OSStatus err = SecIdentityCopyCertificate( inIdentity, &cert );
	require_noerr( err, exit );
	
	err = KeychainAddFormatted( NULL,
		"{"
			"%kO=%O"	// kSecClass
			"%kO=%O"	// kSecValueRef
		"}",
		kSecClass,		kSecClassCertificate,
		kSecValueRef,	cert );
	require_noerr( err, exit );
	
	policySSL = SecPolicyCreateSSL( true, NULL );
	require_action( policySSL, exit, err = kNoResourcesErr );
	
	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &trustSettings,
		"{"
			"%kO=%O"	// kSecTrustSettingsPolicy
			"%kO=%lli"	// kSecTrustSettingsResult
		"}",
		kSecTrustSettingsPolicy, policySSL,
		kSecTrustSettingsResult, (int64_t) kSecTrustSettingsResultTrustRoot );
	require_noerr( err, exit );
	
	err = mdns_security_set_per_user_certificate_trust_settings( cert, trustSettings );
	require_noerr( err, exit );
	
exit:
	CFForget( &cert );
	CFForget( &policySSL );
	CFForget( &trustSettings );
	return( err );
}

//===========================================================================================================================

static OSStatus	_DNSServerCmdCleanUpCertificates( void )
{
	OSStatus					err;
	CFMutableDictionaryRef		certQuery = NULL;
	CFTypeRef					certResults = NULL;
	
	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &certQuery,
		"{"
			"%kO=%O"	// kSecClass
			"%kO=%O"	// kSecAttrLabel
			"%kO=%O"	// kSecMatchLimit
			"%kO=%O"	// kSecReturnAttributes
			"%kO=%O"	// kSecReturnRef
		"}",
		kSecClass,				kSecClassCertificate,
		kSecAttrLabel,			CFSTR( kDNSServerHostname ),
		kSecMatchLimit,			kSecMatchLimitAll,
		kSecReturnAttributes,	kCFBooleanTrue,
		kSecReturnRef,			kCFBooleanTrue );
	require_noerr( err, exit );
	
	err = SecItemCopyMatching( certQuery, &certResults );
	if( err == errSecItemNotFound )
	{
		err = kNoErr;
		goto exit;
	}
	require_noerr( err, exit );
	
	if( certResults )
	{
		const CFArrayApplierBlock applierBlock =
		^( const void * const inValue )
		{
			const CFDictionaryRef item = CFGetCFDictionary( inValue, NULL );
			require_return( item );
			
			const SecCertificateRef itemCert = (SecCertificateRef) CFDictionaryGetValue( item, kSecValueRef );
			require_return( itemCert );
			
			OSStatus localErr = mdns_security_remove_per_user_certificate_trust_settings( itemCert );
			require_noerr_fatal( localErr,
				"mdns_security_remove_per_user_certificate_trust_settings() failed: %#m", localErr );
		};
		const CFArrayRef certResultsArray = CFGetCFArray( certResults, NULL );
		if( certResultsArray )
		{
			CFArrayApplyBlock( certResultsArray, CFRangeMake( 0, CFArrayGetCount( certResultsArray ) ), applierBlock );
		}
		else
		{
			applierBlock( certResults );
		}
		err = KeychainDeleteFormatted(
			"{"
				"%kO=%O"	// kSecClass
				"%kO=%O"	// kSecAttrLabel
			"}",
			kSecClass,		kSecClassCertificate,
			kSecAttrLabel,	CFSTR( kDNSServerHostname ) );
		require_noerr( err, exit );
	}
	
exit:
	CFForget( &certQuery );
	CFForget( &certResults );
	return( err );
}

//===========================================================================================================================

static nw_listener_t
	_DNSServerCmdCreateTLSListener(
		const sec_identity_t	inIdentity,
		const uint16_t			inDesiredPort,
		const Boolean			inUseHTTPS,
		OSStatus * const		outError )
{
	OSStatus					err;
	nw_parameters_t				params		= NULL;
	nw_protocol_options_t		options		= NULL;
	nw_protocol_stack_t			stack		= NULL;
	nw_interface_t				interface	= NULL;
	nw_listener_t				listener	= NULL;
	__block bool				tlsWasConfigured;
	
	tlsWasConfigured = false;
	const nw_parameters_configure_protocol_block_t configureTLS =
	^( const nw_protocol_options_t inOptions )
	{
		sec_protocol_options_t		tlsOptions;
		
		tlsOptions = nw_tls_copy_sec_protocol_options( inOptions );
		require_return( tlsOptions );
		
		sec_protocol_options_set_local_identity( tlsOptions, inIdentity );
		sec_protocol_options_append_tls_ciphersuite_group( tlsOptions, tls_ciphersuite_group_default );
		if( inUseHTTPS )
		{
			sec_protocol_options_add_tls_application_protocol( tlsOptions, "h2" );
		}

		sec_release( tlsOptions );
		tlsWasConfigured = true;
	};
	params = nw_parameters_create_secure_tcp( configureTLS, NW_PARAMETERS_DEFAULT_CONFIGURATION );
	require_action( params, exit, err = kNoResourcesErr );
	require_action( tlsWasConfigured, exit, err = kSecurityErr );
	
	if( inUseHTTPS )
	{
		nw_parameters_set_attach_protocol_listener( params, true );
		options = nw_http2_create_options();
		require_action( options, exit, err = kNoResourcesErr );
		
		stack = nw_parameters_copy_default_protocol_stack( params );
		require_action( stack, exit, err = kNoResourcesErr );
		
		nw_protocol_stack_prepend_application_protocol( stack, options );
	}
	interface = nw_interface_create_with_name( "lo0" );
	require_action( interface, exit, err = kNoResourcesErr );
	
	nw_parameters_require_interface( params, interface );
	nw_parameters_set_server_mode( params, true );
	nw_parameters_set_reuse_local_address( params, true );
	if( inDesiredPort > 0 )
	{
		char portStr[ 16 ];
		SNPrintF( portStr, sizeof( portStr ), "%u", inDesiredPort );
		listener = nw_listener_create_with_port( portStr, params );
		require_action( listener, exit, err = kNoResourcesErr );
	}
	else
	{
		listener = nw_listener_create( params );
		require_action( listener, exit, err = kNoResourcesErr );
	}
	err = kNoErr;
	
exit:
	nw_forget( &params );
	nw_forget( &options );
	nw_forget( &stack );
	nw_forget( &interface );
	if( outError ) *outError = err;
	return( listener );
}

//===========================================================================================================================

static OSStatus
	_DNSServerCmdHandleNewTLSConnection(
		DNSServerCmd * const	inCmd,
		const nw_connection_t	inConnection,
		const uint16_t			inTLSListeningPort,
		const uint16_t			inDNSServerPort )
{
	OSStatus					err;
	nw_connection_t				clientConnection;
	nw_parameters_t				params;
	nw_endpoint_t				localEndpoint	= NULL;
	const struct sockaddr *		localAddr;
	sockaddr_ip					serverAddr;
	mdns_dns_relay_t			relay			= NULL;
	
	clientConnection = inConnection;
	params = nw_connection_copy_parameters( clientConnection );
	require_action( params, exit, err = kNoResourcesErr );
	
	localEndpoint = nw_parameters_copy_local_endpoint( params );
	require_action( localEndpoint, exit, err = kNoResourcesErr );
	
	localAddr = nw_endpoint_get_address( localEndpoint );
	require_action( localAddr, exit, err = kUnexpectedErr );
	
	memset( &serverAddr, 0, sizeof( serverAddr ) );
	switch( localAddr->sa_family )
	{
		case AF_INET:
		case AF_INET6:
			SockAddrCopy( localAddr, &serverAddr.sa );
			SockAddrSetPort( &serverAddr.sa, inDNSServerPort );
			break;
		
		default:
			err = kAddressErr;
			goto exit;
	}
	if( inCmd->protocol == kDNSProtocol_DoH )
	{
		mdns_doh_relay_t		relayDoH;
		
		relayDoH = mdns_doh_relay_create( NULL );
		require_action( relayDoH, exit, err = kNoResourcesErr );
		
		relay = mdns_dns_relay_upcast( relayDoH );
		err = mdns_doh_relay_set_request_uri_path( relayDoH, kDNSServerDoHURLPath );
		require_noerr( err, exit );
		
		err = mdns_doh_relay_set_host_and_port( relayDoH, kDNSServerHostname, inTLSListeningPort );
		require_noerr( err, exit );
	}
	else
	{
		mdns_dot_relay_t		relayDoT;
		
		relayDoT = mdns_dot_relay_create( NULL );
		require_action( relayDoT, exit, err = kNoResourcesErr );
		
		relay = mdns_dns_relay_upcast( relayDoT );
	}
	mdns_dns_relay_set_client_connection( relay, clientConnection );
	clientConnection = NULL;
	mdns_dns_relay_set_server_address( relay, &serverAddr );
	mdns_dns_relay_activate( relay );
	err = kNoErr;
	
exit:
	nw_forget( &params );
	nw_forget( &localEndpoint );
	mdns_forget( &relay );
	if( clientConnection ) nw_connection_cancel( clientConnection );
	return( err );
}

//===========================================================================================================================

typedef struct DNSServerConnectionPrivate *		DNSServerConnectionRef;

typedef struct DNSServerDelayedResponse		DNSServerDelayedResponse;
struct DNSServerDelayedResponse
{
	DNSServerDelayedResponse *		next;		// Next delayed response in list.
	sockaddr_ip						client;		// Destination address.
	uint64_t						dueTicks;	// Time, in ticks, when send is due.
	uint8_t *						msgPtr;		// Response message pointer.
	size_t							msgLen;		// Response message length.
	size_t							index;		// Address index.
	SocketRef						sock;		// Socket to use for send.
};

struct DNSServerPrivate
{
	CFRuntimeBase					base;				// CF object base.
	uint8_t *						domain;				// Parent domain of server's resource records. (malloc'd)
	dispatch_queue_t				queue;				// Queue for DNS server's events.
	sockaddr_ip *					addrArray;			// Array of addresses to listen on.
	size_t							addrCount;			// Number of addresses to listen on.
	dispatch_source_t *				readSourceArrayUDP;	// Array of read sources for UDP sockets.
	dispatch_source_t *				readSourceArrayTCP;	// Array of read sources for TCP listening sockets.
	DNSServerConnectionRef			connectionList;		// List of TCP connections.
	dispatch_source_t				connectionTimer;	// Timer for idle connections.
	DNSServerStartHandler_f			startHandler;		// User's activation handler.
	DNSServerStopHandler_f			stopHandler;		// User's invalidation handler.
    void *							userContext;		// User's handler context.
	DNSServerDelayedResponse *		responseList;		// List of delayed UDP responses.
	dispatch_source_t				responseTimer;		// Timer for when to send next delayed response.
	int *							ignoredQTypes;		// Array of QTYPEs to ignore.
	size_t							ignoredQTypeCount;	// Number of QTYPEs to ignore.
	unsigned int					responseDelayMs;	// Response delay in milliseconds.
	uint32_t						defaultTTL;			// Default TTL for resource records.
	uint32_t						serial;				// Serial number for SOA record.
	OSStatus						stopErr;			// The error, if any, that caused the server to stop.
	uint16_t						portRequested;		// The port that was requested by the user.
	Boolean							started;			// True if the server was started.
	Boolean							stopped;			// True if the server was stopped.
	Boolean							badUDPMode;			// True if the server runs in Bad UDP mode.
	Boolean							suspended;			// True if the server is suspended (intentionally not responding).
};

static void	_DNSServerUDPReadHandler( void *inContext );
static OSStatus
	_DNSServerScheduleDelayedResponse(
		DNSServerRef			inServer,
		SocketRef				inSock,
		const struct sockaddr *	inDestAddr,
		uint8_t *				inMsgPtr,
		size_t					inMsgLen,
		size_t					inIndex );
static void	_DNSServerDelayedResponseFree( DNSServerDelayedResponse *inResponse );
static void	_DNSServerDelayedResponseListFree( DNSServerDelayedResponse *inList );
static void	_DNSServerTCPAcceptHandler( void *inContext );
static void	_DNSServerConnectionTimerHandler( void *inContext );
static void	_DNSServerResetConnectionTimerMs( DNSServerRef me, uint64_t inTimeoutMs );
static OSStatus
	_DNSServerAnswerQuery(
		DNSServerRef	inServer,
		const uint8_t *	inMsgPtr,
		size_t			inMsgLen,
		size_t			inIndex,
		Boolean			inForTCP,
		uint8_t **		outResponsePtr,
		size_t *		outResponseLen );

#define _DNSServerAnswerQueryForUDP( SERVER, QUERY_PTR, QUERY_LEN, INDEX, RESPONSE_PTR, RESPONSE_LEN ) \
	_DNSServerAnswerQuery( SERVER, QUERY_PTR, QUERY_LEN, INDEX, false, RESPONSE_PTR, RESPONSE_LEN )

#define _DNSServerAnswerQueryForTCP( SERVER, QUERY_PTR, QUERY_LEN, INDEX, RESPONSE_PTR, RESPONSE_LEN ) \
	_DNSServerAnswerQuery( SERVER, QUERY_PTR, QUERY_LEN, INDEX, true, RESPONSE_PTR, RESPONSE_LEN )

CF_CLASS_DEFINE( DNSServer );

struct DNSServerConnectionPrivate
{
	CFRuntimeBase				base;				// CF object base.
	DNSServerConnectionRef		next;				// Next connection in list.
	DNSServerRef				server;				// Back pointer to server object.
	sockaddr_ip					local;				// TCP connection's local address.
	sockaddr_ip					remote;				// TCP connection's remote address.
	size_t						index;				// Sever address index.
	uint64_t					expirationTicks;	// Expiration time in ticks. Renewed upon receiving a complete query.
	dispatch_source_t			readSource;			// Dispatch read source for TCP connection.
	dispatch_source_t			writeSource;		// Dispatch write source for TCP connection.
	size_t						offset;				// Offset into receive buffer.
	void *						msgPtr;				// Pointer to dynamically allocated message buffer.
	size_t						msgLen;				// Length of message buffer.
	iovec_t						iov[ 2 ];			// IO vector for writing response message.
	iovec_t *					iovPtr;				// Vector pointer for SocketWriteData().
	int							iovCount;			// Vector count for SocketWriteData().
	Boolean						readSuspended;		// True if the read source is currently suspended.
	Boolean						writeSuspended;		// True if the write source is currently suspended.
	Boolean						haveLen;			// True if currently receiving message instead of message length.
	uint8_t						lenBuf[ 2 ];		// Buffer for two-octet message length field.
};

static CFTypeID	DNSServerConnectionGetTypeID( void );
static OSStatus
	_DNSServerConnectionCreate(
		DNSServerRef				inServer,
		const struct sockaddr *		inLocal,
		const struct sockaddr *		inRemote,
		size_t						inIndex,
		DNSServerConnectionRef *	outCnx );
static OSStatus	_DNSServerConnectionStart( DNSServerConnectionRef inCnx, SocketRef inSock );
static void		_DNSServerConnectionStop( DNSServerConnectionRef inCnx, Boolean inRemoveFromList );
static void		_DNSServerConnectionReadHandler( void *inContext );
static void		_DNSServerConnectionWriteHandler( void *inContext );
static void		_DNSServerConnectionRenewExpiration( DNSServerConnectionRef inCnx );

CF_CLASS_DEFINE( DNSServerConnection );

static OSStatus
	_DNSServerCreate(
		dispatch_queue_t		inQueue,
		DNSServerStartHandler_f	inStartHandler,
		DNSServerStopHandler_f	inStopHandler,
		void *					inUserContext,
		unsigned int			inResponseDelayMs,
		uint32_t				inDefaultTTL,
		const sockaddr_ip *		inAddrArray,
		size_t					inAddrCount,
		const char *			inDomain,
		Boolean					inBadUDPMode,
		DNSServerRef *			outServer )
{
	OSStatus			err;
	DNSServerRef		obj = NULL;
	
	require_action_quiet( inDefaultTTL <= INT32_MAX, exit, err = kRangeErr );
	
	CF_OBJECT_CREATE( DNSServer, obj, err, exit );
	
	ReplaceDispatchQueue( &obj->queue, inQueue );
	obj->startHandler		= inStartHandler;
	obj->stopHandler		= inStopHandler;
	obj->userContext		= inUserContext;
	obj->responseDelayMs	= inResponseDelayMs;
	obj->defaultTTL			= inDefaultTTL;
	obj->badUDPMode			= inBadUDPMode;
	obj->addrCount			= inAddrCount;
	
	obj->addrArray = (sockaddr_ip *) _memdup( inAddrArray, obj->addrCount * sizeof( *obj->addrArray ) );
	require_action( obj->addrArray, exit, err = kNoMemoryErr );
	
	obj->readSourceArrayUDP = (dispatch_source_t *) calloc( obj->addrCount, sizeof( *obj->readSourceArrayUDP ) );
	require_action( obj->readSourceArrayUDP, exit, err = kNoMemoryErr );
	
	obj->readSourceArrayTCP = (dispatch_source_t *) calloc( obj->addrCount, sizeof( *obj->readSourceArrayTCP ) );
	require_action( obj->readSourceArrayTCP, exit, err = kNoMemoryErr );
	
	if( inDomain )
	{
		err = StringToDomainName( inDomain, &obj->domain, NULL );
		require_noerr_quiet( err, exit );
	}
	else
	{
		err = DomainNameDup( kDNSServerDomain_Default, &obj->domain, NULL );
		require_noerr_quiet( err, exit );
	}
	*outServer = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	CFReleaseNullSafe( obj );
	return( err );
}

//===========================================================================================================================

static void	_DNSServerFinalize( CFTypeRef inObj )
{
	DNSServerRef const		me = (DNSServerRef) inObj;
	size_t					i;
	
	check( !me->responseTimer );
	check( !me->connectionList );
	check( !me->connectionTimer );
	ForgetMem( &me->addrArray );
	if( me->readSourceArrayUDP )
	{
		for( i = 0; i < me->addrCount; ++i ) check( !me->readSourceArrayUDP[ i ] );
		ForgetMem( &me->readSourceArrayUDP );
	}
	if( me->readSourceArrayTCP )
	{
		for( i = 0; i < me->addrCount; ++i ) check( !me->readSourceArrayTCP[ i ] );
		ForgetMem( &me->readSourceArrayTCP );
	}
	ForgetMem( &me->domain );
	dispatch_forget( &me->queue );
	ForgetMem( &me->ignoredQTypes );
}

//===========================================================================================================================

static OSStatus	_DNSServerSetIgnoredQType( DNSServerRef me, int inQType )
{
	size_t		newCount;
	int *		mem;
	
	newCount = me->ignoredQTypeCount + 1;
	require_return_value( newCount <= SIZE_MAX / sizeof( int ), kSizeErr );
	
	mem = realloc( me->ignoredQTypes, newCount * sizeof( int ) );
	require_return_value( mem, kNoMemoryErr );
	
	me->ignoredQTypes = mem;
	me->ignoredQTypes[ me->ignoredQTypeCount++ ] = inQType;
	return( kNoErr );
}

//===========================================================================================================================

static void	_DNSServerSetPort( const DNSServerRef me, const uint16_t inPort )
{
	me->portRequested = inPort;
}

//===========================================================================================================================

static void		_DNSServerStartOnQueue( void *inContext );
static void		_DNSServerStartInternal( DNSServerRef inServer );
static OSStatus	_DNSServerSetUpSockets( DNSServerRef inServer, uint16_t *outActualPort );
static void		_DNSServerStopInternal( void *inContext, OSStatus inError );
static SocketContext *
	_DNSServerSocketContextCreate(
		SocketRef		inSock,
		DNSServerRef	inServer,
		size_t			inIndex,
		OSStatus *		outError );

static void	_DNSServerStart( DNSServerRef me )
{
	CFRetain( me );
	dispatch_async_f( me->queue, me, _DNSServerStartOnQueue );
}

static void	_DNSServerStartOnQueue( void *inContext )
{
	const DNSServerRef		me = (DNSServerRef) inContext;
	
	_DNSServerStartInternal( me );
	CFRelease( me );
}

static void _DNSServerStartInternal( DNSServerRef me )
{
	OSStatus				err;
	struct timeval			now;
	SocketRef				sock	= kInvalidSocketRef;
	SocketContext *			sockCtx	= NULL;
	int						year, month, day;
	uint16_t				actualPort;
	
	require_action_quiet( !me->started && !me->stopped, exit, err = kNoErr );
	me->started = true;
	CFRetain( me );
	
	err = _DNSServerSetUpSockets( me, &actualPort );
	require_noerr( err, exit );
	
	if( me->startHandler ) me->startHandler( actualPort, me->userContext );
	
	// Create the serial number for the server's SOA record in the YYYMMDDnn convention recommended by
	// <https://tools.ietf.org/html/rfc1912#section-2.2> using the current time.
	
	gettimeofday( &now, NULL );
	SecondsToYMD_HMS( ( INT64_C_safe( kDaysToUnixEpoch ) * kSecondsPerDay ) + now.tv_sec, &year, &month, &day,
		NULL, NULL, NULL );
	me->serial = (uint32_t)( ( year * 1000000 ) + ( month * 10000 ) + ( day * 100 ) + 1 );
	err = kNoErr;
	
exit:
	ForgetSocket( &sock );
	if( sockCtx ) SocketContextRelease( sockCtx );
	if( err ) _DNSServerStopInternal( me, err );
}

typedef struct
{
	SocketRef		sockUDP;
	SocketRef		sockTCP;
	
}	_DNSServerSocketPair;

#define kDNSServerMaxBindTryCount	10

static OSStatus	_DNSServerSetUpSockets( const DNSServerRef me,  uint16_t * const outActualPort )
{
	OSStatus					err;
	SocketContext *				sockCtx			= NULL;
	_DNSServerSocketPair *		sockPairs		= NULL;
	_DNSServerSocketPair *		sockPairsHeap	= NULL;
	_DNSServerSocketPair		sockPairsStack[ 16 ];
	size_t						i;
	const size_t				addrCount		= me->addrCount; // Don't use me->addrCount to avoid false analyzer warning.
	int							portActual		= 0;
	int							tryCount, tryCountMax;
	
	require_action_quiet( addrCount > 0, exit, err = kNoErr );
	
	if( me->addrCount > countof( sockPairsStack ) )
	{
		sockPairsHeap = (_DNSServerSocketPair *) calloc( me->addrCount, sizeof( *sockPairsHeap ) );
		require_action( sockPairsHeap, exit, err = kNoMemoryErr );
		sockPairs = sockPairsHeap;
	}
	else
	{
		sockPairs = sockPairsStack;
	}
	for( i = 0; i < addrCount; ++i )
	{
		sockPairs[ i ].sockUDP = kInvalidSocketRef;
		sockPairs[ i ].sockTCP = kInvalidSocketRef;
	}
	// Create server sockets.
	
	err = kNoErr;
	tryCountMax = ( me->portRequested == 0 ) ? kDNSServerMaxBindTryCount : 1;
	for( tryCount = 0; tryCount < tryCountMax; ++tryCount )
	{
		int		portDefault = 0;
		
		portActual = 0;
		for( i = 0; i < addrCount; ++i )
		{
			sockaddr_ip * const					sip		= &me->addrArray[ i ];
			_DNSServerSocketPair * const		pair	= &sockPairs[ i ];
			const void *						address;
			SocketRef							sock;
			int									port;
			sockaddr_ip							tmpSA;
			
			switch( sip->sa.sa_family )
			{
				case AF_INET:	address = &sip->v4.sin_addr.s_addr;  break;
				case AF_INET6:	address = sip->v6.sin6_addr.s6_addr; break;
				default:
					ds_ulog( kLogLevelError, "Unhandled address family %d", sip->sa.sa_family );
					err = kTypeErr;
					goto exit;
			}
			// Create UDP socket.
			// If the user's requested port 0, then the user wants any available ephemeral port. The actual port number
			// that was used will be stored in portActual and used for the remaining addresses that don't specify a
			// non-zero port.
			
			port = ( me->portRequested == 0 ) ? portDefault : me->portRequested;
			err = _ServerSocketOpenEx2( sip->sa.sa_family, SOCK_DGRAM, IPPROTO_UDP, address, port, &portActual,
				kSocketBufferSize_DontSet, true, &sock );
			if( err == EADDRINUSE )
			{
				SockAddrCopy( sip, &tmpSA );
				SockAddrSetPort( &tmpSA, port );
				ds_ulog( kLogLevelError, "IP address %##a is already in use for UDP\n", &tmpSA );
				break;
			}
			require_noerr( err, exit );
			check( ( me->portRequested == 0 ) || ( portActual == me->portRequested ) );
			
			ForgetSocket( &pair->sockUDP );
			pair->sockUDP = sock;
			sock = kInvalidSocketRef;
			if( portDefault == 0 ) portDefault = portActual;
			
			// Create TCP socket.
			
			err = _ServerSocketOpenEx2( sip->sa.sa_family, SOCK_STREAM, IPPROTO_TCP, address, portActual, NULL,
				kSocketBufferSize_DontSet, false, &sock );
			if( err == EADDRINUSE )
			{
				SockAddrCopy( sip, &tmpSA );
				SockAddrSetPort( &tmpSA, portActual );
				ds_ulog( kLogLevelError, "IP address %##a is already in use for TCP\n", &tmpSA );
				break;
			}
			require_noerr( err, exit );
			
			ForgetSocket( &pair->sockTCP );
			pair->sockTCP = sock;
			sock = kInvalidSocketRef;
			
			SockAddrSetPort( sip, portActual );
		}
		if( !err ) break;
	}
	require_noerr( err, exit );
	
	// Create read sources for server sockets.
	
	for( i = 0; i < addrCount; ++i )
	{
		const sockaddr_ip * const			sip					= &me->addrArray[ i ];
		dispatch_source_t * const			readSourceUDPPtr	= &me->readSourceArrayUDP[ i ];
		dispatch_source_t * const			readSourceTCPPtr	= &me->readSourceArrayTCP[ i ];
		_DNSServerSocketPair * const		pair				= &sockPairs[ i ];
		
		// Create read source for UDP socket.
		
		check( IsValidSocket( pair->sockUDP ) );
		sockCtx = _DNSServerSocketContextCreate( pair->sockUDP, me, i, &err );
		require_noerr( err, exit );
		pair->sockUDP = kInvalidSocketRef;
		
		err = DispatchReadSourceCreate( sockCtx->sock, me->queue, _DNSServerUDPReadHandler, SocketContextCancelHandler,
			sockCtx, readSourceUDPPtr );
		require_noerr( err, exit );
		dispatch_resume( *readSourceUDPPtr );
		sockCtx = NULL;
		
		// Create read source for TCP socket.
		
		check( IsValidSocket( pair->sockTCP ) );
		sockCtx = _DNSServerSocketContextCreate( pair->sockTCP, me, i, &err );
		require_noerr( err, exit );
		pair->sockTCP = kInvalidSocketRef;
		
		err = DispatchReadSourceCreate( sockCtx->sock, me->queue, _DNSServerTCPAcceptHandler, SocketContextCancelHandler,
			sockCtx, readSourceTCPPtr );
		require_noerr( err, exit );
		dispatch_resume( *readSourceTCPPtr );
		sockCtx = NULL;
		
		ds_ulog( kLogLevelInfo, "Server is listening on %##a\n", sip );
	}
	
exit:
	if( sockPairs )
	{
		for( i = 0; i < addrCount; ++i )
		{
			ForgetSocket( &sockPairs[ i ].sockUDP );
			ForgetSocket( &sockPairs[ i ].sockTCP );
		}
	}
	FreeNullSafe( sockPairsHeap );
	if( sockCtx ) SocketContextRelease( sockCtx );
	if( outActualPort ) *outActualPort = (uint16_t) portActual;
	return( err );
}

//===========================================================================================================================

typedef struct
{
	DNSServerRef	server;
	size_t			index;
	
}	DNSServerContext;

static void	_DNSServerContextFree( DNSServerContext *inCtx );
static void	_DNSServerSocketContextFinalizer( void *inCtx );

static SocketContext *
	_DNSServerSocketContextCreate(
		SocketRef		inSock,
		DNSServerRef	inServer,
		size_t			inIndex,
		OSStatus *		outError )
{
	OSStatus				err;
	SocketContext *			sockCtx = NULL;
	DNSServerContext *		ctx;
	
	ctx = (DNSServerContext *) calloc( 1, sizeof( *ctx ) );
	require_action( ctx, exit, err = kNoMemoryErr );
	
	ctx->index	= inIndex;
	ctx->server	= inServer;
	CFRetain( ctx->server );
	
	sockCtx = SocketContextCreateEx( inSock, ctx, _DNSServerSocketContextFinalizer, &err );
	require_noerr( err, exit );
	ctx	= NULL;
	
exit:
	if( outError ) *outError = err;
	if( ctx ) _DNSServerContextFree( ctx );
	return( sockCtx );
}

static void	_DNSServerSocketContextFinalizer( void *inCtx )
{
	_DNSServerContextFree( (DNSServerContext *) inCtx );
}

static void	_DNSServerContextFree( DNSServerContext *inCtx )
{
	ForgetCF( &inCtx->server );
	free( inCtx );
}

//===========================================================================================================================

static void	_DNSServerStopOnQueue( void *inContext );
static void	_DNSServerStop2( void *inContext );

static void	_DNSServerStop( DNSServerRef me )
{
	CFRetain( me );
	dispatch_async_f( me->queue, me, _DNSServerStopOnQueue );
}

static void	_DNSServerStopOnQueue( void *inContext )
{
	DNSServerRef const		me = (DNSServerRef) inContext;
	
	_DNSServerStopInternal( me, kNoErr );
	CFRelease( me );
}

static void	_DNSServerStopInternal( void *inContext, OSStatus inError )
{
	DNSServerRef const			me = (DNSServerRef) inContext;
	DNSServerConnectionRef		cnx;
	size_t						i;
	
	require_quiet( !me->stopped, exit );
	me->stopped = true;
	
	me->stopErr = inError;
	if( me->responseList )
	{
		_DNSServerDelayedResponseListFree( me->responseList );
		me->responseList = NULL;
	}
	dispatch_source_forget( &me->responseTimer );
	for( i = 0; i < me->addrCount; ++i )
	{
		dispatch_source_forget( &me->readSourceArrayUDP[ i ] );
		dispatch_source_forget( &me->readSourceArrayTCP[ i ] );
	}
	while( ( cnx = me->connectionList ) != NULL )
	{
		me->connectionList = cnx->next;
		_DNSServerConnectionStop( cnx, false );
		cnx->next = NULL;
		CFRelease( cnx );
	}
	dispatch_source_forget( &me->connectionTimer );
	
	CFRetain( me );
	dispatch_async_f( me->queue, me, _DNSServerStop2 );
	if( me->started ) CFRelease( me );
	
exit:
	return;
}

static void	_DNSServerStop2( void *inContext )
{
	DNSServerRef const		me = (DNSServerRef) inContext;
	
	if( me->stopHandler ) me->stopHandler( me->stopErr, me->userContext );
	CFRelease( me );
}

//===========================================================================================================================

static void	_DNSServerUDPReadHandler( void *inContext )
{
	OSStatus							err;
	SocketContext * const				sockCtx	= (SocketContext *) inContext;
	const DNSServerContext * const		ctx		= (DNSServerContext *) sockCtx->userContext;
	const DNSServerRef					me		= ctx->server;
	ssize_t								n;
	sockaddr_ip							client;
	socklen_t							clientLen;
	uint8_t *							respPtr	= NULL;	// malloc'd
	size_t								respLen;
	uint8_t								msg[ 512 ];
	
	// Receive message.
	
	clientLen = (socklen_t) sizeof( client );
	n = recvfrom( sockCtx->sock, (char *) msg, sizeof( msg ), 0, &client.sa, &clientLen );
	err = map_socket_value_errno( sockCtx->sock, n >= 0, n );
	require_noerr( err, exit );
	
	if( n < kDNSHeaderLength )
	{
		ds_ulog( kLogLevelInfo, "UDP: Received %zd bytes from %##a to %##a: Message is too small (< %d bytes)\n",
			n, &client, &me->addrArray[ ctx->index ], kDNSHeaderLength );
		goto exit;
	}
	ds_ulog( kLogLevelInfo, "UDP: Received %zd bytes from %##a to %##a -- %.1{du:dnsmsg}\n",
		n, &client, &me->addrArray[ ctx->index ], msg, (size_t) n );
	
	// Create response.
	
	err = _DNSServerAnswerQueryForUDP( me, msg, (size_t) n, ctx->index + 1, &respPtr, &respLen );
	if( err == kSkipErr ) ds_ulog( kLogLevelInfo, "UDP: Ignoring query\n" );
	require_noerr_quiet( err, exit );
	
	if( me->responseDelayMs > 0 )	// Defer response.
	{
		err = _DNSServerScheduleDelayedResponse( me, sockCtx->sock, &client.sa, respPtr, respLen, ctx->index );
		require_noerr( err, exit );
		respPtr = NULL;
	}
	else							// Send response.
	{
		ds_ulog( kLogLevelInfo, "UDP: Sending %zu byte response from %##a to %##a -- %.1{du:dnsmsg}\n",
			respLen, &me->addrArray[ ctx->index ], &client, respPtr, respLen );
		
		n = sendto( sockCtx->sock, (char *) respPtr, respLen, 0, &client.sa, clientLen );
		err = map_socket_value_errno( sockCtx->sock, n == (ssize_t) respLen, n );
		require_noerr( err, exit );
	}
	
exit:
	FreeNullSafe( respPtr );
}

//===========================================================================================================================

static void	_DNSServerSendDelayedResponses( void *inContext );

static OSStatus
	_DNSServerScheduleDelayedResponse(
		DNSServerRef			me,
		SocketRef				inSock,
		const struct sockaddr *	inDestAddr,
		uint8_t *				inMsgPtr,
		size_t					inMsgLen,
		size_t					inIndex )
{
	OSStatus						err;
	DNSServerDelayedResponse *		resp;
	DNSServerDelayedResponse *		newResp;
	DNSServerDelayedResponse **		ptr;
	uint64_t						dueTicks;
	
	dueTicks = UpTicks() + MillisecondsToUpTicks( me->responseDelayMs );
	newResp = (DNSServerDelayedResponse *) calloc( 1, sizeof( *newResp ) );
	require_action( newResp, exit, err = kNoMemoryErr );
	
	newResp->dueTicks	= dueTicks;
	newResp->msgPtr		= inMsgPtr;
	newResp->msgLen		= inMsgLen;
	newResp->index		= inIndex;
	newResp->sock		= inSock;
	SockAddrCopy( inDestAddr, &newResp->client );
	
	if( !me->responseList || ( _TicksDiff( dueTicks, me->responseList->dueTicks ) < 0 ) )
	{
		dispatch_source_forget( &me->responseTimer );
		err = DispatchTimerOneShotCreate( dispatch_time_milliseconds( me->responseDelayMs ), 0, me->queue,
			_DNSServerSendDelayedResponses, me, &me->responseTimer );
		require_noerr( err, exit );
		dispatch_resume( me->responseTimer );
	}
	for( ptr = &me->responseList; ( resp = *ptr ) != NULL; ptr = &resp->next )
	{
		if( _TicksDiff( newResp->dueTicks, resp->dueTicks ) < 0 ) break;
	}
	newResp->next = resp;
	*ptr = newResp;
	newResp = NULL;
	err = kNoErr;
	
exit:
	if( newResp ) _DNSServerDelayedResponseFree( newResp );
	return( err );
}

static void	_DNSServerSendDelayedResponses( void *inContext )
{
	OSStatus						err;
	const DNSServerRef				me = (DNSServerRef) inContext;
	DNSServerDelayedResponse *		resp;
	DNSServerDelayedResponse *		freeList;
	int64_t							deltaTicks;
	
	dispatch_source_forget( &me->responseTimer );
	
	deltaTicks = -1;
	freeList = NULL;
	while( ( resp = me->responseList ) != NULL )
	{
		ssize_t			n;
		uint64_t		nowTicks = UpTicks();
		
		deltaTicks = _TicksDiff( resp->dueTicks, nowTicks );
		if( deltaTicks > 0 ) break;
		me->responseList = resp->next;
		
		ds_ulog( kLogLevelInfo, "UDP: Sending %zu byte delayed response from %##a to %##a -- %.1{du:dnsmsg}\n",
			resp->msgLen, &me->addrArray[ resp->index ], &resp->client, resp->msgPtr, resp->msgLen );
		
		n = sendto( resp->sock, (char *) resp->msgPtr, resp->msgLen, 0, &resp->client.sa, SockAddrGetSize( &resp->client ) );
		err = map_socket_value_errno( resp->sock, n == (ssize_t) resp->msgLen, n );
		check_noerr( err );
		
		resp->next = freeList;
		freeList = resp;
	}
	if( deltaTicks > 0 )
	{
		uint64_t		deltaNs;
		
		deltaNs = UpTicksToNanoseconds( (uint64_t) deltaTicks );
		if( deltaNs > INT64_MAX ) deltaNs = INT64_MAX;
		
		err = DispatchTimerOneShotCreate( dispatch_time( DISPATCH_TIME_NOW, (int64_t) deltaNs ), 0, me->queue,
			_DNSServerSendDelayedResponses, me, &me->responseTimer );
		require_noerr( err, exit );
		dispatch_resume( me->responseTimer );
	}
	
exit:
	if( freeList ) _DNSServerDelayedResponseListFree( freeList );
}

//===========================================================================================================================

static void	_DNSServerDelayedResponseFree( DNSServerDelayedResponse *inResp )
{
	ForgetMem( &inResp->msgPtr );
	inResp->sock = kInvalidSocketRef;
	free( inResp );
}

//===========================================================================================================================

static void	_DNSServerDelayedResponseListFree( DNSServerDelayedResponse *inList )
{
	DNSServerDelayedResponse *		resp;
	
	while( ( resp = inList ) != NULL )
	{
		inList = resp->next;
		_DNSServerDelayedResponseFree( resp );
	}
}

//===========================================================================================================================

#define kDNSServerConnectionExpirationTimeSecs		5
#define kDNSServerConnectionExpirationTimeMs		( kDNSServerConnectionExpirationTimeSecs * kMillisecondsPerSecond )

static void	_DNSServerTCPAcceptHandler( void *inContext )
{
	OSStatus							err;
	SocketContext * const				sockCtx	= (SocketContext *) inContext;
	const DNSServerContext * const		ctx		= (DNSServerContext *) sockCtx->userContext;
	const DNSServerRef 					me		= ctx->server;
	DNSServerConnectionRef				cnx		= NULL;
	sockaddr_ip							remote, local;
	socklen_t							len;
	SocketRef							sock;
	
	len = (socklen_t) sizeof( remote );
	sock = accept( sockCtx->sock, &remote.sa, &len );
	err = map_socket_creation_errno( sock );
	require_noerr( err, exit );
	
	len = (socklen_t) sizeof( local );
	err = getsockname( sock, &local.sa, &len );
	if( unlikely( err ) ) SockAddrCopy( &me->addrArray[ ctx->index ], &local );
	
	err = _DNSServerConnectionCreate( me, &local.sa, &remote.sa, ctx->index, &cnx );
	require_noerr_quiet( err, exit );
	
	err = _DNSServerConnectionStart( cnx, sock );
	require_noerr( err, exit );
	sock = kInvalidSocketRef;
	
	if( !me->connectionList ) _DNSServerResetConnectionTimerMs( me, kDNSServerConnectionExpirationTimeMs );
	cnx->next = me->connectionList;
	me->connectionList = cnx;
	cnx = NULL;
	
exit:
	ForgetSocket( &sock );
	if( cnx )
	{
		_DNSServerConnectionStop( cnx, true );
		CFRelease( cnx );
	}
}

//===========================================================================================================================

static void	_DNSServerConnectionTimerHandler( void *inContext )
{
	const DNSServerRef				me			= (DNSServerRef) inContext;
	DNSServerConnectionRef			cnx;
	DNSServerConnectionRef *		ptr;
	uint64_t						nowTicks;
	int64_t							delta, deltaMin;
	
	nowTicks = UpTicks();
	deltaMin = INT64_MAX;
	ptr = &me->connectionList;
	while( ( cnx = *ptr ) != NULL )
	{
		delta = _TicksDiff( cnx->expirationTicks, nowTicks );
		if( delta <= 0 )
		{
			ds_ulog( kLogLevelInfo, "Timing out TCP connection: %##a <-> %##a\n", &cnx->local, &cnx->remote );
			*ptr = cnx->next;
			cnx->next = NULL;
			_DNSServerConnectionStop( cnx, false );
			CFRelease( cnx );
		}
		else
		{
			if( delta < deltaMin ) deltaMin = delta;
			ptr = &cnx->next;
		}
	}
	if( me->connectionList )
	{
		const uint64_t		timeMs = UpTicksToMilliseconds( (uint64_t) deltaMin );
		
		check( timeMs <= kDNSServerConnectionExpirationTimeMs );
		_DNSServerResetConnectionTimerMs( me, timeMs + 1 );
	}
}

//===========================================================================================================================

static void	_DNSServerResetConnectionTimerMs( DNSServerRef me, uint64_t inTimeoutMs )
{
	OSStatus		err;
	
	dispatch_source_forget( &me->connectionTimer );
	if( inTimeoutMs == 0 ) inTimeoutMs = 1;
	err = DispatchTimerOneShotCreate( dispatch_time_milliseconds( inTimeoutMs ),
		UINT64_C( 10 ) * kNanosecondsPerMillisecond, me->queue, _DNSServerConnectionTimerHandler, me, &me->connectionTimer );
	if( likely( !err ) )	dispatch_resume( me->connectionTimer );
	else					ds_ulog( kLogLevelError, "Failed to create connection timer: %#m\n", err );
}

//===========================================================================================================================

static OSStatus
	_DNSServerInitializeResponseMessage(
		DataBuffer *	inDB,
		uint16_t		inID,
		uint16_t		inFlags,
		const uint8_t *	inQName,
		uint16_t		inQType,
		uint16_t		inQClass );
static OSStatus
	_DNSServerAnswerQueryDynamically(
		DNSServerRef	inServer,
		const uint8_t *	inQName,
		int				inQType,
		int				inQClass,
		size_t          inIndex,
		size_t			inTruncateLen,
		Boolean			inDNSSEC,
		Boolean			inUseBadAddrs,
		DataBuffer *	inDB );

static OSStatus
	_DNSServerAnswerQuery(
		DNSServerRef			me,
		const uint8_t * const	inMsgPtr,
		const size_t			inMsgLen,
		const size_t			inIndex,
		const Boolean			inForTCP,
		uint8_t ** const		outResponsePtr,
		size_t * const			outResponseLen )
{
	OSStatus				err;
	DataBuffer				db;
	const uint8_t *			qptr;
	const DNSHeader *		hdr;
	const uint8_t *			qOptPtr;
	size_t					qOptLen, truncateLen;
	unsigned int			qflags, rcode;
	uint16_t				msgID, qtype, qclass, rflags;
	uint8_t					qname[ kDomainNameLengthMax ];
	uint8_t					dbBuf[ 512 ];
	Boolean					dnssecOK, usePadding, useBadAddrs;
	
	DataBuffer_Init( &db, dbBuf, sizeof( dbBuf ), kDNSMaxTCPMessageSize );
	
	require_action_quiet( inMsgLen >= kDNSHeaderLength, exit, err = kUnderrunErr );
	
	hdr		= (const DNSHeader *) inMsgPtr;
	qflags	= DNSHeaderGetFlags( hdr );
	
	// Minimal checking of the query message's header.
	
	require_action_quiet( !( qflags & kDNSHeaderFlag_Response ), exit, err = kRequestErr );
	require_action_quiet( DNSFlagsGetOpCode( qflags ) == kDNSOpCode_Query, exit, err = kRequestErr );
	require_action_quiet( DNSHeaderGetQuestionCount( hdr ) == 1, exit, err = kRequestErr );
	
	qptr = (const uint8_t *) &hdr[ 1 ];
	err = DNSMessageExtractQuestion( inMsgPtr, inMsgLen, qptr, qname, &qtype, &qclass, NULL );
	require_noerr( err, exit );
	
	// Check if this query should be ignored because of its QTYPE.
	
	if( qclass == kDNSClassType_IN )
	{
		size_t		i;
		
		for( i = 0; i < me->ignoredQTypeCount; ++i )
		{
			if( qtype == me->ignoredQTypes[ i ] )
			{
				err = kSkipErr;
				goto exit;
			}
		}
	}
	// Set up response flags.
	
	rflags = kDNSHeaderFlag_Response;
	if( qflags & kDNSHeaderFlag_RecursionDesired ) rflags |= kDNSHeaderFlag_RecursionDesired;
	DNSFlagsSetOpCode( rflags, kDNSOpCode_Query );
	
	// Get OPT record, if any.
	
	dnssecOK	= false;
	usePadding	= false;
	err = DNSMessageGetOptRecord( inMsgPtr, inMsgLen, &qOptPtr, &qOptLen );
	require_noerr_action_quiet( err, done, rcode = kDNSRCode_FormErr );
	
	// Create a tentative response message.
	
	msgID = DNSHeaderGetID( hdr );
	if( me->badUDPMode && !inForTCP ) ++msgID;
	err = _DNSServerInitializeResponseMessage( &db, msgID, rflags, qname, qtype, qclass );
	require_noerr_action( err, done, rcode = kDNSRCode_ServFail );
	
	// Complete the response message.
	
	if( qOptPtr )
	{
		const dns_fixed_fields_opt * const		opt = (const dns_fixed_fields_opt *) qOptPtr;
		unsigned int							version;
		
		version = dns_fixed_fields_opt_get_version( opt );
		require_action_quiet( version == 0, done, rcode = kDNSRCode_BADVERS );
		
		if( dns_fixed_fields_opt_get_extended_flags( opt ) & kDNSExtendedFlag_DNSSECOK ) dnssecOK = true;
		
		// If the query includes EDNS(0) padding, then so must the response according to
		// <https://datatracker.ietf.org/doc/html/rfc8467#section-4.1>.
		
		if( inForTCP )
		{
			const uint8_t *				ptr;
			const uint8_t * const		optEnd = &qOptPtr[ qOptLen ];
			
			ptr = (const uint8_t *) &opt[ 1 ];
			while( ptr < optEnd )
			{
				const dns_fixed_fields_option *		optionFields;
				unsigned int						optionCode, optionLen;
				
				require_action_quiet( (size_t)( optEnd - ptr ) >= sizeof( *optionFields ), done, rcode = kDNSRCode_FormErr );
				
				optionFields	= (const dns_fixed_fields_option *) ptr;
				optionCode		= dns_fixed_fields_option_get_code( optionFields );
				optionLen		= dns_fixed_fields_option_get_length( optionFields );
				
				ptr = (const uint8_t *) &optionFields[ 1 ];
				require_action_quiet( (size_t)( optEnd - ptr ) >= optionLen, done, rcode = kDNSRCode_FormErr );
				
				ptr += optionLen;
				if( optionCode == kDNSEDNS0OptionCode_Padding )
				{
					usePadding = true;
					break;
				}
			}
			truncateLen = 0;
		}
		else
		{
			// For UDP, the response's length limit for truncation should account for the inclusion of an OPT record.
			
			check_compile_time_code( sizeof( *opt ) < kDNSMaxUDPMessageSize );
			truncateLen	= kDNSMaxUDPMessageSize - sizeof( *opt );
		}
	}
	else
	{
		truncateLen = inForTCP ? 0 : kDNSMaxUDPMessageSize;
	}
	useBadAddrs = me->badUDPMode && !inForTCP;
	err = _DNSServerAnswerQueryDynamically( me, qname, qtype, qclass, inIndex, truncateLen, dnssecOK, useBadAddrs, &db );
	if( err == kSkipErr ) goto exit;
	require_noerr_action( err, done, rcode = kDNSRCode_ServFail );
	
	rcode = kDNSRCode_NoError;
	
done:
	// Create an error response if necessary.
	
	if( rcode != kDNSRCode_NoError )
	{
		const unsigned int		rcode4BitMax  =   15; // 2^4  - 1
		const unsigned int		rcode12BitMax = 4095; // 2^12 - 1
		
		// One of the following must be true.
		// 1. The RCODE value must completely fit in the response header's 4-bit RCODE field.
		// 2. The response will include an OPT record and the RCODE value can be represented as an unsigned 12-bit
		//    integer (upper eight bits go in the OPT record's EXTENDED-RCODE field).
		
		require_fatal( ( rcode <= rcode4BitMax ) || ( ( rcode <= rcode12BitMax ) && qOptPtr ),
			"Invalid RCODE value %u for response with%s OPT record", rcode, qOptPtr ? "" : "out" );
		
		DNSFlagsSetRCode( rflags, rcode & 0x0FU );
		err = _DNSServerInitializeResponseMessage( &db, DNSHeaderGetID( hdr ), rflags, qname, qtype, qclass );
		require_noerr( err, exit );
	}
	
	// Include an OPT record in the response if the query included one.
	// See <https://datatracker.ietf.org/doc/html/rfc6891#section-6.1.1>.
	
	if( qOptPtr )
	{
		DNSHeader *					rhdr;
		uint16_t					additionalCount;
		dns_fixed_fields_opt		optFields;
		size_t						rOptOffset;
		
		memset( &optFields, 0, sizeof( optFields ) );
		dns_fixed_fields_opt_set_type( &optFields, kDNSRecordType_OPT );
		dns_fixed_fields_opt_set_udp_payload_size( &optFields, kDNSMaxUDPMessageSize );
		dns_fixed_fields_opt_set_extended_rcode( &optFields, ( rcode >> 4 ) & 0xFFU );
		if( dnssecOK ) dns_fixed_fields_opt_set_extended_flags( &optFields, kDNSExtendedFlag_DNSSECOK );
		
		rOptOffset = DataBuffer_GetLen( &db );
		err = DataBuffer_Append( &db, &optFields, sizeof( optFields ) );
		require_noerr( err, exit );
		
		// Pad responses using the block-length padding strategy using a 468-octet block length as recommended by
		// <https://datatracker.ietf.org/doc/html/rfc8467#section-4.1>.
		
		if( usePadding )
		{
			dns_fixed_fields_opt *		rOptPtr;
			dns_fixed_fields_option		padOption;
			size_t						curLen, newLen, rdLen;
			uint16_t					padLen;
			
			curLen = DataBuffer_GetLen( &db );
			newLen = RoundUp( curLen + sizeof( padOption ), 468 );
			require_action( newLen > curLen, exit, err = kSizeErr );
			
			rdLen = newLen - curLen;
			require_action( rdLen <= UINT16_MAX, exit, err = kSizeErr );
			
			memset( &padOption, 0, sizeof( padOption ) );
			dns_fixed_fields_option_set_code( &padOption, kDNSEDNS0OptionCode_Padding );
			padLen = (uint16_t)( rdLen - sizeof( padOption ) );
			dns_fixed_fields_option_set_length( &padOption, padLen );
			err = DataBuffer_Append( &db, &padOption, sizeof( padOption ) );
			require_noerr( err, exit );
			
			if( padLen > 0 )
			{
				err = DataBuffer_AppendF( &db, "%{fill}", 0, padLen );
				require_noerr( err, exit );
			}
			check( DataBuffer_GetLen( &db ) == newLen );
			rOptPtr = (dns_fixed_fields_opt *)( DataBuffer_GetPtr( &db ) + rOptOffset );
			dns_fixed_fields_opt_set_rdlen( rOptPtr, (uint16_t) rdLen );
		}
		check( DataBuffer_GetLen( &db ) >= sizeof( *rhdr ) );
		rhdr = (DNSHeader *) DataBuffer_GetPtr( &db );
		additionalCount = DNSHeaderGetAdditionalCount( rhdr ) + 1;
		DNSHeaderSetAdditionalCount( rhdr, additionalCount );
	}
	err = DataBuffer_Detach( &db, outResponsePtr, outResponseLen );
	require_noerr( err, exit );
	
exit:
	DataBuffer_Free( &db );
	return( err );
}

//===========================================================================================================================

static OSStatus
	_DNSServerInitializeResponseMessage(
		DataBuffer *	inDB,
		uint16_t		inID,
		uint16_t		inFlags,
		const uint8_t *	inQName,
		uint16_t		inQType,
		uint16_t		inQClass )
{
	OSStatus		err;
	DNSHeader		header;
	
	DataBuffer_Reset( inDB );
	
	memset( &header, 0, sizeof( header ) );
	DNSHeaderSetID( &header, inID );
	DNSHeaderSetFlags( &header, inFlags );
	DNSHeaderSetQuestionCount( &header, 1 );
	
	err = DataBuffer_Append( inDB, &header, sizeof( header ) );
	require_noerr( err, exit );
	
	err = _DataBuffer_AppendDNSQuestion( inDB, inQName, DomainNameLength( inQName ), inQType, inQClass );
	require_noerr( err, exit );
	
exit:
	return( err );
}

//===========================================================================================================================

// DNS Server QNAME Labels

#define kLabel_IPv4					"ipv4"
#define kLabel_IPv6					"ipv6"
#define kLabelPrefix_Alias			"alias"
#define kLabelPrefix_AliasTTL		"alias-ttl"
#define kLabelPrefix_Count			"count-"
#define kLabelPrefix_Index			"index-"
#define kLabelPrefix_RCode			"rcode-"
#define kLabelPrefix_SRV			"srv-"
#define kLabelPrefix_Tag			"tag-"
#define kLabelPrefix_TTL			"ttl-"

// Experimental Labels

#define kLabel_CommandSuspend		"command-suspend"	// Causes the DNS server to suspend query responses.
#define kLabel_CommandResume		"command-resume"	// Causes the DNS server to resume query responses.
#define kLabelPrefix_Offset			"offset-"			// Specifies an address offset for A and AAAA record addresses.
#define kLabelPrefix_PDelay			"pdelay-"			// Specifies a simulated processing delay in milliseconds.
#define kLabelPrefix_Zone			"z-"				// format: z-<algorithm mnemonic exclude '-'>-<zone index>

typedef struct
{
	uint16_t			priority;	// Priority from SRV label.
	uint16_t			weight;		// Weight from SRV label.
	uint16_t			port;		// Port number from SRV label.
	uint16_t			targetLen;	// Total length of the target hostname labels that follow an SRV label.
	const uint8_t *		targetPtr;	// Pointer to the target hostname embedded in a domain name.
	
}	ParsedSRV;

typedef uint32_t		DNSNameFlags;
#define kDNSNameFlag_HasA			( 1U << 0 )
#define kDNSNameFlag_HasAAAA		( 1U << 1 )
#define kDNSNameFlag_HasSOA			( 1U << 2 )
#define kDNSNameFlag_HasSRV			( 1U << 3 )
#define kDNSNameFlag_HasPTRv4		( 1U << 4 )
#define kDNSNameFlag_HasPTRv6		( 1U << 5 )
#define kDNSNameFlag_HasRRSIG		( 1U << 6 )
#define kDNSNameFlag_HasDNSKEY		( 1U << 7 )
#define kDNSNameFlag_HasDS			( 1U << 8 )

#define kAliasTTLCountMax		( ( kDomainLabelLengthMax - sizeof_string( kLabelPrefix_AliasTTL ) ) / 2 )
#define kParsedSRVCountMax		( kDomainNameLengthMax / ( 1 + sizeof_string( kLabelPrefix_SRV ) + 5 ) )

typedef enum
{
	kDNSServerAction_None		= 0,
	kDNSServerAction_Suspend	= 1,
	kDNSServerAction_Resume		= 2
	
}	DNSServerAction;

static Boolean
	_DNSServerParseHostName(
		DNSServerRef		inServer,
		const uint8_t *		inQName,
		uint32_t *			outAliasCount,
		uint32_t			outAliasTTLs[ kAliasTTLCountMax ],
		uint32_t *			outAliasTTLCount,
		uint32_t *			outCount,
		uint32_t *			outRandCount,
		uint32_t *			outIndex,
		int *				outRCode,
		uint32_t *			outTTL,
		uint32_t *			outOffset,
		uint32_t *			outProcDelayMs,
		DNSNameFlags *		outFlags,
		const uint8_t **	outZone,
		const uint8_t **	outZoneParent,
		DNSKeyInfoRef *		outZSK,
		DNSKeyInfoRef *		outKSK,
		DNSKeyInfoRef *		outParentZSK,
		DNSServerAction *	outAction );
static Boolean
	_DNSServerParseSRVName(
		DNSServerRef		inServer,
		const uint8_t *		inName,
		const uint8_t **	outDomainPtr,
		size_t *			outDomainLen,
		ParsedSRV			outSRVArray[ kParsedSRVCountMax ],
		size_t *			outSRVCount );
static Boolean	_DNSServerParseReverseIPv4Name( DNSServerRef me, const uint8_t *inQName, unsigned int *outHostID );
static Boolean	_DNSServerParseReverseIPv6Name( DNSServerRef me, const uint8_t *inQName, unsigned int *outHostID );
#if( DEBUG )
static void
	_DNSServerSigCheck(
		const uint8_t *		inOwner,
		int					inTypeCovered,
		const void *		inMsgPtr,
		size_t				inMsgLen,
		const uint8_t *		inSignaturePtr,
		const size_t		inSignatureLen,
		DNSKeyInfoRef		inKeyInfo );
#endif

typedef enum
{
	kQueryStatus_Null			= 0,
	kQueryStatus_OK				= 1,
	kQueryStatus_Truncated		= 2,
	kQueryStatus_NotImplemented	= 3,
	kQueryStatus_Refused		= 4
	
}	QueryStatus;

static OSStatus
	_DNSServerAnswerQueryDynamically(
		DNSServerRef			me,
		const uint8_t * const	inQName,
		const int				inQType,
		const int				inQClass,
		const size_t			inIndex,
		const size_t			inTruncateLen,
		const Boolean			inDNSSEC,
		const Boolean			inUseBadAddrs,
		DataBuffer * const		inDB )
{
	OSStatus			err;
	uint32_t			aliasCount		= 0;
	uint32_t			aliasTTLs[ kAliasTTLCountMax ];
	uint32_t			aliasTTLCount	= 0;
	uint32_t			addrCount		= 0;
	uint32_t			randCount		= 0;
	uint32_t			index			= 0;
	int					rcodeOverride	= -1;
	uint32_t			ttl				= 0;
	uint32_t			offset			= 0;
	uint32_t			procDelayMs		= 0;
	DNSNameFlags		nameFlags		= 0;
	const uint8_t *		zone			= NULL;
	const uint8_t *		zoneParent		= NULL;
	DNSKeyInfoRef		zsk				= NULL;
	DNSKeyInfoRef		ksk				= NULL;
	DNSKeyInfoRef		zskParent		= NULL;
	DNSServerAction		action			= kDNSServerAction_None;
	const uint8_t *		srvDomainPtr	= NULL;
	size_t				srvDomainLen	= 0;
	ParsedSRV			srvArray[ kParsedSRVCountMax ];
	size_t				srvCount		= 0;
	unsigned int		hostID			= 0;
	struct timeval		now;
	DNSHeader *			hdr;
	unsigned int		flags;
	int					rcode;
	QueryStatus			status;
	unsigned int		answerCount		= 0;
	unsigned int		additionalCount	= 0;
	Boolean				nameExists		= false;
	uint8_t *			qnameLower		= NULL;
	size_t				qnameLowerLen;
	const uint8_t *		ownerLower;
	size_t				ownerLowerLen;
	DataBuffer *		sigMsg			= NULL;
	DataBuffer			sigDB;
	uint8_t				sigBuf[ 256 ];
	uint8_t				nameCPtr[ 2 ];
	uint64_t			startTicks;
	Boolean				wasSuspended;
	
	startTicks = UpTicks();
	require_action_quiet( inQClass == kDNSServiceClass_IN, done, status = kQueryStatus_NotImplemented );
	
	wasSuspended = me->suspended;
	nameExists = _DNSServerParseHostName( me, inQName, &aliasCount, aliasTTLs, &aliasTTLCount, &addrCount, &randCount,
		&index, &rcodeOverride, &ttl, &offset, &procDelayMs, &nameFlags, &zone, &zoneParent, &zsk, &ksk, &zskParent,
		&action );
	if( nameExists )
	{
		check( !( ( aliasCount > 0 ) && ( aliasTTLCount > 0 ) ) );
		check( ( randCount == 0 ) || ( ( randCount >= addrCount ) && ( randCount <= 255 ) ) );
		check( rcodeOverride <= 15 );
		check( !( nameFlags & kDNSNameFlag_HasRRSIG ) || ( zsk && ksk && zskParent ) );
		
		switch( action )
		{
			case kDNSServerAction_Suspend:
				me->suspended = true;
				break;
			
			case kDNSServerAction_Resume:
				me->suspended = false;
				break;
			
			case kDNSServerAction_None:
				break;
		}
		if( aliasTTLCount > 0 ) aliasCount = (uint32_t) aliasTTLCount;
		if( index != 0 )
		{
			if( index == inIndex )
			{
				rcodeOverride = -1;
			}
			else if( rcodeOverride < 0 )
			{
				err = kSkipErr;
				goto exit;
			}
			else
			{
				addrCount = 0;
			}
		}
	}
	
	// If the server is in suspended mode, but didn't transition into suspended mode just now, then skip this query.
	
	if( me->suspended && wasSuspended )
	{
		err = kSkipErr;
		goto exit;
	}
	
	if( !nameExists )
	{
		if( ( nameExists = _DNSServerParseSRVName( me, inQName, &srvDomainPtr, &srvDomainLen, srvArray, &srvCount ) ) )
		{
			nameFlags = kDNSNameFlag_HasSRV;
		}
		else if( ( nameExists = _DNSServerParseReverseIPv4Name( me, inQName, &hostID ) ) )
		{
			check( ( hostID >= 1 ) && ( hostID <= 255 ) );
			
			nameFlags = kDNSNameFlag_HasPTRv4;
		}
		else if( ( nameExists = _DNSServerParseReverseIPv6Name( me, inQName, &hostID ) ) )
		{
			check( ( hostID >= 1 ) && ( hostID <= 255 ) );
			
			nameFlags = kDNSNameFlag_HasPTRv6;
		}
	}
	require_action_quiet( nameExists, done, status = kQueryStatus_OK );
	
	err = DomainNameDupLower( inQName, &qnameLower, &qnameLowerLen );
	require_noerr( err, exit );
	
	gettimeofday( &now, NULL );
	if( aliasCount > 0 )
	{
		size_t						nameOffset, rdataLabelLen;
		const uint8_t *				parentLower;
		size_t						parentLowerLen = 0;
		uint32_t					i;
		dns_fixed_fields_record		recFields;
		uint8_t						rdataLabel[ 1 + kDomainLabelLengthMax ];
		uint8_t						parentCPtr[ 2 ];
		Boolean						needSig;
		
		// If aliasCount is non-zero, then the first label of QNAME is either "alias" or "alias-<N>". parentCPtr is a
		// name compression pointer to the second label of QNAME, i.e., the parent domain name of QNAME. It's used for
		// the RDATA of CNAME records whose canonical name ends with the superdomain name. It may also be used to
		// construct CNAME record names when the offset to the previous CNAME's RDATA doesn't fit in a compression
		// pointer.
		
		DNSMessageWriteLabelPointer( parentCPtr, kDNSHeaderLength + ( 1 + inQName[ 0 ] ) );
		
		rdataLabel[ 0 ]	= 0;
		rdataLabelLen	= 1;
		nameOffset		= kDNSHeaderLength; // The name of the first CNAME record is equal to QNAME.
		
		needSig = ( inDNSSEC && ( nameFlags & kDNSNameFlag_HasRRSIG ) ) ? true : false;
		if( needSig )
		{
			parentLower		= DomainNameGetNextLabel( qnameLower );
			parentLowerLen	= DomainNameLength( parentLower );
		}
		for( i = aliasCount; i >= 1; --i )
		{
			size_t				nameLabelLen, nameLen, rdataLen, recordLen;
			uint32_t			aliasTTL;
			uint8_t				nameLabel[ 1 + kDomainLabelLengthMax ];
			Boolean				useNamePtr;
			const Boolean		useAliasTTLs = ( aliasTTLCount > 0 ) ? true : false;
			
			memcpy( nameLabel, rdataLabel, rdataLabelLen );
			nameLabelLen = rdataLabelLen;
			if( nameOffset <= kDNSCompressionOffsetMax )
			{
				DNSMessageWriteLabelPointer( nameCPtr, nameOffset );
				nameLen		= sizeof( nameCPtr );
				useNamePtr	= true;
			}
			else
			{
				nameLen		= nameLabelLen + sizeof( parentCPtr );
				useNamePtr	= false;
			}
			
			if( i > 1 )
			{
				// There's at least one alias/CNAME left.
				
				uint8_t *			dst = &rdataLabel[ 1 ];
				const uint8_t *		lim = &rdataLabel[ countof( rdataLabel ) ];
				size_t				maxLen;
				int					n;
				
				maxLen = (size_t)( lim - dst );
				if( useAliasTTLs )
				{
					uint32_t		j;
					
					n = MemPrintF( dst, maxLen, "%s", kLabelPrefix_AliasTTL );
					require_fatal( ( n > 0 ) && ( ( (size_t) n ) <= maxLen ), "Failed to print AliasTTL label" );
					dst += n;
					
					for( j = aliasCount - ( i - 1 ); j < aliasCount; ++j )
					{
						maxLen = (size_t)( lim - dst );
						n = MemPrintF( dst, maxLen, "-%u", aliasTTLs[ j ] );
						require_fatal( ( n > 0 ) && ( ( (size_t) n ) <= maxLen ), "Failed to print AliasTTL label" );
						dst += n;
					}
				}
				else if( i == 2 )
				{
					n = MemPrintF( dst, maxLen, "%s", kLabelPrefix_Alias );
					require_fatal( ( n > 0 ) && ( ( (size_t) n ) <= maxLen ), "Failed to print Alias label" );
					dst += n;
				}
				else
				{
					n = MemPrintF( dst, maxLen, "%s-%u", kLabelPrefix_Alias, i - 1 );
					require_fatal( ( n > 0 ) && ( ( (size_t) n ) <= maxLen ), "Failed to print Alias label" );
					dst += n;
				}
				rdataLabel[ 0 ]	= (uint8_t)( dst - &rdataLabel[ 1 ] );
				rdataLabelLen	= 1 + rdataLabel[ 0 ];
				rdataLen		= rdataLabelLen + sizeof( parentCPtr );
			}
			else
			{
				// This is the final CNAME.
				
				rdataLen = sizeof( parentCPtr );
			}
			
			if( inTruncateLen > 0 )
			{
				recordLen = nameLen + sizeof( recFields ) + rdataLen;
				if( ( DataBuffer_GetLen( inDB ) + recordLen ) > inTruncateLen )
				{
					status = kQueryStatus_Truncated;
					goto done;
				}
			}
			// Append CNAME record's NAME to response.
			
			if( useNamePtr )
			{
				err = DataBuffer_Append( inDB, nameCPtr, sizeof( nameCPtr ) );
				require_noerr( err, exit );
			}
			else
			{
				err = DataBuffer_Append( inDB, nameLabel, nameLabelLen );
				require_noerr( err, exit );
				
				err = DataBuffer_Append( inDB, parentCPtr, sizeof( parentCPtr ) );
				require_noerr( err, exit );
			}
			// Append CNAME record's TYPE, CLASS, TTL, and RDLENGTH to response.
			
			aliasTTL = useAliasTTLs ? aliasTTLs[ aliasCount - i ] : me->defaultTTL;
			dns_fixed_fields_record_init( &recFields, kDNSServiceType_CNAME, kDNSServiceClass_IN, aliasTTL,
				(uint16_t) rdataLen );
			err = DataBuffer_Append( inDB, &recFields, sizeof( recFields ) );
			require_noerr( err, exit );
			
			// Save offset of CNAME record's RDATA, which may be used for the name of the next CNAME record.
			
			nameOffset = DataBuffer_GetLen( inDB );
			
			// Append CNAME record's RDATA to response.
			
			if( i > 1 )
			{
				// There's at least one CNAME left.
				
				err = DataBuffer_Append( inDB, rdataLabel, rdataLabelLen );
				require_noerr( err, exit );
			}
			err = DataBuffer_Append( inDB, parentCPtr, sizeof( parentCPtr ) );
			require_noerr( err, exit );
			
			++answerCount;
			
			if( needSig )
			{
				dns_fixed_fields_rrsig		sigFields;
				const size_t				signerLen = DomainNameLength( zone );
				uint32_t					inceptionSecs;
				uint8_t						signature[ kDNSServerSignatureLengthMax ];
				size_t						signatureLen;
				int							labelCount;
				Boolean						didSign;
				
				// Initialize signing buffer.
				
				check( !sigMsg );
				sigMsg = &sigDB;
				DataBuffer_Init( sigMsg, sigBuf, sizeof( sigBuf ), SIZE_MAX );
				
				// Append RRSIG record RDATA fixed fields to signing buffer.
				
				memset( &sigFields, 0, sizeof( sigFields ) );
				dns_fixed_fields_rrsig_set_type_covered( &sigFields, kDNSServiceType_CNAME );
				dns_fixed_fields_rrsig_set_algorithm( &sigFields, DNSKeyInfoGetAlgorithm( zsk ) );
				labelCount = DomainNameLabelCount( inQName );
				check( labelCount >= 0 );
				dns_fixed_fields_rrsig_set_labels( &sigFields, (uint8_t) labelCount );
				dns_fixed_fields_rrsig_set_original_ttl( &sigFields, aliasTTL );
				inceptionSecs = (uint32_t) now.tv_sec;
				dns_fixed_fields_rrsig_set_signature_expiration( &sigFields, inceptionSecs + kSecondsPerDay );
				dns_fixed_fields_rrsig_set_signature_inception( &sigFields, inceptionSecs );
				dns_fixed_fields_rrsig_set_key_tag( &sigFields, DNSKeyInfoGetKeyTag( zsk ) );
				
				err = DataBuffer_Append( sigMsg, &sigFields, sizeof( sigFields ) );
				require_noerr( err, exit );
				
				// Append RRSIG record RDATA signer to signing buffer.
				
				err = DataBuffer_Append( sigMsg, zone, signerLen );
				require_noerr( err, exit );
				
				// Append expanded CNAME record owner to signing buffer.
				
				if( i == aliasCount )
				{
					err = DataBuffer_Append( sigMsg, qnameLower, qnameLowerLen );
					require_noerr( err, exit );
				}
				else
				{
					err = DataBuffer_Append( sigMsg, nameLabel, nameLabelLen );
					require_noerr( err, exit );
					
					err = DataBuffer_Append( sigMsg, parentLower, parentLowerLen );
					require_noerr( err, exit );
				}
				// Append CNAME record fixed fields to signing buffer.
				
				err = DataBuffer_Append( sigMsg, &recFields, sizeof( recFields ) );
				require_noerr( err, exit );
				
				// Append expanded CNAME record RDATA to signing buffer.
				
				if( i > 1 )
				{
					// There's at least one CNAME left.
					
					err = DataBuffer_Append( sigMsg, rdataLabel, rdataLabelLen );
					require_noerr( err, exit );
				}
				err = DataBuffer_Append( sigMsg, parentLower, parentLowerLen );
				require_noerr( err, exit );
				
				// Compute signature with ZSK.
				
				memset( signature, 0, sizeof( signature ) );
				didSign = DNSKeyInfoSign( zsk, DataBuffer_GetPtr( sigMsg ), DataBuffer_GetLen( sigMsg ),
					signature, &signatureLen );
				require_quiet( didSign, exit );
				
				#if( DEBUG )
				{
					const uint8_t *		tmpPtr;
					uint8_t				tmpBuf[ kDomainNameLengthMax ];
					
					if( i == aliasCount )
					{
						tmpPtr = inQName;
					}
					else
					{
						memcpy( tmpBuf, nameLabel, nameLabelLen );
						memcpy( &tmpBuf[ nameLabelLen ], parentLower, parentLowerLen );
						tmpPtr = tmpBuf;
					}
					_DNSServerSigCheck( tmpPtr, kDNSServiceType_CNAME, DataBuffer_GetPtr( sigMsg ),
						DataBuffer_GetLen( sigMsg ), signature, signatureLen, zsk );
				}
				#endif
				rdataLen	= sizeof( sigFields ) + signerLen + signatureLen;
				recordLen	= nameLen + sizeof( recFields ) + rdataLen;
				if( ( inTruncateLen > 0 ) && ( ( DataBuffer_GetLen( inDB ) + recordLen ) > inTruncateLen ) )
				{
					status = kQueryStatus_Truncated;
					goto done;
				}
				// Append RRSIG record NAME to response.
				
				if( useNamePtr )
				{
					err = DataBuffer_Append( inDB, nameCPtr, sizeof( nameCPtr ) );
					require_noerr( err, exit );
				}
				else
				{
					err = DataBuffer_Append( inDB, nameLabel, nameLabelLen );
					require_noerr( err, exit );
					
					err = DataBuffer_Append( inDB, parentCPtr, sizeof( parentCPtr ) );
					require_noerr( err, exit );
				}
				// Append RRSIG record TYPE, CLASS, TTL, and RDLENGTH to response.
				
				dns_fixed_fields_record_init( &recFields, kDNSServiceType_RRSIG, kDNSServiceClass_IN, aliasTTL,
					(uint16_t) rdataLen );
				err = DataBuffer_Append( inDB, &recFields, sizeof( recFields ) );
				require_noerr( err, exit );
				
				// Append RRSIG record RDATA fixed fields and signer to response.
				
				err = DataBuffer_Append( inDB, &sigFields, sizeof( sigFields ) );
				require_noerr( err, exit );
				
				err = DataBuffer_Append( inDB, zone, signerLen );
				require_noerr( err, exit );
				
				// Append RRSIG record RDATA signature to response.
				
				err = DataBuffer_Append( inDB, signature, signatureLen );
				require_noerr( err, exit );
				
				++answerCount;
				DataBuffer_Free( sigMsg );
				sigMsg = NULL;
			}
		}
		check_compile_time_code( sizeof( nameCPtr ) == sizeof( parentCPtr ) );
		memcpy( nameCPtr, parentCPtr, sizeof( nameCPtr ) );
		
		ownerLower		= DomainNameGetNextLabel( qnameLower );
		ownerLowerLen	= DomainNameLength( ownerLower );
	}
	else
	{
		// There are no aliases, so initialize the name compression pointer to point to QNAME.
		
		DNSMessageWriteLabelPointer( nameCPtr, kDNSHeaderLength );
		
		ownerLower		= qnameLower;
		ownerLowerLen	= qnameLowerLen;
	}
	
	if( ( inQType == kDNSServiceType_A ) || ( inQType == kDNSServiceType_AAAA ) )
	{
		dns_fixed_fields_record		recFields;
		dns_fixed_fields_rrsig		sigFields;
		uint8_t *					idPtr;					// Pointer to the host identifier portion of an IP address.
		size_t						recordLen;				// Length of the entire record.
		size_t						rdataLen;				// Length of record's RDATA.
		size_t						signerLen = 0;
		unsigned int				i;						// For-loop counter.
		uint8_t						rdata[ 16 ];			// A buffer that's big enough for either A or AAAA RDATA.
		uint8_t						randIntegers[ 255 ];	// Array for random integers in [1, 255].
		Boolean						needSig, nameIsV6Only;
		
		if( inQType == kDNSServiceType_A )
		{
			uint32_t		baseAddr;
			
			require_action_quiet( nameFlags & kDNSNameFlag_HasA, done, status = kQueryStatus_OK );
			
			rdataLen = 4;
			baseAddr = inUseBadAddrs ? kDNSServerBadBaseAddrV4 : kDNSServerBaseAddrV4;
			WriteBig32Typed( rdata, baseAddr );
			idPtr = &rdata[ 3 ]; // The last octet is the host identifier since the IPv4 address block is /24.
		}
		else
		{
			const uint8_t		( *baseAddr )[ 16 ];
			
			require_action_quiet( nameFlags & kDNSNameFlag_HasAAAA, done, status = kQueryStatus_OK );
			
			rdataLen = 16;
			baseAddr = inUseBadAddrs ? &kDNSServerBadBaseAddrV6 : &kDNSServerBaseAddrV6;
			memcpy( rdata, baseAddr, rdataLen );
			idPtr = &rdata[ 14 ]; // The last two octets are the host identifier since we allow up to 511 IPv6 addresses.
		}
		
		if( randCount > 0 )
		{
			// Populate the array with all integers between 1 and <randCount>, inclusive.
			
			for( i = 0; i < randCount; ++i ) randIntegers[ i ] = (uint8_t)( i + 1 );
			
			// Prevent dubious static analyzer warning.
			// Note: _DNSServerParseHostName() already enforces randCount >= addrCount. Also, this require_fatal() check
			// needs to be placed right before the next for-loop. Any earlier, and the static analyzer warning will persist
			// for some reason.
			
			require_fatal( addrCount <= randCount, "Invalid Count label values: addrCount %u > randCount %u",
				addrCount, randCount );
			
			// Create a contiguous subarray starting at index 0 that contains <addrCount> randomly chosen integers between
			// 1 and <randCount>, inclusive.
			// Loop invariant 1: Array elements with indexes in [0, i - 1] have been randomly chosen.
			// Loop invariant 2: Array elements with indexes in [i, randCount - 1] are candidates for being chosen.
			
			for( i = 0; i < addrCount; ++i )
			{
				uint8_t			tmp;
				uint32_t		j;
				
				j = RandomRange( i, randCount - 1 );
				if( i != j )
				{
					tmp = randIntegers[ i ];
					randIntegers[ i ] = randIntegers[ j ];
					randIntegers[ j ] = tmp;
				}
			}
		}
		needSig = ( inDNSSEC && ( nameFlags & kDNSNameFlag_HasRRSIG ) ) ? true : false;
		if( needSig )
		{
			uint32_t		inceptionSecs;
			int				labelCount;
			
			// Initialize signing buffer.
			
			check( !sigMsg );
			sigMsg = &sigDB;
			DataBuffer_Init( sigMsg, sigBuf, sizeof( sigBuf ), SIZE_MAX );
			
			// Append RRSIG record RDATA fixed fields to signing buffer.
			
			memset( &sigFields, 0, sizeof( sigFields ) );
			dns_fixed_fields_rrsig_set_type_covered( &sigFields, (uint16_t) inQType );
			dns_fixed_fields_rrsig_set_algorithm( &sigFields, DNSKeyInfoGetAlgorithm( zsk ) );
			labelCount = DomainNameLabelCount( ownerLower );
			check( labelCount >= 0 );
			dns_fixed_fields_rrsig_set_labels( &sigFields, (uint8_t) labelCount );
			dns_fixed_fields_rrsig_set_original_ttl( &sigFields, ttl );
			inceptionSecs = (uint32_t) now.tv_sec;
			dns_fixed_fields_rrsig_set_signature_expiration( &sigFields, inceptionSecs + kSecondsPerDay );
			dns_fixed_fields_rrsig_set_signature_inception( &sigFields, inceptionSecs );
			dns_fixed_fields_rrsig_set_key_tag( &sigFields, DNSKeyInfoGetKeyTag( zsk ) );
			
			err = DataBuffer_Append( sigMsg, &sigFields, sizeof( sigFields ) );
			require_noerr( err, exit );
			
			// Append RRSIG record RDATA signer to signing buffer.
			
			signerLen = DomainNameLength( zone );
			err = DataBuffer_Append( sigMsg, zone, signerLen );
			require_noerr( err, exit );
		}
		recordLen = sizeof( nameCPtr ) + sizeof( recFields ) + rdataLen;
		dns_fixed_fields_record_init( &recFields, (uint16_t) inQType, kDNSServiceClass_IN, ttl, (uint16_t) rdataLen );
		
		nameIsV6Only = ( ( nameFlags & ( kDNSNameFlag_HasA | kDNSNameFlag_HasAAAA ) ) == kDNSNameFlag_HasAAAA );
		for( i = 0; i < addrCount; ++i )
		{
			uint32_t				addrHostID;
			const unsigned int		modulus = nameIsV6Only ? 512 : 256; // IPv6-only names can have up to 511 addresses.
			
			if( ( inTruncateLen > 0 ) && ( ( DataBuffer_GetLen( inDB ) + recordLen ) > inTruncateLen ) )
			{
				status = kQueryStatus_Truncated;
				goto done;
			}
			// Append A/AAAA record NAME to response.
			
			err = DataBuffer_Append( inDB, nameCPtr, sizeof( nameCPtr ) );
			require_noerr( err, exit );
			
			// Append A/AAAA record TYPE, CLASS, TTL, and RDLENGTH to response.
			
			err = DataBuffer_Append( inDB, &recFields, sizeof( recFields ) );
			require_noerr( err, exit );
			
			// Append A/AAAA record RDATA to response.
			
			addrHostID = ( randCount > 0 ) ? randIntegers[ i ] : ( i + 1 );
			addrHostID = ( addrHostID + offset ) % modulus;
			if( inQType == kDNSServiceType_A )
			{
				*idPtr = (uint8_t) addrHostID;
			}
			else
			{
				WriteBig16Typed( idPtr, (uint16_t) addrHostID );
			}
			err = DataBuffer_Append( inDB, rdata, rdataLen );
			require_noerr( err, exit );
			
			++answerCount;
			if( needSig )
			{
				// Append A/AAAA record to signing buffer.
				
				err = DataBuffer_Append( sigMsg, ownerLower, ownerLowerLen );
				require_noerr( err, exit );
				
				err = DataBuffer_Append( sigMsg, &recFields, sizeof( recFields ) );
				require_noerr( err, exit );
				
				err = DataBuffer_Append( sigMsg, rdata, rdataLen );
				require_noerr( err, exit );
			}
		}
		if( needSig )
		{
			uint8_t		signature[ kDNSServerSignatureLengthMax ];
			size_t		signatureLen;
			Boolean		didSign;
			
			// Compute signature with ZSK.
			
			memset( signature, 0, sizeof( signature ) );
			didSign = DNSKeyInfoSign( zsk, DataBuffer_GetPtr( sigMsg ), DataBuffer_GetLen( sigMsg ),
				signature, &signatureLen );
			require_quiet( didSign, exit );
			
		#if( DEBUG )
			_DNSServerSigCheck( ownerLower, inQType, DataBuffer_GetPtr( sigMsg ), DataBuffer_GetLen( sigMsg ), signature,
				signatureLen, zsk );
		#endif
			rdataLen	= sizeof( sigFields ) + signerLen + signatureLen;
			recordLen	= sizeof( nameCPtr ) + sizeof( recFields ) + rdataLen;
			if( ( inTruncateLen > 0 ) && ( ( DataBuffer_GetLen( inDB ) + recordLen ) > inTruncateLen ) )
			{
				status = kQueryStatus_Truncated;
				goto done;
			}
			// Append RRSIG record NAME to response.
			
			err = DataBuffer_Append( inDB, nameCPtr, sizeof( nameCPtr ) );
			require_noerr( err, exit );
			
			// Append RRSIG record TYPE, CLASS, TTL, and RDLENGTH to response.
			
			dns_fixed_fields_record_init( &recFields, kDNSServiceType_RRSIG, kDNSServiceClass_IN, ttl, (uint16_t) rdataLen );
			err = DataBuffer_Append( inDB, &recFields, sizeof( recFields ) );
			require_noerr( err, exit );
			
			// Append RRSIG record RDATA fixed fields and signer to response.
			
			err = DataBuffer_Append( inDB, &sigFields, sizeof( sigFields ) );
			require_noerr( err, exit );
			
			err = DataBuffer_Append( inDB, zone, signerLen );
			require_noerr( err, exit );
			
			// Append RRSIG record RDATA signature to response.
			
			err = DataBuffer_Append( inDB, signature, signatureLen );
			require_noerr( err, exit );
			
			++answerCount;
			DataBuffer_Free( sigMsg );
			sigMsg = NULL;
		}
	}
	else if( inQType == kDNSServiceType_SRV )
	{
		dns_fixed_fields_record		recFields;
		size_t						i;
		
		require_action_quiet( nameFlags & kDNSNameFlag_HasSRV, done, status = kQueryStatus_OK );
		
		dns_fixed_fields_record_init( &recFields, kDNSServiceType_SRV, kDNSServiceClass_IN, me->defaultTTL, 0 );
		for( i = 0; i < srvCount; ++i )
		{
			dns_fixed_fields_srv		srvFields;
			size_t						rdataLen;
			size_t						recordLen;
			const ParsedSRV * const		srv = &srvArray[ i ];
			
			rdataLen  = sizeof( srvFields ) + srvDomainLen + srv->targetLen + 1;
			recordLen = sizeof( nameCPtr ) + sizeof( recFields ) + rdataLen;
			if( ( inTruncateLen > 0 ) && ( ( DataBuffer_GetLen( inDB ) + recordLen ) > inTruncateLen ) )
			{
				status = kQueryStatus_Truncated;
				goto done;
			}
			// Append record NAME to response.
			
			err = DataBuffer_Append( inDB, nameCPtr, sizeof( nameCPtr ) );
			require_noerr( err, exit );
			
			// Append record TYPE, CLASS, TTL, and RDLENGTH to response.
			
			dns_fixed_fields_record_set_rdlength( &recFields, (uint16_t) rdataLen );
			err = DataBuffer_Append( inDB, &recFields, sizeof( recFields ) );
			require_noerr( err, exit );
			
			// Append SRV RDATA priority, weight, and port to response.
			
			dns_fixed_fields_srv_init( &srvFields, srv->priority, srv->weight, srv->port );
			err = DataBuffer_Append( inDB, &srvFields, sizeof( srvFields ) );
			require_noerr( err, exit );
			
			// Append SRV RDATA target non-root labels to response.
			
			if( srv->targetLen > 0 )
			{
				err = DataBuffer_Append( inDB, srv->targetPtr, srv->targetLen );
				require_noerr( err, exit );
			}
			
			if( srvDomainLen > 0 )
			{
				err = DataBuffer_Append( inDB, srvDomainPtr, srvDomainLen );
				require_noerr( err, exit );
			}
			
			// Append SRV RDATA target root label to response.
			
			err = DataBuffer_Append( inDB, "", 1 );
			require_noerr( err, exit );
			
			++answerCount;
		}
	}
	else if( inQType == kDNSServiceType_SOA )
	{
		size_t		nameLen, recordLen;
		
		require_action_quiet( nameFlags & kDNSNameFlag_HasSOA, done, status = kQueryStatus_OK );
		
		nameLen	= DomainNameLength( me->domain );
		if( inTruncateLen > 0 )
		{
			err = AppendSOARecord( NULL, me->domain, nameLen, 0, 0, 0, kRootLabel, kRootLabel, 0, 0, 0, 0, 0, &recordLen );
			require_noerr( err, exit );
			
			if( ( DataBuffer_GetLen( inDB ) + recordLen ) > inTruncateLen )
			{
				status = kQueryStatus_Truncated;
				goto done;
			}
		}
		err = AppendSOARecord( inDB, me->domain, nameLen, kDNSServiceType_SOA, kDNSServiceClass_IN, me->defaultTTL,
			kRootLabel, kRootLabel, me->serial, 1 * kSecondsPerDay, 2 * kSecondsPerHour, 1000 * kSecondsPerHour,
			me->defaultTTL, NULL );
		require_noerr( err, exit );
		
		++answerCount;
	}
	else if( inQType == kDNSServiceType_PTR )
	{
		dns_fixed_fields_record		recFields;
		size_t						domainLen, rdataLen, recordLen;
		uint8_t						label[ 1 + kDomainLabelLengthMax ];
		uint8_t *					dst = &label[ 1 ];
		const uint8_t *				lim = &label[ countof( label ) ];
		
		if( nameFlags & kDNSNameFlag_HasPTRv4 )
		{
			size_t				maxLen;
			int					n;
			const uint32_t		ipv4Addr = kDNSServerBaseAddrV4 + hostID;
			
			maxLen = (size_t)( lim - dst );
			n = MemPrintF( dst, maxLen, "ipv4-%u-%u-%u-%u",
				( ipv4Addr >> 24 ) & 0xFFU,
				( ipv4Addr >> 16 ) & 0xFFU,
				( ipv4Addr >>  8 ) & 0xFFU,
				  ipv4Addr         & 0xFFU );
			require_fatal( ( n > 0 ) && ( ( (size_t) n ) <= maxLen ), "Failed to print reverse IPv4 hostname label" );
			dst += n;
		}
		else if( nameFlags & kDNSNameFlag_HasPTRv6 )
		{
			size_t		maxLen;
			int			n, i;
			uint8_t		ipv6Addr[ 16 ];
			
			maxLen = (size_t)( lim - dst );
			n = MemPrintF( dst, maxLen, "ipv6" );
			require_fatal( ( n > 0 ) && ( ( (size_t) n ) <= maxLen ), "Failed to print reverse IPv6 hostname label" );
			dst += n;
			
			memcpy( ipv6Addr, kDNSServerBaseAddrV6, 16 );
			ipv6Addr[ 15 ] = (uint8_t) hostID;
			for( i = 0; i < 8; ++i )
			{
				maxLen = (size_t)( lim - dst );
				n = MemPrintF( dst, maxLen, "-%04x", ReadBig16( &ipv6Addr[ i * 2 ] ) );
				require_fatal( ( n > 0 ) && ( ( (size_t) n ) <= maxLen ), "Failed to print reverse IPv6 hostname label" );
				dst += n;
			}
		}
		else
		{
			status = kQueryStatus_OK;
			goto done;
		}
		label[ 0 ] = (uint8_t)( dst - &label[ 1 ] );
		
		domainLen	= DomainNameLength( me->domain );
		rdataLen	= 1 + label[ 0 ] + domainLen;
		recordLen	= sizeof( nameCPtr ) + sizeof( recFields ) + rdataLen;
		if( ( inTruncateLen > 0 ) && ( ( DataBuffer_GetLen( inDB ) + recordLen ) > inTruncateLen ) )
		{
			status = kQueryStatus_Truncated;
			goto done;
		}
		
		// Append PTR record NAME to response.
		
		err = DataBuffer_Append( inDB, nameCPtr, sizeof( nameCPtr ) );
		require_noerr( err, exit );
		
		// Append PTR record TYPE, CLASS, TTL, and RDLENGTH to response.
		
		dns_fixed_fields_record_init( &recFields, kDNSServiceType_PTR, kDNSServiceClass_IN, me->defaultTTL,
			(uint16_t) rdataLen );
		err = DataBuffer_Append( inDB, &recFields, sizeof( recFields ) );
		require_noerr( err, exit );
		
		// Append PTR record RDATA to response.
		
		err = DataBuffer_Append( inDB, label, 1 + label[ 0 ] );
		require_noerr( err, exit );
		
		err = DataBuffer_Append( inDB, me->domain, domainLen );
		require_noerr( err, exit );
		
		++answerCount;
	}
	else if( inQType == kDNSServiceType_DNSKEY )
	{
		size_t						recordLen;
		size_t						signerLen = 0;
		dns_fixed_fields_record		recFields;
		dns_fixed_fields_rrsig		sigFields;
		Boolean						needSig;
		
		require_action_quiet( nameFlags & kDNSNameFlag_HasDNSKEY, done, status = kQueryStatus_OK );
		
		recordLen = sizeof( nameCPtr ) + sizeof( recFields ) + DNSKeyInfoGetRDataLen( zsk );
		if( ( inTruncateLen > 0 ) && ( ( DataBuffer_GetLen( inDB ) + recordLen ) > inTruncateLen ) )
		{
			status = kQueryStatus_Truncated;
			goto done;
		}
		// Append ZSK DNSKEY record NAME to response.
		
		err = DataBuffer_Append( inDB, nameCPtr, sizeof( nameCPtr ) );
		require_noerr( err, exit );
		
		// Append ZSK DNSKEY record TYPE, CLASS, TTL, and RDLENGTH to response.
		
		dns_fixed_fields_record_init( &recFields, kDNSServiceType_DNSKEY, kDNSServiceClass_IN, ttl,
			DNSKeyInfoGetRDataLen( zsk ) );
		err = DataBuffer_Append( inDB, &recFields, sizeof( recFields ) );
		require_noerr( err, exit );
		
		// Append ZSK DNSKEY record RDATA to response.
		
		err = DataBuffer_Append( inDB, DNSKeyInfoGetRDataPtr( zsk ), DNSKeyInfoGetRDataLen( zsk ) );
		require_noerr( err, exit );
		
		++answerCount;
		
		needSig = ( inDNSSEC && ( nameFlags & kDNSNameFlag_HasRRSIG ) ) ? true : false;
		if( needSig )
		{
			uint32_t		inceptionSecs;
			int				labelCount;
			
			// Initialize signing buffer.
			
			check( !sigMsg );
			sigMsg = &sigDB;
			DataBuffer_Init( sigMsg, sigBuf, sizeof( sigBuf ), SIZE_MAX );
			
			// Append RRSIG record RDATA fixed fields to signing buffer.
			
			memset( &sigFields, 0, sizeof( sigFields ) );
			dns_fixed_fields_rrsig_set_type_covered( &sigFields, kDNSServiceType_DNSKEY );
			dns_fixed_fields_rrsig_set_algorithm( &sigFields, DNSKeyInfoGetAlgorithm( ksk ) );
			labelCount = DomainNameLabelCount( ownerLower );
			check( labelCount >= 0 );
			dns_fixed_fields_rrsig_set_labels( &sigFields, (uint8_t) labelCount );
			dns_fixed_fields_rrsig_set_original_ttl( &sigFields, ttl );
			inceptionSecs = (uint32_t) now.tv_sec;
			dns_fixed_fields_rrsig_set_signature_expiration( &sigFields, inceptionSecs + kSecondsPerDay );
			dns_fixed_fields_rrsig_set_signature_inception( &sigFields, inceptionSecs );
			dns_fixed_fields_rrsig_set_key_tag( &sigFields, DNSKeyInfoGetKeyTag( ksk ) );
			
			err = DataBuffer_Append( sigMsg, &sigFields, sizeof( sigFields ) );
			require_noerr( err, exit );
			
			// Append RRSIG record RDATA signer to signing buffer.
			
			signerLen = DomainNameLength( zone );
			err = DataBuffer_Append( sigMsg, zone, signerLen );
			require_noerr( err, exit );
			
			// Append ZSK DNSKEY record to signing buffer.
			
			err = DataBuffer_Append( sigMsg, ownerLower, ownerLowerLen );
			require_noerr( err, exit );
			
			err = DataBuffer_Append( sigMsg, &recFields, sizeof( recFields ) );
			require_noerr( err, exit );
			
			err = DataBuffer_Append( sigMsg, DNSKeyInfoGetRDataPtr( zsk ), DNSKeyInfoGetRDataLen( zsk ) );
			require_noerr( err, exit );
		}
		recordLen = sizeof( nameCPtr ) + sizeof( recFields ) + DNSKeyInfoGetRDataLen( ksk );
		if( ( inTruncateLen > 0 ) && ( ( DataBuffer_GetLen( inDB ) + recordLen ) > inTruncateLen ) )
		{
			status = kQueryStatus_Truncated;
			goto done;
		}
		// Append KSK DNSKEY record NAME to response.
		
		err = DataBuffer_Append( inDB, nameCPtr, sizeof( nameCPtr ) );
		require_noerr( err, exit );
		
		// Append KSK DNSKEY record TYPE, CLASS, TTL, and RDLENGTH to response.
		
		err = DataBuffer_Append( inDB, &recFields, sizeof( recFields ) );
		require_noerr( err, exit );
		
		// Append KSK DNSKEY record RDATA to response.
		
		err = DataBuffer_Append( inDB, DNSKeyInfoGetRDataPtr( ksk ), DNSKeyInfoGetRDataLen( ksk ) );
		require_noerr( err, exit );
		
		++answerCount;
		if( needSig )
		{
			size_t		rdataLen;
			uint8_t		signature[ kDNSServerSignatureLengthMax ];
			size_t		signatureLen;
			Boolean		didSign;
			
			// Append KSK DNSKEY record to signing buffer.
			
			err = DataBuffer_Append( sigMsg, ownerLower, ownerLowerLen );
			require_noerr( err, exit );
			
			err = DataBuffer_Append( sigMsg, &recFields, sizeof( recFields ) );
			require_noerr( err, exit );
			
			err = DataBuffer_Append( sigMsg, DNSKeyInfoGetRDataPtr( ksk ), DNSKeyInfoGetRDataLen( ksk ) );
			require_noerr( err, exit );
			
			// Compute signature with KSK.
			
			memset( signature, 0, sizeof( signature ) );
			didSign = DNSKeyInfoSign( ksk, DataBuffer_GetPtr( sigMsg ), DataBuffer_GetLen( sigMsg ),
				signature, &signatureLen );
			require_quiet( didSign, exit );
			
		#if( DEBUG )
			_DNSServerSigCheck( ownerLower, inQType, DataBuffer_GetPtr( sigMsg ), DataBuffer_GetLen( sigMsg ), signature,
				signatureLen, ksk );
		#endif
			rdataLen	= sizeof( sigFields ) + signerLen + signatureLen;
			recordLen	= sizeof( nameCPtr ) + sizeof( recFields ) + rdataLen;
			if( ( inTruncateLen > 0 ) && ( ( DataBuffer_GetLen( inDB ) + recordLen ) > inTruncateLen ) )
			{
				status = kQueryStatus_Truncated;
				goto done;
			}
			// Append RRSIG record NAME to response.
			
			err = DataBuffer_Append( inDB, nameCPtr, sizeof( nameCPtr ) );
			require_noerr( err, exit );
			
			// Append RRSIG record TYPE, CLASS, TTL, and RDLENGTH to response.
			
			dns_fixed_fields_record_init( &recFields, kDNSServiceType_RRSIG, kDNSServiceClass_IN, ttl, (uint16_t) rdataLen );
			err = DataBuffer_Append( inDB, &recFields, sizeof( recFields ) );
			require_noerr( err, exit );
			
			// Append RRSIG record RDATA fixed fields and signer to response.
			
			err = DataBuffer_Append( inDB, &sigFields, sizeof( sigFields ) );
			require_noerr( err, exit );
			
			err = DataBuffer_Append( inDB, zone, signerLen );
			require_noerr( err, exit );
			
			// Append RRSIG record RDATA signature to response.
			
			err = DataBuffer_Append( inDB, signature, signatureLen );
			require_noerr( err, exit );
			
			++answerCount;
			DataBuffer_Free( sigMsg );
			sigMsg = NULL;
		}
	}
	else if( inQType == kDNSServiceType_DS )
	{
		SHA256_CTX					ctx;
		size_t						rdataLen, i, recordLen;
		size_t						signerLen = 0;
		dns_ds_sha256 *				dsPtr;
		dns_ds_sha256 *				dsPtrs[ 2 ];
		int							cmp;
		dns_ds_sha256				dsZSK, dsKSK;
		dns_fixed_fields_rrsig		sigFields;
		dns_fixed_fields_record		recFields;
		Boolean						needSig;
		
		check_compile_time_code( sizeof( dsPtr->digest ) == SHA256_DIGEST_LENGTH );
		
		require_action_quiet( nameFlags & kDNSNameFlag_HasDS, done, status = kQueryStatus_OK );
		
		// Set up ZSK DS RDATA.
		
		dsPtr = &dsZSK;
		memset( dsPtr, 0, sizeof( *dsPtr ) );
		dns_ds_sha256_set_key_tag( dsPtr, DNSKeyInfoGetKeyTag( zsk ) );
		dns_ds_sha256_set_algorithm( dsPtr, DNSKeyInfoGetAlgorithm( zsk ) );
		dns_ds_sha256_set_digest_type( dsPtr, kDSDigestType_SHA256 );
		
		SHA256_Init( &ctx );
		SHA256_Update( &ctx, ownerLower, ownerLowerLen );
		SHA256_Update( &ctx, DNSKeyInfoGetRDataPtr( zsk ), DNSKeyInfoGetRDataLen( zsk ) );
		SHA256_Final( dsPtr->digest, &ctx );
		
		// Set up KSK DS RDATA.
		
		dsPtr = &dsKSK;
		memset( dsPtr, 0, sizeof( *dsPtr ) );
		dns_ds_sha256_set_key_tag( dsPtr, DNSKeyInfoGetKeyTag( ksk ) );
		dns_ds_sha256_set_algorithm( dsPtr, DNSKeyInfoGetAlgorithm( ksk ) );
		dns_ds_sha256_set_digest_type( dsPtr, kDSDigestType_SHA256 );
		
		SHA256_Init( &ctx );
		SHA256_Update( &ctx, ownerLower, ownerLowerLen );
		SHA256_Update( &ctx, DNSKeyInfoGetRDataPtr( ksk ), DNSKeyInfoGetRDataLen( ksk ) );
		SHA256_Final( dsPtr->digest, &ctx );
		
		// Order the DS RDATAs
		
		cmp = memcmp( &dsZSK, &dsKSK, sizeof( dns_ds_sha256 ) );
		if( cmp <= 0 )
		{
			dsPtrs[ 0 ] = &dsZSK;
			dsPtrs[ 1 ] = ( cmp == 0 ) ? NULL : &dsKSK;
		}
		else
		{
			dsPtrs[ 0 ] = &dsKSK;
			dsPtrs[ 1 ] = &dsZSK;
		}
		needSig = ( inDNSSEC && ( nameFlags & kDNSNameFlag_HasRRSIG ) ) ? true : false;
		if( needSig )
		{
			uint32_t		inceptionSecs;
			int				labelCount;
			
			// Initialize signing buffer.
			
			check( !sigMsg );
			sigMsg = &sigDB;
			DataBuffer_Init( sigMsg, sigBuf, sizeof( sigBuf ), SIZE_MAX );
			
			// Append RRSIG record RDATA fixed fields to signing buffer.
			
			memset( &sigFields, 0, sizeof( sigFields ) );
			dns_fixed_fields_rrsig_set_type_covered( &sigFields, kDNSServiceType_DS );
			dns_fixed_fields_rrsig_set_algorithm( &sigFields, DNSKeyInfoGetAlgorithm( zskParent ) );
			labelCount = DomainNameLabelCount( ownerLower );
			check( labelCount >= 0 );
			dns_fixed_fields_rrsig_set_labels( &sigFields, (uint8_t) labelCount );
			dns_fixed_fields_rrsig_set_original_ttl( &sigFields, ttl );
			inceptionSecs = (uint32_t) now.tv_sec;
			dns_fixed_fields_rrsig_set_signature_expiration( &sigFields, inceptionSecs + kSecondsPerDay );
			dns_fixed_fields_rrsig_set_signature_inception( &sigFields, inceptionSecs );
			dns_fixed_fields_rrsig_set_key_tag( &sigFields, DNSKeyInfoGetKeyTag( zskParent ) );
			
			err = DataBuffer_Append( sigMsg, &sigFields, sizeof( sigFields ) );
			require_noerr( err, exit );
			
			// Append RRSIG record RDATA signer to signing buffer.
			
			signerLen = DomainNameLength( zoneParent );
			err = DataBuffer_Append( sigMsg, zoneParent, signerLen );
			require_noerr( err, exit );
		}
		rdataLen = sizeof( dns_ds_sha256 );
		dns_fixed_fields_record_init( &recFields, kDNSServiceType_DS, kDNSServiceClass_IN, ttl, (uint16_t) rdataLen );
		for( i = 0; i < countof( dsPtrs ); ++i )
		{
			dsPtr = dsPtrs[ i ];
			if( !dsPtr ) continue;
			
			recordLen = sizeof( nameCPtr ) + sizeof( recFields ) + rdataLen;
			if( ( inTruncateLen > 0 ) && ( ( DataBuffer_GetLen( inDB ) + recordLen ) > inTruncateLen ) )
			{
				status = kQueryStatus_Truncated;
				goto done;
			}
			// Append DS record NAME to response.
			
			err = DataBuffer_Append( inDB, nameCPtr, sizeof( nameCPtr ) );
			require_noerr( err, exit );
			
			// Append DS record TYPE, CLASS, TTL, and RDLENGTH to response.
			err = DataBuffer_Append( inDB, &recFields, sizeof( recFields ) );
			require_noerr( err, exit );
			
			// Append DS record RDATA to response.
			
			err = DataBuffer_Append( inDB, dsPtr, sizeof( *dsPtr ) );
			require_noerr( err, exit );
			
			++answerCount;
			if( needSig )
			{
				// Append DS record to signing buffer.
				
				err = DataBuffer_Append( sigMsg, ownerLower, ownerLowerLen );
				require_noerr( err, exit );
				
				err = DataBuffer_Append( sigMsg, &recFields, sizeof( recFields ) );
				require_noerr( err, exit );
				
				err = DataBuffer_Append( sigMsg, dsPtr, sizeof( *dsPtr ) );
				require_noerr( err, exit );
			}
		}
		if( needSig )
		{
			uint8_t		signature[ kDNSServerSignatureLengthMax ];
			size_t		signatureLen;
			Boolean		didSign;
			
			// Compute signature with parent ZSK.
			
			memset( signature, 0, sizeof( signature ) );
			didSign = DNSKeyInfoSign( zskParent, DataBuffer_GetPtr( sigMsg ), DataBuffer_GetLen( sigMsg ),
				signature, &signatureLen );
			require_quiet( didSign, exit );
			
		#if( DEBUG )
			_DNSServerSigCheck( ownerLower, inQType, DataBuffer_GetPtr( sigMsg ), DataBuffer_GetLen( sigMsg ), signature,
				signatureLen, zskParent );
		#endif
			rdataLen	= sizeof( sigFields ) + signerLen + signatureLen;
			recordLen	= sizeof( nameCPtr ) + sizeof( recFields ) + rdataLen;
			if( ( inTruncateLen > 0 ) && ( ( DataBuffer_GetLen( inDB ) + recordLen ) > inTruncateLen ) )
			{
				status = kQueryStatus_Truncated;
				goto done;
			}
			// Append RRSIG record NAME to response.
			
			err = DataBuffer_Append( inDB, nameCPtr, sizeof( nameCPtr ) );
			require_noerr( err, exit );
			
			// Append RRSIG record TYPE, CLASS, TTL, and RDLENGTH to response.
			
			dns_fixed_fields_record_init( &recFields, kDNSServiceType_RRSIG, kDNSServiceClass_IN, ttl, (uint16_t) rdataLen );
			err = DataBuffer_Append( inDB, &recFields, sizeof( recFields ) );
			require_noerr( err, exit );
			
			// Append RRSIG record RDATA fixed fields and signer to response.
			
			err = DataBuffer_Append( inDB, &sigFields, sizeof( sigFields ) );
			require_noerr( err, exit );
			
			err = DataBuffer_Append( inDB, zoneParent, signerLen );
			require_noerr( err, exit );
			
			// Append RRSIG record RDATA signature to response.
			
			err = DataBuffer_Append( inDB, signature, signatureLen );
			require_noerr( err, exit );
			
			++answerCount;
			
			DataBuffer_Free( sigMsg );
			sigMsg = NULL;
		}
	}
	status = kQueryStatus_OK;
	
done:
	hdr = (DNSHeader *) DataBuffer_GetPtr( inDB );
	flags = DNSHeaderGetFlags( hdr );
	switch( status )
	{
		case kQueryStatus_OK:
		case kQueryStatus_Truncated:
			flags |= kDNSHeaderFlag_AuthAnswer;
			if( status == kQueryStatus_Truncated ) flags |= kDNSHeaderFlag_Truncation;
			rcode = nameExists ? kDNSRCode_NoError : kDNSRCode_NXDomain;
			break;
		
		case kQueryStatus_NotImplemented:
			rcode = kDNSRCode_NotImp;
			break;
		
		case kQueryStatus_Refused:
			rcode = kDNSRCode_Refused;
			break;
		
		case kQueryStatus_Null:
		default:
			err = kInternalErr;
			goto exit;
	}
	if( rcodeOverride >= 0 )
	{
		dns_fixed_fields_record		recFields;
		const char *				rcodeStr;
		size_t						maxLen, txtStrLen, rdataLen, recordLen;
		int							n;
		uint8_t						rdataBuf[ 1 + 255 ]; // Enough space for one TXT record string.
		
		// Create the RDATA for an informational TXT record that contains the original rcode to put in the Additional
		// section.
		
		maxLen = sizeof( rdataBuf ) - 1;
		rcodeStr = DNSRCodeToString( rcode );
		if( rcodeStr )	n = MemPrintF( &rdataBuf[ 1 ], maxLen, "original-rcode=%s", rcodeStr );
		else			n = MemPrintF( &rdataBuf[ 1 ], maxLen, "original-rcode=%d", rcode );
		txtStrLen = ( n > 0 ) ? Min( (size_t) n, maxLen ) : 0;
		rdataBuf[ 0 ] = (uint8_t) txtStrLen;
		rdataLen = 1 + txtStrLen;
		
		// The TXT record isn't strictly necessary, so only include it if it fits.
		
		recordLen = sizeof( nameCPtr ) + sizeof( recFields ) + rdataLen;
		maxLen = ( inTruncateLen > 0 ) ? inTruncateLen : kDNSMaxTCPMessageSize;
		if( ( DataBuffer_GetLen( inDB ) < maxLen ) && ( ( maxLen - DataBuffer_GetLen( inDB ) ) >= recordLen ) )
		{
			// Append TXT record NAME to response.
			
			err = DataBuffer_Append( inDB, nameCPtr, sizeof( nameCPtr ) );
			require_noerr( err, exit );
			
			// Append TXT record TYPE, CLASS, TTL, and RDLENGTH to response.
			
			dns_fixed_fields_record_init( &recFields, kDNSRecordType_TXT, kDNSClassType_IN, 0, (uint16_t) rdataLen );
			err = DataBuffer_Append( inDB, &recFields, sizeof( recFields ) );
			require_noerr( err, exit );
			
			// Append TXT record RDATA to response.
			
			err = DataBuffer_Append( inDB, rdataBuf, rdataLen );
			require_noerr( err, exit );
			
			++additionalCount;
		}
		rcode = rcodeOverride;
	}
	DNSFlagsSetRCode( flags, rcode );
	DNSHeaderSetFlags( hdr, flags );
	DNSHeaderSetAnswerCount( hdr, answerCount );
	DNSHeaderSetAdditionalCount( hdr, additionalCount );
	if( procDelayMs > 0 )
	{
		const uint64_t		delayTicks		= MillisecondsToUpTicks( procDelayMs );
		const uint64_t		elapsedTicks	= UpTicks() - startTicks;
		
		if( delayTicks > elapsedTicks ) SleepForUpTicks( delayTicks - elapsedTicks );
	}
	err = kNoErr;
	
exit:
	FreeNullSafe( qnameLower );
	if( sigMsg ) DataBuffer_Free( sigMsg );
	return( err );
}

static Boolean
	_DNSServerNameIsDNSSECZone(
		const uint8_t *		inName,
		const uint8_t **	outZoneParent,
		DNSKeyInfoRef *		outZSK,
		DNSKeyInfoRef *		outKSK,
		DNSKeyInfoRef *		outParentZSK );

static Boolean
	_DNSServerParseHostName(
		DNSServerRef		me,
		const uint8_t *		inQName,
		uint32_t *			outAliasCount,
		uint32_t			outAliasTTLs[ kAliasTTLCountMax ],
		uint32_t *			outAliasTTLCount,
		uint32_t *			outCount,
		uint32_t *			outRandCount,
		uint32_t *			outIndex,
		int *				outRCode,
		uint32_t *			outTTL,
		uint32_t *			outOffset,
		uint32_t *			outProcDelayMs,
		DNSNameFlags *		outFlags,
		const uint8_t **	outZone,
		const uint8_t **	outZoneParent,
		DNSKeyInfoRef *		outZSK,
		DNSKeyInfoRef *		outKSK,
		DNSKeyInfoRef *		outParentZSK,
		DNSServerAction *	outAction )
{
	OSStatus			err;
	const uint8_t *		label;
	size_t				labelLen;
    const uint8_t *		labelNext;
	uint32_t			aliasTTLCount	= 0;	// Count of TTL args from Alias-TTL label.
	uint32_t			aliasCount		= 0;	// Arg from Alias label. Valid values are in [2, 2^31 - 1].
	int32_t				count			= -1;	// First arg from Count label. Valid values are in [0, 255].
	uint32_t			randCount		= 0;	// Second arg from Count label. Valid values are in [count, 255].
	uint32_t			index			= 0;	// Arg from Index label. Valid values are in [1, 2^32 - 1].
	int					rcode			= -1;	// Arg from RCode label. Valid values are in [0, 15].
	int32_t				ttl				= -1;	// Arg from TTL label. Valid values are in [0, 2^31 - 1].
	int32_t				offset			= -1;	// Arg from Offset label. Valid values are in [0, 2^31 - 1].
	int32_t				procDelayMs		= -1;	// Arg from PDelay label. Valid values are in [0, 2000]. Units are in ms.
	DNSNameFlags		flags			= 0;
	int32_t				maxCount;
	const uint8_t *		zone			= NULL;
	const uint8_t *		zoneParent		= NULL;
	DNSKeyInfoRef		zsk				= NULL;
	DNSKeyInfoRef		ksk				= NULL;
	DNSKeyInfoRef		parentZSK		= NULL;
	DNSServerAction		action			= kDNSServerAction_None;
	Boolean				isAlias			= false;
	
	for( label = inQName; ( labelLen = *label ) != 0; label = labelNext )
	{
		const uint8_t *		labelData;
		uint32_t			arg;

		if( labelLen > kDomainLabelLengthMax ) break;
		labelData = &label[ 1 ];
		labelNext = &labelData[ labelLen ];

		if( label == inQName )
		{
			// Check if the first label is a valid alias TTL sequence label.
			// Note: Since "alias" is a prefix of "alias-ttl", check for "alias-ttl" first.

			if( strnicmp_prefix( labelData, labelLen, kLabelPrefix_AliasTTL ) == 0 )
			{
				const char *			ptr = (const char *) &labelData[ sizeof_string( kLabelPrefix_AliasTTL ) ];
				const char * const		end = (const char *) labelNext;

				while( ptr < end )
				{
					if( *ptr++ != '-' ) break;
					err = DecimalTextToUInt32( ptr, end, &arg, &ptr );
					if( err || ( arg > INT32_MAX ) ) break; // TTL must be in [0, 2^31 - 1].
					if( outAliasTTLs ) outAliasTTLs[ aliasTTLCount ] = arg;
					++aliasTTLCount;
				}
				if( ( aliasTTLCount == 0 ) || ( ptr != end ) ) break;
				isAlias = true;
				continue;
			}

			// Check if the first label is a valid alias label.

			if( ( strnicmp_prefix( labelData, labelLen, kLabelPrefix_Alias ) == 0 ) )
			{
				const char *			ptr = (const char *) &labelData[ sizeof_string( kLabelPrefix_Alias ) ];
				const char * const		end = (const char *) labelNext;

				if( ptr < end )
				{
					if( *ptr++ != '-' ) break;
					err = DecimalTextToUInt32( ptr, end, &arg, &ptr );
					if( err || ( arg < 2 ) || ( arg > INT32_MAX ) ) break; // Alias count must be in [2, 2^31 - 1].
					aliasCount = arg;
					if( ptr != end ) break;
				}
				else
				{
					aliasCount = 1;
				}
				isAlias = true;
				continue;
			}
		}

		// Check if this label is a valid count label.

		if( strnicmp_prefix( labelData, labelLen, kLabelPrefix_Count ) == 0  )
		{
			const char *			ptr = (const char *) &labelData[ sizeof_string( kLabelPrefix_Count ) ];
			const char * const		end = (const char *) labelNext;

			if( count >= 0 ) break; // Count cannot be specified more than once.

			err = DecimalTextToUInt32( ptr, end, &arg, &ptr );
			if( err || ( arg > INT32_MAX ) ) break; // The actual upper bound for Count will be verified below.
			count = (int32_t) arg;

			if( ptr < end )
			{
				if( *ptr++ != '-' ) break;
				err = DecimalTextToUInt32( ptr, end, &arg, &ptr );
				if( err || ( arg < ( (uint32_t) count ) ) || ( arg > 255 ) ) break; // Rand count must be in [count, 255].
				randCount = arg;
				if( ptr != end ) break;
			}
			continue;
		}
		
		// Check if this label is a valid Index label.
		
		if( strnicmp_prefix( labelData, labelLen, kLabelPrefix_Index ) == 0  )
		{
			const char *			ptr = (const char *) &labelData[ sizeof_string( kLabelPrefix_Index ) ];
			const char * const		end = (const char *) labelNext;
			
			if( index > 0 ) break; // Index cannot be specified more than once.
			
			err = DecimalTextToUInt32( ptr, end, &arg, &ptr );
			if( err || ( arg < 1 ) || ( arg > UINT32_MAX ) ) break; // Index must be in [1, 2^32 - 1].
			index = arg;
			if( ptr != end ) break;
			continue;
		}
		
		// Check if this label is a valid IPv4 label.
		
		if( strnicmpx( labelData, labelLen, kLabel_IPv4 ) == 0 )
		{
			// Valid names have at most one IPv4 or IPv6 label.
			
			if( flags & ( kDNSNameFlag_HasA | kDNSNameFlag_HasAAAA ) ) break;
			flags |= kDNSNameFlag_HasA;
			continue;
		}
		
		// Check if this label is a valid IPv6 label.
		
		if( strnicmpx( labelData, labelLen, kLabel_IPv6 ) == 0 )
		{
			// Valid names have at most one IPv4 or IPv6 label.
			
			if( flags & ( kDNSNameFlag_HasA | kDNSNameFlag_HasAAAA ) ) break;
			flags |= kDNSNameFlag_HasAAAA;
			continue;
		}
		
		// Check if this label is a valid tag label.
		
		if( strnicmp_prefix( labelData, labelLen, kLabelPrefix_Tag ) == 0  )
		{
			continue;
		}
		
		// Check if this label is a valid RCode label.
		
		if( strnicmp_prefix( labelData, labelLen, kLabelPrefix_RCode ) == 0  )
		{
			const char *			ptr = (const char *) &labelData[ sizeof_string( kLabelPrefix_RCode ) ];
			const char * const		end = (const char *) labelNext;
			const char *			src;
			char *					dst;
			const char *			lim;
			char					argStr[ kDomainLabelLengthMax + 1 ];
			
			if( rcode >= 0 ) break; // RCode cannot be specified more than once.
			
			// First check if the RCode label's argument is an RCODE mnemonic, e.g., ServFail, Refused, etc.
			// The argument part of a label consists of all of the characters up to the start of the next label.
			// In order to treat the argument as a C string, the argument must not contain any NUL characters.
			// For example, a malformed label such as "rcode-refused\x00garbage" has the argument "refused\x00garbage",
			// but as a C string, the NUL character makes it "refused".
			
			src = ptr;
			dst = argStr;
			lim = &argStr[ countof( argStr ) - 1 ];
			while( ( src < end ) && ( *src != '\0' ) && ( dst < lim ) ) *dst++ = *src++;
			if( src == end )
			{
				*dst = '\0';
				rcode = DNSRCodeFromString( argStr );
			}
			
			// If we don't have a valid rcode yet, try to parse the argument as a decimal integer.
			
			if( rcode < 0 )
			{
				err = DecimalTextToUInt32( ptr, end, &arg, &ptr );
				if( err || ( arg > 15 ) ) break; // RCode must be in [0, 15].
				rcode = (int) arg;
				if( ptr != end ) break;
			}
			continue;
		}
		
		// Check if this label is a valid TTL label.
		
		if( strnicmp_prefix( labelData, labelLen, kLabelPrefix_TTL ) == 0  )
		{
			const char *			ptr = (const char *) &labelData[ sizeof_string( kLabelPrefix_TTL ) ];
			const char * const		end = (const char *) labelNext;
			
			if( ttl >= 0 ) break; // TTL cannot be specified more than once.
			
			err = DecimalTextToUInt32( ptr, end, &arg, &ptr );
			if( err || ( arg > INT32_MAX ) ) break; // TTL must be in [0, 2^31 - 1].
			ttl = (int32_t) arg;
			if( ptr != end ) break;
			continue;
		}
		
		// Check if this label is a valid Offset label.
		
		if( strnicmp_prefix( labelData, labelLen, kLabelPrefix_Offset ) == 0 )
		{
			const char *			ptr = (const char *) &labelData[ sizeof_string( kLabelPrefix_Offset ) ];
			const char * const		end = (const char *) labelNext;
			
			if( offset >= 0 ) break; // Offset cannot be specified more than once.
			
			err = DecimalTextToUInt32( ptr, end, &arg, &ptr );
			if( err ) break;
			offset = (int32_t) arg;
			if( ptr != end ) break;
			continue;
		}
		
		// Check if this label is a valid PDelay label.
		
		if( strnicmp_prefix( labelData, labelLen, kLabelPrefix_PDelay ) == 0 )
		{
			const char *			ptr = (const char *) &labelData[ sizeof_string( kLabelPrefix_PDelay ) ];
			const char * const		end = (const char *) labelNext;
			
			if( procDelayMs >= 0 ) break; // PDelay cannot be specified more than once.
			
			err = DecimalTextToUInt32( ptr, end, &arg, &ptr );
			if( err || ( arg > 2000 ) ) break; // PDelay must be in [0, 2000].
			procDelayMs = (int32_t) arg;
			if( ptr != end ) break;
			continue;
		}
		
		// Check if this label is a valid CommandSuspend label.
		
		if( strnicmpx( labelData, labelLen, kLabel_CommandSuspend ) == 0 )
		{
			if( action != kDNSServerAction_None ) break; // A command cannot be specified more than once.
			action = kDNSServerAction_Suspend;
			continue;
		}
		
		// Check if this label is a valid CommandResume label.
		
		if( strnicmpx( labelData, labelLen, kLabel_CommandResume ) == 0 )
		{
			if( action != kDNSServerAction_None ) break; // A command cannot be specified more than once.
			action = kDNSServerAction_Resume;
			continue;
		}
		
		// If this and the remaining labels are equal to "d.test.", then the name exists.
		// Otherwise, this label is invalid. In both cases, there are no more labels to check.
		
		if( _DNSServerNameIsDNSSECZone( label, &zoneParent, &zsk, &ksk, &parentZSK ) )
		{
			zone = label;
			flags |= kDNSNameFlag_HasRRSIG;
			if( ( label == inQName ) || ( isAlias && ( label == DomainNameGetNextLabel( inQName ) ) ) )
			{
				flags |= kDNSNameFlag_HasSOA;
				flags |= kDNSNameFlag_HasDNSKEY;
				flags |= kDNSNameFlag_HasDS;
			}
		}
		else if( DomainNameEqual( label, me->domain ) )
		{
			zone = label;
			if( ( label == inQName ) || ( isAlias && ( label == DomainNameGetNextLabel( inQName ) ) ) )
			{
				flags |= kDNSNameFlag_HasSOA;
			}
		}
		break;
	}
	require_quiet( zone, exit );
	
	// If a Count value of 0 was specified, then the hostname has no A or AAAA records.
	// Otherwise, if the hostname has no IPv4 or IPv6 labels, then it has both A and AAAA records.
	
	if( count == 0 )
	{
		flags &= ~( kDNSNameFlag_HasA | kDNSNameFlag_HasAAAA );
	}
	else if( !( flags & ( kDNSNameFlag_HasA | kDNSNameFlag_HasAAAA ) ) )
	{
		flags |= ( kDNSNameFlag_HasA | kDNSNameFlag_HasAAAA );
	}
	
	// Allow IPv6-only hostnames to have a maximum address count of up to 511 instead of the normal 255.
	
	maxCount = ( ( flags & ( kDNSNameFlag_HasA | kDNSNameFlag_HasAAAA ) ) == kDNSNameFlag_HasAAAA ) ? 511 : 255;
	require_action_quiet( count <= maxCount, exit, zone = NULL );
	
	if( outAliasCount )		*outAliasCount		= aliasCount;
	if( outAliasTTLCount )	*outAliasTTLCount	= aliasTTLCount;
	if( outCount )			*outCount			= ( count       >= 0 ) ? ( (uint32_t) count )       : 1;
	if( outRandCount )		*outRandCount		= randCount;
	if( outIndex )			*outIndex			= index;
	if( outRCode )			*outRCode			= rcode;
	if( outTTL )			*outTTL				= ( ttl         >= 0 ) ? ( (uint32_t) ttl )         : me->defaultTTL;
	if( outOffset )			*outOffset			= ( offset      >= 0 ) ? ( (uint32_t) offset )      : 0;
	if( outProcDelayMs )	*outProcDelayMs		= ( procDelayMs >= 0 ) ? ( (uint32_t) procDelayMs ) : 0;
	if( outFlags )			*outFlags			= flags;
	if( outZone )			*outZone			= zone;
	if( outZoneParent )		*outZoneParent		= zoneParent;
	if( outZSK )			*outZSK				= zsk;
	if( outKSK )			*outKSK				= ksk;
	if( outParentZSK )		*outParentZSK		= parentZSK;
	if( outAction )			*outAction			= action;
	
exit:
	return( zone ? true : false );
}

//===========================================================================================================================

static Boolean
	_DNSServerParseSRVName(
		DNSServerRef		me,
		const uint8_t *		inName,
		const uint8_t **	outDomainPtr,
		size_t *			outDomainLen,
		ParsedSRV			outSRVArray[ kParsedSRVCountMax ],
		size_t *			outSRVCount )
{
	OSStatus			err;
	const uint8_t *		label;
	const uint8_t *		domainPtr;
	size_t				domainLen;
	size_t				srvCount;
	uint32_t			arg;
	int					isNameValid = false;
	
	label = inName;
	
	// Ensure that first label, i.e, the service label, begins with a '_' character.
	
	require_quiet( ( label[ 0 ] > 0 ) && ( label[ 1 ] == '_' ), exit );
	label = DomainNameGetNextLabel( label );
	
	// Ensure that the second label, i.e., the proto label, begins with a '_' character (usually _tcp or _udp).
	
	require_quiet( ( label[ 0 ] > 0 ) && ( label[ 1 ] == '_' ), exit );
	label = DomainNameGetNextLabel( label );
	
	// Parse the domain name, if any.
	
	domainPtr = label;
	while( *label )
	{
		if( DomainNameEqual( label, me->domain ) ||
			( strnicmp_prefix( &label[ 1 ], label[ 0 ], kLabelPrefix_SRV ) == 0 ) ) break;
		label = DomainNameGetNextLabel( label );
	}
	require_quiet( *label, exit );
	
	domainLen = (size_t)( label - domainPtr );
	
	// Parse SRV labels, if any.
	
	srvCount = 0;
	while( strnicmp_prefix( &label[ 1 ], label[ 0 ], kLabelPrefix_SRV ) == 0 )
	{
		const uint8_t * const	nextLabel	= DomainNameGetNextLabel( label );
		const char *			ptr			= (const char *) &label[ 1 + sizeof_string( kLabelPrefix_SRV ) ];
		const char * const		end			= (const char *) nextLabel;
		const uint8_t *			target;
		unsigned int			priority, weight, port;
		
		err = DecimalTextToUInt32( ptr, end, &arg, &ptr );
		require_quiet( !err && ( arg <= UINT16_MAX ), exit );
		priority = (unsigned int) arg;
		
		require_quiet( ( ptr < end ) && ( *ptr == '-' ), exit );
		++ptr;
		
		err = DecimalTextToUInt32( ptr, end, &arg, &ptr );
		require_quiet( !err && ( arg <= UINT16_MAX ), exit );
		weight = (unsigned int) arg;
		
		require_quiet( ( ptr < end ) && ( *ptr == '-' ), exit );
		++ptr;
		
		err = DecimalTextToUInt32( ptr, end, &arg, &ptr );
		require_quiet( !err && ( arg <= UINT16_MAX ), exit );
		port = (unsigned int) arg;
		
		require_quiet( ptr == end, exit );
		
		target = nextLabel;
		for( label = nextLabel; *label; label = DomainNameGetNextLabel( label ) )
		{
			if( DomainNameEqual( label, me->domain ) ||
				( strnicmp_prefix( &label[ 1 ], label[ 0 ], kLabelPrefix_SRV ) == 0 ) ) break;
		}
		require_quiet( *label, exit );
		
		if( outSRVArray )
		{
			outSRVArray[ srvCount ].priority	= (uint16_t) priority;
			outSRVArray[ srvCount ].weight		= (uint16_t) weight;
			outSRVArray[ srvCount ].port		= (uint16_t) port;
			outSRVArray[ srvCount ].targetPtr	= target;
			outSRVArray[ srvCount ].targetLen	= (uint16_t)( label - target );
		}
		++srvCount;
	}
	require_quiet( DomainNameEqual( label, me->domain ), exit );
	isNameValid = true;
	
	if( outDomainPtr )	*outDomainPtr	= domainPtr;
	if( outDomainLen )	*outDomainLen	= domainLen;
	if( outSRVCount )	*outSRVCount	= srvCount;
	
exit:
	return( isNameValid ? true : false );
}

//===========================================================================================================================

static Boolean	_DNSServerParseReverseIPv4Name( DNSServerRef me, const uint8_t *inQName, unsigned int *outHostID )
{
	OSStatus			err;
	const uint8_t *		label;
	size_t				labelLen;
	const uint8_t *		labelData;
    const uint8_t *		labelNext;
	const uint8_t *		ptr;
	uint32_t			hostID;
	int					isNameValid = false;
	
	Unused( me );
	
	label		= inQName;
	labelLen	= *label;
	require_quiet( labelLen > 0, exit );
	
	labelData = &label[ 1 ];
	labelNext = &labelData[ labelLen ];
	err = DecimalTextToUInt32( (const char *) labelData, (const char *) labelNext, &hostID, (const char **) &ptr );
	require_noerr_quiet( err, exit );
	require_quiet( ( hostID >= 1 ) && ( hostID <= 255 ), exit );
	require_quiet( ptr == labelNext, exit );
	
	require_quiet( DomainNameEqual( labelNext, kDNSServerReverseIPv4DomainName ), exit );
	isNameValid = true;
	
	if( outHostID ) *outHostID = (unsigned int) hostID;
	
exit:
	return( isNameValid ? true : false );
}

//===========================================================================================================================

static Boolean	_DNSServerParseReverseIPv6Name( DNSServerRef me, const uint8_t *inQName, unsigned int *outHostID )
{
	const uint8_t *		label;
	unsigned int		hostID;
	int					i;
	int					isNameValid = false;
	
	Unused( me );
	
	hostID	= 0;
	label	= inQName;
	for( i = 0; i < 2; ++i )
	{
		unsigned int		labelLen, c;
		
		labelLen = label[ 0 ];
		require_quiet( labelLen == 1, exit );
		
		c = label[ 1 ];
		require_quiet( isxdigit_safe( c ), exit );
		
		hostID = hostID | ( HexCharToValue( c ) << ( 4 * i ) );
		label = &label[ 1 + labelLen ];
	}
	require_quiet( ( hostID >= 1 ) && ( hostID <= 255 ), exit );
	require_quiet( DomainNameEqual( label, kDNSServerReverseIPv6DomainName ), exit );
	isNameValid = true;
	
	if( outHostID ) *outHostID = hostID;
	
exit:
	return( isNameValid ? true : false );
}

#if( DEBUG )
//===========================================================================================================================

static void
	_DNSServerSigCheck(
		const uint8_t *	inOwner,
		int				inTypeCovered,
		const void *	inMsgPtr,
		size_t			inMsgLen,
		const uint8_t *	inSignaturePtr,
		const size_t	inSignatureLen,
		DNSKeyInfoRef	inKeyInfo )
{
	if( !DNSKeyInfoVerify( inKeyInfo, inMsgPtr, inMsgLen, inSignaturePtr, inSignatureLen ) )
	{
		const char *		typeStr;
		char				typeBuf[ 16 ];
		
		typeStr = DNSRecordTypeValueToString( inTypeCovered );
		if( !typeStr )
		{
			SNPrintF( typeBuf, sizeof( typeBuf ), "TYPE%d", inTypeCovered );
			typeStr = typeBuf;
		}
		ds_ulog( kLogLevelError,
			"Signature for %{du:dname} %s is invalid! -- algorithm: %s (%d), public key: '%H'\n",
			inOwner, typeStr, DNSKeyInfoGetAlgorithmDescription( inKeyInfo ), DNSKeyInfoGetAlgorithm( inKeyInfo ),
			DNSKeyInfoGetPubKeyPtr( inKeyInfo ), DNSKeyInfoGetPubKeyLen( inKeyInfo ), SIZE_MAX );
	}
}
#endif

//===========================================================================================================================

#define kDNSServerDefaultDNSSECAlgorithm		14	// TODO: Think about adding an option for the default algorithm.

static Boolean
	_DNSServerNameIsDNSSECZone(
		const uint8_t *		inName,
		const uint8_t **	outZoneParent,
		DNSKeyInfoRef *		outZSK,
		DNSKeyInfoRef *		outKSK,
		DNSKeyInfoRef *		outParentZSK )
{
	const uint8_t *		label;
	size_t				labelLen;
	const uint8_t *		labelNext;
	const uint8_t *		zoneParent			= NULL;
	DNSKeyInfoRef		zsk;
	DNSKeyInfoRef		ksk;
	DNSKeyInfoRef		parentZSK;
	uint32_t			zoneAlgorithm		= 0;
	uint32_t			zoneIndex			= 0;
	uint32_t			zoneParentAlgorithm	= 0;
	uint32_t			zoneParentIndex		= 0;
	Boolean				parsedAllLabels		= false;
	Boolean				nameIsValid			= false;
	
	for( label = inName; ( labelLen = *label ) != 0; label = labelNext )
	{
		const uint8_t *		labelData;
		
		if( labelLen > kDomainLabelLengthMax ) break;
		labelData = &label[ 1 ];
		labelNext = &labelData[ labelLen ];
		
		if( strnicmp_prefix( labelData, labelLen, kLabelPrefix_Zone ) == 0  )
		{
			OSStatus				err;
			const char *			ptr = (const char *) &labelData[ sizeof_string( kLabelPrefix_Zone ) ];
			const char * const		end = (const char *) labelNext;
			uint32_t				algorithm;
			uint32_t				index;
			
			err = DecimalTextToUInt32( ptr, end, &algorithm, &ptr );
			if( err ) break;
			if( ( ptr >= end ) || ( *ptr++ != '-' ) ) break;
			
			err = DecimalTextToUInt32( ptr, end, &index, &ptr );
			if( err || ( index < kZoneLabelIndexArgMin ) || ( index > kZoneLabelIndexArgMax ) ) break;
			if( ptr != end ) break;
			if( zoneIndex == 0 )
			{
				zoneAlgorithm	= algorithm;
				zoneIndex		= index;
			}
			else if( zoneParentIndex == 0 )
			{
				zoneParentAlgorithm	= algorithm;
				zoneParent			= label;
				zoneParentIndex		= index;
			}
			continue;
		}
		if( DomainNameEqual( label, kDNSServerDomain_DNSSEC ) )
		{
			if( !zoneParent ) zoneParent = label;
			parsedAllLabels = true;
		}
		break;
	}
	require_quiet( parsedAllLabels, exit );
	
	if( zoneAlgorithm == 0 ) zoneAlgorithm = kDNSServerDefaultDNSSECAlgorithm;
	zsk = GetDNSKeyInfoZSK( zoneAlgorithm, zoneIndex );
	require_quiet( zsk, exit );
	
	ksk = GetDNSKeyInfoKSK( zoneAlgorithm, zoneIndex );
	require_quiet( ksk, exit );
	
	if( zoneParentAlgorithm == 0 ) zoneParentAlgorithm = kDNSServerDefaultDNSSECAlgorithm;
	parentZSK = GetDNSKeyInfoZSK( zoneParentAlgorithm, zoneParentIndex );
	require_quiet( parentZSK, exit );
	
	if( outZoneParent )	*outZoneParent	= zoneParent;
	if( outZSK )		*outZSK			= zsk;
	if( outKSK )		*outKSK			= ksk;
	if( outParentZSK )	*outParentZSK	= parentZSK;
	nameIsValid = true;
	
exit:
	return( nameIsValid );
}

//===========================================================================================================================

static OSStatus
	_DNSServerConnectionCreate(
		DNSServerRef				inServer,
		const struct sockaddr *		inLocal,
		const struct sockaddr *		inRemote,
		size_t						inIndex,
		DNSServerConnectionRef *	outCnx )
{
	OSStatus					err;
	DNSServerConnectionRef		obj;
	
	CF_OBJECT_CREATE( DNSServerConnection, obj, err, exit );
	
	obj->index	= inIndex;
	obj->server	= inServer;
	CFRetain( obj->server );
	SockAddrCopy( inLocal, &obj->local );
	SockAddrCopy( inRemote, &obj->remote );
	
	*outCnx = obj;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static void	_DNSServerConnectionFinalize( CFTypeRef inObj )
{
	const DNSServerConnectionRef		me = (DNSServerConnectionRef) inObj;
	
	check( !me->readSource );
	check( !me->writeSource );
	ForgetCF( &me->server );
	ForgetMem( &me->msgPtr );
}

//===========================================================================================================================

static OSStatus	_DNSServerConnectionStart( DNSServerConnectionRef me, SocketRef inSock )
{
	OSStatus			err;
	SocketContext *		sockCtx = NULL;
	
	err = SocketMakeNonBlocking( inSock );
	require_noerr( err, exit );
	
#if( defined( SO_NOSIGPIPE ) )
	setsockopt( inSock, SOL_SOCKET, SO_NOSIGPIPE, &(int){ 1 }, (socklen_t) sizeof( int ) );
#endif
	me->readSource = dispatch_source_create( DISPATCH_SOURCE_TYPE_READ, (uintptr_t) inSock, 0, me->server->queue );
	require_action( me->readSource, exit, err = kNoResourcesErr );
	me->readSuspended = true;
	
	me->writeSource = dispatch_source_create( DISPATCH_SOURCE_TYPE_WRITE, (uintptr_t) inSock, 0, me->server->queue );
	require_action( me->writeSource, exit, err = kNoResourcesErr );
	me->writeSuspended = true;
	
	sockCtx = SocketContextCreateEx( inSock, me, SocketContextFinalizerCF, &err );
	require_noerr( err, exit );
	CFRetain( me );
	
	SocketContextRetain( sockCtx );
	dispatch_set_context( me->readSource, sockCtx );
	dispatch_source_set_event_handler_f( me->readSource, _DNSServerConnectionReadHandler );
	dispatch_source_set_cancel_handler_f( me->readSource, SocketContextCancelHandler );
	dispatch_resume_if_suspended( me->readSource, &me->readSuspended );
	
	SocketContextRetain( sockCtx );
	dispatch_set_context( me->writeSource, sockCtx );
	dispatch_source_set_event_handler_f( me->writeSource, _DNSServerConnectionWriteHandler );
	dispatch_source_set_cancel_handler_f( me->writeSource, SocketContextCancelHandler );
	
	_DNSServerConnectionRenewExpiration( me );
	
exit:
	if( sockCtx ) SocketContextRelease( sockCtx );
	if( err ) _DNSServerConnectionStop( me, true );
	return( err );
}

//===========================================================================================================================

static void	_DNSServerConnectionStop( DNSServerConnectionRef me, Boolean inRemoveFromList )
{
	dispatch_source_forget_ex( &me->readSource, &me->readSuspended );
	dispatch_source_forget_ex( &me->writeSource, &me->writeSuspended );
	if( inRemoveFromList )
	{
		DNSServerConnectionRef *		ptr;
		
		ptr = &me->server->connectionList;
		while( *ptr && ( *ptr != me ) ) ptr = &( *ptr )->next;
		if( *ptr )
		{
			*ptr = me->next;
			me->next = NULL;
			CFRelease( me );
		}
	}
}

//===========================================================================================================================

static void	_DNSServerConnectionReadHandler( void *inContext )
{
	OSStatus							err;
	const SocketContext * const			sockCtx	= (SocketContext *) inContext;
	const DNSServerConnectionRef		me		= (DNSServerConnectionRef) sockCtx->userContext;
	uint8_t *							respPtr	= NULL;	// malloc'd
	size_t								respLen;
	
	// Receive message length.
	
	if( !me->haveLen )
	{
		err = SocketReadData( sockCtx->sock, me->lenBuf, sizeof( me->lenBuf ), &me->offset );
		if( ( err == EWOULDBLOCK ) || ( err == kConnectionErr ) ) goto exit;
		require_noerr( err, exit );
		
		me->haveLen	= true;
		me->offset	= 0;
		me->msgLen	= ReadBig16( me->lenBuf );
		if( me->msgLen < kDNSHeaderLength )
		{
			ds_ulog( kLogLevelInfo, "TCP: Message length of %zu bytes from %##a to %##a is too small (< %d bytes)\n",
				me->msgLen, &me->remote, &me->local, kDNSHeaderLength );
			err = kSizeErr;
			goto exit;
		}
		me->msgPtr = malloc( me->msgLen );
		require_action( me->msgPtr, exit, err = kNoMemoryErr );
	}
	
	// Receive message.
	
	err = SocketReadData( sockCtx->sock, me->msgPtr, me->msgLen, &me->offset );
	if( ( err == EWOULDBLOCK ) || ( err == kConnectionErr ) ) goto exit;
	require_noerr( err, exit );
	dispatch_suspend_if_resumed( me->readSource, &me->readSuspended );
	me->offset	= 0;
	me->haveLen	= false;
	
	ds_ulog( kLogLevelInfo, "TCP: Received %zu bytes from %##a to %##a -- %.1{du:dnsmsg}\n",
		me->msgLen, &me->remote, &me->local, me->msgPtr, me->msgLen );
	
	// Create response.
	
	err = _DNSServerAnswerQueryForTCP( me->server, me->msgPtr, me->msgLen, me->index + 1, &respPtr, &respLen );
	if( err == kSkipErr ) ds_ulog( kLogLevelInfo, "TCP: Ignoring query\n" );
	require_noerr_quiet( err, exit );
	
	_DNSServerConnectionRenewExpiration( me );
	
	// Prepare to send response.
	
	FreeNullSafe( me->msgPtr );
	me->msgPtr = respPtr;
	me->msgLen = respLen;
	respPtr = NULL;
	
	ds_ulog( kLogLevelInfo, "TCP: Sending %zu byte response from %##a to %##a -- %.1{du:dnsmsg}\n",
		me->msgLen, &me->local, &me->remote, me->msgPtr, me->msgLen );
	
	check( me->msgLen <= UINT16_MAX );
	WriteBig16Typed( me->lenBuf, (uint16_t) me->msgLen );
	me->iov[ 0 ].iov_base	= me->lenBuf;
	me->iov[ 0 ].iov_len	= sizeof( me->lenBuf );
	me->iov[ 1 ].iov_base	= me->msgPtr;
	me->iov[ 1 ].iov_len	= me->msgLen;
	me->iovPtr				= me->iov;
	me->iovCount			= 2;
	dispatch_resume_if_suspended( me->writeSource, &me->writeSuspended );
	
exit:
	FreeNullSafe( respPtr );
	if( err && ( err != EWOULDBLOCK ) )
	{
		_DNSServerConnectionStop( me, true );
	}
}

//===========================================================================================================================

static void	_DNSServerConnectionWriteHandler( void *inContext )
{
	OSStatus							err;
	const SocketContext * const			sockCtx	= (SocketContext *) inContext;
	const DNSServerConnectionRef		me		= (DNSServerConnectionRef) sockCtx->userContext;
	
	err = SocketWriteData( sockCtx->sock, &me->iovPtr, &me->iovCount );
	if( !err )
	{
		me->iovPtr		= NULL;
		me->iovCount	= 0;
		memset( me->iov, 0, sizeof( me->iov ) );
		ForgetPtrLen( &me->msgPtr, &me->msgLen );
		dispatch_suspend_if_resumed( me->writeSource, &me->writeSuspended );
		dispatch_resume_if_suspended( me->readSource, &me->readSuspended );
	}
	else if( err != EWOULDBLOCK )
	{
		_DNSServerConnectionStop( me, true );
	}
}

//===========================================================================================================================

static void	_DNSServerConnectionRenewExpiration( DNSServerConnectionRef me )
{
	me->expirationTicks = UpTicks() + SecondsToUpTicks( kDNSServerConnectionExpirationTimeSecs );
}

//===========================================================================================================================
//	MDNSReplierCmd
//===========================================================================================================================

typedef struct
{
	uint8_t *				hostname;			// Used as the base name for hostnames and service names.
	uint8_t *				serviceLabel;		// Label containing the base service name.
	unsigned int			maxInstanceCount;	// Maximum number of service instances and hostnames.
	uint64_t *				bitmaps;			// Array of 64-bit bitmaps for keeping track of needed responses.
	size_t					bitmapCount;		// Number of 64-bit bitmaps.
	dispatch_source_t		readSourceV4;		// Read dispatch source for IPv4 socket.
	dispatch_source_t		readSourceV6;		// Read dispatch source for IPv6 socket.
	uint32_t				ifIndex;			// Index of the interface to run on.
	unsigned int			recordCountA;		// Number of A records per hostname.
	unsigned int			recordCountAAAA;	// Number of AAAA records per hostname.
	unsigned int			maxDropCount;		// If > 0, the drop rates apply to only the first <maxDropCount> responses.
	double					ucastDropRate;		// Probability of dropping a unicast response.
	double					mcastDropRate;		// Probability of dropping a multicast query or response.
	uint8_t *				dropCounters;		// If maxDropCount > 0, array of <maxInstanceCount> response drop counters.
	Boolean					noAdditionals;		// True if responses are to not include additional records.
	Boolean					useIPv4;			// True if the replier is to use IPv4.
	Boolean					useIPv6;			// True if the replier is to use IPv6.
	uint8_t					msgBuf[ kMDNSMessageSizeMax ];	// Buffer for received mDNS message.
#if( TARGET_OS_DARWIN )
	dispatch_source_t		processMonitor;		// Process monitor source for process being followed, if any.
	pid_t					followPID;			// PID of process being followed, if any. (If it exits, we exit).
#endif
	
}	MDNSReplierContext;

typedef struct MRResourceRecord		MRResourceRecord;
struct MRResourceRecord
{
	MRResourceRecord *		next;		// Next item in list.
	uint8_t *				name;		// Resource record name.
	uint16_t				type;		// Resource record type.
	uint16_t				class;		// Resource record class.
	uint32_t				ttl;		// Resource record TTL.
	uint16_t				rdlength;	// Resource record data length.
	uint8_t *				rdata;		// Resource record data.
	const uint8_t *			target;		// For SRV records, pointer to target in RDATA.
};

typedef struct MRNameOffsetItem		MRNameOffsetItem;
struct MRNameOffsetItem
{
	MRNameOffsetItem *	next;		// Next item in list.
	uint16_t			offset;		// Offset of domain name in response message.
	uint8_t				name[ 1 ];	// Variable-length array for domain name.
};

#if( TARGET_OS_DARWIN )
static void		_MDNSReplierFollowedProcessHandler( void *inContext );
#endif
static void		_MDNSReplierReadHandler( void *inContext );
static OSStatus
	_MDNSReplierAnswerQuery(
		MDNSReplierContext *	inContext,
		const uint8_t *			inQueryPtr,
		size_t					inQueryLen,
		sockaddr_ip *			inSender,
		SocketRef				inSock,
		unsigned int			inIndex );
static OSStatus
	_MDNSReplierAnswerListAdd(
		MDNSReplierContext *	inContext,
		MRResourceRecord **		inAnswerList,
		unsigned int			inIndex,
		const uint8_t *			inName,
		unsigned int			inType,
		unsigned int			inClass );
static void
	_MDNSReplierAnswerListRemovePTR(
		MRResourceRecord **	inAnswerListPtr,
		const uint8_t *		inName,
		const uint8_t *		inRData );
static OSStatus
	_MDNSReplierSendOrDropResponse(
		MDNSReplierContext *	inContext,
		MRResourceRecord *		inAnswerList,
		sockaddr_ip *			inQuerier,
		SocketRef				inSock,
		unsigned int			inIndex,
		Boolean					inUnicast );
static OSStatus
	_MDNSReplierCreateResponse(
		MDNSReplierContext *	inContext,
		MRResourceRecord *		inAnswerList,
		unsigned int			inIndex,
		uint8_t **				outResponsePtr,
		size_t *				outResponseLen );
static OSStatus
	_MDNSReplierAppendNameToResponse(
		DataBuffer *		inResponse,
		const uint8_t *		inName,
		MRNameOffsetItem **	inNameOffsetListPtr );
static Boolean
	_MDNSReplierServiceTypeMatch(
		const MDNSReplierContext *	inContext,
		const uint8_t *				inName,
		unsigned int *				outTXTSize,
		unsigned int *				outCount );
static Boolean
	_MDNSReplierServiceInstanceNameMatch(
		const MDNSReplierContext *	inContext,
		const uint8_t *				inName,
		unsigned int *				outIndex,
		unsigned int *				outTXTSize,
		unsigned int *				outCount );
static Boolean	_MDNSReplierAboutRecordNameMatch( const MDNSReplierContext *inContext, const uint8_t *inName );
static Boolean
	_MDNSReplierHostnameMatch(
		const MDNSReplierContext *	inContext,
		const uint8_t *				inName,
		unsigned int *				outIndex );
static OSStatus	_MDNSReplierCreateTXTRecord( const uint8_t *inRecordName, size_t inSize, uint8_t **outTXT );
static OSStatus
	_MRResourceRecordCreate(
		uint8_t *			inName,
		uint16_t			inType,
		uint16_t			inClass,
		uint32_t			inTTL,
		uint16_t			inRDLength,
		uint8_t *			inRData,
		MRResourceRecord **	outRecord );
static void		_MRResourceRecordFree( MRResourceRecord *inRecord );
static void		_MRResourceRecordFreeList( MRResourceRecord *inList );
static OSStatus	_MRNameOffsetItemCreate( const uint8_t *inName, uint16_t inOffset, MRNameOffsetItem **outItem );
static void		_MRNameOffsetItemFree( MRNameOffsetItem *inItem );
static void		_MRNameOffsetItemFreeList( MRNameOffsetItem *inList );

ulog_define_ex( kDNSSDUtilIdentifier, MDNSReplier, kLogLevelInfo, kLogFlags_None, "MDNSReplier", NULL );
#define mr_ulog( LEVEL, ... )		ulog( &log_category_from_name( MDNSReplier ), (LEVEL), __VA_ARGS__ )

static void	MDNSReplierCmd( void )
{
	OSStatus					err;
	MDNSReplierContext *		context;
	SocketRef					sockV4	= kInvalidSocketRef;
	SocketRef					sockV6	= kInvalidSocketRef;
	const char *				ifname;
	size_t						len;
	uint8_t						name[ 1 + kDomainLabelLengthMax + 1 ];
	char						ifnameBuf[ IF_NAMESIZE + 1 ];
	
	err = CheckIntegerArgument( gMDNSReplier_MaxInstanceCount, "max instance count", 1, UINT16_MAX );
	require_noerr_quiet( err, exit );
	
	err = CheckIntegerArgument( gMDNSReplier_RecordCountA, "A record count", 0, 255 );
	require_noerr_quiet( err, exit );
	
	err = CheckIntegerArgument( gMDNSReplier_RecordCountAAAA, "AAAA record count", 0, 255 );
	require_noerr_quiet( err, exit );
	
	err = CheckDoubleArgument( gMDNSReplier_UnicastDropRate, "unicast drop rate", 0.0, 1.0 );
	require_noerr_quiet( err, exit );
	
	err = CheckDoubleArgument( gMDNSReplier_MulticastDropRate, "multicast drop rate", 0.0, 1.0 );
	require_noerr_quiet( err, exit );
	
	err = CheckIntegerArgument( gMDNSReplier_MaxDropCount, "drop count", 0, 255 );
	require_noerr_quiet( err, exit );
	
	if( gMDNSReplier_Foreground )
	{
		LogControl( "MDNSReplier:output=file;stdout,MDNSReplier:flags=time;prefix" );
	}
	
	context = (MDNSReplierContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	context->maxInstanceCount	= (unsigned int) gMDNSReplier_MaxInstanceCount;
	context->recordCountA		= (unsigned int) gMDNSReplier_RecordCountA;
	context->recordCountAAAA	= (unsigned int) gMDNSReplier_RecordCountAAAA;
	context->maxDropCount		= (unsigned int) gMDNSReplier_MaxDropCount;
	context->ucastDropRate		= gMDNSReplier_UnicastDropRate;
	context->mcastDropRate		= gMDNSReplier_MulticastDropRate;
	context->noAdditionals		= gMDNSReplier_NoAdditionals ? true : false;
	context->useIPv4			= ( gMDNSReplier_UseIPv4 || !gMDNSReplier_UseIPv6 ) ? true : false;
	context->useIPv6			= ( gMDNSReplier_UseIPv6 || !gMDNSReplier_UseIPv4 ) ? true : false;
	context->bitmapCount		= ( context->maxInstanceCount + 63 ) / 64;
	
#if( TARGET_OS_DARWIN )
	if( gMDNSReplier_FollowPID )
	{
		context->followPID = _StringToPID( gMDNSReplier_FollowPID, &err );
		if( err || ( context->followPID < 0 ) )
		{
			FPrintF( stderr, "error: Invalid follow PID: %s\n", gMDNSReplier_FollowPID );
			goto exit;
		}
		
		err = DispatchProcessMonitorCreate( context->followPID, DISPATCH_PROC_EXIT, dispatch_get_main_queue(),
			_MDNSReplierFollowedProcessHandler, NULL, context, &context->processMonitor );
		require_noerr( err, exit );
		dispatch_resume( context->processMonitor );
	}
	else
	{
		context->followPID = -1;
	}
#endif
	
	if( context->maxDropCount > 0 )
	{
		context->dropCounters = (uint8_t *) calloc( context->maxInstanceCount, sizeof( *context->dropCounters ) );
		require_action( context->dropCounters, exit, err = kNoMemoryErr );
	}
	
	context->bitmaps = (uint64_t *) calloc( context->bitmapCount, sizeof( *context->bitmaps ) );
	require_action( context->bitmaps, exit, err = kNoMemoryErr );
	
	// Create the base hostname label.
	
	len = strlen( gMDNSReplier_Hostname );
	if( context->maxInstanceCount > 1 )
	{
		unsigned int		maxInstanceCount, digitCount;
		
		// When there's more than one instance, extra bytes are needed to append " (<instance index>)" or
		// "-<instance index>" to the base hostname.
		
		maxInstanceCount = context->maxInstanceCount;
		for( digitCount = 0; maxInstanceCount > 0; ++digitCount ) maxInstanceCount /= 10;
		len += ( 3 + digitCount );
	}
	
	if( len <= kDomainLabelLengthMax )
	{
		uint8_t *		dst = &name[ 1 ];
		uint8_t *		lim = &name[ countof( name ) ];
		
		SNPrintF_Add( (char **) &dst, (char *) lim, "%s", gMDNSReplier_Hostname );
		name[ 0 ] = (uint8_t)( dst - &name[ 1 ] );
		
		err = DomainNameDupLower( name, &context->hostname, NULL );
		require_noerr( err, exit );
	}
	else
	{
		FPrintF( stderr, "error: Base name \"%s\" is too long for max instance count of %u.\n",
			gMDNSReplier_Hostname, context->maxInstanceCount );
		goto exit;
	}
	
	// Create the service label.
	
	len = strlen( gMDNSReplier_ServiceTypeTag ) + 3;	// We need three extra bytes for the service type prefix "_t-".
	if( len <= kDomainLabelLengthMax )
	{
		uint8_t *		dst = &name[ 1 ];
		uint8_t *		lim = &name[ countof( name ) ];
		
		SNPrintF_Add( (char **) &dst, (char *) lim, "_t-%s", gMDNSReplier_ServiceTypeTag );
		name[ 0 ] = (uint8_t)( dst - &name[ 1 ] );
		
		err = DomainNameDupLower( name, &context->serviceLabel, NULL );
		require_noerr( err, exit );
	}
	else
	{
		FPrintF( stderr, "error: Service type tag is too long.\n" );
		goto exit;
	}
	
	err = InterfaceIndexFromArgString( gInterface, &context->ifIndex );
	require_noerr_quiet( err, exit );
	
	ifname = if_indextoname( context->ifIndex, ifnameBuf );
	require_action( ifname, exit, err = kNameErr );
	
	// Set up IPv4 socket.
	
	if( context->useIPv4 )
	{
		err = CreateMulticastSocket( GetMDNSMulticastAddrV4(), kMDNSPort, ifname, context->ifIndex, true, NULL, &sockV4 );
		require_noerr( err, exit );
	}
	
	// Set up IPv6 socket.
	
	if( context->useIPv6 )
	{
		err = CreateMulticastSocket( GetMDNSMulticastAddrV6(), kMDNSPort, ifname, context->ifIndex, true, NULL, &sockV6 );
		require_noerr( err, exit );
	}
	
	// Create dispatch read sources for socket(s).
	
	if( IsValidSocket( sockV4 ) )
	{
		SocketContext *		sockCtx;
		
		sockCtx = SocketContextCreate( sockV4, context, &err );
		require_noerr( err, exit );
		sockV4 = kInvalidSocketRef;
		
		err = DispatchReadSourceCreate( sockCtx->sock, NULL, _MDNSReplierReadHandler, SocketContextCancelHandler, sockCtx,
			&context->readSourceV4 );
		if( err ) ForgetSocketContext( &sockCtx );
		require_noerr( err, exit );
		
		dispatch_resume( context->readSourceV4 );
	}
	
	if( IsValidSocket( sockV6 ) )
	{
		SocketContext *		sockCtx;
		
		sockCtx = SocketContextCreate( sockV6, context, &err );
		require_noerr( err, exit );
		sockV6 = kInvalidSocketRef;
		
		err = DispatchReadSourceCreate( sockCtx->sock, NULL, _MDNSReplierReadHandler, SocketContextCancelHandler, sockCtx,
			&context->readSourceV6 );
		if( err ) ForgetSocketContext( &sockCtx );
		require_noerr( err, exit );
		
		dispatch_resume( context->readSourceV6 );
	}
	
	dispatch_main();
	
exit:
	ForgetSocket( &sockV4 );
	ForgetSocket( &sockV6 );
	exit( 1 );
}

#if( TARGET_OS_DARWIN )
//===========================================================================================================================
//	_MDNSReplierFollowedProcessHandler
//===========================================================================================================================

static void	_MDNSReplierFollowedProcessHandler( void *inContext )
{
	MDNSReplierContext * const		context = (MDNSReplierContext *) inContext;
	
	if( dispatch_source_get_data( context->processMonitor ) & DISPATCH_PROC_EXIT )
	{
		mr_ulog( kLogLevelNotice, "Exiting: followed process (%lld) exited.\n", (int64_t) context->followPID );
		exit( 0 );
	}
}
#endif

//===========================================================================================================================
//	_MDNSReplierReadHandler
//===========================================================================================================================

#define ShouldDrop( P )		( ( (P) > 0.0 ) && ( ( (P) >= 1.0 ) || RandomlyTrue( P ) ) )

static void	_MDNSReplierReadHandler( void *inContext )
{
	OSStatus						err;
	SocketContext * const			sockCtx = (SocketContext *) inContext;
	MDNSReplierContext * const		context = (MDNSReplierContext *) sockCtx->userContext;
	size_t							msgLen;
	sockaddr_ip						sender;
	const DNSHeader *				hdr;
	unsigned int					flags, questionCount, i, j;
	const uint8_t *					ptr;
	int								drop, isMetaQuery;
	
	err = SocketRecvFrom( sockCtx->sock, context->msgBuf, sizeof( context->msgBuf ), &msgLen, &sender, sizeof( sender ),
		NULL, NULL, NULL, NULL );
	require_noerr( err, exit );
	
	if( msgLen < kDNSHeaderLength )
	{
		mr_ulog( kLogLevelInfo, "Message is too small (%zu < %d).\n", msgLen, kDNSHeaderLength );
		goto exit;
	}
	
	// Perform header field checks.
	// The message ID and most flag bits are silently ignored (see <https://tools.ietf.org/html/rfc6762#section-18>).
	
	hdr = (DNSHeader *) context->msgBuf;
	flags = DNSHeaderGetFlags( hdr );
	require_quiet( ( flags & kDNSHeaderFlag_Response ) == 0, exit );		// Reject responses.
	require_quiet( DNSFlagsGetOpCode( flags ) == kDNSOpCode_Query, exit );	// Reject opcodes other than standard query.
	require_quiet( DNSFlagsGetRCode( flags )  == kDNSRCode_NoError, exit );	// Reject non-zero rcodes.
	
	drop = ( !context->maxDropCount && ShouldDrop( context->mcastDropRate ) ) ? true : false;
	
	mr_ulog( kLogLevelInfo, "Received %zu byte message from %##a%?s -- %#.1{du:dnsmsg}\n",
		msgLen, &sender, drop, " (dropping)", context->msgBuf, msgLen );
	
	// Based on the QNAMEs in the query message, determine from which sets of records we may possibly need answers.
	
	questionCount = DNSHeaderGetQuestionCount( hdr );
	require_quiet( questionCount > 0, exit );
	
	memset( context->bitmaps, 0, context->bitmapCount * sizeof_element( context->bitmaps ) );
	
	isMetaQuery = false;
	ptr = (const uint8_t *) &hdr[ 1 ];
	for( i = 0; i < questionCount; ++i )
	{
		unsigned int		count, index;
		uint16_t			qtype, qclass;
		uint8_t				qname[ kDomainNameLengthMax ];
		
		err = DNSMessageExtractQuestion( context->msgBuf, msgLen, ptr, qname, &qtype, &qclass, &ptr );
		require_noerr_quiet( err, exit );
		
		if( ( qclass & ~kMDNSClassUnicastResponseBit ) != kDNSServiceClass_IN ) continue;
		
		if( _MDNSReplierHostnameMatch( context, qname, &index ) ||
			_MDNSReplierServiceInstanceNameMatch( context, qname, &index, NULL, NULL ) )
		{
			if( ( index >= 1 ) && ( index <= context->maxInstanceCount ) )
			{
				context->bitmaps[ ( index - 1 ) / 64 ] |= ( UINT64_C( 1 ) << ( ( index - 1 ) % 64 ) );
			}
		}
		else if( _MDNSReplierServiceTypeMatch( context, qname, NULL, &count ) )
		{
			if( ( count >= 1 ) && ( count <= context->maxInstanceCount ) )
			{
				for( j = 0; j < (unsigned int) context->bitmapCount; ++j )
				{
					if( count < 64 )
					{
						context->bitmaps[ j ] |= ( ( UINT64_C( 1 ) << count ) - 1 );
						break;
					}
					else
					{
						context->bitmaps[ j ] = ~UINT64_C( 0 );
						count -= 64;
					}
				}
			}
		}
		else if( _MDNSReplierAboutRecordNameMatch( context, qname ) )
		{
			isMetaQuery = true;
		}
	}
	
	// Attempt to answer the query message using selected record sets.
	
	if( isMetaQuery )
	{
		err = _MDNSReplierAnswerQuery( context, context->msgBuf, msgLen, &sender, sockCtx->sock, 0 );
		check_noerr( err );
	}
	if( drop ) goto exit;
	
	for( i = 0; i < context->bitmapCount; ++i )
	{
		for( j = 0; ( context->bitmaps[ i ] != 0 ) && ( j < 64 ); ++j )
		{
			const uint64_t		bitmask = UINT64_C( 1 ) << j;
			
			if( context->bitmaps[ i ] & bitmask )
			{
				context->bitmaps[ i ] &= ~bitmask;
				
				err = _MDNSReplierAnswerQuery( context, context->msgBuf, msgLen, &sender, sockCtx->sock,
					( i * 64 ) + j + 1 );
				check_noerr( err );
			}
		}
	}
	
exit:
	return;
}

//===========================================================================================================================
//	_MDNSReplierAnswerQuery
//===========================================================================================================================

static OSStatus
	_MDNSReplierAnswerQuery(
		MDNSReplierContext *	inContext,
		const uint8_t *			inQueryPtr,
		size_t					inQueryLen,
		sockaddr_ip *			inSender,
		SocketRef				inSock,
		unsigned int			inIndex )
{
	OSStatus				err;
	const DNSHeader *		hdr;
	const uint8_t *			ptr;
	unsigned int			questionCount, answerCount, i;
	MRResourceRecord *		ucastAnswerList = NULL;
	MRResourceRecord *		mcastAnswerList = NULL;
	
	require_action( inIndex <= inContext->maxInstanceCount, exit, err = kRangeErr );
	
	// Get answers for questions.
	
	check( inQueryLen >= kDNSHeaderLength );
	hdr = (const DNSHeader *) inQueryPtr;
	questionCount = DNSHeaderGetQuestionCount( hdr );
	
	ptr = (const uint8_t *) &hdr[ 1 ];
	for( i = 0; i < questionCount; ++i )
	{
		MRResourceRecord **		answerListPtr;
		uint16_t				qtype, qclass;
		uint8_t					qname[ kDomainNameLengthMax ];
		
		err = DNSMessageExtractQuestion( inQueryPtr, inQueryLen, ptr, qname, &qtype, &qclass, &ptr );
		require_noerr_quiet( err, exit );
		
		if( qclass & kMDNSClassUnicastResponseBit )
		{
			qclass &= ~kMDNSClassUnicastResponseBit;
			answerListPtr = &ucastAnswerList;
		}
		else
		{
			answerListPtr = &mcastAnswerList;
		}
		
		err = _MDNSReplierAnswerListAdd( inContext, answerListPtr, inIndex, qname, qtype, qclass );
		require_noerr( err, exit );
	}
	require_action_quiet( mcastAnswerList || ucastAnswerList, exit, err = kNoErr );
	
	// Suppress known answers.
	// Records in the Answer section of the query message are known answers, so remove them from the answer lists.
	// See <https://tools.ietf.org/html/rfc6762#section-7.1>.
	
	answerCount = DNSHeaderGetAnswerCount( hdr );
	for( i = 0; i < answerCount; ++i )
	{
		const uint8_t *		rdataPtr;
		const uint8_t *		recordPtr;
		uint16_t			type, class;
		uint8_t				name[ kDomainNameLengthMax ];
		uint8_t				instance[ kDomainNameLengthMax ];
		
		recordPtr = ptr;
		err = DNSMessageExtractRecord( inQueryPtr, inQueryLen, ptr, NULL, &type, &class, NULL, NULL, NULL, &ptr );
		require_noerr_quiet( err, exit );
		
		if( ( type != kDNSServiceType_PTR ) || ( class != kDNSServiceClass_IN ) ) continue;
		
		err = DNSMessageExtractRecord( inQueryPtr, inQueryLen, recordPtr, name, NULL, NULL, NULL, &rdataPtr, NULL, NULL );
		require_noerr( err, exit );
		
		err = DNSMessageExtractDomainName( inQueryPtr, inQueryLen, rdataPtr, instance, NULL );
		require_noerr_quiet( err, exit );
		
		if( ucastAnswerList ) _MDNSReplierAnswerListRemovePTR( &ucastAnswerList, name, instance );
		if( mcastAnswerList ) _MDNSReplierAnswerListRemovePTR( &mcastAnswerList, name, instance );
	}
	require_action_quiet( mcastAnswerList || ucastAnswerList, exit, err = kNoErr );
	
	// Send or drop responses.
	
	if( ucastAnswerList )
	{
		err = _MDNSReplierSendOrDropResponse( inContext, ucastAnswerList, inSender, inSock, inIndex, true );
		require_noerr( err, exit );
	}
	
	if( mcastAnswerList )
	{
		err = _MDNSReplierSendOrDropResponse( inContext, mcastAnswerList, inSender, inSock, inIndex, false );
		require_noerr( err, exit );
	}
	err = kNoErr;
	
exit:
	_MRResourceRecordFreeList( ucastAnswerList );
	_MRResourceRecordFreeList( mcastAnswerList );
	return( err );
}

//===========================================================================================================================
//	_MDNSReplierAnswerListAdd
//===========================================================================================================================

static OSStatus
	_MDNSReplierAnswerListAdd(
		MDNSReplierContext *	inContext,
		MRResourceRecord **		inAnswerList,
		unsigned int			inIndex,
		const uint8_t *			inName,
		unsigned int			inType,
		unsigned int			inClass )
{
	OSStatus					err;
	uint8_t *					recordName	= NULL;
	uint8_t *					rdataPtr	= NULL;
	size_t						rdataLen;
	MRResourceRecord *			answer;
	MRResourceRecord **			answerPtr;
	const uint8_t * const		hostname	= inContext->hostname;
	unsigned int				i;
	uint32_t					index;
	unsigned int				count, txtSize;
	
	require_action( inIndex <= inContext->maxInstanceCount, exit, err = kRangeErr );
	require_action_quiet( inClass == kDNSServiceClass_IN, exit, err = kNoErr );
	
	for( answerPtr = inAnswerList; ( answer = *answerPtr ) != NULL; answerPtr = &answer->next )
	{
		if( ( answer->type == inType ) && DomainNameEqual( answer->name, inName ) )
		{
			err = kNoErr;
			goto exit;
		}
	}
	
	// Index 0 is reserved for answering queries about the mdnsreplier, while all other index values up to the maximum
	// instance count are for answering queries about service instances.
	
	if( inIndex == 0 )
	{
		if( _MDNSReplierAboutRecordNameMatch( inContext, inName ) )
		{
			int		listHasTXT = false;
			
			if( inType == kDNSServiceType_ANY )
			{
				for( answer = *inAnswerList; answer; answer = answer->next )
				{
					if( ( answer->type == kDNSServiceType_TXT ) && DomainNameEqual( answer->name, inName ) )
					{
						listHasTXT = true;
						break;
					}
				}
			}
			
			if( ( inType == kDNSServiceType_TXT ) || ( ( inType == kDNSServiceType_ANY ) && !listHasTXT ) )
			{
				err = DomainNameDupLower( inName, &recordName, NULL );
				require_noerr( err, exit );
				
				err = CreateTXTRecordDataFromString( "ready=yes", ',', &rdataPtr, &rdataLen );
				require_noerr( err, exit );
				
				err = _MRResourceRecordCreate( recordName, kDNSServiceType_TXT, kDNSServiceClass_IN, kMDNSRecordTTL_Other,
					(uint16_t) rdataLen, rdataPtr, &answer );
				require_noerr( err, exit );
				recordName	= NULL;
				rdataPtr	= NULL;
				
				*answerPtr = answer;
			}
			else if( inType == kDNSServiceType_NSEC )
			{
				err = DomainNameDupLower( inName, &recordName, NULL );
				require_noerr( err, exit );
				
				err = CreateNSECRecordData( recordName, &rdataPtr, &rdataLen, 1, kDNSServiceType_TXT );
				require_noerr( err, exit );
				
				err = _MRResourceRecordCreate( recordName, kDNSServiceType_NSEC, kDNSServiceClass_IN, kMDNSRecordTTL_Host,
					(uint16_t) rdataLen, rdataPtr, &answer );
				require_noerr( err, exit );
				recordName	= NULL;
				rdataPtr	= NULL;
				
				*answerPtr = answer;
			}
		}
	}
	else if( _MDNSReplierHostnameMatch( inContext, inName, &index ) && ( index == inIndex ) )
	{
		int		listHasA	= false;
		int		listHasAAAA	= false;
		
		if( inType == kDNSServiceType_ANY )
		{
			for( answer = *inAnswerList; answer; answer = answer->next )
			{
				if( answer->type == kDNSServiceType_A )
				{
					if( !listHasA && DomainNameEqual( answer->name, inName ) ) listHasA = true;
				}
				else if( answer->type == kDNSServiceType_AAAA )
				{
					if( !listHasAAAA && DomainNameEqual( answer->name, inName ) ) listHasAAAA = true;
				}
				if( listHasA && listHasAAAA ) break;
			}
		}
		
		if( ( inType == kDNSServiceType_A ) || ( ( inType == kDNSServiceType_ANY ) && !listHasA ) )
		{
			for( i = 1; i <= inContext->recordCountA; ++i )
			{
				err = DomainNameDupLower( inName, &recordName, NULL );
				require_noerr( err, exit );
				
				rdataLen = 4;
				rdataPtr = (uint8_t *) malloc( rdataLen );
				require_action( rdataPtr, exit, err = kNoMemoryErr );
				
				rdataPtr[ 0 ] = 0;
				WriteBig16Typed( &rdataPtr[ 1 ], (uint16_t) inIndex );
				rdataPtr[ 3 ] = (uint8_t) i;
				
				err = _MRResourceRecordCreate( recordName, kDNSServiceType_A, kDNSServiceClass_IN, kMDNSRecordTTL_Host,
					(uint16_t) rdataLen, rdataPtr, &answer );
				require_noerr( err, exit );
				recordName	= NULL;
				rdataPtr	= NULL;
				
				*answerPtr = answer;
				 answerPtr = &answer->next;
			}
		}
		
		if( ( inType == kDNSServiceType_AAAA ) || ( ( inType == kDNSServiceType_ANY ) && !listHasAAAA ) )
		{
			for( i = 1; i <= inContext->recordCountAAAA; ++i )
			{
				const uint8_t		( *baseAddr )[ 16 ];
				
				err = DomainNameDupLower( inName, &recordName, NULL );
				require_noerr( err, exit );
				
				rdataLen = 16;
				baseAddr = ( i == 1 ) ? &kMDNSReplierLinkLocalBaseAddrV6 : &kMDNSReplierBaseAddrV6;
				rdataPtr = (uint8_t *) _memdup( baseAddr, rdataLen );
				require_action( rdataPtr, exit, err = kNoMemoryErr );
				
				WriteBig16Typed( &rdataPtr[ 12 ], (uint16_t) inIndex );
				rdataPtr[ 15 ] = (uint8_t) i;
				
				err = _MRResourceRecordCreate( recordName, kDNSServiceType_AAAA, kDNSServiceClass_IN, kMDNSRecordTTL_Host,
					(uint16_t) rdataLen, rdataPtr, &answer );
				require_noerr( err, exit );
				recordName	= NULL;
				rdataPtr	= NULL;
				
				*answerPtr = answer;
				 answerPtr = &answer->next;
			}
		}
		else if( inType == kDNSServiceType_NSEC )
		{
			err = DomainNameDupLower( inName, &recordName, NULL );
			require_noerr( err, exit );
			
			if( ( inContext->recordCountA > 0 ) && ( inContext->recordCountAAAA > 0 ) )
			{
				err = CreateNSECRecordData( recordName, &rdataPtr, &rdataLen, 2, kDNSServiceType_A, kDNSServiceType_AAAA );
				require_noerr( err, exit );
			}
			else if( inContext->recordCountA > 0 )
			{
				err = CreateNSECRecordData( recordName, &rdataPtr, &rdataLen, 1, kDNSServiceType_A );
				require_noerr( err, exit );
			}
			else if( inContext->recordCountAAAA > 0 )
			{
				err = CreateNSECRecordData( recordName, &rdataPtr, &rdataLen, 1, kDNSServiceType_AAAA );
				require_noerr( err, exit );
			}
			else
			{
				err = CreateNSECRecordData( recordName, &rdataPtr, &rdataLen, 0 );
				require_noerr( err, exit );
			}
			
			err = _MRResourceRecordCreate( recordName, kDNSServiceType_NSEC, kDNSServiceClass_IN, kMDNSRecordTTL_Host,
				(uint16_t) rdataLen, rdataPtr, &answer );
			require_noerr( err, exit );
			recordName	= NULL;
			rdataPtr	= NULL;
			
			*answerPtr = answer;
		}
	}
	else if( _MDNSReplierServiceTypeMatch( inContext, inName, NULL, &count ) && ( count >= inIndex ) )
	{
		int		listHasPTR = false;
		
		if( inType == kDNSServiceType_ANY )
		{
			for( answer = *inAnswerList; answer; answer = answer->next )
			{
				if( ( answer->type == kDNSServiceType_PTR ) && DomainNameEqual( answer->name, inName ) )
				{
					listHasPTR = true;
					break;
				}
			}
		}
		
		if( ( inType == kDNSServiceType_PTR ) || ( ( inType == kDNSServiceType_ANY ) && !listHasPTR ) )
		{
			size_t				recordNameLen;
			uint8_t *			ptr;
			uint8_t *			lim;
			
			err = DomainNameDupLower( inName, &recordName, &recordNameLen );
			require_noerr( err, exit );
			
			rdataLen = 1 + hostname[ 0 ] + 10 + recordNameLen;
			rdataPtr = (uint8_t *) malloc( rdataLen );
			require_action( rdataPtr, exit, err = kNoMemoryErr );
			
			lim = &rdataPtr[ rdataLen ];
			
			ptr = &rdataPtr[ 1 ];
			memcpy( ptr, &hostname[ 1 ], hostname[ 0 ] );
			ptr += hostname[ 0 ];
			if( inIndex != 1 ) SNPrintF_Add( (char **) &ptr, (char *) lim, " (%u)", inIndex );
			rdataPtr[ 0 ] = (uint8_t)( ptr - &rdataPtr[ 1 ] );
			
			check( (size_t)( lim - ptr ) >= recordNameLen );
			memcpy( ptr, recordName, recordNameLen );
			ptr += recordNameLen;
			
			rdataLen = (size_t)( ptr - rdataPtr );
			
			err = _MRResourceRecordCreate( recordName, kDNSServiceType_PTR, kDNSServiceClass_IN, kMDNSRecordTTL_Other,
				(uint16_t) rdataLen, rdataPtr, &answer );
			require_noerr( err, exit );
			recordName	= NULL;
			rdataPtr	= NULL;
			
			*answerPtr = answer;
		}
	}
	else if( _MDNSReplierServiceInstanceNameMatch( inContext, inName, &index, &txtSize, &count ) &&
		( index == inIndex ) && ( count >= inIndex ) )
	{
		int		listHasSRV = false;
		int		listHasTXT = false;
		
		if( inType == kDNSServiceType_ANY )
		{
			for( answer = *inAnswerList; answer; answer = answer->next )
			{
				if( answer->type == kDNSServiceType_SRV )
				{
					if( !listHasSRV && DomainNameEqual( answer->name, inName ) ) listHasSRV = true;
				}
				else if( answer->type == kDNSServiceType_TXT )
				{
					if( !listHasTXT && DomainNameEqual( answer->name, inName ) ) listHasTXT = true;
				}
				if( listHasSRV && listHasTXT ) break;
			}
		}
		
		if( ( inType == kDNSServiceType_SRV ) || ( ( inType == kDNSServiceType_ANY ) && !listHasSRV ) )
		{
			dns_fixed_fields_srv *		fields;
			uint8_t *					ptr;
			uint8_t *					lim;
			uint8_t *					targetPtr;
			
			err = DomainNameDupLower( inName, &recordName, NULL );
			require_noerr( err, exit );
			
			rdataLen = sizeof( dns_fixed_fields_srv ) + 1 + hostname[ 0 ] + 10 + kLocalNameLen;
			rdataPtr = (uint8_t *) malloc( rdataLen );
			require_action( rdataPtr, exit, err = kNoMemoryErr );
			
			lim = &rdataPtr[ rdataLen ];
			
			fields = (dns_fixed_fields_srv *) rdataPtr;
			dns_fixed_fields_srv_init( fields, 0, 0, (uint16_t)( kMDNSReplierPortBase + txtSize ) );
			
			targetPtr = (uint8_t *) &fields[ 1 ];
			
			ptr = &targetPtr[ 1 ];
			memcpy( ptr, &hostname[ 1 ], hostname[ 0 ] );
			ptr += hostname[ 0 ];
			if( inIndex != 1 ) SNPrintF_Add( (char **) &ptr, (char *) lim, "-%u", inIndex );
			targetPtr[ 0 ] = (uint8_t)( ptr - &targetPtr[ 1 ] );
			
			check( (size_t)( lim - ptr ) >= kLocalNameLen );
			memcpy( ptr, kLocalName, kLocalNameLen );
			ptr += kLocalNameLen;
			
			rdataLen = (size_t)( ptr - rdataPtr );
			
			err = _MRResourceRecordCreate( recordName, kDNSServiceType_SRV, kDNSServiceClass_IN, kMDNSRecordTTL_Host,
				(uint16_t) rdataLen, rdataPtr, &answer );
			require_noerr( err, exit );
			recordName	= NULL;
			rdataPtr	= NULL;
			
			*answerPtr = answer;
			 answerPtr = &answer->next;
		}
		
		if( ( inType == kDNSServiceType_TXT ) || ( ( inType == kDNSServiceType_ANY ) && !listHasTXT ) )
		{
			err = DomainNameDupLower( inName, &recordName, NULL );
			require_noerr( err, exit );
			
			rdataLen = txtSize;
			err = _MDNSReplierCreateTXTRecord( inName, rdataLen, &rdataPtr );
			require_noerr( err, exit );
			
			err = _MRResourceRecordCreate( recordName, kDNSServiceType_TXT, kDNSServiceClass_IN, kMDNSRecordTTL_Other,
				(uint16_t) rdataLen, rdataPtr, &answer );
			require_noerr( err, exit );
			recordName	= NULL;
			rdataPtr	= NULL;
			
			*answerPtr = answer;
		}
		else if( inType == kDNSServiceType_NSEC )
		{
			err = DomainNameDupLower( inName, &recordName, NULL );
			require_noerr( err, exit );
			
			err = CreateNSECRecordData( recordName, &rdataPtr, &rdataLen, 2, kDNSServiceType_TXT, kDNSServiceType_SRV );
			require_noerr( err, exit );
			
			err = _MRResourceRecordCreate( recordName, kDNSServiceType_NSEC, kDNSServiceClass_IN, kMDNSRecordTTL_Host,
				(uint16_t) rdataLen, rdataPtr, &answer );
			require_noerr( err, exit );
			recordName	= NULL;
			rdataPtr	= NULL;
			
			*answerPtr = answer;
		}
	}
	err = kNoErr;
	
exit:
	FreeNullSafe( recordName );
	FreeNullSafe( rdataPtr );
	return( err );
}

//===========================================================================================================================
//	_MDNSReplierAnswerListRemovePTR
//===========================================================================================================================

static void
	_MDNSReplierAnswerListRemovePTR(
		MRResourceRecord **	inAnswerListPtr,
		const uint8_t *		inName,
		const uint8_t *		inRData )
{
	MRResourceRecord *		answer;
	MRResourceRecord **		answerPtr;
	
	for( answerPtr = inAnswerListPtr; ( answer = *answerPtr ) != NULL; answerPtr = &answer->next )
	{
		if( ( answer->type == kDNSServiceType_PTR ) && ( answer->class == kDNSServiceClass_IN ) &&
			DomainNameEqual( answer->name, inName ) && DomainNameEqual( answer->rdata, inRData ) ) break;
	}
	if( answer )
	{
		*answerPtr = answer->next;
		_MRResourceRecordFree( answer );
	}
}

//===========================================================================================================================
//	_MDNSReplierSendOrDropResponse
//===========================================================================================================================

static OSStatus
	_MDNSReplierSendOrDropResponse(
		MDNSReplierContext *	inContext,
		MRResourceRecord *		inAnswerList,
		sockaddr_ip *			inQuerier,
		SocketRef				inSock,
		unsigned int			inIndex,
		Boolean					inUnicast )
{
	OSStatus					err;
	uint8_t *					responsePtr	= NULL;
	size_t						responseLen;
	const struct sockaddr *		destAddr;
	ssize_t						n;
	const double				dropRate	= inUnicast ? inContext->ucastDropRate : inContext->mcastDropRate;
	int							drop;
	
	check( inIndex <= inContext->maxInstanceCount );
	
	// If maxDropCount > 0, then the drop rates apply only to the first maxDropCount responses. Otherwise, all messages are
	// subject to their respective drop rate. Also, responses to queries about mDNS replier itself (indicated by index 0),
	// as opposed to those for service instance records, are never dropped.
	
	drop = false;
	if( inIndex > 0 )
	{
		if( inContext->maxDropCount > 0 )
		{
			uint8_t * const		dropCount = &inContext->dropCounters[ inIndex - 1 ];
			
			if( *dropCount < inContext->maxDropCount )
			{
				if( ShouldDrop( dropRate ) ) drop = true;
				*dropCount += 1;
			}
		}
		else if( ShouldDrop( dropRate ) )
		{
			drop = true;
		}
	}
	
	err = _MDNSReplierCreateResponse( inContext, inAnswerList, inIndex, &responsePtr, &responseLen );
	require_noerr( err, exit );
	
	if( inUnicast )
	{
		destAddr = &inQuerier->sa;
	}
	else
	{
		destAddr = ( inQuerier->sa.sa_family == AF_INET ) ? GetMDNSMulticastAddrV4() : GetMDNSMulticastAddrV6();
	}
	
	mr_ulog( kLogLevelInfo, "%s %zu byte response to %##a -- %#.1{du:dnsmsg}\n",
		drop ? "Dropping" : "Sending", responseLen, destAddr, responsePtr, responseLen );
	
	if( !drop )
	{
		n = sendto( inSock, (char *) responsePtr, responseLen, 0, destAddr, SockAddrGetSize( destAddr ) );
		err = map_socket_value_errno( inSock, n == (ssize_t) responseLen, n );
		require_noerr( err, exit );
	}
	
exit:
	FreeNullSafe( responsePtr );
	return( err );
}

//===========================================================================================================================
//	_MDNSReplierCreateResponse
//===========================================================================================================================

static OSStatus
	_MDNSReplierCreateResponse(
		MDNSReplierContext *	inContext,
		MRResourceRecord *		inAnswerList,
		unsigned int			inIndex,
		uint8_t **				outResponsePtr,
		size_t *				outResponseLen )
{
	OSStatus				err;
	DataBuffer				responseDB;
	DNSHeader				hdr;
	MRResourceRecord *		answer;
	uint8_t *				responsePtr;
	size_t					responseLen, len;
	unsigned int			answerCount, recordCount;
	MRNameOffsetItem *		nameOffsetList = NULL;
	
	DataBuffer_Init( &responseDB, NULL, 0, SIZE_MAX );
	
	// The current answers in the answer list will make up the response's Answer Record Section.
	
	answerCount = 0;
	for( answer = inAnswerList; answer; answer = answer->next ) { ++answerCount; }
	
	// Unless configured not to, add any additional answers to the answer list for the Additional Record Section.
	
	if( !inContext->noAdditionals )
	{
		for( answer = inAnswerList; answer; answer = answer->next )
		{
			switch( answer->type )
			{
				case kDNSServiceType_PTR:
					err = _MDNSReplierAnswerListAdd( inContext, &inAnswerList, inIndex, answer->rdata, kDNSServiceType_SRV,
						answer->class );
					require_noerr( err, exit );
					
					err = _MDNSReplierAnswerListAdd( inContext, &inAnswerList, inIndex, answer->rdata, kDNSServiceType_TXT,
						answer->class );
					require_noerr( err, exit );
					break;
				
				case kDNSServiceType_SRV:
					err = _MDNSReplierAnswerListAdd( inContext, &inAnswerList, inIndex, answer->target, kDNSServiceType_A,
						answer->class );
					require_noerr( err, exit );
					
					err = _MDNSReplierAnswerListAdd( inContext, &inAnswerList, inIndex, answer->target, kDNSServiceType_AAAA,
						answer->class );
					require_noerr( err, exit );
					
					err = _MDNSReplierAnswerListAdd( inContext, &inAnswerList, inIndex, answer->name, kDNSServiceType_NSEC,
						answer->class );
					require_noerr( err, exit );
					break;
				
				case kDNSServiceType_TXT:
					err = _MDNSReplierAnswerListAdd( inContext, &inAnswerList, inIndex, answer->name, kDNSServiceType_NSEC,
						answer->class );
					require_noerr( err, exit );
					break;
				
				case kDNSServiceType_A:
					err = _MDNSReplierAnswerListAdd( inContext, &inAnswerList, inIndex, answer->name, kDNSServiceType_AAAA,
						answer->class );
					require_noerr( err, exit );
					
					err = _MDNSReplierAnswerListAdd( inContext, &inAnswerList, inIndex, answer->name, kDNSServiceType_NSEC,
						answer->class );
					require_noerr( err, exit );
					break;
				
				case kDNSServiceType_AAAA:
					err = _MDNSReplierAnswerListAdd( inContext, &inAnswerList, inIndex, answer->name, kDNSServiceType_A,
						answer->class );
					require_noerr( err, exit );
					
					err = _MDNSReplierAnswerListAdd( inContext, &inAnswerList, inIndex, answer->name, kDNSServiceType_NSEC,
						answer->class );
					require_noerr( err, exit );
					break;
				
				default:
					break;
			}
		}
	}
	
	// Append a provisional header to the response message.
	
	memset( &hdr, 0, sizeof( hdr ) );
	DNSHeaderSetFlags( &hdr, kDNSHeaderFlag_Response | kDNSHeaderFlag_AuthAnswer );
	
	err = DataBuffer_Append( &responseDB, &hdr, sizeof( hdr ) );
	require_noerr( err, exit );
	
	// Append answers to response message.
	
	responseLen = DataBuffer_GetLen( &responseDB );
	recordCount = 0;
	for( answer = inAnswerList; answer; answer = answer->next )
	{
		dns_fixed_fields_record		fields;
		unsigned int				class;
		
		// Append record NAME.
		
		err = _MDNSReplierAppendNameToResponse( &responseDB, answer->name, &nameOffsetList );
		require_noerr( err, exit );
		
		// Append record TYPE, CLASS, TTL, and provisional RDLENGTH.
		
		class = answer->class;
		if( ( answer->type == kDNSServiceType_SRV ) || ( answer->type == kDNSServiceType_TXT )  ||
			( answer->type == kDNSServiceType_A )   || ( answer->type == kDNSServiceType_AAAA ) ||
			( answer->type == kDNSServiceType_NSEC ) )
		{
			class |= kMDNSClassCacheFlushBit;
		}
		
		dns_fixed_fields_record_init( &fields, answer->type, (uint16_t) class, answer->ttl, (uint16_t) answer->rdlength );
		err = DataBuffer_Append( &responseDB, &fields, sizeof( fields ) );
		require_noerr( err, exit );
		
		// Append record RDATA.
		// The RDATA of PTR, SRV, and NSEC records contain domain names, which are subject to name compression.
		
		if( ( answer->type == kDNSServiceType_PTR ) || ( answer->type == kDNSServiceType_SRV ) ||
			( answer->type == kDNSServiceType_NSEC ) )
		{
			size_t				rdlength;
			uint8_t *			rdLengthPtr;
			const size_t		rdLengthOffset	= DataBuffer_GetLen( &responseDB ) - 2;
			const size_t		rdataOffset		= DataBuffer_GetLen( &responseDB );
			
			if( answer->type == kDNSServiceType_PTR )
			{
				err = _MDNSReplierAppendNameToResponse( &responseDB, answer->rdata, &nameOffsetList );
				require_noerr( err, exit );
			}
			else if( answer->type == kDNSServiceType_SRV )
			{
				require_fatal( answer->target == &answer->rdata[ 6 ], "Bad SRV record target pointer." );
				
				err = DataBuffer_Append( &responseDB, answer->rdata, (size_t)( answer->target - answer->rdata ) );
				require_noerr( err, exit );
				
				err = _MDNSReplierAppendNameToResponse( &responseDB, answer->target, &nameOffsetList );
				require_noerr( err, exit );
			}
			else
			{
				const size_t		nameLen = DomainNameLength( answer->rdata );
				
				err = _MDNSReplierAppendNameToResponse( &responseDB, answer->rdata, &nameOffsetList );
				require_noerr( err, exit );
				
				require_fatal( answer->rdlength > nameLen, "Bad NSEC record data length." );
				
				err = DataBuffer_Append( &responseDB, &answer->rdata[ nameLen ], answer->rdlength - nameLen );
				require_noerr( err, exit );
			}
			
			// Set the actual RDLENGTH, which may be less than the original due to name compression.
			
			rdlength = DataBuffer_GetLen( &responseDB ) - rdataOffset;
			check( rdlength <= UINT16_MAX );
			
			rdLengthPtr = DataBuffer_GetPtr( &responseDB ) + rdLengthOffset;
			WriteBig16Typed( rdLengthPtr, (uint16_t) rdlength );
		}
		else
		{
			err = DataBuffer_Append( &responseDB, answer->rdata, answer->rdlength );
			require_noerr( err, exit );
		}
		
		if( DataBuffer_GetLen( &responseDB ) > kMDNSMessageSizeMax ) break;
		responseLen = DataBuffer_GetLen( &responseDB );
		++recordCount;
	}
	
	// Set the response header's Answer and Additional record counts.
	// Note: recordCount may be less than answerCount if including all answerCount records would cause the size of the
	// response message to exceed the maximum mDNS message size.
	
	if( recordCount <= answerCount )
	{
		DNSHeaderSetAnswerCount( (DNSHeader *) DataBuffer_GetPtr( &responseDB ), recordCount );
	}
	else
	{
		DNSHeaderSetAnswerCount( (DNSHeader *) DataBuffer_GetPtr( &responseDB ), answerCount );
		DNSHeaderSetAdditionalCount( (DNSHeader *) DataBuffer_GetPtr( &responseDB ), recordCount - answerCount );
	}
	
	err = DataBuffer_Detach( &responseDB, &responsePtr, &len );
	require_noerr( err, exit );
	
	if( outResponsePtr ) *outResponsePtr = responsePtr;
	if( outResponseLen ) *outResponseLen = responseLen;
	
exit:
	_MRNameOffsetItemFreeList( nameOffsetList );
	DataBuffer_Free( &responseDB );
	return( err );
}

//===========================================================================================================================
//	_MDNSReplierAppendNameToResponse
//===========================================================================================================================

static OSStatus
	_MDNSReplierAppendNameToResponse(
		DataBuffer *		inResponse,
		const uint8_t *		inName,
		MRNameOffsetItem **	inNameOffsetListPtr )
{
	OSStatus				err;
	const uint8_t *			subname;
	const uint8_t *			limit;
	size_t					nameOffset;
	MRNameOffsetItem *		item;
	uint8_t					compressionPtr[ 2 ];
	
	nameOffset = DataBuffer_GetLen( inResponse );
	
	// Find the name's longest subname (more accurately, its longest sub-FQDN) in the name compression list.
	
	for( subname = inName; subname[ 0 ] != 0; subname += ( 1 + subname[ 0 ] ) )
	{
		for( item = *inNameOffsetListPtr; item; item = item->next )
		{
			if( DomainNameEqual( item->name, subname ) ) break;
		}
		
		// If an item was found for this subname, then append a name compression pointer and we're done. Otherwise, append
		// the subname's first label.
		
		if( item )
		{
			DNSMessageWriteLabelPointer( compressionPtr, item->offset );
			
			err = DataBuffer_Append( inResponse, compressionPtr, sizeof( compressionPtr ) );
			require_noerr( err, exit );
			break;
		}
		else
		{
			err = DataBuffer_Append( inResponse, subname, 1 + subname[ 0 ] );
			require_noerr( err, exit );
		}
	}
		
	// If we made it to the root label, then no subname was able to be compressed. All of the name's labels up to the root
	// label were appended to the response message, so a root label is needed to terminate the complete name.
	
	if( subname[ 0 ] == 0 )
	{
		err = DataBuffer_Append( inResponse, "", 1 );
		require_noerr( err, exit );
	}
	
	// Add subnames that weren't able to be compressed and their offsets to the name compression list.
	
	limit = subname;
	for( subname = inName; subname < limit; subname += ( 1 + subname[ 0 ] ) )
	{
		const size_t		subnameOffset = nameOffset + (size_t)( subname - inName );
		
		if( subnameOffset > kDNSCompressionOffsetMax ) break;
		
		err = _MRNameOffsetItemCreate( subname, (uint16_t) subnameOffset, &item );
		require_noerr( err, exit );
		
		item->next = *inNameOffsetListPtr;
		*inNameOffsetListPtr = item;
	}
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_MDNSReplierServiceTypeMatch
//===========================================================================================================================

static Boolean
	_MDNSReplierServiceTypeMatch(
		const MDNSReplierContext *	inContext,
		const uint8_t *				inName,
		unsigned int *				outTXTSize,
		unsigned int *				outCount )
{
	OSStatus					err;
	const char *				ptr;
	const char *				end;
	uint32_t					txtSize, count;
	const uint8_t * const		serviceLabel	= inContext->serviceLabel;
	int							nameMatches		= false;
	
	require_quiet( inName[ 0 ] >= serviceLabel[ 0 ], exit );
	if( _memicmp( &inName[ 1 ], &serviceLabel[ 1 ], serviceLabel[ 0 ] ) != 0 ) goto exit;
	
	ptr = (const char *) &inName[ 1 + serviceLabel[ 0 ] ];
	end = (const char *) &inName[ 1 + inName[ 0 ] ];
	
	require_quiet( ( ptr < end ) && ( *ptr == '-' ), exit );
	++ptr;
	
	err = DecimalTextToUInt32( ptr, end, &txtSize, &ptr );
	require_noerr_quiet( err, exit );
	require_quiet( txtSize <= UINT16_MAX, exit );
	
	require_quiet( ( ptr < end ) && ( *ptr == '-' ), exit );
	++ptr;
	
	err = DecimalTextToUInt32( ptr, end, &count, &ptr );
	require_noerr_quiet( err, exit );
	require_quiet( count <= UINT16_MAX, exit );
	require_quiet( ptr == end, exit );
	
	if( !DomainNameEqual( (const uint8_t *) ptr, (const uint8_t *) "\x04" "_tcp" "\x05" "local" ) ) goto exit;
	nameMatches = true;
	
	if( outTXTSize )	*outTXTSize	= txtSize;
	if( outCount )		*outCount	= count;
	
exit:
	return( nameMatches ? true : false );
}

//===========================================================================================================================
//	_MDNSReplierServiceInstanceNameMatch
//===========================================================================================================================

static Boolean
	_MDNSReplierServiceInstanceNameMatch(
		const MDNSReplierContext *	inContext,
		const uint8_t *				inName,
		unsigned int *				outIndex,
		unsigned int *				outTXTSize,
		unsigned int *				outCount )
{
	OSStatus					err;
	const uint8_t *				ptr;
	const uint8_t *				end;
	uint32_t					index;
	unsigned int				txtSize, count;
	const uint8_t * const		hostname	= inContext->hostname;
	int							nameMatches	= false;
	
	require_quiet( inName[ 0 ] >= hostname[ 0 ], exit );
	if( _memicmp( &inName[ 1 ], &hostname[ 1 ], hostname[ 0 ] ) != 0 ) goto exit;
	
	ptr = &inName[ 1 + hostname[ 0 ] ];
	end = &inName[ 1 + inName[ 0 ] ];
	if( ptr < end )
	{
		require_quiet( ( end - ptr ) >= 2, exit );
		require_quiet( ( ptr[ 0 ] == ' ' ) && ( ptr[ 1 ] == '(' ), exit );
		ptr += 2;
		
        err = DecimalTextToUInt32( (const char *) ptr, (const char *) end, &index, (const char **) &ptr );
		require_noerr_quiet( err, exit );
		require_quiet( ( index >= 2 ) && ( index <= UINT16_MAX ), exit );
		
		require_quiet( ( ( end - ptr ) == 1 ) && ( *ptr == ')' ), exit );
		++ptr;
	}
	else
	{
		index = 1;
	}
	
	if( !_MDNSReplierServiceTypeMatch( inContext, ptr, &txtSize, &count ) ) goto exit;
	nameMatches = true;
	
	if( outIndex )		*outIndex	= index;
	if( outTXTSize )	*outTXTSize	= txtSize;
	if( outCount )		*outCount	= count;
	
exit:
	return( nameMatches ? true : false );
}

//===========================================================================================================================
//	_MDNSReplierAboutRecordNameMatch
//===========================================================================================================================

#define _MemIEqual( PTR1, LEN1, PTR2, LEN2 ) \
	( ( ( LEN1 ) == ( LEN2 ) ) && ( _memicmp( ( PTR1 ), ( PTR2 ), ( LEN1 ) ) == 0 ) )

static Boolean	_MDNSReplierAboutRecordNameMatch( const MDNSReplierContext *inContext, const uint8_t *inName )
{
	const uint8_t *				subname;
	const uint8_t * const		hostname	= inContext->hostname;
	int							nameMatches	= false;
	
	if( strnicmpx( &inName[ 1 ], inName[ 0 ], "about" ) != 0 ) goto exit;
	subname = DomainNameGetNextLabel( inName );
	
	if( !_MemIEqual( &subname[ 1 ], subname[ 0 ], &hostname[ 1 ], hostname[ 0 ] ) ) goto exit;
	subname = DomainNameGetNextLabel( subname );
	
	if( !DomainNameEqual( subname, kLocalName ) ) goto exit;
	nameMatches = true;
	
exit:
	return( nameMatches ? true : false );
}

//===========================================================================================================================
//	_MDNSReplierHostnameMatch
//===========================================================================================================================

static Boolean
	_MDNSReplierHostnameMatch(
		const MDNSReplierContext *	inContext,
		const uint8_t *				inName,
		unsigned int *				outIndex )
{
	OSStatus					err;
	const uint8_t *				ptr;
	const uint8_t *				end;
	uint32_t					index;
	const uint8_t * const		hostname	= inContext->hostname;
	int							nameMatches	= false;
	
	require_quiet( inName[ 0 ] >= hostname[ 0 ], exit );
	if( _memicmp( &inName[ 1 ], &hostname[ 1 ], hostname[ 0 ] ) != 0 ) goto exit;
	
	ptr = &inName[ 1 + hostname[ 0 ] ];
	end = &inName[ 1 + inName[ 0 ] ];
	if( ptr < end )
	{
		require_quiet( *ptr == '-', exit );
		++ptr;
		
		err = DecimalTextToUInt32( (const char *) ptr, (const char *) end, &index, (const char **) &ptr );
		require_noerr_quiet( err, exit );
		require_quiet( ( index >= 2 ) && ( index <= UINT16_MAX ), exit );
		require_quiet( ptr == end, exit );
	}
	else
	{
		index = 1;
	}
	
	if( !DomainNameEqual( ptr, kLocalName ) ) goto exit;
	nameMatches = true;
	
	if( outIndex ) *outIndex = index;
	
exit:
	return( nameMatches ? true : false );
}

//===========================================================================================================================
//	_MDNSReplierCreateTXTRecord
//===========================================================================================================================

static OSStatus	_MDNSReplierCreateTXTRecord( const uint8_t *inRecordName, size_t inSize, uint8_t **outTXT )
{
	OSStatus		err;
	uint8_t *		txt;
	uint8_t *		ptr;
	size_t			i, wholeCount, remCount;
	uint32_t		hash;
	int				n;
	uint8_t			txtStr[ 16 ];
	
	require_action_quiet( inSize > 0, exit, err = kSizeErr );
	
	txt = (uint8_t *) malloc( inSize );
	require_action( txt, exit, err = kNoMemoryErr );
	
	hash = _FNV1( inRecordName, DomainNameLength( inRecordName ) );
	
	txtStr[ 0 ] = 15;
	n = MemPrintF( &txtStr[ 1 ], 15, "hash=0x%08X", hash );
	check( n == 15 );
	
	ptr = txt;
	wholeCount = inSize / 16;
	for( i = 0; i < wholeCount; ++i )
	{
		memcpy( ptr, txtStr, 16 );
		ptr += 16;
	}
	
	remCount = inSize % 16;
	if( remCount > 0 )
	{
		txtStr[ 0 ] = (uint8_t)( remCount - 1 );
		memcpy( ptr, txtStr, remCount );
		ptr += remCount;
	}
	check( ptr == &txt[ inSize ] );
	
	*outTXT = txt;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_MRResourceRecordCreate
//===========================================================================================================================

static OSStatus
	_MRResourceRecordCreate(
		uint8_t *			inName,
		uint16_t			inType,
		uint16_t			inClass,
		uint32_t			inTTL,
		uint16_t			inRDLength,
		uint8_t *			inRData,
		MRResourceRecord **	outRecord )
{
	OSStatus				err;
	MRResourceRecord *		obj;
	
	obj = (MRResourceRecord *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->name		= inName;
	obj->type		= inType;
	obj->class		= inClass;
	obj->ttl		= inTTL;
	obj->rdlength	= inRDLength;
	obj->rdata		= inRData;
	
	if( inType == kDNSServiceType_SRV )
	{
		require_action_quiet( obj->rdlength > sizeof( dns_fixed_fields_srv ), exit, err = kMalformedErr );
		obj->target = obj->rdata + sizeof( dns_fixed_fields_srv );
	}
	
	*outRecord = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	FreeNullSafe( obj );
	return( err );
}

//===========================================================================================================================
//	_MRResourceRecordFree
//===========================================================================================================================

static void	_MRResourceRecordFree( MRResourceRecord *inRecord )
{
	ForgetMem( &inRecord->name );
	ForgetMem( &inRecord->rdata );
	free( inRecord );
}

//===========================================================================================================================
//	_MRResourceRecordFreeList
//===========================================================================================================================

static void	_MRResourceRecordFreeList( MRResourceRecord *inList )
{
	MRResourceRecord *		record;
	
	while( ( record = inList ) != NULL )
	{
		inList = record->next;
		_MRResourceRecordFree( record );
	}
}

//===========================================================================================================================
//	_MRNameOffsetItemCreate
//===========================================================================================================================

static OSStatus	_MRNameOffsetItemCreate( const uint8_t *inName, uint16_t inOffset, MRNameOffsetItem **outItem )
{
	OSStatus				err;
	MRNameOffsetItem *		obj;
	size_t					nameLen;
	
	require_action_quiet( inOffset <= kDNSCompressionOffsetMax, exit, err = kSizeErr );
	
	nameLen = DomainNameLength( inName );
	obj = (MRNameOffsetItem *) calloc( 1, offsetof( MRNameOffsetItem, name ) + nameLen );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->offset = inOffset;
	memcpy( obj->name, inName, nameLen );
	
	*outItem = obj;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_MRNameOffsetItemFree
//===========================================================================================================================

static void	_MRNameOffsetItemFree( MRNameOffsetItem *inItem )
{
	free( inItem );
}

//===========================================================================================================================
//	_MRNameOffsetItemFreeList
//===========================================================================================================================

static void	_MRNameOffsetItemFreeList( MRNameOffsetItem *inList )
{
	MRNameOffsetItem *		item;
	
	while( ( item = inList ) != NULL )
	{
		inList = item->next;
		_MRNameOffsetItemFree( item );
	}
}

//===========================================================================================================================
//	GAIPerfCmd
//===========================================================================================================================

#define kGAIPerfStandardTTL		( 1 * kSecondsPerHour )

typedef struct GAITesterPrivate *		GAITesterRef;
typedef struct GAITestCase				GAITestCase;

typedef struct
{
	const char *		name;				// Domain name that was resolved.
	uint64_t			connectionTimeUs;	// Time in microseconds that it took to create a DNS-SD connection.
	uint64_t			firstTimeUs;		// Time in microseconds that it took to get the first address result.
	uint64_t			timeUs;				// Time in microseconds that it took to get all expected address results.
	OSStatus			error;
	
}	GAITestItemResult;

typedef void ( *GAITesterStopHandler_f )( void *inContext, OSStatus inError );
typedef void
	( *GAITesterResultsHandler_f )(
		const char *				inCaseTitle,
		NanoTime64					inCaseStartTime,
		NanoTime64					inCaseEndTime,
		const GAITestItemResult *	inResultArray,
		size_t						inResultCount,
		void *						inContext );

typedef unsigned int		GAITestAddrType;
#define kGAITestAddrType_None		0
#define kGAITestAddrType_IPv4		( 1U << 0 )
#define kGAITestAddrType_IPv6		( 1U << 1 )
#define kGAITestAddrType_Both		( kGAITestAddrType_IPv4 | kGAITestAddrType_IPv6 )

#define GAITestAddrTypeIsValid( X ) \
	( ( (X) & kGAITestAddrType_Both ) && ( ( (X) & ~kGAITestAddrType_Both ) == 0 ) )

typedef struct
{
	GAITesterRef			tester;				// GAI tester object.
	CFMutableArrayRef		testCaseResults;	// Array of test case results.
	unsigned int			iterTimeLimitMs;	// Amount of time to allow each iteration to complete.
	unsigned int			callDelayMs;		// Amount of time to wait before calling DNSServiceGetAddrInfo().
	unsigned int			serverDelayMs;		// Amount of additional time to have server delay its responses.
	unsigned int			defaultIterCount;	// Default test case iteration count.
	dispatch_source_t		sigIntSource;		// Dispatch source for SIGINT.
	dispatch_source_t		sigTermSource;		// Dispatch source for SIGTERM.
	char *					outputFilePath;		// File to write test results to. If NULL, then write to stdout.
	OutputFormatType		outputFormat;		// Format of test results output.
	DNSProtocol				protocol;			// DNS protocol to use, e.g., Do53, DoT, or DoH.
	Boolean					skipPathEval;		// True if DNSServiceGetAddrInfo() path evaluation is to be skipped.
	Boolean					badUDPMode;			// True if the test DNS server is to run in Bad UDP mode.
	Boolean					testFailed;			// True if at least one test case iteration failed.
	
}	GAIPerfContext;

static void		GAIPerfContextFree( GAIPerfContext *inContext );
static OSStatus	GAIPerfAddAdvancedTestCases( GAIPerfContext *inContext );
static OSStatus	GAIPerfAddBasicTestCases( GAIPerfContext *inContext );
static void		GAIPerfTesterStopHandler( void *inContext, OSStatus inError );
static void
	GAIPerfResultsHandler(
		const char *				inCaseTitle,
		NanoTime64					inCaseStartTime,
		NanoTime64					inCaseEndTime,
		const GAITestItemResult *	inResultArray,
		size_t						inResultCount,
		void *						inContext );
static void		GAIPerfSignalHandler( void *inContext );

static CFTypeID	GAITesterGetTypeID( void );
static OSStatus
	GAITesterCreate(
		dispatch_queue_t	inQueue,
		DNSProtocol			inProtocol,
		unsigned int		inCallDelayMs,
		int					inServerDelayMs,
		int					inServerDefaultTTL,
		Boolean				inSkipPathEvaluation,
		Boolean				inBadUDPMode,
		GAITesterRef *		outTester );
static void		GAITesterStart( GAITesterRef inTester );
static void		GAITesterStop( GAITesterRef inTester );
static OSStatus	GAITesterAddTestCase( GAITesterRef inTester, GAITestCase *inCase );
static void
	GAITesterSetStopHandler(
		GAITesterRef			inTester,
		GAITesterStopHandler_f	inEventHandler,
		void *					inEventContext );
static void
	GAITesterSetResultsHandler(
		GAITesterRef				inTester,
		GAITesterResultsHandler_f	inResultsHandler,
		void *						inResultsContext );

static OSStatus	GAITestCaseCreate( const char *inTitle, GAITestCase **outCase );
static void		GAITestCaseFree( GAITestCase *inCase );
static OSStatus
	GAITestCaseAddItem(
		GAITestCase *	inCase,
		unsigned int	inAliasCount,
		unsigned int	inAddressCount,
		int				inTTL,
		GAITestAddrType	inHasAddrs,
		GAITestAddrType	inWantAddrs,
		unsigned int	inTimeLimitMs,
		unsigned int	inItemCount );
static OSStatus
	GAITestCaseAddLocalHostItem(
		GAITestCase *	inCase,
		GAITestAddrType	inWantAddrs,
		unsigned int	inTimeLimitMs,
		unsigned int	inItemCount );

static void	GAIPerfCmd( void )
{
	OSStatus				err;
	GAIPerfContext *		context = NULL;
	
	err = CheckRootUser();
	require_noerr_quiet( err, exit );
	
	err = CheckIntegerArgument( gGAIPerf_CallDelayMs, "call delay (ms)", 0, INT_MAX );
	require_noerr_quiet( err, exit );
	
	err = CheckIntegerArgument( gGAIPerf_ServerDelayMs, "server delay (ms)", 0, INT_MAX );
	require_noerr_quiet( err, exit );
	
	err = CheckIntegerArgument( gGAIPerf_IterationCount, "iteration count", 1, INT_MAX );
	require_noerr_quiet( err, exit );
	
	err = CheckIntegerArgument( gGAIPerf_IterationTimeLimitMs, "iteration time limit (ms)", 0, INT_MAX );
	require_noerr_quiet( err, exit );
	
	context = (GAIPerfContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	context->testCaseResults = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
	require_action( context->testCaseResults, exit, err = kNoMemoryErr );
	
	context->iterTimeLimitMs	= (unsigned int) gGAIPerf_IterationTimeLimitMs;
	context->callDelayMs		= (unsigned int) gGAIPerf_CallDelayMs;
	context->serverDelayMs		= (unsigned int) gGAIPerf_ServerDelayMs;
	context->defaultIterCount	= (unsigned int) gGAIPerf_IterationCount;
	context->skipPathEval		= gGAIPerf_SkipPathEvalulation	? true : false;
	context->badUDPMode			= gGAIPerf_BadUDPMode			? true : false;
	
	context->protocol = (DNSProtocol) CLIArgToValue( "protocol", gGAIPerf_Protocol, &err,
		kDNSProtocolStr_Do53,	(int) kDNSProtocol_Do53,
		kDNSProtocolStr_DoT,	(int) kDNSProtocol_DoT,
		kDNSProtocolStr_DoH,	(int) kDNSProtocol_DoH,
		NULL );
	require_noerr_quiet( err, exit );
	
	if( gGAIPerf_OutputFilePath )
	{
		context->outputFilePath = strdup( gGAIPerf_OutputFilePath );
		require_action( context->outputFilePath, exit, err = kNoMemoryErr );
	}
	
	err = OutputFormatFromArgString( gGAIPerf_OutputFormat, &context->outputFormat );
	require_noerr_quiet( err, exit );
	
	err = GAITesterCreate( dispatch_get_main_queue(), context->protocol, context->callDelayMs, (int) context->serverDelayMs,
		kGAIPerfStandardTTL, context->skipPathEval, context->badUDPMode, &context->tester );
	require_noerr( err, exit );
	
	check( gGAIPerf_TestSuite );
	if( strcasecmp( gGAIPerf_TestSuite, kGAIPerfTestSuiteName_Basic ) == 0 )
	{
		err = GAIPerfAddBasicTestCases( context );
		require_noerr( err, exit );
	}
	else if( strcasecmp( gGAIPerf_TestSuite, kGAIPerfTestSuiteName_Advanced ) == 0 )
	{
		err = GAIPerfAddAdvancedTestCases( context );
		require_noerr( err, exit );
	}
	else
	{
		FPrintF( stderr, "error: Invalid test suite name: %s.\n", gGAIPerf_TestSuite );
		goto exit;
	}
	
	GAITesterSetStopHandler( context->tester, GAIPerfTesterStopHandler, context );
	GAITesterSetResultsHandler( context->tester, GAIPerfResultsHandler, context );
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), GAIPerfSignalHandler, context,
		&context->sigIntSource );
	require_noerr( err, exit );
	dispatch_resume( context->sigIntSource );
	
	signal( SIGTERM, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGTERM, dispatch_get_main_queue(), GAIPerfSignalHandler, context,
		&context->sigTermSource );
	require_noerr( err, exit );
	dispatch_resume( context->sigTermSource );
	
	GAITesterStart( context->tester );
	dispatch_main();
	
exit:
	if( context ) GAIPerfContextFree( context );
	exit( 1 );
}

//===========================================================================================================================
//	GAIPerfContextFree
//===========================================================================================================================

static void	GAIPerfContextFree( GAIPerfContext *inContext )
{
	ForgetCF( &inContext->tester );
	ForgetCF( &inContext->testCaseResults );
	ForgetMem( &inContext->outputFilePath );
	dispatch_source_forget( &inContext->sigIntSource );
	dispatch_source_forget( &inContext->sigTermSource );
	free( inContext );
}

//===========================================================================================================================
//	GAIPerfAddAdvancedTestCases
//===========================================================================================================================

#define kTestCaseTitleBufferSize		128

static void
	_GAIPerfWriteTestCaseTitle(
		char			inBuffer[ kTestCaseTitleBufferSize ],
		unsigned int	inCNAMERecordCount,
		unsigned int	inARecordCount,
		unsigned int	inAAAARecordCount,
		GAITestAddrType	inRequested,
		unsigned int	inIterationCount,
		Boolean			inIterationsAreUnique );
static void
	_GAIPerfWriteLocalHostTestCaseTitle(
		char			inBuffer[ kTestCaseTitleBufferSize ],
		GAITestAddrType	inRequested,
		unsigned int	inIterationCount );

#define kGAIPerfAdvancedTestSuite_MaxAliasCount		4
#define kGAIPerfAdvancedTestSuite_MaxAddrCount		8

static OSStatus	GAIPerfAddAdvancedTestCases( GAIPerfContext *inContext )
{
	OSStatus			err;
	unsigned int		aliasCount, addressCount, i;
	GAITestCase *		testCase = NULL;
	char				title[ kTestCaseTitleBufferSize ];
	
	aliasCount = 0;
	while( aliasCount <= kGAIPerfAdvancedTestSuite_MaxAliasCount )
	{
		for( addressCount = 1; addressCount <= kGAIPerfAdvancedTestSuite_MaxAddrCount; addressCount *= 2 )
		{
			// Add a test case to resolve a domain name with
			//
			//     <aliasCount> CNAME records, <addressCount> A records, and <addressCount> AAAA records
			//
			// to its IPv4 and IPv6 addresses. Each iteration resolves a unique instance of such a domain name, which
			// requires server queries.
			
			_GAIPerfWriteTestCaseTitle( title, aliasCount, addressCount, addressCount, kGAITestAddrType_Both,
				inContext->defaultIterCount, true );
			
			err = GAITestCaseCreate( title, &testCase );
			require_noerr( err, exit );
			
			for( i = 0; i < inContext->defaultIterCount; ++i )
			{
				err = GAITestCaseAddItem( testCase, aliasCount, addressCount, kGAIPerfStandardTTL,
					kGAITestAddrType_Both, kGAITestAddrType_Both, inContext->iterTimeLimitMs, 1 );
				require_noerr( err, exit );
			}
			
			err = GAITesterAddTestCase( inContext->tester, testCase );
			require_noerr( err, exit );
			testCase = NULL;
			
			// Add a test case to resolve a domain name with
			//
			//     <aliasCount> CNAME records, <addressCount> A records, and <addressCount> AAAA records
			//
			// to its IPv4 and IPv6 addresses. A preliminary iteration resolves a unique domain name, which requires a server
			// query. The subsequent iterations resolve the same domain name as the preliminary iteration, which should
			// ideally require no server queries, i.e., the results should come from the cache.
			
			_GAIPerfWriteTestCaseTitle( title, aliasCount, addressCount, addressCount, kGAITestAddrType_Both,
				inContext->defaultIterCount, false );
			
			err = GAITestCaseCreate( title, &testCase );
			require_noerr( err, exit );
			
			err = GAITestCaseAddItem( testCase, aliasCount, addressCount, kGAIPerfStandardTTL,
				kGAITestAddrType_Both, kGAITestAddrType_Both, inContext->iterTimeLimitMs, inContext->defaultIterCount + 1 );
			require_noerr( err, exit );
			
			err = GAITesterAddTestCase( inContext->tester, testCase );
			require_noerr( err, exit );
			testCase = NULL;
		}
		
		aliasCount = ( aliasCount == 0 ) ? 1 : ( 2 * aliasCount );
	}
	
	// Finally, add a test case to resolve localhost to its IPv4 and IPv6 addresses.
	
	_GAIPerfWriteLocalHostTestCaseTitle( title, kGAITestAddrType_Both, inContext->defaultIterCount );
	
	err = GAITestCaseCreate( title, &testCase );
	require_noerr( err, exit );
	
	err = GAITestCaseAddLocalHostItem( testCase, kGAITestAddrType_Both, inContext->iterTimeLimitMs,
		inContext->defaultIterCount );
	require_noerr( err, exit );
	
	err = GAITesterAddTestCase( inContext->tester, testCase );
	require_noerr( err, exit );
	testCase = NULL;
	
exit:
	if( testCase ) GAITestCaseFree( testCase );
	return( err );
}

//===========================================================================================================================
//	_GAIPerfWriteTestCaseTitle
//===========================================================================================================================

#define GAITestAddrTypeToRequestKeyValue( X ) (				\
	( (X) == kGAITestAddrType_Both ) ? "ipv4\\,ipv6"	:	\
	( (X) == kGAITestAddrType_IPv4 ) ? "ipv4"			:	\
	( (X) == kGAITestAddrType_IPv6 ) ? "ipv6"			:	\
									   "" )

static void
	_GAIPerfWriteTestCaseTitle(
		char			inBuffer[ kTestCaseTitleBufferSize ],
		unsigned int	inCNAMERecordCount,
		unsigned int	inARecordCount,
		unsigned int	inAAAARecordCount,
		GAITestAddrType	inRequested,
		unsigned int	inIterationCount,
		Boolean			inIterationsAreUnique )
{
	SNPrintF( inBuffer, kTestCaseTitleBufferSize, "name=dynamic,cname=%u,a=%u,aaaa=%u,req=%s,iterations=%u%?s",
		inCNAMERecordCount, inARecordCount, inAAAARecordCount, GAITestAddrTypeToRequestKeyValue( inRequested ),
		inIterationCount, inIterationsAreUnique, ",unique" );
}

//===========================================================================================================================
//	_GAIPerfWriteLocalHostTestCaseTitle
//===========================================================================================================================

static void
	_GAIPerfWriteLocalHostTestCaseTitle(
		char			inBuffer[ kTestCaseTitleBufferSize ],
		GAITestAddrType	inRequested,
		unsigned int	inIterationCount )
{
	SNPrintF( inBuffer, kTestCaseTitleBufferSize, "name=localhost,req=%s,iterations=%u",
		GAITestAddrTypeToRequestKeyValue( inRequested ), inIterationCount );
}

//===========================================================================================================================
//	GAIPerfAddBasicTestCases
//===========================================================================================================================

#define kGAIPerfBasicTestSuite_AliasCount		2
#define kGAIPerfBasicTestSuite_AddrCount		4

static OSStatus	GAIPerfAddBasicTestCases( GAIPerfContext *inContext )
{
	OSStatus			err;
	GAITestCase *		testCase = NULL;
	char				title[ kTestCaseTitleBufferSize ];
	unsigned int		i;
	
	// Test Case #1:
	// Resolve a domain name with
	//
	//     2 CNAME records, 4 A records, and 4 AAAA records
	//
	// to its IPv4 and IPv6 addresses. Each of the iterations resolves a unique domain name, which requires server
	// queries.
	
	_GAIPerfWriteTestCaseTitle( title, kGAIPerfBasicTestSuite_AliasCount,
		kGAIPerfBasicTestSuite_AddrCount, kGAIPerfBasicTestSuite_AddrCount, kGAITestAddrType_Both,
		inContext->defaultIterCount, true );
	
	err = GAITestCaseCreate( title, &testCase );
	require_noerr( err, exit );
	
	for( i = 0; i < inContext->defaultIterCount; ++i )
	{
		err = GAITestCaseAddItem( testCase, kGAIPerfBasicTestSuite_AliasCount, kGAIPerfBasicTestSuite_AddrCount,
			kGAIPerfStandardTTL, kGAITestAddrType_Both, kGAITestAddrType_Both, inContext->iterTimeLimitMs, 1 );
		require_noerr( err, exit );
	}
	
	err = GAITesterAddTestCase( inContext->tester, testCase );
	require_noerr( err, exit );
	testCase = NULL;
	
	// Test Case #2:
	// Resolve a domain name with
	//
	//     2 CNAME records, 4 A records, and 4 AAAA records
	//
	// to its IPv4 and IPv6 addresses. A preliminary iteration resolves a unique instance of such a domain name, which
	// requires server queries. Each of the subsequent iterations resolves the same domain name as the preliminary
	// iteration, which should ideally require no additional server queries, i.e., the results should come from the cache.
	
	_GAIPerfWriteTestCaseTitle( title, kGAIPerfBasicTestSuite_AliasCount,
		kGAIPerfBasicTestSuite_AddrCount, kGAIPerfBasicTestSuite_AddrCount, kGAITestAddrType_Both,
		inContext->defaultIterCount, false );
	
	err = GAITestCaseCreate( title, &testCase );
	require_noerr( err, exit );
	
	err = GAITestCaseAddItem( testCase, kGAIPerfBasicTestSuite_AliasCount, kGAIPerfBasicTestSuite_AddrCount,
		kGAIPerfStandardTTL, kGAITestAddrType_Both, kGAITestAddrType_Both, inContext->iterTimeLimitMs,
		inContext->defaultIterCount + 1 );
	require_noerr( err, exit );
	
	err = GAITesterAddTestCase( inContext->tester, testCase );
	require_noerr( err, exit );
	testCase = NULL;
	
	// Test Case #3:
	// Each iteration resolves localhost to its IPv4 and IPv6 addresses.
	
	_GAIPerfWriteLocalHostTestCaseTitle( title, kGAITestAddrType_Both, inContext->defaultIterCount );
	
	err = GAITestCaseCreate( title, &testCase );
	require_noerr( err, exit );
	
	err = GAITestCaseAddLocalHostItem( testCase, kGAITestAddrType_Both, inContext->iterTimeLimitMs,
		inContext->defaultIterCount );
	require_noerr( err, exit );
	
	err = GAITesterAddTestCase( inContext->tester, testCase );
	require_noerr( err, exit );
	testCase = NULL;
	
exit:
	if( testCase ) GAITestCaseFree( testCase );
	return( err );
}

//===========================================================================================================================
//	GAIPerfTesterStopHandler
//===========================================================================================================================

#define kGAIPerfResultsKey_Info				CFSTR( "info" )
#define kGAIPerfResultsKey_TestCases		CFSTR( "testCases" )
#define kGAIPerfResultsKey_Success			CFSTR( "success" )

#define kGAIPerfInfoKey_CallDelay			CFSTR( "callDelayMs" )
#define kGAIPerfInfoKey_Protocol			CFSTR( "protocol" )
#define kGAIPerfInfoKey_ServerDelay			CFSTR( "serverDelayMs" )
#define kGAIPerfInfoKey_SkippedPathEval		CFSTR( "skippedPathEval" )
#define kGAIPerfInfoKey_UsedBadUDPMode		CFSTR( "usedBadUPDMode" )

static void	GAIPerfTesterStopHandler( void *inContext, OSStatus inError )
{
	OSStatus					err;
	GAIPerfContext * const		context = (GAIPerfContext *) inContext;
	CFPropertyListRef			plist;
	int							exitCode;
	
	err = inError;
	require_noerr_quiet( err, exit );
	
	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &plist,
		"{"
			"%kO="			// info
			"{"
				"%kO=%lli"	// callDelayMs
				"%kO=%s"	// protocol
				"%kO=%lli"	// serverDelayMs
				"%kO=%b"	// skippedPathEval
				"%kO=%b"	// usedBadUPDMode
			"}"
			"%kO=%O"		// testCases
			"%kO=%b"		// success
		"}",
		kGAIPerfResultsKey_Info,
		kGAIPerfInfoKey_CallDelay,			(int64_t) context->callDelayMs,
		kGAIPerfInfoKey_Protocol,			_DNSProtocolToString( context->protocol ),
		kGAIPerfInfoKey_ServerDelay,		(int64_t) context->serverDelayMs,
		kGAIPerfInfoKey_SkippedPathEval,	context->skipPathEval,
		kGAIPerfInfoKey_UsedBadUDPMode,		context->badUDPMode,
		kGAIPerfResultsKey_TestCases,		context->testCaseResults,
		kGAIPerfResultsKey_Success,			!context->testFailed );
	require_noerr( err, exit );
	
	err = OutputPropertyList( plist, context->outputFormat, context->outputFilePath );
	CFRelease( plist );
	require_noerr( err, exit );
	
exit:
	exitCode = err ? 1 : ( context->testFailed ? 2 : 0 );
	GAIPerfContextFree( context );
	exit( exitCode );
}

//===========================================================================================================================
//	GAIPerfResultsHandler
//===========================================================================================================================

// Keys for test case dictionary

#define kGAIPerfTestCaseKey_Title				CFSTR( "title" )
#define kGAIPerfTestCaseKey_StartTime			CFSTR( "startTime" )
#define kGAIPerfTestCaseKey_EndTime				CFSTR( "endTime" )
#define kGAIPerfTestCaseKey_Results				CFSTR( "results" )
#define kGAIPerfTestCaseKey_FirstStats			CFSTR( "firstStats" )
#define kGAIPerfTestCaseKey_ConnectionStats		CFSTR( "connectionStats" )
#define kGAIPerfTestCaseKey_Stats				CFSTR( "stats" )

// Keys for test case results array entry dictionaries

#define kGAIPerfTestCaseResultKey_Name					CFSTR( "name" )
#define kGAIPerfTestCaseResultKey_ConnectionTime		CFSTR( "connectionTimeUs" )
#define kGAIPerfTestCaseResultKey_FirstTime				CFSTR( "firstTimeUs" )
#define kGAIPerfTestCaseResultKey_Time					CFSTR( "timeUs" )

// Keys for test case stats dictionaries

#define kGAIPerfTestCaseStatsKey_Count		CFSTR( "count" )
#define kGAIPerfTestCaseStatsKey_Min		CFSTR( "min" )
#define kGAIPerfTestCaseStatsKey_Max		CFSTR( "max" )
#define kGAIPerfTestCaseStatsKey_Mean		CFSTR( "mean" )
#define kGAIPerfTestCaseStatsKey_StdDev		CFSTR( "sd" )

typedef struct
{
	double		min;
	double		max;
	double		mean;
	double		stdDev;
	
}	GAIPerfStats;

#define GAIPerfStatsInit( X ) \
	do { (X)->min = DBL_MAX; (X)->max = DBL_MIN; (X)->mean = 0.0; (X)->stdDev = 0.0; } while( 0 )

static void
	GAIPerfResultsHandler(
		const char *				inCaseTitle,
		NanoTime64					inCaseStartTime,
		NanoTime64					inCaseEndTime,
		const GAITestItemResult *	inResultArray,
		size_t						inResultCount,
		void *						inContext )
{
	OSStatus						err;
	GAIPerfContext * const			context	= (GAIPerfContext *) inContext;
	int								namesAreDynamic, namesAreUnique;
	const char *					ptr;
	size_t							startIndex;
	CFMutableArrayRef				results	= NULL;
	GAIPerfStats					stats, firstStats, connStats;
	double							sum, firstSum, connSum;
	size_t							keyValueLen, i;
	uint32_t						count;
	char							keyValue[ 16 ];	// Size must be at least strlen( "name=dynamic" ) + 1 bytes.
	char							startTime[ 32 ];
	char							endTime[ 32 ];
	const GAITestItemResult *		result;
	
	// If this test case resolves the same "d.test." name in each iteration (title contains the "name=dynamic" key-value
	// pair, but not the "unique" key), then don't count the first iteration, whose purpose is to populate the cache with
	// the domain name's CNAME, A, and AAAA records.
	
	namesAreDynamic	= false;
	namesAreUnique	= false;
	ptr = inCaseTitle;
	while( _ParseQuotedEscapedString( ptr, NULL, ",", keyValue, sizeof( keyValue ), &keyValueLen, NULL, &ptr ) )
	{
		if( strnicmpx( keyValue, keyValueLen, "name=dynamic" ) == 0 )
		{
			namesAreDynamic = true;
		}
		else if( strnicmpx( keyValue, keyValueLen, "unique" ) == 0 )
		{
			namesAreUnique = true;
		}
		if( namesAreDynamic && namesAreUnique ) break;
	}
	
	startIndex = ( ( inResultCount > 0 ) && namesAreDynamic && !namesAreUnique ) ? 1 : 0;
	results = CFArrayCreateMutable( NULL, (CFIndex)( inResultCount - startIndex ), &kCFTypeArrayCallBacks );
	require_action( results, exit, err = kNoMemoryErr );
	
	GAIPerfStatsInit( &stats );
	GAIPerfStatsInit( &firstStats );
	GAIPerfStatsInit( &connStats );
	
	sum			= 0.0;
	firstSum	= 0.0;
	connSum		= 0.0;
	count		= 0;
	for( i = startIndex; i < inResultCount; ++i )
	{
		double		value;
		
		result = &inResultArray[ i ];
		
		err = CFPropertyListAppendFormatted( kCFAllocatorDefault, results,
			"{"
				"%kO=%s"	// name
				"%kO=%lli"	// connectionTimeUs
				"%kO=%lli"	// firstTimeUs
				"%kO=%lli"	// timeUs
				"%kO=%lli"	// error
			"}",
			kGAIPerfTestCaseResultKey_Name,				result->name,
			kGAIPerfTestCaseResultKey_ConnectionTime,	(int64_t) result->connectionTimeUs,
			kGAIPerfTestCaseResultKey_FirstTime,		(int64_t) result->firstTimeUs,
			kGAIPerfTestCaseResultKey_Time,				(int64_t) result->timeUs,
			CFSTR( "error" ),							(int64_t) result->error );
		require_noerr( err, exit );
		
		if( !result->error )
		{
			value = (double) result->timeUs;
			if( value < stats.min ) stats.min = value;
			if( value > stats.max ) stats.max = value;
			sum += value;
			
			value = (double) result->firstTimeUs;
			if( value < firstStats.min ) firstStats.min = value;
			if( value > firstStats.max ) firstStats.max = value;
			firstSum += value;
			
			value = (double) result->connectionTimeUs;
			if( value < connStats.min ) connStats.min = value;
			if( value > connStats.max ) connStats.max = value;
			connSum += value;
			
			++count;
		}
		else
		{
			context->testFailed = true;
		}
	}
	
	if( count > 0 )
	{
		stats.mean		= sum      / (double) count;
		firstStats.mean	= firstSum / (double) count;
		connStats.mean	= connSum  / (double) count;
		
		sum			= 0.0;
		firstSum	= 0.0;
		connSum		= 0.0;
		for( i = startIndex; i < inResultCount; ++i )
		{
			double		diff;
			
			result = &inResultArray[ i ];
			if( result->error ) continue;
			
			diff		 = stats.mean - (double) result->timeUs;
			sum			+= ( diff * diff );
			
			diff		 = firstStats.mean - (double) result->firstTimeUs;
			firstSum	+= ( diff * diff );
			
			diff		 = connStats.mean - (double) result->connectionTimeUs;
			connSum		+= ( diff * diff );
		}
		stats.stdDev		= sqrt( sum      / (double) count );
		firstStats.stdDev	= sqrt( firstSum / (double) count );
		connStats.stdDev	= sqrt( connSum  / (double) count );
	}
	
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, context->testCaseResults,
		"{"
			"%kO=%s"
			"%kO=%s"
			"%kO=%s"
			"%kO=%O"
			"%kO="
			"{"
				"%kO=%lli"
				"%kO=%f"
				"%kO=%f"
				"%kO=%f"
				"%kO=%f"
			"}"
			"%kO="
			"{"
				"%kO=%lli"
				"%kO=%f"
				"%kO=%f"
				"%kO=%f"
				"%kO=%f"
			"}"
			"%kO="
			"{"
				"%kO=%lli"
				"%kO=%f"
				"%kO=%f"
				"%kO=%f"
				"%kO=%f"
			"}"
		"}",
		kGAIPerfTestCaseKey_Title,			inCaseTitle,
		kGAIPerfTestCaseKey_StartTime,		_NanoTime64ToTimestamp( inCaseStartTime, startTime, sizeof( startTime ) ),
		kGAIPerfTestCaseKey_EndTime,		_NanoTime64ToTimestamp( inCaseEndTime, endTime, sizeof( endTime ) ),
		kGAIPerfTestCaseKey_Results,		results,
		kGAIPerfTestCaseKey_Stats,
		kGAIPerfTestCaseStatsKey_Count,		(int64_t) count,
		kGAIPerfTestCaseStatsKey_Min,		stats.min,
		kGAIPerfTestCaseStatsKey_Max,		stats.max,
		kGAIPerfTestCaseStatsKey_Mean,		stats.mean,
		kGAIPerfTestCaseStatsKey_StdDev,	stats.stdDev,
		kGAIPerfTestCaseKey_FirstStats,
		kGAIPerfTestCaseStatsKey_Count,		(int64_t) count,
		kGAIPerfTestCaseStatsKey_Min,		firstStats.min,
		kGAIPerfTestCaseStatsKey_Max,		firstStats.max,
		kGAIPerfTestCaseStatsKey_Mean,		firstStats.mean,
		kGAIPerfTestCaseStatsKey_StdDev,	firstStats.stdDev,
		kGAIPerfTestCaseKey_ConnectionStats,
		kGAIPerfTestCaseStatsKey_Count,		(int64_t) count,
		kGAIPerfTestCaseStatsKey_Min,		connStats.min,
		kGAIPerfTestCaseStatsKey_Max,		connStats.max,
		kGAIPerfTestCaseStatsKey_Mean,		connStats.mean,
		kGAIPerfTestCaseStatsKey_StdDev,	connStats.stdDev );
	require_noerr( err, exit );
	
exit:
	CFReleaseNullSafe( results );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	GAIPerfSignalHandler
//===========================================================================================================================

static void	GAIPerfSignalHandler( void *inContext )
{
	GAIPerfContext * const		context = (GAIPerfContext *) inContext;
	
	if( !context->tester ) exit( 1 );
	GAITesterStop( context->tester );
	context->tester = NULL;
}

//===========================================================================================================================
//	GAITesterCreate
//===========================================================================================================================

// A character set of lower-case alphabet characters and digits and a string length of six allows for 36^6 = 2,176,782,336
// possible strings to use in the Tag label.

#define kGAITesterTagStringLen		6

typedef struct GAITestItem		GAITestItem;
struct GAITestItem
{
	GAITestItem *		next;				// Next test item in list.
	char *				name;				// Domain name to resolve.
	uint64_t			connectionTimeUs;	// Time in microseconds that it took to create a DNS-SD connection.
	uint64_t			firstTimeUs;		// Time in microseconds that it took to get the first address result.
	uint64_t			timeUs;				// Time in microseconds that it took to get all expected address results.
	unsigned int		addressCount;		// Address count of the domain name, i.e., the Count label argument.
	OSStatus			error;				// Current status/error.
	unsigned int		timeLimitMs;		// Time limit in milliseconds for the test item's completion.
	Boolean				hasV4;				// True if the domain name has one or more IPv4 addresses.
	Boolean				hasV6;				// True if the domain name has one or more IPv6 addresses.
	Boolean				wantV4;				// True if DNSServiceGetAddrInfo() should be called to get IPv4 addresses.
	Boolean				wantV6;				// True if DNSServiceGetAddrInfo() should be called to get IPv6 addresses.
};

struct GAITestCase
{
	GAITestCase *		next;		// Next test case in list.
	GAITestItem *		itemList;	// List of test items.
	char *				title;		// Title of the test case.
};

struct GAITesterPrivate
{
	CFRuntimeBase					base;				// CF object base.
	dispatch_queue_t				queue;				// Serial work queue.
	DNSServiceRef					connection;			// Reference to the shared DNS-SD connection.
	DNSServiceRef					getAddrInfo;		// Reference to the current DNSServiceGetAddrInfo operation.
	GAITestCase *					caseList;			// List of test cases.
	GAITestCase *					currentCase;		// Pointer to the current test case.
	GAITestItem *					currentItem;		// Pointer to the current test item.
	DNSProtocol						protocol;			// DNS protocol to use, e.g., Do53, DoT, or DoH.
	NanoTime64						caseStartTime;		// Start time of current test case in Unix time as nanoseconds.
	NanoTime64						caseEndTime;		// End time of current test case in Unix time as nanoseconds.
	unsigned int					callDelayMs;		// Amount of time to wait before calling DNSServiceGetAddrInfo().
	Boolean							skipPathEval;		// True if DNSServiceGetAddrInfo() path evaluation is to be skipped.
	Boolean							stopped;			// True if the tester has been stopped.
	Boolean							badUDPMode;			// True if the test DNS server is to run in Bad UDP mode.
	dispatch_source_t				timer;				// Timer for enforcing a test item's time limit.
	pcap_t *						pcap;				// Captures traffic between mDNSResponder and test DNS server.
	pid_t							serverPID;			// PID of the test DNS server.
	int								serverDelayMs;		// Additional time to have the server delay its responses by.
	int								serverDefaultTTL;	// Default TTL for the server's records.
	GAITesterStopHandler_f			stopHandler;		// User's stop handler.
	void *							stopContext;		// User's event handler context.
	GAITesterResultsHandler_f		resultsHandler;		// User's results handler.
	void *							resultsContext;		// User's results handler context.
	int								probeTryCount;		// Number of remaining probe queries.
	uint16_t						serverPortDo53;		// Do53 server port. Only relevant if server protocol is Do53.
	
	// Variables for current test item.
	
	uint64_t						bitmapV4;		// Bitmap of IPv4 results that have yet to be received.
	uint64_t						bitmapV6;		// Bitmap of IPv6 results that have yet to be received.
	uint64_t						startTicks;		// Start ticks of DNSServiceGetAddrInfo().
	uint64_t						connTicks;		// Ticks when the connection was created.
	uint64_t						firstTicks;		// Ticks when the first DNSServiceGetAddrInfo result was received.
	uint64_t						endTicks;		// Ticks when the last DNSServiceGetAddrInfo result was received.
	Boolean							gotFirstResult;	// True if the first result has been received.
};

CF_CLASS_DEFINE( GAITester );

static void		_GAITesterStartNextTest( GAITesterRef inTester );
static OSStatus	_GAITesterCreatePacketCapture( uint16_t inDNSServerPort, pcap_t **outPCap );
static void		_GAITesterProbeTimerEventHandler( void *inContext );
static void		_GAITesterTimeout( void *inContext );
static void DNSSD_API
	_GAITesterProbeCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext );
static void DNSSD_API
	_GAITesterGetAddrInfoCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext );
static void		_GAITesterCompleteCurrentTest( GAITesterRef inTester, OSStatus inError );

#define ForgetPacketCapture( X )		ForgetCustom( X, pcap_close )

static OSStatus
	GAITestItemCreate(
		const char *	inName,
		unsigned int	inAddressCount,
		GAITestAddrType	inHasAddrs,
		GAITestAddrType	inWantAddrs,
		unsigned int	inTimeLimitMs,
		GAITestItem **	outItem );
static OSStatus	GAITestItemDup( const GAITestItem *inItem, GAITestItem **outItem );
static void		GAITestItemFree( GAITestItem *inItem );

static OSStatus
	GAITesterCreate(
		dispatch_queue_t	inQueue,
		DNSProtocol			inProtocol,
		unsigned int		inCallDelayMs,
		int					inServerDelayMs,
		int					inServerDefaultTTL,
		Boolean				inSkipPathEvaluation,
		Boolean				inBadUDPMode,
		GAITesterRef *		outTester )
{
	OSStatus			err;
	GAITesterRef		obj = NULL;
	
	CF_OBJECT_CREATE( GAITester, obj, err, exit );
	
	ReplaceDispatchQueue( &obj->queue, inQueue );
	obj->protocol			= inProtocol;
	obj->callDelayMs		= inCallDelayMs;
	obj->serverPID			= -1;
	obj->serverDelayMs		= inServerDelayMs;
	obj->serverDefaultTTL	= inServerDefaultTTL;
	obj->skipPathEval		= inSkipPathEvaluation;
	obj->badUDPMode			= inBadUDPMode;
	
	*outTester = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	CFReleaseNullSafe( obj );
	return( err );
}

//===========================================================================================================================
//	_GAITesterFinalize
//===========================================================================================================================

static void	_GAITesterFinalize( CFTypeRef inObj )
{
	GAITesterRef const		me = (GAITesterRef) inObj;
	GAITestCase *			testCase;
	
	check( !me->getAddrInfo );
	check( !me->connection );
	check( !me->timer );
	dispatch_forget( &me->queue );
	while( ( testCase = me->caseList ) != NULL )
	{
		me->caseList = testCase->next;
		GAITestCaseFree( testCase );
	}
}

//===========================================================================================================================
//	GAITesterStart
//===========================================================================================================================

static void	_GAITesterStart( void *inContext );
static void	_GAITesterStop( GAITesterRef me, OSStatus inError );

static void	GAITesterStart( GAITesterRef me )
{
	CFRetain( me );
	dispatch_async_f( me->queue, me, _GAITesterStart );
}

static void	_GAITesterStart( void *inContext )
{
	OSStatus				err;
	GAITesterRef const		me = (GAITesterRef) inContext;
	
	const char * const protocolStr = _DNSProtocolToString( me->protocol );
	require_action( protocolStr, exit, err = kValueErr );
	
	int serverPort = -1;
	switch( me->protocol )
	{
		case kDNSProtocol_Do53:
			// Use a specific port for the Do53 server so that it can be used by the PCAP to filter Do53 traffic.
		#if( DNSSDUTIL_TEST_USE_ALTERNATE_SERVER_PORT_FOR_DO53 )
			me->serverPortDo53 = kDNSPort_Do53Alt;
		#else
			me->serverPortDo53 = kDNSPort_Do53;
		#endif
			serverPort = me->serverPortDo53;
			break;
		
		case kDNSProtocol_DoT:
		#if( DNSSDUTIL_TEST_USE_ALTERNATE_SERVER_PORT_FOR_DOT )
			serverPort = 0; // Use any ephemeral port.
		#endif
			break;
		
		case kDNSProtocol_DoH:
			break;
	}
	err = _SpawnCommand( &me->serverPID, NULL, NULL,
		"dnssdutil server --loopback --follow %lld --protocol %s%?s%?d%?s%?d%?s%?s%?d",
		(int64_t) getpid(),
		protocolStr,
		me->serverDefaultTTL >= 0,	" --defaultTTL ",
		me->serverDefaultTTL >= 0,	me->serverDefaultTTL,
		me->serverDelayMs    >= 0,	" --responseDelay ",
		me->serverDelayMs    >= 0,	me->serverDelayMs,
		me->badUDPMode,				" --badUDPMode",
		serverPort >= 0,			" --port ",
		serverPort >= 0,			serverPort );
	require_noerr_quiet( err, exit );
	
	me->probeTryCount = 4;
	err = DispatchTimerCreate( DISPATCH_TIME_NOW, 1 * kNanosecondsPerSecond, 0, me->queue, _GAITesterProbeTimerEventHandler,
		NULL, me, &me->timer );
	require_noerr( err, exit );
	
	dispatch_resume( me->timer );
	
exit:
	if( err ) _GAITesterStop( me, err );
}

//===========================================================================================================================
//	GAITesterStop
//===========================================================================================================================

static void	_GAITesterUserStop( void *inContext );

static void	GAITesterStop( GAITesterRef me )
{
	CFRetain( me );
	dispatch_async_f( me->queue, me, _GAITesterUserStop );
}

static void	_GAITesterUserStop( void *inContext )
{
	GAITesterRef const		me = (GAITesterRef) inContext;
	
	_GAITesterStop( me, kCanceledErr );
	CFRelease( me );
}

static void	_GAITesterStop( GAITesterRef me, OSStatus inError )
{
	OSStatus		err;
	
	ForgetPacketCapture( &me->pcap );
	dispatch_source_forget( &me->timer );
	DNSServiceForget( &me->getAddrInfo );
	DNSServiceForget( &me->connection );
	if( me->serverPID != -1 )
	{
		err = kill( me->serverPID, SIGTERM );
		err = map_global_noerr_errno( err );
		check_noerr( err );
		me->serverPID = -1;
	}
	
	if( !me->stopped )
	{
		me->stopped = true;
		if( me->stopHandler ) me->stopHandler( me->stopContext, inError );
		CFRelease( me );
	}
}

//===========================================================================================================================
//	GAITesterAddTestCase
//===========================================================================================================================

static OSStatus	GAITesterAddTestCase( GAITesterRef me, GAITestCase *inCase )
{
	OSStatus			err;
	GAITestCase **		ptr;
	
	require_action_quiet( inCase->itemList, exit, err = kCountErr );
	
	for( ptr = &me->caseList; *ptr; ptr = &( *ptr )->next ) {}
	*ptr = inCase;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	GAITesterSetStopHandler
//===========================================================================================================================

static void	GAITesterSetStopHandler( GAITesterRef me, GAITesterStopHandler_f inStopHandler, void *inStopContext )
{
	me->stopHandler = inStopHandler;
	me->stopContext = inStopContext;
}

//===========================================================================================================================
//	GAITesterSetResultsHandler
//===========================================================================================================================

static void	GAITesterSetResultsHandler( GAITesterRef me, GAITesterResultsHandler_f inResultsHandler, void *inResultsContext )
{
	me->resultsHandler = inResultsHandler;
	me->resultsContext = inResultsContext;
}

//===========================================================================================================================
//	_GAITesterStartNextTest
//===========================================================================================================================

static void	_GAITesterStartNextTest( GAITesterRef me )
{
	OSStatus				err;
	GAITestItem *			item;
	DNSServiceFlags			flags;
	DNSServiceProtocol		protocols;
	int						done = false;
	
	if( me->currentItem ) me->currentItem = me->currentItem->next;
	
	if( !me->currentItem )
	{
		if( me->currentCase )
		{
			// No more test items means that the current test case has completed.
			
			me->caseEndTime = NanoTimeGetCurrent();
			
			if( me->resultsHandler )
			{
				size_t					resultCount, i;
				GAITestItemResult *		resultArray;
				
				resultCount	= 0;
				for( item = me->currentCase->itemList; item; item = item->next ) ++resultCount;
				check( resultCount > 0 );
				
				resultArray = (GAITestItemResult *) calloc( resultCount, sizeof( *resultArray ) );
				require_action( resultArray, exit, err = kNoMemoryErr );
				
				item = me->currentCase->itemList;
				for( i = 0; i < resultCount; ++i )
				{
					resultArray[ i ].name				= item->name;
					resultArray[ i ].connectionTimeUs	= item->connectionTimeUs;
					resultArray[ i ].firstTimeUs		= item->firstTimeUs;
					resultArray[ i ].timeUs				= item->timeUs;
					resultArray[ i ].error				= item->error;
					item = item->next;
				}
				me->resultsHandler( me->currentCase->title, me->caseStartTime, me->caseEndTime, resultArray, resultCount,
					me->resultsContext );
				ForgetMem( &resultArray );
			}
			
			me->currentCase = me->currentCase->next;
			if( !me->currentCase )
			{
				done = true;
				err = kNoErr;
				goto exit;
			}
		}
		else
		{
			me->currentCase = me->caseList;
		}
		require_action_quiet( me->currentCase->itemList, exit, err = kInternalErr );
		me->currentItem = me->currentCase->itemList;
	}
	
	item = me->currentItem;
	check( ( item->addressCount >= 1 ) && ( item->addressCount <= 64 ) );
	
	if(      !item->wantV4 )			me->bitmapV4 = 0;
	else if( !item->hasV4 )				me->bitmapV4 = 1;
	else if(  item->addressCount < 64 )	me->bitmapV4 = ( UINT64_C( 1 ) << item->addressCount ) - 1;
	else								me->bitmapV4 =  ~UINT64_C( 0 );
	
	if(      !item->wantV6 )			me->bitmapV6 = 0;
	else if( !item->hasV6 )				me->bitmapV6 = 1;
	else if(  item->addressCount < 64 )	me->bitmapV6 = ( UINT64_C( 1 ) << item->addressCount ) - 1;
	else								me->bitmapV6 =  ~UINT64_C( 0 );
	check( ( me->bitmapV4 != 0 ) || ( me->bitmapV6 != 0 ) );
	me->gotFirstResult = false;
	
	// Perform preliminary tasks if this is the start of a new test case.
	
	if( item == me->currentCase->itemList )
	{
		// Flush mDNSResponder's cache.
		
		err = systemf( NULL, "killall -HUP mDNSResponder" );
		require_noerr( err, exit );
		sleep( 1 );
		
		me->caseStartTime	= NanoTimeGetCurrent();
		me->caseEndTime		= kNanoTime_Invalid;
	}
	
	// Start a packet capture for Do53 traffic.
	
	if( me->protocol == kDNSProtocol_Do53 )
	{
		check( !me->pcap );
		err = _GAITesterCreatePacketCapture( me->serverPortDo53, &me->pcap );
		require_noerr( err, exit );
	}
	
	// Start timer for test item's time limit.
	
	check( !me->timer );
	if( item->timeLimitMs > 0 )
	{
		unsigned int		timeLimitMs;
		
		timeLimitMs = item->timeLimitMs;
		if( me->callDelayMs   > 0 ) timeLimitMs += (unsigned int) me->callDelayMs;
		if( me->serverDelayMs > 0 ) timeLimitMs += (unsigned int) me->serverDelayMs;
		
		err = DispatchTimerCreate( dispatch_time_milliseconds( timeLimitMs ), DISPATCH_TIME_FOREVER,
			( (uint64_t) timeLimitMs ) * kNanosecondsPerMillisecond / 10,
			me->queue, _GAITesterTimeout, NULL, me, &me->timer );
		require_noerr( err, exit );
		dispatch_resume( me->timer );
	}
	
	// Call DNSServiceGetAddrInfo().
	
	if( me->callDelayMs > 0 ) usleep( ( (useconds_t) me->callDelayMs ) * kMicrosecondsPerMillisecond );
	
	flags = kDNSServiceFlagsShareConnection | kDNSServiceFlagsReturnIntermediates;
	if( me->skipPathEval ) flags |= kDNSServiceFlagsPathEvaluationDone;
	
	protocols = 0;
	if( item->wantV4 ) protocols |= kDNSServiceProtocol_IPv4;
	if( item->wantV6 ) protocols |= kDNSServiceProtocol_IPv6;
	
	me->startTicks = UpTicks();
	
	check( !me->connection );
	err = DNSServiceCreateConnection( &me->connection );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( me->connection, me->queue );
	require_noerr( err, exit );
	
	me->connTicks = UpTicks();
	
	check( !me->getAddrInfo );
	me->getAddrInfo = me->connection;
	err = DNSServiceGetAddrInfo( &me->getAddrInfo, flags, kDNSServiceInterfaceIndexAny, protocols, item->name,
		_GAITesterGetAddrInfoCallback, me );
	require_noerr( err, exit );
	
exit:
	if( err || done ) _GAITesterStop( me, err );
}

//===========================================================================================================================
//	_GAITesterCreatePacketCapture
//===========================================================================================================================

static OSStatus	_GAITesterCreatePacketCapture( const uint16_t inDNSServerPort, pcap_t ** const outPCap )
{
	OSStatus				err;
	pcap_t *				pcap;
	struct bpf_program		program;
	char					errBuf[ PCAP_ERRBUF_SIZE ];
	
	pcap = pcap_create( "lo0", errBuf );
	require_action_string( pcap, exit, err = kUnknownErr, errBuf );
	
	err = pcap_set_buffer_size( pcap, 512 * kBytesPerKiloByte );
	require_noerr_action( err, exit, err = kUnknownErr );
	
	err = pcap_set_snaplen( pcap, 512 );
	require_noerr_action( err, exit, err = kUnknownErr );
	
	err = pcap_set_immediate_mode( pcap, 0 );
	require_noerr_action_string( err, exit, err = kUnknownErr, pcap_geterr( pcap ) );
	
	err = pcap_activate( pcap );
	require_noerr_action_string( err, exit, err = kUnknownErr, pcap_geterr( pcap ) );
	
	err = pcap_setdirection( pcap, PCAP_D_INOUT );
	require_noerr_action_string( err, exit, err = kUnknownErr, pcap_geterr( pcap ) );
	
	err = pcap_setnonblock( pcap, 1, errBuf );
	require_noerr_action_string( err, exit, err = kUnknownErr, errBuf );
	
	char *programStr = NULL;
	ASPrintF( &programStr, "udp port %u", inDNSServerPort );
	require_action( programStr, exit, err = kNoMemoryErr );
	
	err = pcap_compile( pcap, &program, programStr, 1, PCAP_NETMASK_UNKNOWN );
	ForgetMem( &programStr );
	require_noerr_action_string( err, exit, err = kUnknownErr, pcap_geterr( pcap ) );
	
	err = pcap_setfilter( pcap, &program );
	pcap_freecode( &program );
	require_noerr_action_string( err, exit, err = kUnknownErr, pcap_geterr( pcap ) );
	
	*outPCap = pcap;
	pcap = NULL;
	
exit:
	if( pcap ) pcap_close( pcap );
	return( err );
}

//===========================================================================================================================
//	_GAITesterProbeTimerEventHandler
//===========================================================================================================================

static void	_GAITesterProbeTimerEventHandler( void *inContext )
{
	OSStatus				err;
	GAITesterRef const		me = (GAITesterRef) inContext;
	
	if( me->probeTryCount > 0 )
	{
		DNSServiceFlags		flags;
		char				name[ 64 ];
		char				tag[ kGAITesterTagStringLen + 1 ];
		
		flags = 0;
		if( me->skipPathEval ) flags |= kDNSServiceFlagsPathEvaluationDone;
		
		SNPrintF( name, sizeof( name ), "tag-gaitester-probe-%s.ipv4.d.test.",
			_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
		
		DNSServiceForget( &me->getAddrInfo );
		err = DNSServiceGetAddrInfo( &me->getAddrInfo, flags, kDNSServiceInterfaceIndexAny, kDNSServiceProtocol_IPv4, name,
			_GAITesterProbeCallback, me );
		require_noerr( err, exit );
		
		err = DNSServiceSetDispatchQueue( me->getAddrInfo, me->queue );
		require_noerr( err, exit );
		
		--me->probeTryCount;
	}
	else
	{
		err = kNotPreparedErr;
	}
	
exit:
	if( err ) _GAITesterStop( me, err );
}

//===========================================================================================================================
//	_GAITesterTimeout
//===========================================================================================================================

static void	_GAITesterTimeout( void *inContext )
{
	GAITesterRef const		me = (GAITesterRef) inContext;
	
	_GAITesterCompleteCurrentTest( me, kTimeoutErr );
}

//===========================================================================================================================
//	_GAITesterProbeCallback
//===========================================================================================================================

static void DNSSD_API
	_GAITesterProbeCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext )
{
	GAITesterRef const		me = (GAITesterRef) inContext;
	
	Unused( inSDRef );
	Unused( inInterfaceIndex );
	Unused( inHostname );
	Unused( inSockAddr );
	Unused( inTTL );
	
	if( ( inFlags & kDNSServiceFlagsAdd ) && !inError )
	{
		dispatch_source_forget( &me->timer );
		DNSServiceForget( &me->getAddrInfo );
		
		_GAITesterStartNextTest( me );
	}
}

//===========================================================================================================================
//	_GAITesterGetAddrInfoCallback
//===========================================================================================================================

static void DNSSD_API
	_GAITesterGetAddrInfoCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext )
{
	OSStatus						err;
	GAITesterRef const				me		= (GAITesterRef) inContext;
	GAITestItem * const				item	= me->currentItem;
	const sockaddr_ip * const		sip		= (const sockaddr_ip *) inSockAddr;
	uint64_t						nowTicks;
	uint64_t *						bitmapPtr;
	uint64_t						bitmask;
	int								hasAddr;
	
	Unused( inSDRef );
	Unused( inInterfaceIndex );
	Unused( inHostname );
	Unused( inTTL );
	
	nowTicks = UpTicks();
	
	require_action_quiet( inFlags & kDNSServiceFlagsAdd, exit, err = kFlagErr );
	
	// Check if we were expecting an IP address result of this type.
	
	if( sip->sa.sa_family == AF_INET )
	{
		bitmapPtr	= &me->bitmapV4;
		hasAddr		= item->hasV4;
	}
	else if( sip->sa.sa_family == AF_INET6 )
	{
		bitmapPtr	= &me->bitmapV6;
		hasAddr		= item->hasV6;
	}
	else
	{
		err = kTypeErr;
		goto exit;
	}
	
	bitmask = 0;
	if( hasAddr )
	{
		uint32_t		addrOffset;
		
		require_noerr_action_quiet( inError, exit, err = inError );
		
		if( sip->sa.sa_family == AF_INET )
		{
			const uint32_t		addrV4 = ntohl( sip->v4.sin_addr.s_addr );
			
			if( strcasecmp( item->name, "localhost." ) == 0 )
			{
				if( addrV4 == INADDR_LOOPBACK ) bitmask = 1;
			}
			else
			{
				addrOffset = addrV4 - kDNSServerBaseAddrV4;
				if( ( addrOffset >= 1 ) && ( addrOffset <= item->addressCount ) )
				{
					bitmask = UINT64_C( 1 ) << ( addrOffset - 1 );
				}
			}
		}
		else
		{
			const uint8_t * const		addrV6 = sip->v6.sin6_addr.s6_addr;
			
			if( strcasecmp( item->name, "localhost." ) == 0 )
			{
				if( memcmp( addrV6, in6addr_loopback.s6_addr, 16 ) == 0 ) bitmask = 1;
			}
			else if( memcmp( addrV6, kDNSServerBaseAddrV6, 15 ) == 0 )
			{
				addrOffset = addrV6[ 15 ];
				if( ( addrOffset >= 1 ) && ( addrOffset <= item->addressCount ) )
				{
					bitmask = UINT64_C( 1 ) << ( addrOffset - 1 );
				}
			}
		}
	}
	else
	{
		require_action_quiet( inError == kDNSServiceErr_NoSuchRecord, exit, err = inError ? inError : kUnexpectedErr );
		bitmask = 1;
	}
	require_action_quiet( bitmask != 0, exit, err = kValueErr );
	require_action_quiet( *bitmapPtr & bitmask, exit, err = kDuplicateErr );
	
	*bitmapPtr &= ~bitmask;
	if( !me->gotFirstResult )
	{
		me->firstTicks		= nowTicks;
		me->gotFirstResult	= true;
	}
	err = kNoErr;
	
exit:
	if( err || ( ( me->bitmapV4 == 0 ) && ( me->bitmapV6 == 0 ) ) )
	{
		me->endTicks = nowTicks;
		_GAITesterCompleteCurrentTest( me, err );
	}
}

//===========================================================================================================================
//	_GAITesterCompleteCurrentTest
//===========================================================================================================================

static OSStatus
	_GAITesterGetDNSMessageFromPacket(
		const uint8_t *		inPacketPtr,
		size_t				inPacketLen,
		const uint8_t **	outMsgPtr,
		size_t *			outMsgLen );

static void	_GAITesterCompleteCurrentTest( GAITesterRef me, OSStatus inError )
{
	OSStatus				err;
	GAITestItem * const		item	= me->currentItem;
	struct timeval			timeStamps[ 4 ];
	struct timeval *		tsPtr;
	struct timeval *		tsQA	= NULL;
	struct timeval *		tsQAAAA	= NULL;
	struct timeval *		tsRA	= NULL;
	struct timeval *		tsRAAAA	= NULL;
	struct timeval *		t1;
	struct timeval *		t2;
	int64_t					idleTimeUs;
	uint8_t					name[ kDomainNameLengthMax ];
	
	dispatch_source_forget( &me->timer );
	DNSServiceForget( &me->getAddrInfo );
	DNSServiceForget( &me->connection );
	
	item->error = inError;
	if( item->error )
	{
		err = kNoErr;
		goto exit;
	}
	
	err = DomainNameFromString( name, item->name, NULL );
	require_noerr( err, exit );
	
	tsPtr = &timeStamps[ 0 ];
	if( me->pcap )
	{
		for( ;; )
		{
			int							status;
			struct pcap_pkthdr *		pktHdr;
			const uint8_t *				packet;
			const uint8_t *				msgPtr;
			size_t						msgLen;
			const DNSHeader *			hdr;
			unsigned int				flags;
			const uint8_t *				ptr;
			uint16_t					qtype, qclass;
			uint8_t						qname[ kDomainNameLengthMax ];
			
			status = pcap_next_ex( me->pcap, &pktHdr, &packet );
			if( status != 1 ) break;
			if( _GAITesterGetDNSMessageFromPacket( packet, pktHdr->caplen, &msgPtr, &msgLen ) != kNoErr ) continue;
			if( msgLen < kDNSHeaderLength ) continue;
			
			hdr = (const DNSHeader *) msgPtr;
			flags = DNSHeaderGetFlags( hdr );
			if( DNSFlagsGetOpCode( flags ) != kDNSOpCode_Query ) continue;
			if( DNSHeaderGetQuestionCount( hdr ) < 1 ) continue;
			
			ptr = (const uint8_t *) &hdr[ 1 ];
			if( DNSMessageExtractQuestion( msgPtr, msgLen, ptr, qname, &qtype, &qclass, NULL ) != kNoErr ) continue;
			if( qclass != kDNSServiceClass_IN ) continue;
			if( !DomainNameEqual( qname, name ) ) continue;
			
			if( item->wantV4 && ( qtype == kDNSServiceType_A ) )
			{
				if( flags & kDNSHeaderFlag_Response )
				{
					if( tsQA && !tsRA )
					{
						tsRA  = tsPtr++;
						*tsRA = pktHdr->ts;
					}
				}
				else if( !tsQA )
				{
					tsQA  = tsPtr++;
					*tsQA = pktHdr->ts;
				}
			}
			else if( item->wantV6 && ( qtype == kDNSServiceType_AAAA ) )
			{
				if( flags & kDNSHeaderFlag_Response )
				{
					if( tsQAAAA && !tsRAAAA )
					{
						tsRAAAA  = tsPtr++;
						*tsRAAAA = pktHdr->ts;
					}
				}
				else if( !tsQAAAA )
				{
					tsQAAAA  = tsPtr++;
					*tsQAAAA = pktHdr->ts;
				}
			}
		}
	}
	// t1 is the time when the last query was sent.
	
	if( tsQA && tsQAAAA )	t1 = TIMEVAL_GT( *tsQA, *tsQAAAA ) ? tsQA : tsQAAAA;
	else					t1 = tsQA ? tsQA : tsQAAAA;
	
	// t2 is when the first response was received.
	
	if( tsRA && tsRAAAA )	t2 = TIMEVAL_LT( *tsRA, *tsRAAAA ) ? tsRA : tsRAAAA;
	else					t2 = tsRA ? tsRA : tsRAAAA;
	
	if( t1 && t2 )
	{
		idleTimeUs = TIMEVAL_USEC64_DIFF( *t2, *t1 );
		if( idleTimeUs < 0 ) idleTimeUs = 0;
	}
	else
	{
		idleTimeUs = 0;
	}
	
	item->connectionTimeUs	= UpTicksToMicroseconds( me->connTicks  - me->startTicks );
	item->firstTimeUs		= UpTicksToMicroseconds( me->firstTicks - me->connTicks  ) - (uint64_t) idleTimeUs;
	item->timeUs			= UpTicksToMicroseconds( me->endTicks   - me->connTicks  ) - (uint64_t) idleTimeUs;
	
exit:
	ForgetPacketCapture( &me->pcap );
	if( err )	_GAITesterStop( me, err );
	else		_GAITesterStartNextTest( me );
}

//===========================================================================================================================
//	_GAITesterGetDNSMessageFromPacket
//===========================================================================================================================

#define kHeaderSizeNullLink		 4
#define kHeaderSizeIPv4Min		20
#define kHeaderSizeIPv6			40
#define kHeaderSizeUDP			 8

#define kIPProtocolUDP		0x11

static OSStatus
	_GAITesterGetDNSMessageFromPacket(
		const uint8_t *		inPacketPtr,
		size_t				inPacketLen,
		const uint8_t **	outMsgPtr,
		size_t *			outMsgLen )
{
	OSStatus					err;
	const uint8_t *				nullLink;
	uint32_t					addressFamily;
	const uint8_t *				ip;
	int							ipHeaderLen;
	int							protocol;
	const uint8_t *				msg;
	const uint8_t * const		end = &inPacketPtr[ inPacketLen ];
	
	nullLink = &inPacketPtr[ 0 ];
	require_action_quiet( ( end - nullLink ) >= kHeaderSizeNullLink, exit, err = kUnderrunErr );
	addressFamily = ReadHost32( &nullLink[ 0 ] );
	
	ip = &nullLink[ kHeaderSizeNullLink ];
	if( addressFamily == AF_INET )
	{
		require_action_quiet( ( end - ip ) >= kHeaderSizeIPv4Min, exit, err = kUnderrunErr );
		ipHeaderLen	= ( ip[ 0 ] & 0x0F ) * 4;
		protocol	=   ip[ 9 ];
	}
	else if( addressFamily == AF_INET6 )
	{
		require_action_quiet( ( end - ip ) >= kHeaderSizeIPv6, exit, err = kUnderrunErr );
		ipHeaderLen	= kHeaderSizeIPv6;
		protocol	= ip[ 6 ];
	}
	else
	{
		err = kTypeErr;
		goto exit;
	}
	require_action_quiet( protocol == kIPProtocolUDP, exit, err = kTypeErr );
	require_action_quiet( ( end - ip ) >= ( ipHeaderLen + kHeaderSizeUDP ), exit, err = kUnderrunErr );
	
	msg = &ip[ ipHeaderLen + kHeaderSizeUDP ];
	
	*outMsgPtr = msg;
	*outMsgLen = (size_t)( end - msg );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	GAITestCaseCreate
//===========================================================================================================================

static OSStatus	GAITestCaseCreate( const char *inTitle, GAITestCase **outCase )
{
	OSStatus			err;
	GAITestCase *		obj;
	
	obj = (GAITestCase *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->title = strdup( inTitle );
	require_action( obj->title, exit, err = kNoMemoryErr );
	
	*outCase = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) GAITestCaseFree( obj );
	return( err );
}

//===========================================================================================================================
//	GAITestCaseFree
//===========================================================================================================================

static void	GAITestCaseFree( GAITestCase *inCase )
{
	GAITestItem *		item;
	
	while( ( item = inCase->itemList ) != NULL )
	{
		inCase->itemList = item->next;
		GAITestItemFree( item );
	}
	ForgetMem( &inCase->title );
	free( inCase );
}

//===========================================================================================================================
//	GAITestCaseAddItem
//===========================================================================================================================

static OSStatus
	GAITestCaseAddItem(
		GAITestCase *	inCase,
		unsigned int	inAliasCount,
		unsigned int	inAddressCount,
		int				inTTL,
		GAITestAddrType	inHasAddrs,
		GAITestAddrType	inWantAddrs,
		unsigned int	inTimeLimitMs,
		unsigned int	inItemCount )
{
	OSStatus			err;
	GAITestItem *		item;
	GAITestItem *		item2;
	GAITestItem *		newItemList = NULL;
	GAITestItem **		itemPtr;
	char *				ptr;
	char *				end;
	unsigned int		i;
	char				name[ 64 ];
	char				tag[ kGAITesterTagStringLen + 1 ];
	
	require_action_quiet( inItemCount > 0, exit, err = kNoErr );
	
	// Limit address count to 64 because we use 64-bit bitmaps for keeping track of addresses.
	
	require_action_quiet( ( inAddressCount >= 1 ) && ( inAddressCount <= 64 ), exit, err = kCountErr );
	require_action_quiet( ( inAliasCount >= 0 ) && ( inAliasCount <= INT32_MAX ), exit, err = kCountErr );
	require_action_quiet( GAITestAddrTypeIsValid( inHasAddrs ), exit, err = kValueErr );
	
	ptr = &name[ 0 ];
	end = &name[ countof( name ) ];
	
	// Add Alias label.
	
	if(      inAliasCount == 1 ) SNPrintF_Add( &ptr, end, "alias." );
	else if( inAliasCount >= 2 ) SNPrintF_Add( &ptr, end, "alias-%u.", inAliasCount );
	
	// Add Count label.
	
	SNPrintF_Add( &ptr, end, "count-%u.", inAddressCount );
	
	// Add TTL label.
	
	if( inTTL >= 0 ) SNPrintF_Add( &ptr, end, "ttl-%d.", inTTL );
	
	// Add Tag label.
	
	SNPrintF_Add( &ptr, end, "tag-%s.",
		_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
	
	// Add IPv4 or IPv6 label if necessary.
	
	if(      inHasAddrs == kGAITestAddrType_IPv4 ) SNPrintF_Add( &ptr, end, "ipv4." );
	else if( inHasAddrs == kGAITestAddrType_IPv6 ) SNPrintF_Add( &ptr, end, "ipv6." );
	
	// Finally, add the d.test. labels.
	
	SNPrintF_Add( &ptr, end, "d.test." );
	
	// Create item.
	
	err = GAITestItemCreate( name, inAddressCount, inHasAddrs, inWantAddrs, inTimeLimitMs, &item );
	require_noerr( err, exit );
	
	newItemList	= item;
	itemPtr		= &item->next;
	
	// Create repeat items.
	
	for( i = 1; i < inItemCount; ++i )
	{
		err = GAITestItemDup( item, &item2 );
		require_noerr( err, exit );
		
		*itemPtr	= item2;
		itemPtr		= &item2->next;
	}
	
	// Append to test case's item list.
	
	for( itemPtr = &inCase->itemList; *itemPtr; itemPtr = &( *itemPtr )->next ) {}
	*itemPtr	= newItemList;
	newItemList	= NULL;
	
exit:
	while( ( item = newItemList ) != NULL )
	{
		newItemList = item->next;
		GAITestItemFree( item );
	}
	return( err );
}

//===========================================================================================================================
//	GAITestCaseAddLocalHostItem
//===========================================================================================================================

static OSStatus
	GAITestCaseAddLocalHostItem(
		GAITestCase *	inCase,
		GAITestAddrType	inWantAddrs,
		unsigned int	inTimeLimitMs,
		unsigned int	inItemCount )
{
	OSStatus			err;
	GAITestItem *		item;
	GAITestItem *		item2;
	GAITestItem *		newItemList = NULL;
	GAITestItem **		itemPtr;
	unsigned int		i;
	
	require_action_quiet( inItemCount > 1, exit, err = kNoErr );
	
	err = GAITestItemCreate( "localhost.", 1, kGAITestAddrType_Both, inWantAddrs, inTimeLimitMs, &item );
	require_noerr( err, exit );
	
	newItemList	= item;
	itemPtr		= &item->next;
	
	// Create repeat items.
	
	for( i = 1; i < inItemCount; ++i )
	{
		err = GAITestItemDup( item, &item2 );
		require_noerr( err, exit );
		
		*itemPtr	= item2;
		itemPtr		= &item2->next;
	}
	
	for( itemPtr = &inCase->itemList; *itemPtr; itemPtr = &( *itemPtr )->next ) {}
	*itemPtr	= newItemList;
	newItemList	= NULL;
	
exit:
	while( ( item = newItemList ) != NULL )
	{
		newItemList = item->next;
		GAITestItemFree( item );
	}
	return( err );
}

//===========================================================================================================================
//	GAITestItemCreate
//===========================================================================================================================

static OSStatus
	GAITestItemCreate(
		const char *	inName,
		unsigned int	inAddressCount,
		GAITestAddrType	inHasAddrs,
		GAITestAddrType	inWantAddrs,
		unsigned int	inTimeLimitMs,
		GAITestItem **	outItem )
{
	OSStatus			err;
	GAITestItem *		obj = NULL;
	
	require_action_quiet( inAddressCount >= 1, exit, err = kCountErr );
	require_action_quiet( GAITestAddrTypeIsValid( inHasAddrs ), exit, err = kValueErr );
	require_action_quiet( GAITestAddrTypeIsValid( inWantAddrs ), exit, err = kValueErr );
	
	obj = (GAITestItem *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->name = strdup( inName );
	require_action( obj->name, exit, err = kNoMemoryErr );
	
	obj->addressCount	= inAddressCount;
	obj->hasV4			= ( inHasAddrs  & kGAITestAddrType_IPv4 ) ? true : false;
	obj->hasV6			= ( inHasAddrs  & kGAITestAddrType_IPv6 ) ? true : false;
	obj->wantV4			= ( inWantAddrs & kGAITestAddrType_IPv4 ) ? true : false;
	obj->wantV6			= ( inWantAddrs & kGAITestAddrType_IPv6 ) ? true : false;
	obj->error			= kInProgressErr;
	obj->timeLimitMs	= inTimeLimitMs;
	
	*outItem = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) GAITestItemFree( obj );
	return( err );
}

//===========================================================================================================================
//	GAITestItemDup
//===========================================================================================================================

static OSStatus	GAITestItemDup( const GAITestItem *inItem, GAITestItem **outItem )
{
	OSStatus			err;
	GAITestItem *		obj;
	
	obj = (GAITestItem *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	*obj = *inItem;
	obj->next = NULL;
	if( inItem->name )
	{
		obj->name = strdup( inItem->name );
		require_action( obj->name, exit, err = kNoMemoryErr );
	}
	
	*outItem = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) GAITestItemFree( obj );
	return( err );
}

//===========================================================================================================================
//	GAITestItemFree
//===========================================================================================================================

static void	GAITestItemFree( GAITestItem *inItem )
{
	ForgetMem( &inItem->name );
	free( inItem );
}

//===========================================================================================================================
//	MDNSDiscoveryTestCmd
//===========================================================================================================================

#define kMDNSDiscoveryTestFirstQueryTimeoutSecs		4

typedef struct
{
	DNSServiceRef			query;					// Reference to DNSServiceQueryRecord for replier's "about" TXT record.
	dispatch_source_t		queryTimer;				// Used to time out the "about" TXT record query.
	NanoTime64				startTime;				// When the test started.
	NanoTime64				endTime;				// When the test ended.
	uint64_t				heapByteLimit;			// If > 0, limit for mDNSResponder's heap memory usage in bytes. [1]
	uint64_t				heapByteCount;			// mDNSResponder's heap memory usage in bytes. [1]
	char *					memgraphPath;			// Path to mDNSResponder's initial memgraph file. [1]
	pid_t					replierPID;				// PID of mDNS replier.
	uint32_t				ifIndex;				// Index of interface to run the replier on.
	unsigned int			instanceCount;			// Desired number of service instances.
	unsigned int			txtSize;				// Desired size of each service instance's TXT record data.
	unsigned int			recordCountA;			// Desired number of A records per replier hostname.
	unsigned int			recordCountAAAA;		// Desired number of AAAA records per replier hostname.
	unsigned int			maxDropCount;			// Replier's --maxDropCount option argument.
	double					ucastDropRate;			// Replier's probability of dropping a unicast response.
	double					mcastDropRate;			// Replier's probability of dropping a multicast query or response.
	Boolean					noAdditionals;			// True if the replier is to not include additional records in responses.
	Boolean					useIPv4;				// True if the replier is to use IPv4.
	Boolean					useIPv6;				// True if the replier is to use IPv6.
	Boolean					useNewGAI;				// True if the browser is to use dnssd_getaddrinfo to resolve hostnames.
	Boolean					flushedCache;			// True if mDNSResponder's record cache was flushed before testing.
	char *					replierCommand;			// Command used to run the replier.
	char *					serviceType;			// Type of services to browse for.
	ServiceBrowserRef		browser;				// Service browser.
	unsigned int			browseTimeSecs;			// Amount of time to spend browsing in seconds.
	const char *			outputFilePath;			// File to write test results to. If NULL, then write to stdout.
	OutputFormatType		outputFormat;			// Format of test results output.
	Boolean					outputAppendNewline;	// True if a newline character should be appended to JSON output.
	char					hostname[ 16 + 1 ];		// Base hostname that the replier is to use for instance and host names.
	char					tag[ 4 + 1 ];			// Tag that the replier is to use in its service types.
	
}	MDNSDiscoveryTestContext;

// Notes:
// 1. If a non-zero heap memory limit is specified, then mDNSResponder's memgraph will be captured with the leaks
//    command before the test starts any client requests. Then, right before all of the test's client requests are
//    stopped, the heap command will be used to determine the difference in mDNSResponder's heap memory usage at
//    that point relative to the memgraph. If the difference exceeds the specified limit, then the test will be
//    considered a failure.

static void		_MDNSDiscoveryTestFirstQueryTimeout( void *inContext );
static void DNSSD_API
	_MDNSDiscoveryTestAboutQueryCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext );
static void
	_MDNSDiscoveryTestServiceBrowserCallback(
		ServiceBrowserResults *	inResults,
		OSStatus				inError,
		void *					inContext );
static Boolean	_MDNSDiscoveryTestTXTRecordIsValid( const uint8_t *inRecordName, const uint8_t *inTXTPtr, size_t inTXTLen );

static void	MDNSDiscoveryTestCmd( void )
{
	OSStatus						err;
	MDNSDiscoveryTestContext *		context;
	char							queryName[ sizeof_field( MDNSDiscoveryTestContext, hostname ) + 15 ];
	
	context = (MDNSDiscoveryTestContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	err = CheckIntegerArgument( gMDNSDiscoveryTest_InstanceCount, "instance count", 1, UINT16_MAX );
	require_noerr_quiet( err, exit );
	
	err = CheckIntegerArgument( gMDNSDiscoveryTest_TXTSize, "TXT size", 1, UINT16_MAX );
	require_noerr_quiet( err, exit );
	
	err = CheckIntegerArgument( gMDNSDiscoveryTest_BrowseTimeSecs, "browse time (seconds)", 1, INT_MAX );
	require_noerr_quiet( err, exit );
	
	err = CheckIntegerArgument( gMDNSDiscoveryTest_RecordCountA, "A record count", 0, 64 );
	require_noerr_quiet( err, exit );
	
	err = CheckIntegerArgument( gMDNSDiscoveryTest_RecordCountAAAA, "AAAA record count", 0, 64 );
	require_noerr_quiet( err, exit );
	
	err = CheckDoubleArgument( gMDNSDiscoveryTest_UnicastDropRate, "unicast drop rate", 0.0, 1.0 );
	require_noerr_quiet( err, exit );
	
	err = CheckDoubleArgument( gMDNSDiscoveryTest_MulticastDropRate, "multicast drop rate", 0.0, 1.0 );
	require_noerr_quiet( err, exit );
	
	err = CheckIntegerArgument( gMDNSDiscoveryTest_MaxDropCount, "drop count", 0, 255 );
	require_noerr_quiet( err, exit );
	
	context->replierPID				= -1;
	context->instanceCount			= (unsigned int) gMDNSDiscoveryTest_InstanceCount;
	context->txtSize				= (unsigned int) gMDNSDiscoveryTest_TXTSize;
	context->browseTimeSecs			= (unsigned int) gMDNSDiscoveryTest_BrowseTimeSecs;
	context->recordCountA			= (unsigned int) gMDNSDiscoveryTest_RecordCountA;
	context->recordCountAAAA		= (unsigned int) gMDNSDiscoveryTest_RecordCountAAAA;
	context->ucastDropRate			= gMDNSDiscoveryTest_UnicastDropRate;
	context->mcastDropRate			= gMDNSDiscoveryTest_MulticastDropRate;
	context->maxDropCount			= (unsigned int) gMDNSDiscoveryTest_MaxDropCount;
	context->outputFilePath			= gMDNSDiscoveryTest_OutputFilePath;
	context->outputAppendNewline	= gMDNSDiscoveryTest_OutputAppendNewline	? true : false;
	context->noAdditionals			= gMDNSDiscoveryTest_NoAdditionals			? true : false;
	context->useIPv4				= ( gMDNSDiscoveryTest_UseIPv4 || !gMDNSDiscoveryTest_UseIPv6 ) ? true : false;
	context->useIPv6				= ( gMDNSDiscoveryTest_UseIPv6 || !gMDNSDiscoveryTest_UseIPv4 ) ? true : false;
	context->useNewGAI				= gMDNSDiscoveryTest_UseNewGAI				? true : false;
	
	if( gMDNSDiscoveryTest_Interface )
	{
		err = InterfaceIndexFromArgString( gMDNSDiscoveryTest_Interface, &context->ifIndex );
		require_noerr_quiet( err, exit );
	}
	else
	{
		err = _MDNSInterfaceGetAny( kMDNSInterfaceSubset_All, NULL, &context->ifIndex );
		require_noerr_quiet( err, exit );
	}
	
	err = OutputFormatFromArgString( gMDNSDiscoveryTest_OutputFormat, &context->outputFormat );
	require_noerr_quiet( err, exit );
	
	if( gMDNSDiscoveryTest_HeapBytesLimit )
	{
		context->heapByteLimit = _StringToUInt64( gMDNSDiscoveryTest_HeapBytesLimit, &err );
		if( err )
		{
			FPrintF( stderr, "error: Invalid heap bytes limit '%s'. Valid range is [0, %llu].\n",
				gMDNSDiscoveryTest_HeapBytesLimit, UINT64_MAX );
		}
		if( context->heapByteLimit > 0 )
		{
			err = CheckRootUser();
			require_noerr_quiet( err, exit );
		}
	}
	if( gMDNSDiscoveryTest_FlushCache )
	{
		err = CheckRootUser();
		require_noerr_quiet( err, exit );
		
		err = systemf( NULL, "killall -HUP mDNSResponder" );
		require_noerr( err, exit );
		sleep( 1 );
		context->flushedCache = true;
	}
	
	_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( context->hostname ) - 1,
		context->hostname );
	_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( context->tag ) - 1, context->tag );
	
	ASPrintF( &context->serviceType, "_t-%s-%u-%u._tcp", context->tag, context->txtSize, context->instanceCount );
	require_action( context->serviceType, exit, err = kUnknownErr );
	
	ASPrintF( &context->replierCommand,
		"dnssdutil mdnsreplier --follow %lld --interface %u --hostname %s --tag %s --maxInstanceCount %u "
		"--countA %u --countAAAA %u --udrop %.1f --mdrop %.1f --maxDropCount %u %?s%?s%?s",
		(int64_t) getpid(),
		context->ifIndex,
		context->hostname,
		context->tag,
		context->instanceCount,
		context->recordCountA,
		context->recordCountAAAA,
		context->ucastDropRate,
		context->mcastDropRate,
		context->maxDropCount,
		context->noAdditionals,	" --noAdditionals",
		context->useIPv4,		" --ipv4",
		context->useIPv6,		" --ipv6" );
	require_action_quiet( context->replierCommand, exit, err = kUnknownErr );
	
	err = _SpawnCommand( &context->replierPID, NULL, NULL, "%s", context->replierCommand );
	require_noerr_quiet( err, exit );
	
	if( context->heapByteLimit > 0 )
	{
		uuid_t uuid;
		uuid_generate_random( uuid );
		uuid_string_t uuidStr;
		uuid_unparse_upper( uuid, uuidStr );
		ASPrintF( &context->memgraphPath, "/tmp/mDNSResponder-%s.memgraph", uuidStr );
		require_action( context->memgraphPath, exit, err = kNoMemoryErr );
		
		err = systemf( NULL, "/usr/bin/leaks mDNSResponder --outputGraph=%s 2>&1", context->memgraphPath );
		require_noerr( err, exit );
		
		atexit_b(
		^ {
			remove( context->memgraphPath );
		} );
	}
	
	// Query for the replier's about TXT record. A response means that it's fully up and running.
	
	SNPrintF( queryName, sizeof( queryName ), "about.%s.local.", context->hostname );
	err = DNSServiceQueryRecord( &context->query, kDNSServiceFlagsForceMulticast, context->ifIndex, queryName,
		kDNSServiceType_TXT, kDNSServiceClass_IN, _MDNSDiscoveryTestAboutQueryCallback, context );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( context->query, dispatch_get_main_queue() );
	require_noerr( err, exit );
	
	err = DispatchTimerCreate( dispatch_time_seconds( kMDNSDiscoveryTestFirstQueryTimeoutSecs ),
		DISPATCH_TIME_FOREVER, UINT64_C_safe( kMDNSDiscoveryTestFirstQueryTimeoutSecs ) * kNanosecondsPerSecond / 10, NULL,
		_MDNSDiscoveryTestFirstQueryTimeout, NULL, context, &context->queryTimer );
	require_noerr( err, exit );
	dispatch_resume( context->queryTimer );
	
	context->startTime = NanoTimeGetCurrent();
	dispatch_main();
	
exit:
	FPrintF( stderr, "error: %#m\n", err );
	exit( 1 );
}

//===========================================================================================================================
//	_MDNSDiscoveryTestFirstQueryTimeout
//===========================================================================================================================

static void	_MDNSDiscoveryTestFirstQueryTimeout( void *inContext )
{
	MDNSDiscoveryTestContext * const		context = (MDNSDiscoveryTestContext *) inContext;
	
	dispatch_source_forget( &context->queryTimer );
	
	FPrintF( stderr, "error: Query for mdnsreplier's \"about\" TXT record timed out.\n" );
	exit( 1 );
}

//===========================================================================================================================
//	_MDNSDiscoveryTestAboutQueryCallback
//===========================================================================================================================

static void DNSSD_API
	_MDNSDiscoveryTestAboutQueryCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext )
{
	OSStatus								err;
	MDNSDiscoveryTestContext * const		context = (MDNSDiscoveryTestContext *) inContext;
	
	Unused( inSDRef );
	Unused( inInterfaceIndex );
	Unused( inFullName );
	Unused( inType );
	Unused( inClass );
	Unused( inRDataLen );
	Unused( inRDataPtr );
	Unused( inTTL );
	
	err = inError;
	require_noerr( err, exit );
	require_quiet( inFlags & kDNSServiceFlagsAdd, exit );
	
	DNSServiceForget( &context->query );
	dispatch_source_forget( &context->queryTimer );
	
	err = ServiceBrowserCreate( dispatch_get_main_queue(), 0, "local.", context->browseTimeSecs, false, &context->browser );
	require_noerr( err, exit );
	
	err = ServiceBrowserAddServiceType( context->browser, context->serviceType );
	require_noerr( err, exit );
	
	ServiceBrowserSetUseNewGAI( context->browser, context->useNewGAI );
	ServiceBrowserSetCallback( context->browser, _MDNSDiscoveryTestServiceBrowserCallback, context );
	ServiceBrowserStart( context->browser );
	
exit:
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	_MDNSDiscoveryTestRunHeap
//===========================================================================================================================

static OSStatus	_MDNSDiscoveryTestRunHeap( MDNSDiscoveryTestContext * const inContext )
{
	OSStatus		err;
	char *			line = NULL;
	
	require_action_quiet( inContext->memgraphPath, exit, err = kNoErr );
	
	char *heapCmd = NULL;
	ASPrintF( &heapCmd, "/usr/bin/heap --showSizes --diffFrom=%s mDNSResponder", inContext->memgraphPath );
	require_action( heapCmd, exit, err = kNoMemoryErr );
	
	FILE *heapStream = popen( heapCmd, "r" );
	ForgetMem( &heapCmd );
	err = map_global_value_errno( heapStream, heapStream );
	require_noerr( err, exit );
	
	Boolean heapByteCountObtained = false;
	for( ;; )
	{
		err = fcopyline( heapStream, &line, NULL );
		if( err == kEndingErr )
		{
			err = kNoErr;
			break;
		}
		require_noerr( err, exit );
		
		if( !heapByteCountObtained )
		{
			unsigned long long heapByteCount = 0;
			int n = 0;
			const int matchCount = sscanf( line, "All zones: %*u nodes (%llu bytes)%n", &heapByteCount, &n );
			if( ( matchCount == 1 ) && ( n > 0 ) )
			{
				inContext->heapByteCount = heapByteCount;
				heapByteCountObtained = true;
			}
		}
		FPrintF( stderr, "%s\n", line );
		ForgetMem( &line );
	}
	if( !heapByteCountObtained )
	{
		FPrintF( stderr, "error: Did not match line for total heap byte count.\n" );
	}
	err = pclose( heapStream );
	heapStream = NULL;
	if( err == -1 )
	{
		const int errnoVal = errno_compat();
		if( errnoVal )
		{
			err = errnoVal;
		}
	}
	else if( err )
	{
		err = WEXITSTATUS( err );
	}
	require_noerr( err, exit );
	require_action( heapByteCountObtained, exit, err = kFormatErr );
	
exit:
	ForgetMem( &line );
	return( err );
}

//===========================================================================================================================
//	_MDNSDiscoveryTestServiceBrowserCallback
//===========================================================================================================================

#define kMDNSDiscoveryTestResultsKey_ReplierInfo					CFSTR( "replierInfo" )
#define kMDNSDiscoveryTestResultsKey_StartTime						CFSTR( "startTime" )
#define kMDNSDiscoveryTestResultsKey_EndTime						CFSTR( "endTime" )
#define kMDNSDiscoveryTestResultsKey_BrowseTimeSecs					CFSTR( "browseTimeSecs" )
#define kMDNSDiscoveryTestResultsKey_ServiceType					CFSTR( "serviceType" )
#define kMDNSDiscoveryTestResultsKey_FlushedCache					CFSTR( "flushedCache" )
#define kMDNSDiscoveryTestResultsKey_UsedNewGAI						CFSTR( "usedNewGAI" )
#define kMDNSDiscoveryTestResultsKey_UnexpectedInstances			CFSTR( "unexpectedInstances" )
#define kMDNSDiscoveryTestResultsKey_MissingInstances				CFSTR( "missingInstances" )
#define kMDNSDiscoveryTestResultsKey_IncorrectInstances				CFSTR( "incorrectInstances" )
#define kMDNSDiscoveryTestResultsKey_HeapByteLimit					CFSTR( "heapByteLimit" )
#define kMDNSDiscoveryTestResultsKey_HeapByteCount					CFSTR( "heapByteCount" )
#define kMDNSDiscoveryTestResultsKey_Success						CFSTR( "success" )
#define kMDNSDiscoveryTestResultsKey_TotalResolveTime				CFSTR( "totalResolveTimeUs" )

#define kMDNSDiscoveryTestReplierInfoKey_Command					CFSTR( "command" )
#define kMDNSDiscoveryTestReplierInfoKey_InstanceCount				CFSTR( "instanceCount" )
#define kMDNSDiscoveryTestReplierInfoKey_TXTSize					CFSTR( "txtSize" )
#define kMDNSDiscoveryTestReplierInfoKey_RecordCountA				CFSTR( "recordCountA" )
#define kMDNSDiscoveryTestReplierInfoKey_RecordCountAAAA			CFSTR( "recordCountAAAA" )
#define kMDNSDiscoveryTestReplierInfoKey_Hostname					CFSTR( "hostname" )
#define kMDNSDiscoveryTestReplierInfoKey_NoAdditionals				CFSTR( "noAdditionals" )
#define kMDNSDiscoveryTestReplierInfoKey_UnicastDropRate			CFSTR( "ucastDropRate" )
#define kMDNSDiscoveryTestReplierInfoKey_MulticastDropRate			CFSTR( "mcastDropRate" )
#define kMDNSDiscoveryTestReplierInfoKey_MaxDropCount				CFSTR( "maxDropCount" )

#define kMDNSDiscoveryTestUnexpectedInstanceKey_Name				CFSTR( "name" )
#define kMDNSDiscoveryTestUnexpectedInstanceKey_InterfaceIndex		CFSTR( "interfaceIndex" )

#define kMDNSDiscoveryTestIncorrectInstanceKey_Name					CFSTR( "name" )
#define kMDNSDiscoveryTestIncorrectInstanceKey_DidResolve			CFSTR( "didResolve" )
#define kMDNSDiscoveryTestIncorrectInstanceKey_BadHostname			CFSTR( "badHostname" )
#define kMDNSDiscoveryTestIncorrectInstanceKey_BadPort				CFSTR( "badPort" )
#define kMDNSDiscoveryTestIncorrectInstanceKey_BadTXT				CFSTR( "badTXT" )
#define kMDNSDiscoveryTestIncorrectInstanceKey_UnexpectedAddrs		CFSTR( "unexpectedAddrs" )
#define kMDNSDiscoveryTestIncorrectInstanceKey_MissingAddrs			CFSTR( "missingAddrs" )

static void	_MDNSDiscoveryTestServiceBrowserCallback( ServiceBrowserResults *inResults, OSStatus inError, void *inContext )
{
	OSStatus								err;
	MDNSDiscoveryTestContext * const		context			= (MDNSDiscoveryTestContext *) inContext;
	const SBRDomain *						domain;
	const SBRServiceType *					type;
	const SBRServiceInstance *				instance;
	const SBRServiceInstance **				instanceArray	= NULL;
	const SBRIPAddress *					ipaddr;
	size_t									hostnameLen;
	const char *							ptr;
	const char *							end;
	unsigned int							i;
	uint32_t								u32;
	CFMutableArrayRef						unexpectedInstances;
	CFMutableArrayRef						missingInstances;
	CFMutableArrayRef						incorrectInstances;
	CFMutableDictionaryRef					plist			= NULL;
	CFMutableDictionaryRef					badDict			= NULL;
	CFMutableArrayRef						unexpectedAddrs	= NULL;
	CFMutableArrayRef						missingAddrs	= NULL;
	uint64_t								maxResolveTimeUs;
	int										success			= false;
	char									startTime[ 32 ];
	char									endTime[ 32 ];
	
	context->endTime = NanoTimeGetCurrent();
	
	err = inError;
	require_noerr( err, exit );
	
	err = _MDNSDiscoveryTestRunHeap( context );
	require_noerr( err, exit );
	
	_NanoTime64ToTimestamp( context->startTime, startTime, sizeof( startTime ) );
	_NanoTime64ToTimestamp( context->endTime, endTime, sizeof( endTime ) );
	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &plist,
		"{"
			"%kO="
			"{"
				"%kO=%s"	// replierCommand
				"%kO=%lli"	// txtSize
				"%kO=%lli"	// instanceCount
				"%kO=%lli"	// recordCountA
				"%kO=%lli"	// recordCountAAAA
				"%kO=%s"	// hostname
				"%kO=%b"	// noAdditionals
				"%kO=%f"	// ucastDropRate
				"%kO=%f"	// mcastDropRate
				"%kO=%i"	// maxDropCount
			"}"
			"%kO=%s"	// startTime
			"%kO=%s"	// endTime
			"%kO=%lli"	// browseTimeSecs
			"%kO=%s"	// serviceType
			"%kO=%b"	// flushedCache
		#if( MDNSRESPONDER_PROJECT )
			"%kO=%b"	// usedNewGAI
		#endif
			"%kO=[%@]"	// unexpectedInstances
			"%kO=[%@]"	// missingInstances
			"%kO=[%@]"	// incorrectInstances
			"%kO=%lli"	// heapByteLimit
		"}",
		kMDNSDiscoveryTestResultsKey_ReplierInfo,
		kMDNSDiscoveryTestReplierInfoKey_Command,			context->replierCommand,
		kMDNSDiscoveryTestReplierInfoKey_InstanceCount,		(int64_t) context->instanceCount,
		kMDNSDiscoveryTestReplierInfoKey_TXTSize,			(int64_t) context->txtSize,
		kMDNSDiscoveryTestReplierInfoKey_RecordCountA,		(int64_t) context->recordCountA,
		kMDNSDiscoveryTestReplierInfoKey_RecordCountAAAA,	(int64_t) context->recordCountAAAA,
		kMDNSDiscoveryTestReplierInfoKey_Hostname,			context->hostname,
		kMDNSDiscoveryTestReplierInfoKey_NoAdditionals,		context->noAdditionals,
		kMDNSDiscoveryTestReplierInfoKey_UnicastDropRate,	context->ucastDropRate,
		kMDNSDiscoveryTestReplierInfoKey_MulticastDropRate,	context->mcastDropRate,
		kMDNSDiscoveryTestReplierInfoKey_MaxDropCount,		context->maxDropCount,
		kMDNSDiscoveryTestResultsKey_StartTime,				startTime,
		kMDNSDiscoveryTestResultsKey_EndTime,				endTime,
		kMDNSDiscoveryTestResultsKey_BrowseTimeSecs,		(int64_t) context->browseTimeSecs,
		kMDNSDiscoveryTestResultsKey_ServiceType,			context->serviceType,
		kMDNSDiscoveryTestResultsKey_FlushedCache,			context->flushedCache,
	#if( MDNSRESPONDER_PROJECT )
		kMDNSDiscoveryTestResultsKey_UsedNewGAI,			context->useNewGAI,
	#endif
		kMDNSDiscoveryTestResultsKey_UnexpectedInstances,	&unexpectedInstances,
		kMDNSDiscoveryTestResultsKey_MissingInstances,		&missingInstances,
		kMDNSDiscoveryTestResultsKey_IncorrectInstances,	&incorrectInstances,
		kMDNSDiscoveryTestResultsKey_HeapByteLimit,			(int64_t) context->heapByteLimit );
	require_noerr( err, exit );
	
	if( context->heapByteLimit > 0 )
	{
		err = CFPropertyListAppendFormatted( kCFAllocatorDefault, plist, "%kO=%lli",
			kMDNSDiscoveryTestResultsKey_HeapByteCount, context->heapByteCount );
		require_noerr( err, exit );
	}
	for( domain = inResults->domainList; domain && ( strcasecmp( domain->name, "local." ) != 0 ); domain = domain->next ) {}
	require_action( domain, exit, err = kInternalErr );
	
	for( type = domain->typeList; type && ( strcasecmp( type->name, context->serviceType ) != 0 ); type = type->next ) {}
	require_action( type, exit, err = kInternalErr );
	
	instanceArray = (const SBRServiceInstance **) calloc( context->instanceCount, sizeof( *instanceArray ) );
	require_action( instanceArray, exit, err = kNoMemoryErr );
	
	hostnameLen = strlen( context->hostname );
	for( instance = type->instanceList; instance; instance = instance->next )
	{
		unsigned int		instanceNumber = 0;
		
		if( strcmp_prefix( instance->name, context->hostname ) == 0 )
		{
			ptr = &instance->name[ hostnameLen ];
			if( ( ptr[ 0 ] == ' ' ) && ( ptr[ 1 ] == '(' ) )
			{
				ptr += 2;
				for( end = ptr; isdigit_safe( *end ); ++end ) {}
				if( DecimalTextToUInt32( ptr, end, &u32, &ptr ) == kNoErr )
				{
					if( ( u32 >= 2 ) && ( u32 <= context->instanceCount ) && ( ptr[ 0 ] == ')' ) && ( ptr[ 1 ] == '\0' ) )
					{
						instanceNumber = u32;
					}
				}
			}
			else if( *ptr == '\0' )
			{
				instanceNumber = 1;
			}
		}
		if( ( instanceNumber != 0 ) && ( instance->ifIndex == context->ifIndex ) )
		{
			check( !instanceArray[ instanceNumber - 1 ] );
			instanceArray[ instanceNumber - 1 ] = instance;
		}
		else
		{
			err = CFPropertyListAppendFormatted( kCFAllocatorDefault, unexpectedInstances,
				"{"
					"%kO=%s"
					"%kO=%lli"
				"}",
				kMDNSDiscoveryTestUnexpectedInstanceKey_Name,			instance->name,
				kMDNSDiscoveryTestUnexpectedInstanceKey_InterfaceIndex,	(int64_t) instance->ifIndex );
			require_noerr( err, exit );
		}
	}
	
	maxResolveTimeUs = 0;
	for( i = 1; i <= context->instanceCount; ++i )
	{
		int		isHostnameValid;
		int		isTXTValid;
		
		instance = instanceArray[ i - 1 ];
		if( !instance )
		{
			if( i == 1 )
			{
				err = CFPropertyListAppendFormatted( kCFAllocatorDefault, missingInstances, "%s", context->hostname );
				require_noerr( err, exit );
			}
			else
			{
				char *		instanceName = NULL;
				
				ASPrintF( &instanceName, "%s (%u)", context->hostname, i );
				require_action( instanceName, exit, err = kUnknownErr );
				
				err = CFPropertyListAppendFormatted( kCFAllocatorDefault, missingInstances, "%s", instanceName );
				free( instanceName );
				require_noerr( err, exit );
			}
			continue;
		}
		
		if( !instance->hostname )
		{
			err = CFPropertyListAppendFormatted( kCFAllocatorDefault, incorrectInstances,
				"{"
					"%kO=%s"
					"%kO=%b"
				"}",
				kMDNSDiscoveryTestIncorrectInstanceKey_Name,		instance->name,
				kMDNSDiscoveryTestIncorrectInstanceKey_DidResolve,	false );
			require_noerr( err, exit );
			continue;
		}
		
		badDict = CFDictionaryCreateMutable( NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks );
		require_action( badDict, exit, err = kNoMemoryErr );
		
		isHostnameValid = false;
		if( strcmp_prefix( instance->hostname, context->hostname ) == 0 )
		{
			ptr = &instance->hostname[ hostnameLen ];
			if( i == 1 )
			{
				if( strcmp( ptr, ".local." ) == 0 ) isHostnameValid = true;
			}
			else if( *ptr == '-' )
			{
				++ptr;
				for( end = ptr; isdigit_safe( *end ); ++end ) {}
				if( DecimalTextToUInt32( ptr, end, &u32, &ptr ) == kNoErr )
				{
					if( ( u32 == i ) && ( strcmp( ptr, ".local." ) == 0 ) ) isHostnameValid = true;
				}
			}
		}
		if( !isHostnameValid )
		{
			err = CFDictionarySetCString( badDict, kMDNSDiscoveryTestIncorrectInstanceKey_BadHostname, instance->hostname,
				kSizeCString );
			require_noerr( err, exit );
		}
		
		if( instance->port != (uint16_t)( kMDNSReplierPortBase + context->txtSize ) )
		{
			err = CFDictionarySetInt64( badDict, kMDNSDiscoveryTestIncorrectInstanceKey_BadPort, instance->port );
			require_noerr( err, exit );
		}
		
		isTXTValid = false;
		if( instance->txtLen == context->txtSize )
		{
			uint8_t		name[ kDomainNameLengthMax ];
			
			err = DomainNameFromString( name, instance->name, NULL );
			require_noerr( err, exit );
			
			err = DomainNameAppendString( name, type->name, NULL );
			require_noerr( err, exit );
			
			err = DomainNameAppendString( name, "local", NULL );
			require_noerr( err, exit );
			
			if( _MDNSDiscoveryTestTXTRecordIsValid( name, instance->txtPtr, instance->txtLen ) ) isTXTValid = true;
		}
		if( !isTXTValid )
		{
			char *		hexStr = NULL;
			
			ASPrintF( &hexStr, "%.4H", instance->txtPtr, (int) instance->txtLen, (int) instance->txtLen );
			require_action( hexStr, exit, err = kUnknownErr );
			
			err = CFDictionarySetCString( badDict, kMDNSDiscoveryTestIncorrectInstanceKey_BadTXT, hexStr, kSizeCString );
			free( hexStr );
			require_noerr( err, exit );
		}
		
		if( isHostnameValid )
		{
			uint64_t			addrV4Bitmap, addrV6Bitmap, bitmask, resolveTimeUs;
			unsigned int		j;
			uint8_t				addrV4[ 4 ];
			uint8_t				addrV6[ 16 ];
			uint8_t				addrV6LL[ 16 ];
			
			if( context->recordCountA < 64 )	addrV4Bitmap = ( UINT64_C( 1 ) << context->recordCountA ) - 1;
			else								addrV4Bitmap =  ~UINT64_C( 0 );
			
			if( context->recordCountAAAA < 64 ) addrV6Bitmap = ( UINT64_C( 1 ) << context->recordCountAAAA ) - 1;
			else								addrV6Bitmap =  ~UINT64_C( 0 );
			
			addrV4[ 0 ] = 0;
			WriteBig16Typed( &addrV4[ 1 ], (uint16_t) i );
			addrV4[ 3 ] = 0;
			
			memcpy( addrV6, kMDNSReplierBaseAddrV6, 16 );
			WriteBig16Typed( &addrV6[ 12 ], (uint16_t) i );
			
			memcpy( addrV6LL, kMDNSReplierLinkLocalBaseAddrV6, 16 );
			WriteBig16Typed( &addrV6LL[ 12 ], (uint16_t) i );
			
			unexpectedAddrs = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
			require_action( unexpectedAddrs, exit, err = kNoMemoryErr );
			
			resolveTimeUs = 0;
			for( ipaddr = instance->ipaddrList; ipaddr; ipaddr = ipaddr->next )
			{
				const uint8_t *		addrPtr;
				unsigned int		lsb;
				int					isAddrValid = false;
				
				if( ipaddr->sip.sa.sa_family == AF_INET )
				{
					addrPtr	= (const uint8_t *) &ipaddr->sip.v4.sin_addr.s_addr;
					lsb		= addrPtr[ 3 ];
					if( ( memcmp( addrPtr, addrV4, 3 ) == 0 ) && ( lsb >= 1 ) && ( lsb <= context->recordCountA ) )
					{
						bitmask = UINT64_C( 1 ) << ( lsb - 1 );
						addrV4Bitmap &= ~bitmask;
						isAddrValid = true;
					}
				}
				else if( ipaddr->sip.sa.sa_family == AF_INET6 )
				{
					const struct sockaddr_in6 * const		sin6 = &ipaddr->sip.v6;
					
					addrPtr	= sin6->sin6_addr.s6_addr;
					lsb		= addrPtr[ 15 ];
					if( ( lsb >= 1 ) && ( lsb <= context->recordCountAAAA ) )
					{
						const uint32_t		scopeID = ( lsb == 1 ) ? context->ifIndex : 0;
						
						if( ( memcmp( addrPtr, ( lsb == 1 ) ? addrV6LL : addrV6, 15 ) == 0 ) &&
							( sin6->sin6_scope_id == scopeID ) )
						{
							bitmask = UINT64_C( 1 ) << ( lsb - 1 );
							addrV6Bitmap &= ~bitmask;
							isAddrValid = true;
						}
					}
				}
				if( isAddrValid )
				{
					if( ipaddr->resolveTimeUs > resolveTimeUs ) resolveTimeUs = ipaddr->resolveTimeUs;
				}
				else
				{
					err = CFPropertyListAppendFormatted( kCFAllocatorDefault, unexpectedAddrs, "%##a", &ipaddr->sip );
					require_noerr( err, exit );
				}
			}
			
			resolveTimeUs += ( instance->discoverTimeUs + instance->resolveTimeUs );
			if( resolveTimeUs > maxResolveTimeUs ) maxResolveTimeUs = resolveTimeUs;
			
			if( CFArrayGetCount( unexpectedAddrs ) > 0 )
			{
				CFDictionarySetValue( badDict, kMDNSDiscoveryTestIncorrectInstanceKey_UnexpectedAddrs, unexpectedAddrs );
			}
			ForgetCF( &unexpectedAddrs );
			
			missingAddrs = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
			require_action( missingAddrs, exit, err = kNoMemoryErr );
			
			for( j = 1; addrV4Bitmap != 0; ++j )
			{
				bitmask = UINT64_C( 1 ) << ( j - 1 );
				if( addrV4Bitmap & bitmask )
				{
					addrV4Bitmap &= ~bitmask;
					addrV4[ 3 ] = (uint8_t) j;
					err = CFPropertyListAppendFormatted( kCFAllocatorDefault, missingAddrs, "%.4a", addrV4 );
					require_noerr( err, exit );
				}
			}
			for( j = 1; addrV6Bitmap != 0; ++j )
			{
				bitmask = UINT64_C( 1 ) << ( j - 1 );
				if( addrV6Bitmap & bitmask )
				{
					struct sockaddr_in6		sin6;
					uint8_t					missingIPv6[ 16 ];
					
					addrV6Bitmap &= ~bitmask;
					memcpy( missingIPv6, ( j == 1 ) ? addrV6LL : addrV6, 16 );
					missingIPv6[ 15 ] = (uint8_t) j;
					_SockAddrInitIPv6( &sin6, missingIPv6, ( j == 1 ) ? context->ifIndex : 0, 0 );
					err = CFPropertyListAppendFormatted( kCFAllocatorDefault, missingAddrs, "%##a", &sin6 );
					require_noerr( err, exit );
				}
			}
			
			if( CFArrayGetCount( missingAddrs ) > 0 )
			{
				CFDictionarySetValue( badDict, kMDNSDiscoveryTestIncorrectInstanceKey_MissingAddrs, missingAddrs );
			}
			ForgetCF( &missingAddrs );
		}
		
		if( CFDictionaryGetCount( badDict ) > 0 )
		{
			err = CFDictionarySetCString( badDict, kMDNSDiscoveryTestIncorrectInstanceKey_Name, instance->name,
				kSizeCString );
			require_noerr( err, exit );
			
			CFDictionarySetBoolean( badDict, kMDNSDiscoveryTestIncorrectInstanceKey_DidResolve, true );
			CFArrayAppendValue( incorrectInstances, badDict );
		}
		ForgetCF( &badDict );
	}
	
	if( ( CFArrayGetCount( unexpectedInstances ) == 0 ) &&
		( CFArrayGetCount( missingInstances )    == 0 ) &&
		( CFArrayGetCount( incorrectInstances )  == 0 ) )
	{
		err = CFDictionarySetInt64( plist, kMDNSDiscoveryTestResultsKey_TotalResolveTime, (int64_t) maxResolveTimeUs );
		require_noerr( err, exit );
		
		if( ( context->heapByteLimit == 0 ) || ( context->heapByteCount <= context->heapByteLimit ) )
		{
			success = true;
		}
	}
	CFDictionarySetBoolean( plist, kMDNSDiscoveryTestResultsKey_Success, success );
	
	err = OutputPropertyList( plist, context->outputFormat, context->outputFilePath );
	require_noerr_quiet( err, exit );
	
exit:
	ForgetCF( &context->browser );
	if( context->replierPID != -1 )
	{
		kill( context->replierPID, SIGTERM );
		context->replierPID = -1;
	}
	FreeNullSafe( instanceArray );
	CFReleaseNullSafe( plist );
	CFReleaseNullSafe( badDict );
	CFReleaseNullSafe( unexpectedAddrs );
	CFReleaseNullSafe( missingAddrs );
	exit( err ? 1 : ( success ? 0 : 2 ) );
}

//===========================================================================================================================
//	_MDNSDiscoveryTestTXTRecordIsValid
//===========================================================================================================================

static Boolean	_MDNSDiscoveryTestTXTRecordIsValid( const uint8_t *inRecordName, const uint8_t *inTXTPtr, size_t inTXTLen )
{
	uint32_t			hash;
	int					n;
	const uint8_t *		ptr;
	size_t				i, wholeCount, remCount;
	uint8_t				txtStr[ 16 ];
	
	if( inTXTLen == 0 ) return( false );
	
	hash = _FNV1( inRecordName, DomainNameLength( inRecordName ) );
	
	txtStr[ 0 ] = 15;
	n = MemPrintF( &txtStr[ 1 ], 15, "hash=0x%08X", hash );
	check( n == 15 );
	
	ptr = inTXTPtr;
	wholeCount = inTXTLen / 16;
	for( i = 0; i < wholeCount; ++i )
	{
		if( memcmp( ptr, txtStr, 16 ) != 0 ) return( false );
		ptr += 16;
	}
	
	remCount = inTXTLen % 16;
	if( remCount > 0 )
	{
		txtStr[ 0 ] = (uint8_t)( remCount - 1 );
		if( memcmp( ptr, txtStr, remCount ) != 0 ) return( false );
		ptr += remCount;
	}
	check( ptr == &inTXTPtr[ inTXTLen ] );
	return( true );
}

//===========================================================================================================================
//	DotLocalTestCmd
//===========================================================================================================================

#define kDotLocalTestPreparationTimeLimitSecs		5
#define kDotLocalTestSubtestDurationSecs			5

// Constants for SRV record query subtest.

#define kDotLocalTestSRV_Priority		1
#define kDotLocalTestSRV_Weight			0
#define kDotLocalTestSRV_Port			80
#define kDotLocalTestSRV_TargetName		( (const uint8_t *) "\x03" "www" "\x07" "example" "\x03" "com" )
#define kDotLocalTestSRV_TargetStr		"www.example.com."
#define kDotLocalTestSRV_ResultStr		"1 0 80 " kDotLocalTestSRV_TargetStr

typedef enum
{
	kDotLocalTestState_Unset				= 0,
	kDotLocalTestState_Preparing			= 1,
	kDotLocalTestState_GAIMDNSOnly			= 2,
	kDotLocalTestState_GAIDNSOnly			= 3,
	kDotLocalTestState_GAIBoth				= 4,
	kDotLocalTestState_GAINeither			= 5,
	kDotLocalTestState_GAINoSuchRecord		= 6,
	kDotLocalTestState_QuerySRV				= 7,
	kDotLocalTestState_Done					= 8
	
}	DotLocalTestState;

typedef struct
{
	const char *			testDesc;			// Description of the current subtest.
	char *					queryName;			// Query name for GetAddrInfo or QueryRecord operation.
	dispatch_source_t		timer;				// Timer used for limiting the time for each subtest.
	NanoTime64				startTime;			// Timestamp of when the subtest started.
	NanoTime64				endTime;			// Timestamp of when the subtest ended.
	CFMutableArrayRef		correctResults;		// Operation results that were expected.
	CFMutableArrayRef		duplicateResults;	// Operation results that were expected, but were already received.
	CFMutableArrayRef		unexpectedResults;	// Operation results that were unexpected.
	OSStatus				error;				// Subtest's error code.
	uint32_t				addrDNSv4;			// If hasDNSv4 is true, the expected DNS IPv4 address for queryName.
	uint32_t				addrMDNSv4;			// If hasMDNSv4 is true, the expected MDNS IPv4 address for queryName.
	uint8_t					addrDNSv6[ 16 ];	// If hasDNSv6 is true, the expected DNS IPv6 address for queryName.
	uint8_t					addrMDNSv6[ 16 ];	// If hasMDNSv6 is true, the expected MDNS IPv6 address for queryName.
	Boolean					hasDNSv4;			// True if queryName has a DNS IPv4 address.
	Boolean					hasDNSv6;			// True if queryName has a DNS IPv6 address.
	Boolean					hasMDNSv4;			// True if queryName has an MDNS IPv4 address.
	Boolean					hasMDNSv6;			// True if queryName has an MDNS IPv6 address.
	Boolean					needDNSv4;			// True if operation is expecting, but hasn't received a DNS IPv4 result.
	Boolean					needDNSv6;			// True if operation is expecting, but hasn't received a DNS IPv6 result.
	Boolean					needMDNSv4;			// True if operation is expecting, but hasn't received an MDNS IPv4 result.
	Boolean					needMDNSv6;			// True if operation is expecting, but hasn't received an MDNS IPv6 result.
	Boolean					needSRV;			// True if operation is expecting, but hasn't received an SRV result.
	
}	DotLocalSubtest;

typedef struct
{
	dispatch_source_t		timer;				// Timer used for limiting the time for each state/subtest.
	DotLocalSubtest *		subtest;			// Current subtest's state.
	DNSServiceRef			connection;			// Shared connection for DNS-SD operations.
	DNSServiceRef			op;					// Reference for the current DNS-SD operation.
	DNSServiceRef			op2;				// Reference for mdnsreplier probe query used during preparing state.
	DNSRecordRef			localSOARef;		// Reference returned by DNSServiceRegisterRecord() for local. SOA record.
	char *					replierCmd;			// Command used to invoke the mdnsreplier.
	char *					serverCmd;			// Command used to invoke the test DNS server.
	CFMutableArrayRef		reportsGAI;			// Reports for subtests that use DNSServiceGetAddrInfo.
	CFMutableArrayRef		reportsQuerySRV;	// Reports for subtests that use DNSServiceQueryRecord for SRV records.
	NanoTime64				startTime;			// Timestamp for when the test started.
	NanoTime64				endTime;			// Timestamp for when the test ended.
	DotLocalTestState		state;				// The test's current state.
	pid_t					replierPID;			// PID of spawned mdnsreplier.
	pid_t					serverPID;			// PID of spawned test DNS server.
	uint32_t				ifIndex;			// Interface index used for mdnsreplier.
	char *					outputFilePath;		// File to write test results to. If NULL, then write to stdout.
	OutputFormatType		outputFormat;		// Format of test results output.
	Boolean					registeredSOA;		// True if the dummy local. SOA record was successfully registered.
	Boolean					serverIsReady;		// True if response was received for test DNS server probe query.
	Boolean					replierIsReady;		// True if response was received for mdnsreplier probe query.
	Boolean					testFailed;			// True if at least one subtest failed.
	char					labelStr[ 20 + 1 ];	// Unique label string used for for making the query names used by subtests.
												// The format of this string is "dotlocal-test-<six random chars>".
}	DotLocalTestContext;

static void	_DotLocalTestStateMachine( DotLocalTestContext *inContext );
static void DNSSD_API
	_DotLocalTestProbeQueryRecordCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext );
static void DNSSD_API
	_DotLocalTestRegisterRecordCallback(
		DNSServiceRef		inSDRef,
		DNSRecordRef		inRecordRef,
		DNSServiceFlags		inFlags,
		DNSServiceErrorType	inError,
		void *				inContext );
static void	_DotLocalTestTimerHandler( void *inContext );
static void DNSSD_API
	_DotLocalTestGAICallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext );
static void DNSSD_API
	_DotLocalTestQueryRecordCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext );

static void	DotLocalTestCmd( void )
{
	OSStatus					err;
	DotLocalTestContext *		context;
	uint8_t *					rdataPtr;
	size_t						rdataLen;
	DNSServiceFlags				flags;
	char						queryName[ 64 ];
	char						randBuf[ 6 + 1 ];	// Large enough for four and six character random strings below.
	
	context = (DotLocalTestContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	context->startTime	= NanoTimeGetCurrent();
	context->endTime	= kNanoTime_Invalid;
	
	context->state = kDotLocalTestState_Preparing;
	
	if( gDotLocalTest_Interface )
	{
		err = InterfaceIndexFromArgString( gDotLocalTest_Interface, &context->ifIndex );
		require_noerr_quiet( err, exit );
	}
	else
	{
		err = _MDNSInterfaceGetAny( kMDNSInterfaceSubset_All, NULL, &context->ifIndex );
		require_noerr_quiet( err, exit );
	}
	
	if( gDotLocalTest_OutputFilePath )
	{
		context->outputFilePath = strdup( gDotLocalTest_OutputFilePath );
		require_action( context->outputFilePath, exit, err = kNoMemoryErr );
	}
	
	err = OutputFormatFromArgString( gDotLocalTest_OutputFormat, &context->outputFormat );
	require_noerr_quiet( err, exit );
	
	context->reportsGAI = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
	require_action( context->reportsGAI, exit, err = kNoMemoryErr );
	
	context->reportsQuerySRV = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
	require_action( context->reportsQuerySRV, exit, err = kNoMemoryErr );
	
	SNPrintF( context->labelStr, sizeof( context->labelStr ), "dotlocal-test-%s",
		_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, 6, randBuf ) );
	
	// Spawn an mdnsreplier.
	
	ASPrintF( &context->replierCmd,
		"dnssdutil mdnsreplier --follow %lld --interface %u --hostname %s --tag %s --maxInstanceCount 2 --countA 1"
		" --countAAAA 1",
		(int64_t) getpid(), context->ifIndex, context->labelStr,
		_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, 4, randBuf ) );
	require_action_quiet( context->replierCmd, exit, err = kUnknownErr );
	
	err = _SpawnCommand( &context->replierPID, NULL, NULL, "%s", context->replierCmd );
	require_noerr( err, exit );
	
	// Spawn a test DNS server.
	// Use the --registerSC option because this test depends on a GAI operation for a domain name that matches a *.local
	// seach domain. SystemConfiguration will set up a search domain for each of the DNS service's match domains.
	
	ASPrintF( &context->serverCmd,
		DNSSDUTIL_TEST_DNS_SERVER_COMMAND_PREAMBLE " --loopback --registerSC --follow %lld --defaultTTL 300"
		" --domain %s.local.",
		(int64_t) getpid(), context->labelStr );
	require_action_quiet( context->serverCmd, exit, err = kUnknownErr );
	
	err = _SpawnCommand( &context->serverPID, NULL, NULL, "%s", context->serverCmd );
	require_noerr( err, exit );
	
	// Create a shared DNS-SD connection.
	
	err = DNSServiceCreateConnection( &context->connection );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( context->connection, dispatch_get_main_queue() );
	require_noerr( err, exit );
	
	// Create probe query for DNS server, i.e., query for any name that has an A record.
	
	SNPrintF( queryName, sizeof( queryName ), "tag-dotlocal-test-probe.ipv4.%s.local.", context->labelStr );
	
	flags = kDNSServiceFlagsShareConnection;
#if( TARGET_OS_WATCH )
	flags |= kDNSServiceFlagsPathEvaluationDone;
#endif
	
	context->op = context->connection;
	err = DNSServiceQueryRecord( &context->op, flags, kDNSServiceInterfaceIndexAny, queryName, kDNSServiceType_A,
		kDNSServiceClass_IN, _DotLocalTestProbeQueryRecordCallback, context );
	require_noerr( err, exit );
	
	// Create probe query for mdnsreplier's "about" TXT record.
	
	SNPrintF( queryName, sizeof( queryName ), "about.%s.local.", context->labelStr );
	
	flags = kDNSServiceFlagsShareConnection | kDNSServiceFlagsForceMulticast;
#if( TARGET_OS_WATCH )
	flags |= kDNSServiceFlagsPathEvaluationDone;
#endif
	
	context->op2 = context->connection;
	err = DNSServiceQueryRecord( &context->op2, flags, context->ifIndex, queryName, kDNSServiceType_TXT, kDNSServiceClass_IN,
		_DotLocalTestProbeQueryRecordCallback, context );
	require_noerr( err, exit );
	
	// Register a dummy local. SOA record.
	
	err = CreateSOARecordData( kRootLabel, kRootLabel, 1976040101, 1 * kSecondsPerDay, 2 * kSecondsPerHour,
		1000 * kSecondsPerHour, 2 * kSecondsPerDay, &rdataPtr, &rdataLen );
	require_noerr( err, exit );
	
	err = DNSServiceRegisterRecord( context->connection, &context->localSOARef, kDNSServiceFlagsUnique,
		kDNSServiceInterfaceIndexLocalOnly, "local.", kDNSServiceType_SOA, kDNSServiceClass_IN,
		(uint16_t) rdataLen, rdataPtr, 1 * kSecondsPerHour, _DotLocalTestRegisterRecordCallback, context );
	require_noerr( err, exit );
	
	// Start timer for probe responses and SOA record registration.
	
	err = DispatchTimerOneShotCreate( dispatch_time_seconds( kDotLocalTestPreparationTimeLimitSecs ),
		INT64_C_safe( kDotLocalTestPreparationTimeLimitSecs ) * kNanosecondsPerSecond / 10, dispatch_get_main_queue(),
		_DotLocalTestTimerHandler, context, &context->timer );
	require_noerr( err, exit );
	dispatch_resume( context->timer );
	
	dispatch_main();
	
exit:
	if( err ) ErrQuit( 1, "error: %#m\n", err );
}

//===========================================================================================================================
//	_DotLocalTestStateMachine
//===========================================================================================================================

static OSStatus	_DotLocalSubtestCreate( DotLocalSubtest **outSubtest );
static void		_DotLocalSubtestFree( DotLocalSubtest *inSubtest );
static OSStatus	_DotLocalTestStartSubtest( DotLocalTestContext *inContext );
static OSStatus	_DotLocalTestFinalizeSubtest( DotLocalTestContext *inContext );
static void		_DotLocalTestFinalizeAndExit( DotLocalTestContext *inContext ) ATTRIBUTE_NORETURN;

static void	_DotLocalTestStateMachine( DotLocalTestContext *inContext )
{
	OSStatus				err;
	DotLocalTestState		nextState;
	
	DNSServiceForget( &inContext->op );
	DNSServiceForget( &inContext->op2 );
	dispatch_source_forget( &inContext->timer );
	
	switch( inContext->state )
	{
		case kDotLocalTestState_Preparing:			nextState = kDotLocalTestState_GAIMDNSOnly;		break;
		case kDotLocalTestState_GAIMDNSOnly:		nextState = kDotLocalTestState_GAIDNSOnly;		break;
		case kDotLocalTestState_GAIDNSOnly:			nextState = kDotLocalTestState_GAIBoth;			break;
		case kDotLocalTestState_GAIBoth:			nextState = kDotLocalTestState_GAINeither;		break;
		case kDotLocalTestState_GAINeither:			nextState = kDotLocalTestState_GAINoSuchRecord;	break;
		case kDotLocalTestState_GAINoSuchRecord:	nextState = kDotLocalTestState_QuerySRV;		break;
		case kDotLocalTestState_QuerySRV:			nextState = kDotLocalTestState_Done;			break;
		default:									err = kStateErr;								goto exit;
	}
	
	if( inContext->state == kDotLocalTestState_Preparing )
	{
		if( !inContext->registeredSOA || !inContext->serverIsReady || !inContext->replierIsReady )
		{
			FPrintF( stderr, "Preparation timed out: Registered SOA? %s. Server ready? %s. mdnsreplier ready? %s.\n",
				YesNoStr( inContext->registeredSOA ),
				YesNoStr( inContext->serverIsReady ),
				YesNoStr( inContext->replierIsReady ) );
			err = kNotPreparedErr;
			goto exit;
		}
	}
	else
	{
		err = _DotLocalTestFinalizeSubtest( inContext );
		require_noerr( err, exit );
	}
	
	inContext->state = nextState;
	if( inContext->state == kDotLocalTestState_Done ) _DotLocalTestFinalizeAndExit( inContext );
	err = _DotLocalTestStartSubtest( inContext );
	
exit:
	if( err ) ErrQuit( 1, "error: %#m\n", err );
}

//===========================================================================================================================
//	_DotLocalSubtestCreate
//===========================================================================================================================

static OSStatus	_DotLocalSubtestCreate( DotLocalSubtest **outSubtest )
{
	OSStatus				err;
	DotLocalSubtest *		obj;
	
	obj = (DotLocalSubtest *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->correctResults = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
	require_action( obj->correctResults, exit, err = kNoMemoryErr );
	
	obj->duplicateResults = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
	require_action( obj->duplicateResults, exit, err = kNoMemoryErr );
	
	obj->unexpectedResults = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
	require_action( obj->unexpectedResults, exit, err = kNoMemoryErr );
	
	*outSubtest = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) _DotLocalSubtestFree( obj );
	return( err );
}

//===========================================================================================================================
//	_DotLocalSubtestFree
//===========================================================================================================================

static void	_DotLocalSubtestFree( DotLocalSubtest *inSubtest )
{
	ForgetMem( &inSubtest->queryName );
	ForgetCF( &inSubtest->correctResults );
	ForgetCF( &inSubtest->duplicateResults );
	ForgetCF( &inSubtest->unexpectedResults );
	free( inSubtest );
}

//===========================================================================================================================
//	_DotLocalTestStartSubtest
//===========================================================================================================================

static OSStatus	_DotLocalTestStartSubtest( DotLocalTestContext *inContext )
{
	OSStatus				err;
	DotLocalSubtest *		subtest	= NULL;
	DNSServiceRef			op		= NULL;
	DNSServiceFlags			flags;
	
	err = _DotLocalSubtestCreate( &subtest );
	require_noerr( err, exit );
	
	if( inContext->state == kDotLocalTestState_GAIMDNSOnly )
	{
		ASPrintF( &subtest->queryName, "%s-2.local.", inContext->labelStr );
		require_action_quiet( subtest->queryName, exit, err = kNoMemoryErr );
		
		subtest->hasMDNSv4 = subtest->needMDNSv4 = true;
		subtest->hasMDNSv6 = subtest->needMDNSv6 = true;
		
		subtest->addrMDNSv4 = htonl( 0x00000201 );							// 0.0.2.1
		memcpy( subtest->addrMDNSv6, kMDNSReplierLinkLocalBaseAddrV6, 16 );	// fe80::2:1
		subtest->addrMDNSv6[ 13 ] = 2;
		subtest->addrMDNSv6[ 15 ] = 1;
		
		subtest->testDesc = kDotLocalTestSubtestDesc_GAIMDNSOnly;
	}
	
	else if( inContext->state == kDotLocalTestState_GAIDNSOnly )
	{
		ASPrintF( &subtest->queryName, "tag-dns-only.%s.local.", inContext->labelStr );
		require_action_quiet( subtest->queryName, exit, err = kNoMemoryErr );
		
		subtest->hasDNSv4 = subtest->needDNSv4 = true;
		subtest->hasDNSv6 = subtest->needDNSv6 = true;
		
		subtest->addrDNSv4 = htonl( kDNSServerBaseAddrV4 + 1 );				// 203.0.113.1
		memcpy( subtest->addrDNSv6, kDNSServerBaseAddrV6, 16 );				// 2001:db8:1::1
		subtest->addrDNSv6[ 15 ] = 1;
		
		subtest->testDesc = kDotLocalTestSubtestDesc_GAIDNSOnly;
	}
	
	else if( inContext->state == kDotLocalTestState_GAIBoth )
	{
		ASPrintF( &subtest->queryName, "%s.local.", inContext->labelStr );
		require_action_quiet( subtest->queryName, exit, err = kNoMemoryErr );
		
		subtest->hasDNSv4	= subtest->needDNSv4	= true;
		subtest->hasDNSv6	= subtest->needDNSv6	= true;
		subtest->hasMDNSv4	= subtest->needMDNSv4	= true;
		subtest->hasMDNSv6	= subtest->needMDNSv6	= true;
		
		subtest->addrDNSv4 = htonl( kDNSServerBaseAddrV4 + 1 );				// 203.0.113.1
		memcpy( subtest->addrDNSv6, kDNSServerBaseAddrV6, 16 );				// 2001:db8:1::1
		subtest->addrDNSv6[ 15 ] = 1;
		
		subtest->addrMDNSv4 = htonl( 0x00000101 );							// 0.0.1.1
		memcpy( subtest->addrMDNSv6, kMDNSReplierLinkLocalBaseAddrV6, 16 );	// fe80::1:1
		subtest->addrMDNSv6[ 13 ] = 1;
		subtest->addrMDNSv6[ 15 ] = 1;
		
		subtest->testDesc = kDotLocalTestSubtestDesc_GAIBoth;
	}
	
	else if( inContext->state == kDotLocalTestState_GAINeither )
	{
		ASPrintF( &subtest->queryName, "doesnotexit-%s.local.", inContext->labelStr );
		require_action_quiet( subtest->queryName, exit, err = kNoMemoryErr );
		
		subtest->testDesc = kDotLocalTestSubtestDesc_GAINeither;
	}
	
	else if( inContext->state == kDotLocalTestState_GAINoSuchRecord )
	{
		ASPrintF( &subtest->queryName, "doesnotexit-dns.%s.local.", inContext->labelStr );
		require_action_quiet( subtest->queryName, exit, err = kNoMemoryErr );
		
		subtest->hasDNSv4 = subtest->needDNSv4 = true;
		subtest->hasDNSv6 = subtest->needDNSv6 = true;
		subtest->testDesc = kDotLocalTestSubtestDesc_GAINoSuchRecord;
	}
	
	else if( inContext->state == kDotLocalTestState_QuerySRV )
	{
		ASPrintF( &subtest->queryName, "_http._tcp.srv-%u-%u-%u.%s%s.local.",
			kDotLocalTestSRV_Priority, kDotLocalTestSRV_Weight, kDotLocalTestSRV_Port, kDotLocalTestSRV_TargetStr,
			inContext->labelStr );
		require_action_quiet( subtest->queryName, exit, err = kNoMemoryErr );
		
		subtest->needSRV	= true;
		subtest->testDesc	= kDotLocalTestSubtestDesc_QuerySRV;
	}
	
	else
	{
		err = kStateErr;
		goto exit;
	}
	
	// Start new operation.
	
	flags = kDNSServiceFlagsShareConnection | kDNSServiceFlagsReturnIntermediates;
#if( TARGET_OS_WATCH )
	flags |= kDNSServiceFlagsPathEvaluationDone;
#endif
	
	subtest->startTime	= NanoTimeGetCurrent();
	subtest->endTime	= kNanoTime_Invalid;
	
	if( inContext->state == kDotLocalTestState_QuerySRV )
	{
		op = inContext->connection;
		err = DNSServiceQueryRecord( &op, flags, kDNSServiceInterfaceIndexAny, subtest->queryName,
			kDNSServiceType_SRV, kDNSServiceClass_IN, _DotLocalTestQueryRecordCallback, inContext );
		require_noerr( err, exit );
	}
	else
	{
		op = inContext->connection;
		err = DNSServiceGetAddrInfo( &op, flags, kDNSServiceInterfaceIndexAny,
			kDNSServiceProtocol_IPv4 | kDNSServiceProtocol_IPv6, subtest->queryName, _DotLocalTestGAICallback, inContext );
		require_noerr( err, exit );
	}
	
	// Start timer.
	
	check( !inContext->timer );
	err = DispatchTimerOneShotCreate( dispatch_time_seconds( kDotLocalTestSubtestDurationSecs ),
		INT64_C_safe( kDotLocalTestSubtestDurationSecs ) * kNanosecondsPerSecond / 10, dispatch_get_main_queue(),
		_DotLocalTestTimerHandler, inContext, &inContext->timer );
	require_noerr( err, exit );
	dispatch_resume( inContext->timer );
	
	check( !inContext->op );
	inContext->op = op;
	op = NULL;
	
	check( !inContext->subtest );
	inContext->subtest = subtest;
	subtest = NULL;
	
exit:
	if( subtest )	_DotLocalSubtestFree( subtest );
	if( op )		DNSServiceRefDeallocate( op );
	return( err );
}

//===========================================================================================================================
//	_DotLocalTestFinalizeSubtest
//===========================================================================================================================

#define kDotLocalTestReportKey_StartTime				CFSTR( "startTime" )		// String.
#define kDotLocalTestReportKey_EndTime					CFSTR( "endTime" )			// String.
#define kDotLocalTestReportKey_Success					CFSTR( "success" )			// Boolean.
#define kDotLocalTestReportKey_MDNSReplierCmd			CFSTR( "replierCmd" )		// String.
#define kDotLocalTestReportKey_DNSServerCmd				CFSTR( "serverCmd" )		// String.
#define kDotLocalTestReportKey_GetAddrInfoTests			CFSTR( "testsGAI" )			// Array of Dictionaries.
#define kDotLocalTestReportKey_QuerySRVTests			CFSTR( "testsQuerySRV" )	// Array of Dictionaries.
#define kDotLocalTestReportKey_Description				CFSTR( "description" )		// String.
#define kDotLocalTestReportKey_QueryName				CFSTR( "queryName" )		// String.
#define kDotLocalTestReportKey_Error					CFSTR( "error" )			// Integer.
#define kDotLocalTestReportKey_Results					CFSTR( "results" )			// Dictionary of Arrays.
#define kDotLocalTestReportKey_CorrectResults			CFSTR( "correct" )			// Array of Strings
#define kDotLocalTestReportKey_DuplicateResults			CFSTR( "duplicates" )		// Array of Strings.
#define kDotLocalTestReportKey_UnexpectedResults		CFSTR( "unexpected" )		// Array of Strings.
#define kDotLocalTestReportKey_MissingResults			CFSTR( "missing" )			// Array of Strings.

static OSStatus	_DotLocalTestFinalizeSubtest( DotLocalTestContext *inContext )
{
	OSStatus					err;
	DotLocalSubtest *			subtest;
	CFMutableDictionaryRef		reportDict;
	CFMutableDictionaryRef		resultsDict;
	CFMutableArrayRef			missingResults, reportArray;
	char						startTime[ 32 ];
	char						endTime[ 32 ];
	
	subtest = inContext->subtest;
	inContext->subtest = NULL;
	
	subtest->endTime = NanoTimeGetCurrent();
	_NanoTime64ToTimestamp( subtest->startTime, startTime, sizeof( startTime ) );
	_NanoTime64ToTimestamp( subtest->endTime, endTime, sizeof( endTime ) );
	
	reportDict = NULL;
	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &reportDict,
		"{"
			"%kO=%s"	// startTime
			"%kO=%s"	// endTime
			"%kO=%s"	// queryName
			"%kO=%s"	// description
			"%kO={%@}"	// results
		"}",
		kDotLocalTestReportKey_StartTime,	startTime,
		kDotLocalTestReportKey_EndTime,		endTime,
		kDotLocalTestReportKey_QueryName,	subtest->queryName,
		kDotLocalTestReportKey_Description,	subtest->testDesc,
		kDotLocalTestReportKey_Results,		&resultsDict );
	require_noerr( err, exit );
	
	missingResults = NULL;
	switch( inContext->state )
	{
		case kDotLocalTestState_GAIMDNSOnly:
		case kDotLocalTestState_GAIDNSOnly:
		case kDotLocalTestState_GAIBoth:
		case kDotLocalTestState_GAINeither:
			if( subtest->needDNSv4 || subtest->needDNSv6 || subtest->needMDNSv4 || subtest->needMDNSv6 )
			{
				err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &missingResults,
					"["
						"%.4a"	// Expected DNS IPv4 address
						"%.16a"	// Expected DNS IPv6 address
						"%.4a"	// Expected MDNS IPv4 address
						"%.16a"	// Expected MDNS IPv6 address
					"]",
					subtest->needDNSv4  ? &subtest->addrDNSv4  : NULL,
					subtest->needDNSv6  ?  subtest->addrDNSv6  : NULL,
					subtest->needMDNSv4 ? &subtest->addrMDNSv4 : NULL,
					subtest->needMDNSv6 ?  subtest->addrMDNSv6 : NULL );
				require_noerr( err, exit );
			}
			break;
		
		case kDotLocalTestState_QuerySRV:
			if( subtest->needSRV )
			{
				err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &missingResults,
					"["
						"%s"	// Expected SRV record data as a string.
					"]",
					kDotLocalTestSRV_ResultStr );
				require_noerr( err, exit );
			}
			break;
		
		case kDotLocalTestState_GAINoSuchRecord:
			if( subtest->needDNSv4 || subtest->needDNSv6 )
			{
				err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &missingResults,
					"["
						"%s" // No Such Record (A)
						"%s" // No Such Record (AAAA)
					"]",
					subtest->needDNSv4 ? kNoSuchRecordAStr    : NULL,
					subtest->needDNSv6 ? kNoSuchRecordAAAAStr : NULL );
				require_noerr( err, exit );
			}
			break;
		
		default:
			err = kStateErr;
			goto exit;
	}
	
	CFDictionarySetValue( resultsDict, kDotLocalTestReportKey_CorrectResults, subtest->correctResults );
	
	if( missingResults )
	{
		CFDictionarySetValue( resultsDict, kDotLocalTestReportKey_MissingResults, missingResults );
		ForgetCF( &missingResults );
		if( !subtest->error ) subtest->error = kNotFoundErr;
	}
	
	if( CFArrayGetCount( subtest->unexpectedResults ) > 0 )
	{
		CFDictionarySetValue( resultsDict, kDotLocalTestReportKey_UnexpectedResults, subtest->unexpectedResults );
		if( !subtest->error ) subtest->error = kUnexpectedErr;
	}
	
	if( CFArrayGetCount( subtest->duplicateResults ) > 0 )
	{
		CFDictionarySetValue( resultsDict, kDotLocalTestReportKey_DuplicateResults, subtest->duplicateResults );
		if( !subtest->error ) subtest->error = kDuplicateErr;
	}
	
	if( subtest->error ) inContext->testFailed = true;
	err = CFDictionarySetInt64( reportDict, kDotLocalTestReportKey_Error, subtest->error );
	require_noerr( err, exit );
	
	reportArray = ( inContext->state == kDotLocalTestState_QuerySRV ) ? inContext->reportsQuerySRV : inContext->reportsGAI;
	CFArrayAppendValue( reportArray, reportDict );
	
exit:
	_DotLocalSubtestFree( subtest );
	CFReleaseNullSafe( reportDict );
	return( err );
}

//===========================================================================================================================
//	_DotLocalTestFinalizeAndExit
//===========================================================================================================================

static void	_DotLocalTestFinalizeAndExit( DotLocalTestContext *inContext )
{
	OSStatus				err;
	CFPropertyListRef		plist;
	char					timestampStart[ 32 ];
	char					timestampEnd[ 32 ];
	
	check( !inContext->subtest );
	inContext->endTime = NanoTimeGetCurrent();
	
	if( inContext->replierPID != -1 )
	{
		kill( inContext->replierPID, SIGTERM );
		inContext->replierPID = -1;
	}
	if( inContext->serverPID != -1 )
	{
		kill( inContext->serverPID, SIGTERM );
		inContext->serverPID = -1;
	}
	err = DNSServiceRemoveRecord( inContext->connection, inContext->localSOARef, 0 );
	require_noerr( err, exit );
	
	_NanoTime64ToTimestamp( inContext->startTime, timestampStart, sizeof( timestampStart ) );
	_NanoTime64ToTimestamp( inContext->endTime, timestampEnd, sizeof( timestampEnd ) );
	
	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &plist,
		"{"
			"%kO=%s"	// startTime
			"%kO=%s"	// endTime
			"%kO=%O"	// testsGAI
			"%kO=%O"	// testsQuerySRV
			"%kO=%b"	// success
			"%kO=%s"	// replierCmd
			"%kO=%s"	// serverCmd
		"}",
		kDotLocalTestReportKey_StartTime,			timestampStart,
		kDotLocalTestReportKey_EndTime,				timestampEnd,
		kDotLocalTestReportKey_GetAddrInfoTests,	inContext->reportsGAI,
		kDotLocalTestReportKey_QuerySRVTests,		inContext->reportsQuerySRV,
		kDotLocalTestReportKey_Success,				inContext->testFailed ? false : true,
		kDotLocalTestReportKey_MDNSReplierCmd,		inContext->replierCmd,
		kDotLocalTestReportKey_DNSServerCmd,		inContext->serverCmd );
	require_noerr( err, exit );
	
	ForgetCF( &inContext->reportsGAI );
	ForgetCF( &inContext->reportsQuerySRV );
	
	err = OutputPropertyList( plist, inContext->outputFormat, inContext->outputFilePath );
	CFRelease( plist );
	require_noerr( err, exit );
	
	exit( inContext->testFailed ? 2 : 0 );
	
exit:
	ErrQuit( 1, "error: %#m\n", err );
}

//===========================================================================================================================
//	_DotLocalTestProbeQueryRecordCallback
//===========================================================================================================================

static void DNSSD_API
	_DotLocalTestProbeQueryRecordCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext )
{
	DotLocalTestContext * const		context = (DotLocalTestContext *) inContext;
	
	Unused( inInterfaceIndex );
	Unused( inFullName );
	Unused( inType );
	Unused( inClass );
	Unused( inRDataLen );
	Unused( inRDataPtr );
	Unused( inTTL );
	
	check( context->state == kDotLocalTestState_Preparing );
	
	require_quiet( ( inFlags & kDNSServiceFlagsAdd ) && !inError, exit );
	
	if( inSDRef == context->op )
	{
		DNSServiceForget( &context->op );
		context->serverIsReady = true;
	}
	else if( inSDRef == context->op2 )
	{
		DNSServiceForget( &context->op2 );
		context->replierIsReady = true;
	}
	
	if( context->registeredSOA && context->serverIsReady && context->replierIsReady )
	{
		_DotLocalTestStateMachine( context );
	}
	
exit:
	return;
}

//===========================================================================================================================
//	_DotLocalTestRegisterRecordCallback
//===========================================================================================================================

static void DNSSD_API
	_DotLocalTestRegisterRecordCallback(
		DNSServiceRef		inSDRef,
		DNSRecordRef		inRecordRef,
		DNSServiceFlags		inFlags,
		DNSServiceErrorType	inError,
		void *				inContext )
{
	DotLocalTestContext * const		context = (DotLocalTestContext *) inContext;
	
	Unused( inSDRef );
	Unused( inRecordRef );
	Unused( inFlags );
	
	if( inError ) ErrQuit( 1, "error: local. SOA record registration failed: %#m\n", inError );
	
	if( !context->registeredSOA )
	{
		context->registeredSOA = true;
		if( context->serverIsReady && context->replierIsReady ) _DotLocalTestStateMachine( context );
	}
}

//===========================================================================================================================
//	_DotLocalTestTimerHandler
//===========================================================================================================================

static void	_DotLocalTestTimerHandler( void *inContext )
{
	_DotLocalTestStateMachine( (DotLocalTestContext *) inContext );
}

//===========================================================================================================================
//	_DotLocalTestGAICallback
//===========================================================================================================================

static void DNSSD_API
	_DotLocalTestGAICallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext )
{
	OSStatus						err;
	DotLocalTestContext * const		context = (DotLocalTestContext *) inContext;
	DotLocalSubtest * const			subtest	= context->subtest;
	const sockaddr_ip * const		sip		= (const sockaddr_ip *) inSockAddr;
	
	Unused( inSDRef );
	Unused( inInterfaceIndex );
	Unused( inHostname );
	Unused( inTTL );
	
	require_action_quiet( inFlags & kDNSServiceFlagsAdd, exit, err = kFlagErr );
	require_action_quiet( ( sip->sa.sa_family == AF_INET ) || ( sip->sa.sa_family == AF_INET6 ), exit, err = kTypeErr );
	
	if( context->state == kDotLocalTestState_GAINoSuchRecord )
	{
		if( inError == kDNSServiceErr_NoSuchRecord )
		{
			CFMutableArrayRef		array = NULL;	
			const char *			noSuchRecordStr;
			
			if( sip->sa.sa_family == AF_INET )
			{
				array = subtest->needDNSv4 ? subtest->correctResults : subtest->duplicateResults;
				subtest->needDNSv4 = false;
				
				noSuchRecordStr = kNoSuchRecordAStr;
			}
			else
			{
				array = subtest->needDNSv6 ? subtest->correctResults : subtest->duplicateResults;
				subtest->needDNSv6 = false;
				
				noSuchRecordStr = kNoSuchRecordAAAAStr;
			}
			err = CFPropertyListAppendFormatted( kCFAllocatorDefault, array, "%s", noSuchRecordStr );
			require_noerr( err, fatal );
		}
		else if( !inError )
		{
			err = CFPropertyListAppendFormatted( kCFAllocatorDefault, subtest->unexpectedResults, "%##a", sip );
			require_noerr( err, fatal );
		}
		else
		{
			err = inError;
			goto exit;
		}
	}
	else
	{
		if( !inError )
		{
			CFMutableArrayRef		array = NULL;	
			
			if( sip->sa.sa_family == AF_INET )
			{
				const uint32_t		addrV4 = sip->v4.sin_addr.s_addr;
				
				if( subtest->hasDNSv4 && ( addrV4 == subtest->addrDNSv4 ) )
				{
					array = subtest->needDNSv4 ? subtest->correctResults : subtest->duplicateResults;
					subtest->needDNSv4 = false;
				}
				else if( subtest->hasMDNSv4 && ( addrV4 == subtest->addrMDNSv4 ) )
				{
					array = subtest->needMDNSv4 ? subtest->correctResults : subtest->duplicateResults;
					subtest->needMDNSv4 = false;
				}
			}
			else
			{
				const uint8_t * const		addrV6 = sip->v6.sin6_addr.s6_addr;
				
				if( subtest->hasDNSv6 && ( memcmp( addrV6, subtest->addrDNSv6, 16 ) == 0 ) )
				{
					array = subtest->needDNSv6 ? subtest->correctResults : subtest->duplicateResults;
					subtest->needDNSv6 = false;
				}
				else if( subtest->hasMDNSv6 && ( memcmp( addrV6, subtest->addrMDNSv6, 16 ) == 0 ) )
				{
					array = subtest->needMDNSv6 ? subtest->correctResults : subtest->duplicateResults;
					subtest->needMDNSv6 = false;
				}
			}
			if( !array ) array = subtest->unexpectedResults;
			err = CFPropertyListAppendFormatted( kCFAllocatorDefault, array, "%##a", sip );
			require_noerr( err, fatal );
		}
		else if( inError == kDNSServiceErr_NoSuchRecord )
		{
			err = CFPropertyListAppendFormatted( kCFAllocatorDefault, subtest->unexpectedResults, "%s",
				( sip->sa.sa_family == AF_INET ) ? kNoSuchRecordAStr : kNoSuchRecordAAAAStr );
			require_noerr( err, fatal );
		}
		else
		{
			err = inError;
			goto exit;
		}
	}
	
exit:
	if( err )
	{
		subtest->error = err;
		_DotLocalTestStateMachine( context );
	}
	return;
	
fatal:
	ErrQuit( 1, "error: %#m\n", err );
}

//===========================================================================================================================
//	_DotLocalTestQueryRecordCallback
//===========================================================================================================================

static void DNSSD_API
	_DotLocalTestQueryRecordCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext )
{
	OSStatus							err;
	DotLocalTestContext * const			context = (DotLocalTestContext *) inContext;
	DotLocalSubtest * const				subtest = context->subtest;
	const dns_fixed_fields_srv *		fields;
	const uint8_t *						target;
	const uint8_t *						ptr;
	const uint8_t *						end;
	char *								rdataStr;
	unsigned int						priority, weight, port;
	CFMutableArrayRef					array;
	
	Unused( inSDRef );
	Unused( inInterfaceIndex );
	Unused( inFullName );
	Unused( inTTL );
	
	check( context->state == kDotLocalTestState_QuerySRV );
	
	err = inError;
	require_noerr_quiet( err, exit );
	require_action_quiet( inFlags & kDNSServiceFlagsAdd, exit, err = kFlagErr );
	require_action_quiet( ( inType == kDNSServiceType_SRV ) && ( inClass == kDNSServiceClass_IN ), exit, err = kTypeErr );
	require_action_quiet( inRDataLen > sizeof( dns_fixed_fields_srv ), exit, err = kSizeErr );
	
	fields		= (const dns_fixed_fields_srv *) inRDataPtr;
	priority	= dns_fixed_fields_srv_get_priority( fields );
	weight		= dns_fixed_fields_srv_get_weight( fields );
	port		= dns_fixed_fields_srv_get_port( fields );
	target		= (const uint8_t *) &fields[ 1 ];
	end			= ( (const uint8_t *) inRDataPtr ) + inRDataLen;
	for( ptr = target; ( ptr < end ) && ( *ptr != 0 ); ptr += ( 1 + *ptr ) ) {}
	
	if( ( priority == kDotLocalTestSRV_Priority ) &&
		( weight   == kDotLocalTestSRV_Weight )   &&
		( port     == kDotLocalTestSRV_Port )     &&
		( ptr < end ) && DomainNameEqual( target, kDotLocalTestSRV_TargetName ) )
	{
		array = subtest->needSRV ? subtest->correctResults : subtest->duplicateResults;
		subtest->needSRV = false;
	}
	else
	{
		array = subtest->unexpectedResults;
	}
	
	rdataStr = NULL;
	DNSRecordDataToString( inRDataPtr, inRDataLen, kDNSServiceType_SRV, &rdataStr );
	if( !rdataStr )
	{
		ASPrintF( &rdataStr, "%#H", inRDataPtr, inRDataLen, inRDataLen );
		require_action( rdataStr, fatal, err = kNoMemoryErr );
	}
	
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, array, "%s", rdataStr );
	free( rdataStr );
	require_noerr( err, fatal );
	
exit:
	if( err )
	{
		subtest->error = err;
		_DotLocalTestStateMachine( context );
	}
	return;
	
fatal:
	ErrQuit( 1, "error: %#m\n", err );
}

//===========================================================================================================================
//	ProbeConflictTestCmd
//===========================================================================================================================

#define kProbeConflictTestService_DefaultName		"pctest-name"
#define kProbeConflictTestService_Port				60000

#define kProbeConflictTestTXTPtr		"\x13" "PROBE-CONFLICT-TEST"
#define kProbeConflictTestTXTLen		sizeof_string( kProbeConflictTestTXTPtr )

typedef struct
{
	const char *		description;
	const char *		program;
	Boolean				expectsRename;
	
}	ProbeConflictTestCase;

// Wait 1 second before sending gratuitous response.

#define kPCTProgPreWait		"wait 1000;"

// Wait at least 10 seconds after sending gratuitous response. This allows ~2.75 seconds for probing, ~5 seconds for a
// rename, and a 2 second fudge factor for unexpected system delays.

#define kProbeConflictTestPostWaitMs		( 10 * kMillisecondsPerSecond )

static const ProbeConflictTestCase		kProbeConflictTestCases[] =
{
	// No conflicts
	
	{ "No probe conflicts.",                       kPCTProgPreWait "probes n-n-n;"       "send;", false },
	
	// One multicast probe conflict
	
	{ "One multicast probe conflict (1).",         kPCTProgPreWait "probes m;"           "send;", false },
	{ "One multicast probe conflict (2).",         kPCTProgPreWait "probes n-m;"         "send;", false },
	{ "One multicast probe conflict (3).",         kPCTProgPreWait "probes n-n-m;"       "send;", false },
	
	// One unicast probe conflict
	
	{ "One unicast probe conflict (1).",           kPCTProgPreWait "probes u;"           "send;", true },
	{ "One unicast probe conflict (2).",           kPCTProgPreWait "probes n-u;"         "send;", true },
	{ "One unicast probe conflict (3).",           kPCTProgPreWait "probes n-n-u;"       "send;", true },
	
	// One multicast and one unicast probe conflict
	
	{ "Multicast and unicast probe conflict (1).", kPCTProgPreWait "probes m-u;"         "send;", true },
	{ "Multicast and unicast probe conflict (2).", kPCTProgPreWait "probes m-n-u;"       "send;", true },
	{ "Multicast and unicast probe conflict (3).", kPCTProgPreWait "probes m-n-n-u;"     "send;", true },
	{ "Multicast and unicast probe conflict (4).", kPCTProgPreWait "probes n-m-u;"       "send;", true },
	{ "Multicast and unicast probe conflict (5).", kPCTProgPreWait "probes n-m-n-u;"     "send;", true },
	{ "Multicast and unicast probe conflict (6).", kPCTProgPreWait "probes n-m-n-n-u;"   "send;", true },
	{ "Multicast and unicast probe conflict (7).", kPCTProgPreWait "probes n-n-m-u;"     "send;", true },
	{ "Multicast and unicast probe conflict (8).", kPCTProgPreWait "probes n-n-m-n-u;"   "send;", true },
	{ "Multicast and unicast probe conflict (9).", kPCTProgPreWait "probes n-n-m-n-n-u;" "send;", true },
	
	// Two multicast probe conflicts
	
	{ "Two multicast probe conflicts (1).",        kPCTProgPreWait "probes m-m;"         "send;", true },
	{ "Two multicast probe conflicts (2).",        kPCTProgPreWait "probes m-n-m;"       "send;", true },
	{ "Two multicast probe conflicts (3).",        kPCTProgPreWait "probes m-n-n-m;"     "send;", true },
	{ "Two multicast probe conflicts (4).",        kPCTProgPreWait "probes n-m-m;"       "send;", true },
	{ "Two multicast probe conflicts (5).",        kPCTProgPreWait "probes n-m-n-m-n;"   "send;", true },
	{ "Two multicast probe conflicts (6).",        kPCTProgPreWait "probes n-m-n-n-m;"   "send;", true },
	{ "Two multicast probe conflicts (7).",        kPCTProgPreWait "probes n-n-m-m;"     "send;", true },
	{ "Two multicast probe conflicts (8).",        kPCTProgPreWait "probes n-n-m-n-m;"   "send;", true },
	{ "Two multicast probe conflicts (9).",        kPCTProgPreWait "probes n-n-m-n-n-m;" "send;", true },
};

#define kProbeConflictTestCaseCount		countof( kProbeConflictTestCases )

typedef struct
{
	DNSServiceRef				registration;		// Test service registration.
	NanoTime64					testStartTime;		// Test's start time.
	NanoTime64					startTime;			// Current test case's start time.
	MDNSColliderRef				collider;			// mDNS collider object.
	CFMutableArrayRef			results;			// Array of test case results.
	char *						serviceName;		// Test service's instance name as a string. (malloced)
	char *						serviceType;		// Test service's service type as a string. (malloced)
	uint8_t *					recordName;			// FQDN of collider's record (same as test service's records). (malloced)
	dispatch_source_t			sigSourceINT;		// SIGINT signal handler.
	dispatch_source_t			sigSourceTERM;		// SIGTERM signal handler.
	CFStringRef					exComputerName;		// Previous ComputerName.
	CFStringRef					exLocalHostName;	// Previous LocalHostName.
	CFStringEncoding			exCompNameEnc;		// Previous ComputerName's encoding.
	unsigned int				testCaseIndex;		// Index of the current test case.
	uint32_t					conflictIfIndex;	// Index of the interface that the collider is to operate on.
	uint32_t					registerIfIndex;	// Index of the interface to register the test service on.
	uint32_t					extraWaitMs;		// Extra amount of time to wait for renames in milliseconds.
	MDNSColliderProtocols		protocol;			// mDNS collider's IP protocol.
	char *						outputFilePath;		// File to write test results to. If NULL, write to stdout. (malloced)
	OutputFormatType			outputFormat;		// Format of test report output.
	Boolean						registered;			// True if the test service instance is currently registered.
	Boolean						testFailed;			// True if at least one test case failed.
	
}	ProbeConflictTestContext;

static void DNSSD_API
	_ProbeConflictTestRegisterCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		DNSServiceErrorType	inError,
		const char *		inName,
		const char *		inType,
		const char *		inDomain,
		void *				inContext );
static void		_ProbeConflictTestColliderStopHandler( void *inContext, OSStatus inError );
static OSStatus	_ProbeConflictTestStartNextTest( ProbeConflictTestContext *inContext );
static OSStatus	_ProbeConflictTestStopCurrentTest( ProbeConflictTestContext *inContext, Boolean inRenamed );
static void		_ProbeConflictTestFinalizeAndExit( ProbeConflictTestContext *inContext ) ATTRIBUTE_NORETURN;
static void		_ProbeConflictTestRestoreSystemNames( ProbeConflictTestContext *inContext );
static void		_ProbeConflictTestSignalHandler( void *inContext );

static void	ProbeConflictTestCmd( void )
{
	OSStatus						err;
	ProbeConflictTestContext *		context;
	const char *					serviceName;
	CFStringRef						computerName	= NULL;
	CFStringRef						localHostName	= NULL;
	char *							uniqueName;
	char							tag[ 6 + 1 ];
	
	context = (ProbeConflictTestContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	if( gProbeConflictTest_Interface )
	{
		err = InterfaceIndexFromArgString( gProbeConflictTest_Interface, &context->conflictIfIndex );
		require_noerr_quiet( err, exit );
	}
	else
	{
		err = _MDNSInterfaceGetAny( kMDNSInterfaceSubset_All, NULL, &context->conflictIfIndex );
		require_noerr_quiet( err, exit );
	}
	
	if( gProbeConflictTest_UseIPv6 )
	{
		if( gProbeConflictTest_UseIPv4 )
		{
			FPrintF( stderr, "error: --ipv4 and --ipv6 are mutually exclusive options.\n" );
			goto exit;
		}
		context->protocol = kMDNSColliderProtocol_IPv6;
	}
	else
	{
		context->protocol = kMDNSColliderProtocol_IPv4;
	}
	context->registerIfIndex = gProbeConflictTest_RegisterOnAny ? kDNSServiceInterfaceIndexAny : context->conflictIfIndex;
	
	// Make sure that the sum of kProbeConflictTestPostWaitMs and extraWaitMs doesn't wrap under uint32_t arithmetic.
	
	check_compile_time_code( INT_MAX <= ( UINT32_MAX - kProbeConflictTestPostWaitMs ) );
	err = CheckIntegerArgument( gProbeConflictTest_ExtraWaitMs, "extraWait", 0, INT_MAX );
	require_noerr_quiet( err, exit );
	
	context->extraWaitMs = (uint32_t) gProbeConflictTest_ExtraWaitMs;
	if( gProbeConflictTest_OutputFilePath )
	{
		context->outputFilePath = strdup( gProbeConflictTest_OutputFilePath );
		require_action( context->outputFilePath, exit, err = kNoMemoryErr );
	}
	
	err = OutputFormatFromArgString( gProbeConflictTest_OutputFormat, &context->outputFormat );
	require_noerr_quiet( err, exit );
	
	context->results = CFArrayCreateMutable( NULL, kProbeConflictTestCaseCount, &kCFTypeArrayCallBacks );
	require_action( context->results, exit, err = kNoMemoryErr );
	
	context->testStartTime = NanoTimeGetCurrent();
	
	// Set a unique ComputerName.
	
	computerName = SCDynamicStoreCopyComputerName( NULL, &context->exCompNameEnc );
	err = map_scerror( computerName );
	require_noerr( err, exit );
	
	_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag );
	ASPrintF( &uniqueName, "dnssdutil-pctest-computer-name-%s", tag );
	require_action( uniqueName, exit, err = kNoMemoryErr );
	
	err = _SetComputerNameWithUTF8CString( uniqueName );
	ForgetMem( &uniqueName );
	require_noerr( err, exit );
	context->exComputerName = computerName;
	computerName = NULL;
	
	// Set a unique LocalHostName.
	
	localHostName = SCDynamicStoreCopyLocalHostName( NULL );
	err = map_scerror( localHostName );
	require_noerr( err, exit );
	
	ASPrintF( &uniqueName, "dnssdutil-pctest-local-hostname-%s", tag );
	require_action( uniqueName, exit, err = kNoMemoryErr );
	
	err = _SetLocalHostNameWithUTF8CString( uniqueName );
	ForgetMem( &uniqueName );
	require_noerr( err, exit );
	context->exLocalHostName = localHostName;
	localHostName = NULL;
	
	// Set up SIGINT signal handler.
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), _ProbeConflictTestSignalHandler, context,
		&context->sigSourceINT );
	require_noerr( err, exit );
	dispatch_resume( context->sigSourceINT );
	
	// Set up SIGTERM signal handler.
	
	signal( SIGTERM, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGTERM, dispatch_get_main_queue(), _ProbeConflictTestSignalHandler, context,
		&context->sigSourceTERM );
	require_noerr( err, exit );
	dispatch_resume( context->sigSourceTERM );
	
	// Register the test service instance.
	
	serviceName = gProbeConflictTest_UseComputerName ? NULL : kProbeConflictTestService_DefaultName;
	
	ASPrintF( &context->serviceType, "_pctest-%s._udp", tag );
	require_action( context->serviceType, exit, err = kNoMemoryErr );
	
	err = DNSServiceRegister( &context->registration, 0, context->registerIfIndex, serviceName, context->serviceType,
		"local.", NULL, htons( kProbeConflictTestService_Port ), 0, NULL, _ProbeConflictTestRegisterCallback, context );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( context->registration, dispatch_get_main_queue() );
	require_noerr( err, exit );
	
	dispatch_main();
	
exit:
	CFReleaseNullSafe( computerName );
	CFReleaseNullSafe( localHostName );
	if( context ) _ProbeConflictTestRestoreSystemNames( context );
	ErrQuit( 1, "error: %#m\n", err );
}

//===========================================================================================================================
//	_ProbeConflictTestRegisterCallback
//===========================================================================================================================

static void DNSSD_API
	_ProbeConflictTestRegisterCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		DNSServiceErrorType	inError,
		const char *		inName,
		const char *		inType,
		const char *		inDomain,
		void *				inContext )
{
	OSStatus								err;
	ProbeConflictTestContext * const		context = (ProbeConflictTestContext *) inContext;
	
	Unused( inSDRef );
	Unused( inType );
	Unused( inDomain );
	
	err = inError;
	require_noerr( err, exit );
	
	if( !context->registered )
	{
		if( inFlags & kDNSServiceFlagsAdd )
		{
			uint8_t *			ptr;
			size_t				recordNameLen;
			unsigned int		len;
			uint8_t				name[ kDomainNameLengthMax ];
			
			context->registered = true;
			
			FreeNullSafe( context->serviceName );
			context->serviceName = strdup( inName );
			require_action( context->serviceName, exit, err = kNoMemoryErr );
			
			err = DomainNameFromString( name, context->serviceName, NULL );
			require_noerr( err, exit );
			
			err = DomainNameAppendString( name, context->serviceType, NULL );
			require_noerr( err, exit );
			
			err = DomainNameAppendString( name, "local", NULL );
			require_noerr( err, exit );
			
			ForgetMem( &context->recordName );
			err = DomainNameDup( name, &context->recordName, &recordNameLen );
			require_noerr( err, exit );
			require_fatal( recordNameLen > 0, "Record name length is zero." );	// Prevents dubious static analyzer warning.
			
			// Make the first label all caps so that it's easier to spot in system logs.
			
			ptr = context->recordName;
			for( len = *ptr++; len > 0; --len, ++ptr ) *ptr = (uint8_t) toupper_safe( *ptr );
			
			err = _ProbeConflictTestStartNextTest( context );
			require_noerr( err, exit );
		}
	}
	else
	{
		if( !( inFlags & kDNSServiceFlagsAdd ) )
		{
			context->registered = false;
			err = _ProbeConflictTestStopCurrentTest( context, true );
			require_noerr( err, exit );
		}
	}
	err = kNoErr;
	
exit:
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	_ProbeConflictTestColliderStopHandler
//===========================================================================================================================

static void	_ProbeConflictTestColliderStopHandler( void *inContext, OSStatus inError )
{
	OSStatus								err;
	ProbeConflictTestContext * const		context = (ProbeConflictTestContext *) inContext;
	
	err = inError;
	require_noerr_quiet( err, exit );
	
	ForgetCF( &context->collider );
	
	err = _ProbeConflictTestStopCurrentTest( context, false );
	require_noerr( err, exit );
	
	err = _ProbeConflictTestStartNextTest( context );
	require_noerr( err, exit );
	
exit:
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	_ProbeConflictTestStartNextTest
//===========================================================================================================================

static OSStatus	_ProbeConflictTestStartNextTest( ProbeConflictTestContext * const inContext )
{
	OSStatus							err;
	const ProbeConflictTestCase *		testCase;
	char *								program = NULL;
	uint32_t							totalWaitMs;
	
	check( !inContext->collider );
	
	if( inContext->testCaseIndex < kProbeConflictTestCaseCount )
	{
		testCase = &kProbeConflictTestCases[ inContext->testCaseIndex ];
	}
	else
	{
		_ProbeConflictTestFinalizeAndExit( inContext );
	}
	
	err = MDNSColliderCreate( dispatch_get_main_queue(), &inContext->collider );
	require_noerr( err, exit );
	
	totalWaitMs = kProbeConflictTestPostWaitMs + inContext->extraWaitMs;
	ASPrintF( &program, "%s wait %u;", testCase->program, totalWaitMs );
	require_action( program, exit, err = kNoMemoryErr );
	
	err = MDNSColliderSetProgram( inContext->collider, program );
	require_noerr( err, exit );
	
	err = MDNSColliderSetRecord( inContext->collider, inContext->recordName, kDNSServiceType_TXT,
		kProbeConflictTestTXTPtr, kProbeConflictTestTXTLen );
	require_noerr( err, exit );
	
	MDNSColliderSetProtocols( inContext->collider, inContext->protocol );
	MDNSColliderSetInterfaceIndex( inContext->collider, inContext->conflictIfIndex );
	MDNSColliderSetStopHandler( inContext->collider, _ProbeConflictTestColliderStopHandler, inContext );
	
	inContext->startTime = NanoTimeGetCurrent();
	err = MDNSColliderStart( inContext->collider );
	require_noerr( err, exit );
	
exit:
	ForgetMem( &program );
	return( err );
}

//===========================================================================================================================
//	_ProbeConflictTestStopCurrentTest
//===========================================================================================================================

#define kProbeConflictTestCaseResultKey_Description			CFSTR( "description" )
#define kProbeConflictTestCaseResultKey_StartTime			CFSTR( "startTime" )
#define kProbeConflictTestCaseResultKey_EndTime				CFSTR( "endTime" )
#define kProbeConflictTestCaseResultKey_ExpectedRename		CFSTR( "expectedRename" )
#define kProbeConflictTestCaseResultKey_ServiceName			CFSTR( "serviceName" )
#define kProbeConflictTestCaseResultKey_Passed				CFSTR( "passed" )

static OSStatus	_ProbeConflictTestStopCurrentTest( ProbeConflictTestContext *inContext, Boolean inRenamed )
{
	OSStatus							err;
	const ProbeConflictTestCase *		testCase;
	NanoTime64							now;
	Boolean								passed;
	char								startTime[ 32 ];
	char								endTime[ 32 ];
	
	now = NanoTimeGetCurrent();
	
	if( inContext->collider )
	{
		MDNSColliderSetStopHandler( inContext->collider, NULL, NULL );
		MDNSColliderStop( inContext->collider );
		CFRelease( inContext->collider );
		inContext->collider = NULL;
	}
	
	testCase = &kProbeConflictTestCases[ inContext->testCaseIndex ];
	passed = ( ( testCase->expectsRename && inRenamed ) || ( !testCase->expectsRename && !inRenamed ) ) ? true : false;
	if( !passed ) inContext->testFailed = true;
	
	_NanoTime64ToTimestamp( inContext->startTime, startTime, sizeof( startTime ) );
	_NanoTime64ToTimestamp( now, endTime, sizeof( endTime ) );
	
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, inContext->results,
		"{"
			"%kO=%s"	// description
			"%kO=%b"	// expectedRename
			"%kO=%s"	// startTime
			"%kO=%s"	// endTime
			"%kO=%s"	// serviceName
			"%kO=%b"	// passed
		"}",
		kProbeConflictTestCaseResultKey_Description,	testCase->description,
		kProbeConflictTestCaseResultKey_ExpectedRename,	testCase->expectsRename,
		kProbeConflictTestCaseResultKey_StartTime,		startTime,
		kProbeConflictTestCaseResultKey_EndTime,		endTime,
		kProbeConflictTestCaseResultKey_ServiceName,	inContext->serviceName,
		kProbeConflictTestCaseResultKey_Passed,			passed );
	require_noerr( err, exit );
	
	++inContext->testCaseIndex;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_ProbeConflictTestFinalizeAndExit
//===========================================================================================================================

#define kProbeConflictTestReportKey_StartTime				CFSTR( "startTime" )
#define kProbeConflictTestReportKey_EndTime					CFSTR( "endTime" )
#define kProbeConflictTestReportKey_ServiceType				CFSTR( "serviceType" )
#define kProbeConflictTestReportKey_RegistrationInterface	CFSTR( "registrationInterface" )
#define kProbeConflictTestReportKey_ConflictInterface		CFSTR( "conflictInterface" )
#define kProbeConflictTestReportKey_Index					CFSTR( "index" )
#define kProbeConflictTestReportKey_Name					CFSTR( "name" )
#define kProbeConflictTestReportKey_ExtraWaitMs				CFSTR( "extraWaitMs" )
#define kProbeConflictTestReportKey_Results					CFSTR( "results" )
#define kProbeConflictTestReportKey_Passed					CFSTR( "passed" )

static void	_ProbeConflictTestFinalizeAndExit( ProbeConflictTestContext *inContext )
{
	OSStatus				err;
	CFPropertyListRef		plist;
	NanoTime64				now;
	int						exitCode;
	char					startTime[ 32 ];
	char					endTime[ 32 ];
	char					registerIfName[ kInterfaceNameBufLen ];
	char					conflictIfName[ kInterfaceNameBufLen ];
	
	now = NanoTimeGetCurrent();
	
	check( !inContext->collider );
	
	_NanoTime64ToTimestamp( inContext->testStartTime, startTime, sizeof( startTime ) );
	_NanoTime64ToTimestamp( now, endTime, sizeof( endTime ) );
	InterfaceIndexToName( inContext->registerIfIndex, registerIfName );
	InterfaceIndexToName( inContext->conflictIfIndex, conflictIfName );
	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &plist,
		"{"
			"%kO=%s"	// startTime
			"%kO=%s"	// endTime
			"%kO=%s"	// serviceType
			"%kO="		// registrationInterface
			"{"
				"%kO=%lli"	// index
				"%kO=%s"	// name
			"}"
			"%kO="		// conflictInterface
			"{"
				"%kO=%lli"	// index
				"%kO=%s"	// name
			"}"
			"%kO=%lli"	// extraWaitMs
			"%kO=%O"	// results
			"%kO=%b"	// passed
		"}",
		kProbeConflictTestReportKey_StartTime,				startTime,
		kProbeConflictTestReportKey_EndTime,				endTime,
		kProbeConflictTestReportKey_ServiceType,			inContext->serviceType,
		kProbeConflictTestReportKey_RegistrationInterface,
		kProbeConflictTestReportKey_Index,					(int64_t) inContext->registerIfIndex,
		kProbeConflictTestReportKey_Name,					registerIfName,
		kProbeConflictTestReportKey_ConflictInterface,
		kProbeConflictTestReportKey_Index,					(int64_t) inContext->conflictIfIndex,
		kProbeConflictTestReportKey_Name,					conflictIfName,
		kProbeConflictTestReportKey_ExtraWaitMs,			(int64_t) inContext->extraWaitMs,
		kProbeConflictTestReportKey_Results,				inContext->results,
		kProbeConflictTestReportKey_Passed,					inContext->testFailed ? false : true );
	require_noerr( err, exit );
	ForgetCF( &inContext->results );
	
	err = OutputPropertyList( plist, inContext->outputFormat, inContext->outputFilePath );
	CFRelease( plist );
	require_noerr( err, exit );
	
exit:
	_ProbeConflictTestRestoreSystemNames( inContext );
	if( err )
	{
		FPrintF( stderr, "error: %#m\n", err );
		exitCode = 1;
	}
	else
	{
		exitCode = inContext->testFailed ? 2 : 0;
	}
	exit( exitCode );
}

//===========================================================================================================================
//	_ProbeConflictTestRestoreSystemNames
//===========================================================================================================================

static void	_ProbeConflictTestRestoreSystemNames( ProbeConflictTestContext *inContext )
{
	OSStatus		err;
	
	if( inContext->exComputerName )
	{
		err = _SetComputerName( inContext->exComputerName, inContext->exCompNameEnc );
		check_noerr( err );
		ForgetCF( &inContext->exComputerName );
	}
	if( inContext->exLocalHostName )
	{
		err = _SetLocalHostName( inContext->exLocalHostName );
		check_noerr( err );
		ForgetCF( &inContext->exLocalHostName );
	}
}

//===========================================================================================================================
//	_ProbeConflictTestSignalHandler
//===========================================================================================================================

static void	_ProbeConflictTestSignalHandler( void *inContext )
{
	_ProbeConflictTestRestoreSystemNames( inContext );
	FPrintF( stderr, "Probe conflict test got a SIGINT or SIGTERM signal, exiting...\n" );
	exit( 1 );
}

#if( MDNSRESPONDER_PROJECT )
//===========================================================================================================================
//    FallbackTestCmd
//===========================================================================================================================

typedef struct
{
	unsigned int		serverIndex;		// Index of server that is soley capable of answering query.
	
}	FallbackSubtestParams;

#define kFallbackTestSubtestCount		8

const FallbackSubtestParams		kFallbackSubtestParams[] = { { 2 }, { 4 }, { 1 }, { 3 }, { 2 }, { 1 }, { 4 }, { 3 } };
check_compile_time( countof( kFallbackSubtestParams ) == kFallbackTestSubtestCount );

typedef struct
{
	char *			hostname;	// Hostname to resolve.
	NanoTime64		startTime;	// Subtest's start time.
	NanoTime64		endTime;	// Subtest's end time.
	OSStatus		error;		// Subtest's current error.
	
}	FallbackSubtest;

typedef struct
{
	dispatch_queue_t			queue;			// Serial queue for test events.
	dispatch_semaphore_t		doneSem;		// Semaphore to signal when the test is done.
	DNSServiceRef				gai;			// Current DNSServiceGetAddrInfo request.
	dispatch_source_t			timer;			// Timer for enforcing time limit on current DNSServiceGetAddrInfo request.
	size_t						subtestIndex;	// Index of current subtest.
	pid_t						serverPID;		// PID of spawned test DNS server.
	OSStatus					error;			// Current test error.
	NanoTime64					startTime;		// Test's start time.
	NanoTime64					endTime;		// Test's end time.
	char *						serverCmd;		// Command used to invoke the test DNS server.
	char *						probeHostname;	// Hostname queried to verify that server is up and running.
	FallbackSubtest				subtests[ kFallbackTestSubtestCount ];
	Boolean						useRefused;		// True if server uses Refused RCODE for queries it's not allowed to answer.
	
}	FallbackTest;

static OSStatus	_FallbackTestCreate( FallbackTest **outTest );
static OSStatus	_FallbackTestRun( FallbackTest *inTest );
static void		_FallbackTestFree( FallbackTest *inTest );

ulog_define_ex( kDNSSDUtilIdentifier, FallbackTest, kLogLevelInfo, kLogFlags_None, "FallbackTest", NULL );
#define ft_ulog( LEVEL, ... )		ulog( &log_category_from_name( FallbackTest ), (LEVEL), __VA_ARGS__ )

static void	FallbackTestCmd( void )
{
	OSStatus				err;
	FallbackTest *			test		= NULL;
	CFPropertyListRef		plist		= NULL;
	OutputFormatType		outputFormat;
	size_t					i;
	CFMutableArrayRef		results;
	Boolean					testPassed	= false;
	Boolean					subtestFailed;
	char					startTime[ 32 ];
	char					endTime[ 32 ];
	
	err = CheckRootUser();
	require_noerr_quiet( err, exit );
	
	err = OutputFormatFromArgString( gFallbackTest_OutputFormat, &outputFormat );
	require_noerr_quiet( err, exit );
	
	err = _FallbackTestCreate( &test );
	require_noerr( err, exit );
	
	if( gFallbackTest_UseRefused ) test->useRefused = true;
	err = _FallbackTestRun( test );
	require_noerr( err, exit );
	
	_NanoTime64ToTimestamp( test->startTime, startTime, sizeof( startTime ) );
	_NanoTime64ToTimestamp( test->endTime, endTime, sizeof( endTime ) );
	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &plist,
		"{"
			"%kO=%s"	// startTime
			"%kO=%s"	// endTime
			"%kO=%s"	// serverCmd
			"%kO=[%@]"	// results
		"}",
		CFSTR( "startTime" ),	startTime,
		CFSTR( "endTime" ),		endTime,
		CFSTR( "serverCmd" ),	test->serverCmd,
		CFSTR( "results" ),		&results );
	require_noerr( err, exit );
	
	subtestFailed = false;
	check( test->subtestIndex == kFallbackTestSubtestCount );
	for( i = 0; i < kFallbackTestSubtestCount; ++i )
	{
		CFMutableDictionaryRef		resultDict;
		FallbackSubtest * const		subtest = &test->subtests[ i ];
		char						errorDesc[ 128 ];
		
		err = CFPropertyListAppendFormatted( kCFAllocatorDefault, results, "{%@}", &resultDict );
		require_noerr( err, exit );
		
		err = CFDictionarySetCString( resultDict, CFSTR( "name" ), subtest->hostname, kSizeCString );
		require_noerr( err, exit );
		
		_NanoTime64ToTimestamp( subtest->startTime, startTime, sizeof( startTime ) );
		err = CFDictionarySetCString( resultDict, CFSTR( "startTime" ), startTime, kSizeCString );
		require_noerr( err, exit );
		
		_NanoTime64ToTimestamp( subtest->endTime, endTime, sizeof( endTime ) );
		err = CFDictionarySetCString( resultDict, CFSTR( "endTime" ), endTime, kSizeCString );
		require_noerr( err, exit );
		
		SNPrintF( errorDesc, sizeof( errorDesc ), "%m", subtest->error );
		err = CFPropertyListAppendFormatted( kCFAllocatorDefault, resultDict,
			"%kO="
			"{"
				"%kO=%lli"	// code
				"%kO=%s"	// description
			"}",
			CFSTR( "error" ),
			CFSTR( "code" ),		(int64_t) subtest->error,
			CFSTR( "description" ),	errorDesc );
		require_noerr( err, exit );
		
		if( subtest->error ) subtestFailed = true;
	}
	if( !subtestFailed ) testPassed = true;
	CFPropertyListAppendFormatted( kCFAllocatorDefault, plist, "%kO=%b", CFSTR( "pass" ), testPassed );
	
	err = OutputPropertyList( plist, outputFormat, gFallbackTest_OutputFilePath );
	require_noerr( err, exit );
	
exit:
	if( test ) _FallbackTestFree( test );
	CFReleaseNullSafe( plist );
	gExitCode = err ? 1 : ( testPassed ? 0 : 2 );
}

//===========================================================================================================================

static void		_FallbackTestStart( void *inContext );
static void		_FallbackTestStop( FallbackTest *inTest, OSStatus inError );
static void DNSSD_API
	_FallbackTestProbeGAICallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext );
static void		_FallbackTestProbeTimerHandler( void *inContext );
static void DNSSD_API
	_FallbackTestGetAddrInfoCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext );
static void		_FallbackTestGAITimerHandler( void *inContext );
static OSStatus	_FallbackTestStartSubtest( FallbackTest *inTest );
static void		_FallbackTestForgetSources( FallbackTest *inTest );

#define kFallbackTestProbeTimeLimitSecs		 5
#define kFallbackTestGAITimeLimitSecs		75

static OSStatus	_FallbackTestCreate( FallbackTest **outTest )
{
	OSStatus			err;
	FallbackTest *		test;
	
	test = (FallbackTest *) calloc( 1, sizeof( *test ) );
	require_action( test, exit, err = kNoMemoryErr );
	
	test->error		= kInProgressErr;
	test->serverPID	= -1;
	
	test->queue = dispatch_queue_create( "com.apple.dnssdutil.fallback-test", DISPATCH_QUEUE_SERIAL );
	require_action( test->queue, exit, err = kNoResourcesErr );
	
	test->doneSem = dispatch_semaphore_create( 0 );
	require_action( test->doneSem, exit, err = kNoResourcesErr );
	
	*outTest = test;
	test = NULL;
	err = kNoErr;
	
exit:
	if( test ) _FallbackTestFree( test );
	return( err );
}

//===========================================================================================================================

static OSStatus	_FallbackTestRun( FallbackTest *inTest )
{
	dispatch_async_f( inTest->queue, inTest, _FallbackTestStart );
	dispatch_semaphore_wait( inTest->doneSem, DISPATCH_TIME_FOREVER );
	return( inTest->error );
}

//===========================================================================================================================

static void	_FallbackTestFree( FallbackTest *inTest )
{
	size_t		i;
	
	check( !inTest->gai );
	check( !inTest->timer );
	check( inTest->serverPID < 0 );
	
	ForgetMem( &inTest->serverCmd );
	ForgetMem( &inTest->probeHostname );
	dispatch_forget( &inTest->queue );
	dispatch_forget( &inTest->doneSem );
	for( i = 0; i < kFallbackTestSubtestCount; ++i )
	{
		FallbackSubtest * const		subtest = &inTest->subtests[ i ];
		
		ForgetMem( &subtest->hostname );
	}
	free( inTest );
}

//===========================================================================================================================

static void	_FallbackTestStart( void *inContext )
{
	OSStatus					err;
	FallbackTest * const		test = (FallbackTest *) inContext;
	char						tag[ 6 + 1 ];
	
	test->startTime = NanoTimeGetCurrent();
	
	// The "dnssdutil server" command will create a resolver entry for the server's "d.test." domain containing an array
	// of the server's IP addresses. Because configd favors IPv6 addresses, when there's a mix of IPv4 and IPv6
	// addresses, configd may rearrange the array in order to ensure that IPv6 addresses come before the IPv4 addresses.
	// To preserve the original address order, the server is specified to run in IPv6-only mode. This way,
	// mDNSResponder's view of the address will be such that address with index value 1 is first, address with index
	// value 2 is second, etc.
	
	ASPrintF( &test->serverCmd,
		DNSSDUTIL_TEST_DNS_SERVER_COMMAND_PREAMBLE " --loopback --follow %lld --ipv6 --extraIPv6 3%s%s",
		(int64_t) getpid(), test->useRefused ? " --useRefused" : "" );
	require_action_quiet( test->serverCmd, exit, err = kUnknownErr );
	
	err = _SpawnCommand( &test->serverPID, "/dev/null", "/dev/null", "%s", test->serverCmd );
	require_noerr( err, exit );
	
	ASPrintF( &test->probeHostname, "tag-fallback-test-probe-%s.count-1.ipv4.ttl-900.d.test.",
		_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
	require_action( test->probeHostname, exit, err = kNoMemoryErr );
	
	ft_ulog( kLogLevelInfo, "Starting GetAddrInfo request for %s\n", test->probeHostname );
	
	err = DNSServiceGetAddrInfo( &test->gai, 0, kDNSServiceInterfaceIndexAny, kDNSServiceProtocol_IPv4,
		test->probeHostname, _FallbackTestProbeGAICallback, test );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( test->gai, test->queue );
	require_noerr( err, exit );
	
	err = DispatchTimerOneShotCreate( dispatch_time_seconds( kFallbackTestProbeTimeLimitSecs ),
		kFallbackTestProbeTimeLimitSecs * ( UINT64_C_safe( kNanosecondsPerSecond ) / 10 ), test->queue,
		_FallbackTestProbeTimerHandler, test, &test->timer );
	require_noerr( err, exit );
	dispatch_resume( test->timer );
	
exit:
	if( err ) _FallbackTestStop( test, err );
}

//===========================================================================================================================

static void	_FallbackTestStop( FallbackTest *inTest, OSStatus inError )
{
	inTest->error	= inError;
	inTest->endTime	= NanoTimeGetCurrent();
	_FallbackTestForgetSources( inTest );
	if( inTest->serverPID >= 0 )
	{
		OSStatus		err;
		
		err = kill( inTest->serverPID, SIGTERM );
		err = map_global_noerr_errno( err );
		check_noerr( err );
		inTest->serverPID = -1;
	}
	dispatch_semaphore_signal( inTest->doneSem );
}

//===========================================================================================================================

static void DNSSD_API
	_FallbackTestProbeGAICallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext )
{
	OSStatus					err;
	FallbackTest * const		test = (FallbackTest *) inContext;
	
	Unused( inSDRef );
	Unused( inInterfaceIndex );
	Unused( inHostname );
	Unused( inTTL );
	
	if( ( inFlags & kDNSServiceFlagsAdd ) && !inError )
	{
		_FallbackTestForgetSources( test );
		
		ft_ulog( kLogLevelInfo, "Probe: Got GAI address %##a for %s\n", inSockAddr, test->probeHostname );
		
		check( test->subtestIndex == 0 );
		err = _FallbackTestStartSubtest( test );
		require_noerr( err, exit );
	}
	err = kNoErr;
	
exit:
	if( err ) _FallbackTestStop( test, err );
}

//===========================================================================================================================

static void	_FallbackTestProbeTimerHandler( void *inContext )
{
	FallbackTest * const		test = (FallbackTest *) inContext;
	
	ft_ulog( kLogLevelInfo, "GetAddrInfo probe request for \"%s\" timed out.\n", test->probeHostname );
	_FallbackTestStop( test, kNotPreparedErr );
}

//===========================================================================================================================

static void DNSSD_API
	_FallbackTestGetAddrInfoCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext )
{
	OSStatus					err;
	struct sockaddr_in			sin;
	FallbackTest * const		test		= (FallbackTest *) inContext;
	FallbackSubtest *const		subtest		= &test->subtests[ test->subtestIndex ];
	Boolean						complete	= false;
	
	Unused( inSDRef );
	Unused( inInterfaceIndex );
	Unused( inTTL );
	
	_FallbackTestForgetSources( test );
	
	if( strcasecmp( inHostname, subtest->hostname ) != 0 )
	{
		ft_ulog( kLogLevelError, "GetAddrInfo(%s) result: Got unexpected hostname \"%s\".\n",
			subtest->hostname, inHostname );
		err = kUnexpectedErr;
		goto done;
	}
	if( inError )
	{
		ft_ulog( kLogLevelError, "GetAddrInfo(%s) result: Got unexpected error %#m.\n", subtest->hostname, inError );
		err = inError;
		goto done;
	}
	if( ( inFlags & kDNSServiceFlagsAdd ) == 0 )
	{
		ft_ulog( kLogLevelError, "GetAddrInfo(%s) result: Missing Add flag.\n", subtest->hostname );
		err = kUnexpectedErr;
		goto done;
	}
	_SockAddrInitIPv4( &sin, kDNSServerBaseAddrV4 + 1, 0 );
	if( SockAddrCompareAddr( inSockAddr, &sin ) != 0 )
	{
		ft_ulog( kLogLevelError, "GetAddrInfo(%s) result: Got unexpected address %##a (expected %##a).\n",
			subtest->hostname, inSockAddr, &sin );
		err = kUnexpectedErr;
		goto done;
	}
	ft_ulog( kLogLevelInfo, "Subtest %zu/%d: Got expected GAI address %##a for %s\n",
		test->subtestIndex + 1, kFallbackTestSubtestCount, inSockAddr, subtest->hostname );
	err = kNoErr;
	
done:
	subtest->endTime	= NanoTimeGetCurrent();
	subtest->error		= err;
	err = kNoErr;
	if( ++test->subtestIndex < kFallbackTestSubtestCount )
	{
		err = _FallbackTestStartSubtest( test );
		require_noerr( err, exit );
	}
	else
	{
		complete = true;
	}
	
exit:
	if( err || complete ) _FallbackTestStop( test, err );
}

//===========================================================================================================================

static void	_FallbackTestGAITimerHandler( void *inContext )
{
	OSStatus					err;
	FallbackTest * const		test		= (FallbackTest *) inContext;
	FallbackSubtest * const		subtest		= &test->subtests[ test->subtestIndex ];
	Boolean						complete	= false;
	
	_FallbackTestForgetSources( test );
	
	ft_ulog( kLogLevelInfo, "GetAddrInfo request for \"%s\" timed out.\n", subtest->hostname );
	
	subtest->endTime	= NanoTimeGetCurrent();
	subtest->error		= kTimeoutErr;
	if( ++test->subtestIndex < kFallbackTestSubtestCount )
	{
		err = _FallbackTestStartSubtest( test );
		require_noerr( err, exit );
	}
	else
	{
		complete = true;
		err = kNoErr;
	}
	
exit:
	if( err || complete ) _FallbackTestStop( test, err );
}

//===========================================================================================================================

static OSStatus	_FallbackTestStartSubtest( FallbackTest *inTest )
{
	OSStatus					err;
	FallbackSubtest * const		subtest = &inTest->subtests[ inTest->subtestIndex ];
	char						tag[ 6 + 1 ];
	
	subtest->error		= kInProgressErr;
	subtest->startTime	= NanoTimeGetCurrent();
	
	ForgetMem( &subtest->hostname );
	ASPrintF( &subtest->hostname, "index-%u.tag-fallback-test-%s.count-1.ipv4.ttl-900.d.test.",
		kFallbackSubtestParams[ inTest->subtestIndex ].serverIndex,
		_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
	require_action( subtest->hostname, exit, err = kNoMemoryErr );
	
	ft_ulog( kLogLevelInfo, "Starting GetAddrInfo request for %s\n", subtest->hostname );
	
	check( !inTest->gai );
	err = DNSServiceGetAddrInfo( &inTest->gai, 0, kDNSServiceInterfaceIndexAny, kDNSServiceProtocol_IPv4,
		subtest->hostname, _FallbackTestGetAddrInfoCallback, inTest );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( inTest->gai, inTest->queue );
	require_noerr( err, exit );
	
	check( !inTest->timer );
	err = DispatchTimerOneShotCreate( dispatch_time_seconds( kFallbackTestGAITimeLimitSecs ),
		kFallbackTestGAITimeLimitSecs * ( UINT64_C_safe( kNanosecondsPerSecond ) / 10 ), inTest->queue,
		_FallbackTestGAITimerHandler, inTest, &inTest->timer );
	require_noerr( err, exit );
	dispatch_resume( inTest->timer );
	
exit:
	return( err );
}

//===========================================================================================================================

static void	_FallbackTestForgetSources( FallbackTest *inTest )
{
	DNSServiceForget( &inTest->gai );
	dispatch_source_forget( &inTest->timer );
}

//===========================================================================================================================
//    ExpensiveConstrainedsTestCmd
//===========================================================================================================================

#define NOTIFICATION_TIME_THRESHOLD 1500    // The maximum wating time allowed before notification happens
#define TEST_REPETITION             2       // the number of repetition that one test has to passed
#define LOOPBACK_INTERFACE_NAME     "lo0"
#define WIFI_TEST_QUESTION_NAME     "www.example.com"
#define EXPENSIVE_CONSTRAINED_MAX_RETRIES 1
#define EXPENSIVE_CONSTRAINED_TEST_INTERVAL 5
// Use "-n tag-expensive-test.ttl-86400.d.test." to run the test locally
// #define LOOPBACK_TEST_QUESTION_NAME "tag-expensive-test.ttl-86400.d.test."

#define EXPENSIVE_CONSTRAINED_TEST_REPORT_KEY_START_TIME                CFSTR( "Start Time" )
#define EXPENSIVE_CONSTRAINED_TEST_REPORT_KEY_END_TIME                  CFSTR( "End Time" )
#define EXPENSIVE_CONSTRAINED_TEST_REPORT_KEY_ALL_PASSED                CFSTR( "All Tests Passed" )
#define EXPENSIVE_CONSTRAINED_TEST_REPORT_KEY_SUBTEST_RESULT            CFSTR( "Subtest Results" )

#define EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_START_TIME             CFSTR( "Start Time" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_END_TIME               CFSTR( "End Time" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_QNAME                  CFSTR( "Question Name" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_FLAGS                  CFSTR( "DNS Service Flags" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_PROTOCOLS              CFSTR( "Protocols" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_INTERFACE_INDEX        CFSTR( "Interface Index" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_INTERFACE_NAME         CFSTR( "Interface Name" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_RESULT                 CFSTR( "Result" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_ERROR                  CFSTR( "Error Description" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_TEST_PROGRESS          CFSTR( "Test Progress" )

#define EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_START_TIME           CFSTR( "Start Time" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_END_TIME             CFSTR( "End Time" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_STATE                CFSTR( "State" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_EXPECT_RESULT        CFSTR( "Expected Result" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_ACTUAL_RESULT        CFSTR( "Actual Result" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_EXPENSIVE_PREV_NOW   CFSTR( "Expensive Prev->Now" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_CONSTRAINED_PREV_NOW CFSTR( "Constrained Prev->Now" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_CALL_BACK            CFSTR( "Call Back" )

#define EXPENSIVE_CONSTRAINED_SUBTEST_ACTUAL_RESULT_KEY_TIMESTAMP       CFSTR( "Timestamp" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_ACTUAL_RESULT_KEY_NAME            CFSTR( "Answer Name" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_ACTUAL_RESULT_KEY_FLAGS           CFSTR( "Add or Remove" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_ACTUAL_RESULT_KEY_INTERFACE       CFSTR( "Interface Index" )
#define EXPENSIVE_CONSTRAINED_SUBTEST_ACTUAL_RESULT_KEY_ADDRESS         CFSTR( "Address" )

// All the states that ends with _PREPARE represents the state where the test state is reset and initialized.
enum ExpensiveConstrainedTestState
{
    TEST_BEGIN,
    TEST_EXPENSIVE_PREPARE,
    TEST_EXPENSIVE,                     // Test if mDNSResponder can handle "expensive" status change of the corresponding interface
    TEST_CONSTRAINED_PREPARE,
    TEST_CONSTRAINED,                   // Test if mDNSResponder can handle "constrained" status change of the corresponding interface
    TEST_EXPENSIVE_CONSTRAINED_PREPARE,
    TEST_EXPENSIVE_CONSTRAINED,          // Test if mDNSResponder can handle "expensive" and "constrained" status change of the corresponding interface at the same time
    TEST_FAILED,
    TEST_SUCCEEDED
};
enum ExpensiveConstrainedTestOperation
{
    RESULT_ADD, // received response for the given query, which means mDNSResponder is able to send out the query over the interface, because the interface status is changed.
    RESULT_RMV, // received negative response for the given query, which means mDNSResponder is not able to send out the query over the interface, because the interface status is changed.
    NO_UPDATE   // no status update notification
};

typedef struct
{
    uint32_t                    subtestIndex;           // The index of parameter for the subtest
    DNSServiceRef               opRef;                  // sdRef for the DNSServiceGetAddrInfo operation.
    const char *                name;                   // Hostname to resolve.
    DNSServiceFlags             flags;                  // Flags argument for DNSServiceGetAddrInfo().
    DNSServiceProtocol          protocols;              // Protocols argument for DNSServiceGetAddrInfo().
    uint32_t                    ifIndex;                // Interface index argument for DNSServiceGetAddrInfo().
    char                        ifName[IFNAMSIZ];       // Interface name for the given interface index.
    dispatch_source_t           timer;                  // The test will check if the current behavior is valid, which is called by
                                                        // the timer per 2s.
    pid_t                       serverPID;
    Boolean                     isExpensivePrev;        // If the interface is expensive in the previous test step.
    Boolean                     isExpensiveNow;         // If the interface is expensive now.
    Boolean                     isConstrainedPrev;      // If the interface is constrained in the previous test step.
    Boolean                     isConstrainedNow;       // If the interface is constrained now.
    Boolean                     startFromExpensive;     // All the test will start from expensive/constrained interface, so there won's be an answer until the interface is changed.
    uint8_t                     numOfRetries;           // the number of retries we can have if the test fail
    struct timeval              updateTime;             // The time when interface status(expensive or constrained) is changed.
    struct timeval              notificationTime;       // The time when callback function, which is passed to DNSServiceGetAddrInfo, gets called.
    uint32_t                    counter;                // To record how many times the test has repeated.
    enum ExpensiveConstrainedTestState          state;              // The current test state.
    enum ExpensiveConstrainedTestOperation      expectedOperation;  // the test expects this kind of notification
    enum ExpensiveConstrainedTestOperation      operation;          // represents what notification the callback function gets.

    NanoTime64                  testReport_startTime;       // when the entire test starts
    CFMutableArrayRef           subtestReport;              // stores the log message for every subtest
    NanoTime64                  subtestReport_startTime;    // when the subtest starts
    CFMutableArrayRef           subtestProgress;            // one test iteration
    NanoTime64                  subtestProgress_startTime;  // when the test iteration starts
    CFMutableArrayRef           subtestProgress_callBack;   // array of ADD/REMOVE events
    char *                      outputFilePath;             // File to write test results to. If NULL, then write to stdout. (malloced)
    OutputFormatType            outputFormat;               // Format of test report output.
} ExpensiveConstrainedContext;

// structure that controls how the subtest is run
typedef struct
{
    const char  *qname;                 // the name of the query, when the ends with ".d.test.", test will send query to local DNS server
    Boolean     deny_expensive;         // if the query should avoid using expensive interface
    Boolean     deny_constrained;       // if the query should avoid using constrained interface
    Boolean     start_from_expensive;   // if the query should starts from using an expensive interface
    Boolean     ipv4_query;             // only allow IPv4 query
    Boolean     ipv6_query;             // only allow IPv6 query
    int8_t      test_passed;            // if the subtest passes
} ExpensiveConstrainedTestParams;

static ExpensiveConstrainedTestParams   ExpensiveConstrainedSubtestParams[] =
{
//  qname                                                   deny_expensive  deny_constrained    start_from_expensive    ipv4_query  ipv6_query
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    true,           false,              false,                  true,       true,       -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    true,           false,              true,                   true,       true,       -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    false,          true,               false,                  true,       true,       -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    false,          true,               true,                   true,       true,       -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    true,           true,               false,                  true,       true,       -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    true,           true,               true,                   true,       true,       -1},
// IPv4 Only
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    true,           false,              false,                  true,       false,      -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    true,           false,              true,                   true,       false,      -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    false,          true,               false,                  true,       false,      -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    false,          true,               true,                   true,       false,      -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    true,           true,               false,                  true,       false,      -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    true,           true,               true,                   true,       false,      -1},
// IPv6 Only
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    true,           false,              false,                  false,      true,       -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    true,           false,              true,                   false,      true,       -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    false,          true,               false,                  false,      true,       -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    false,          true,               true,                   false,      true,       -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    true,           true,               false,                  false,      true,       -1},
    {"tag-expensive_constrained-test.ttl-86400.d.test.",    true,           true,               true,                   false,      true,       -1}
};

static void ExpensiveConstrainedSetupLocalDNSServer( ExpensiveConstrainedContext *context );
static void ExpensiveConstrainedStartTestHandler( ExpensiveConstrainedContext *context );
static void ExpensiveConstrainedStopTestHandler( ExpensiveConstrainedContext *context );
static void ExpensiveConstrainedSetupTimer( ExpensiveConstrainedContext *context, uint32_t second );
static void ExpensiveConstrainedTestTimerEventHandler( ExpensiveConstrainedContext *context );
static void DNSSD_API
    ExpensiveConstrainedCallback(
        DNSServiceRef           inSDRef,
        DNSServiceFlags         inFlags,
        uint32_t                inInterfaceIndex,
        DNSServiceErrorType     inError,
        const char *            inHostname,
        const struct sockaddr * inSockAddr,
        uint32_t                inTTL,
        void *                  inContext );
static void ExpensiveConstrainedInitializeContext( ExpensiveConstrainedContext *context );
static void ExpensiveConstrainedStopAndCleanTheTest( ExpensiveConstrainedContext *context );
static void ExpensiveConstrainedSubtestProgressReport( ExpensiveConstrainedContext *context );
static void ExpensiveConstrainedSubtestReport( ExpensiveConstrainedContext *context, const char *error_description );
static void ExpensiveConstrainedFinalResultReport( ExpensiveConstrainedContext *context, Boolean allPassed );
static const char *ExpensiveConstrainedProtocolString(DNSServiceProtocol protocol);
static const char *ExpensiveConstrainedStateString(enum ExpensiveConstrainedTestState state);
static const char *ExpensiveConstrainedOperationString(enum ExpensiveConstrainedTestOperation operation);
static Boolean expensiveConstrainedEndsWith( const char *str, const char *suffix );

//===========================================================================================================================
//    ExpensiveConstrainedTestCmd
//===========================================================================================================================

static void ExpensiveConstrainedTestCmd( void )
{
    OSStatus                        err;
    dispatch_source_t               signalSource   = NULL;
    ExpensiveConstrainedContext *   context        = NULL;

    // Set up SIGINT handler.
    signal( SIGINT, SIG_IGN );
    err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), Exit, kExitReason_SIGINT, &signalSource );
    require_noerr( err, exit );
    dispatch_resume( signalSource );

    // create the test context
    context = (ExpensiveConstrainedContext *) calloc( 1, sizeof(*context) );
    require_action( context, exit, err = kNoMemoryErr );

    // get the command line option
    err = OutputFormatFromArgString( gExpensiveConstrainedTest_OutputFormat, &context->outputFormat );
    require_noerr_quiet( err, exit );
    if ( gExpensiveConstrainedTest_OutputFilePath )
    {
        context->outputFilePath = strdup( gExpensiveConstrainedTest_OutputFilePath );
        require_noerr_quiet( context->outputFilePath, exit );
    }

    // initialize context
    context->subtestIndex = 0;
    context->numOfRetries = EXPENSIVE_CONSTRAINED_MAX_RETRIES;

    // initialize the CFArray used to store the log
    context->subtestReport = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
    context->testReport_startTime = NanoTimeGetCurrent();

    // setup local DNS server
    ExpensiveConstrainedSetupLocalDNSServer( context );

    ExpensiveConstrainedStartTestHandler( context );

    dispatch_main();

exit:
    exit( 1 );
}

//===========================================================================================================================
//    ExpensiveConstrainedSetupLocalDNSServer
//===========================================================================================================================

static void ExpensiveConstrainedSetupLocalDNSServer( ExpensiveConstrainedContext *context )
{
    pid_t current_pid = getpid();
    OSStatus err = _SpawnCommand( &context->serverPID, NULL, NULL, "dnssdutil server -l --port 0 --follow %d", current_pid );
    if (err != 0)
    {
        FPrintF( stdout, "dnssdutil server -l --port 0 --follow <PID> failed, error: %d\n", err );
        exit( 1 );
    }
    sleep(2);
}

//===========================================================================================================================
//    ExpensiveConstrainedStartTestHandler
//===========================================================================================================================

static void ExpensiveConstrainedStartTestHandler( ExpensiveConstrainedContext *context )
{
    // setup 3s timer
    ExpensiveConstrainedSetupTimer( context, EXPENSIVE_CONSTRAINED_TEST_INTERVAL );

    // set the event handler for the 3s timer
    dispatch_source_set_event_handler( context->timer, ^{
        ExpensiveConstrainedTestTimerEventHandler( context );
    } );

    dispatch_resume( context->timer );
}

//===========================================================================================================================
//    ExpensiveConstrainedStartTestHandler
//===========================================================================================================================

static void ExpensiveConstrainedStopTestHandler( ExpensiveConstrainedContext *context )
{
    dispatch_cancel( context->timer );
    dispatch_release( context->timer );
    context->timer = NULL;
}

//===========================================================================================================================
//    ExpensiveConstrainedSetupTimer
//===========================================================================================================================

static void ExpensiveConstrainedSetupTimer( ExpensiveConstrainedContext *context, uint32_t second )
{
    // set the timer source, the event handler will be called for every "second" seconds
    context->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue() );
    if ( context->timer == NULL )
    {
        FPrintF( stdout, "dispatch_source_create:DISPATCH_SOURCE_TYPE_TIMER failed\n" );
        exit( 1 );
    }
    // the first block will be put into the queue "second"s after calling dispatch_resume
    dispatch_source_set_timer( context->timer, dispatch_time( DISPATCH_TIME_NOW, second * NSEC_PER_SEC ),
                               (unsigned long long)(second) * NSEC_PER_SEC, 100ull * NSEC_PER_MSEC );
}

//===========================================================================================================================
//    ExpensiveConstrainedTestTimerEventHandler
//===========================================================================================================================

static void ExpensiveConstrainedTestTimerEventHandler( ExpensiveConstrainedContext *context )
{
    OSStatus    err;
    char        buffer[ 1024 ];
    const char *errorDescription = NULL;

    // do not log the state if we are in transition state
    if (context->state != TEST_BEGIN
        && context->state != TEST_SUCCEEDED
        && context->state != TEST_CONSTRAINED_PREPARE
        && context->state != TEST_EXPENSIVE_CONSTRAINED_PREPARE)
        ExpensiveConstrainedSubtestProgressReport( context );

    switch ( context->state ) {
        case TEST_BEGIN:
        {
            ExpensiveConstrainedStopTestHandler( context );

            // clear mDNSResponder cache
            err = systemf( NULL, "killall -HUP mDNSResponder" );
            require_noerr_action( err, test_failed, errorDescription = "systemf failed");

            // initialize the global parameters
            ExpensiveConstrainedInitializeContext( context );

            // The local DNS server is set up on the local only interface.
            gExpensiveConstrainedTest_Interface = LOOPBACK_INTERFACE_NAME;
            strncpy( context->ifName, gExpensiveConstrainedTest_Interface, sizeof( context->ifName ) );

            // The local DNS server is unscoped, so we must set our question to unscoped.
            context->ifIndex = kDNSServiceInterfaceIndexAny;

            // The question name must end with "d.test.", "tag-expensive-test.ttl-86400.d.test." for example, then the test will
            // use the local dns server set up previously to run the test locally.
            require_action( gExpensiveConstrainedTest_Name != NULL && expensiveConstrainedEndsWith( gExpensiveConstrainedTest_Name, "d.test." ), test_failed,
                           SNPrintF( buffer, sizeof( buffer ), "The question name (%s) must end with \"d.test.\".\n", gExpensiveConstrainedTest_Name );
                           errorDescription = buffer );

            // get the quesion name
            context->name = gExpensiveConstrainedTest_Name;

            // set the initial state for the interface
            context->startFromExpensive = gExpensiveConstrainedTest_StartFromExpensive;
            err = systemf( NULL, "ifconfig %s %sexpensive && ifconfig %s -constrained", context->ifName, context->startFromExpensive ? "" : "-", context->ifName );
            require_noerr_action( err, test_failed, errorDescription = "systemf failed");
            sleep( 5 ); // wait for 5s to allow the interface change event de delivered to others

            // get question flag
            if ( gExpensiveConstrainedTest_DenyExpensive )    context->flags     |= kDNSServiceFlagsDenyExpensive;
            if ( gExpensiveConstrainedTest_DenyConstrained )  context->flags     |= kDNSServiceFlagsDenyConstrained;
            if ( gExpensiveConstrainedTest_ProtocolIPv4 )     context->protocols |= kDNSServiceProtocol_IPv4;
            if ( gExpensiveConstrainedTest_ProtocolIPv6 )     context->protocols |= kDNSServiceProtocol_IPv6;

            // prevent mDNSResponder from doing extra path evaluation and changing the interface to others(such as Bluetooth)
            #if( TARGET_OS_WATCH )
            context->flags |= kDNSServiceFlagsPathEvaluationDone;
            #endif

            // start the query
            DNSServiceGetAddrInfo( &context->opRef, context->flags, context->ifIndex, context->protocols, context->name, ExpensiveConstrainedCallback, context );

            // set the initial test status
            context->subtestReport_startTime    = NanoTimeGetCurrent();
            context->subtestProgress_startTime  = NanoTimeGetCurrent();
            context->state                      = TEST_EXPENSIVE_PREPARE; // start from expensive test
            context->isExpensiveNow             = context->startFromExpensive ? true : false;
            context->isConstrainedNow           = false;
            context->expectedOperation          = context->isExpensiveNow && ( context->flags & kDNSServiceFlagsDenyExpensive ) ? NO_UPDATE : RESULT_ADD;
            context->operation                  = NO_UPDATE;
            context->subtestProgress            = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks);
            require_action( context->subtestProgress != NULL, test_failed, errorDescription = "CFArrayCreateMutable failed" );
            context->subtestProgress_callBack   = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks);
            require_action( context->subtestProgress != NULL, test_failed, errorDescription = "CFArrayCreateMutable failed" );

            // set the queue where the callback will be called when there is an answer for the query
            err = DNSServiceSetDispatchQueue( context->opRef, dispatch_get_main_queue() );
            require_noerr( err, test_failed );

            ExpensiveConstrainedStartTestHandler( context );
        }
            break;
        case TEST_EXPENSIVE_PREPARE:
            require_action( context->isConstrainedNow == false, test_failed,
                            SNPrintF( buffer, sizeof( buffer ), "Interface %s should be unconstrained.\n", context->ifName );
                            errorDescription = buffer );
            require_action( context->expectedOperation == context->operation, test_failed,
                            errorDescription = "Operation is not expected" );

            context->subtestProgress_startTime  = NanoTimeGetCurrent();
            context->state                      = TEST_EXPENSIVE;   // begin to test expensive flag
            context->counter                    = 0;                // the number of test repetition that has passed
            context->isExpensivePrev            = context->isExpensiveNow;
            context->isExpensiveNow             = !context->isExpensiveNow; // flip the expensive status
            context->isConstrainedPrev          = false;            // the interface is currently unconstrained
            context->isConstrainedNow           = false;            // the interface will be unconstrained in the current test
            if ( gExpensiveConstrainedTest_DenyExpensive )
                context->expectedOperation      = context->isExpensiveNow ? RESULT_RMV : RESULT_ADD;
            else
                context->expectedOperation      = NO_UPDATE;
            context->operation                  = NO_UPDATE;        // NO_UPDATE means the call back function has not been called
            context->subtestProgress_callBack   = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
            require_action( context->subtestProgress_callBack != NULL, test_failed, errorDescription = "CFArrayCreateMutable failed" );

            err = systemf( NULL, "ifconfig %s %sexpensive", context->ifName, context->isExpensiveNow ? "" : "-" );
            require_noerr_action( err, test_failed, errorDescription = "systemf failed" );

            // record the starting timestamp
            gettimeofday( &context->updateTime, NULL );

            break;
        case TEST_EXPENSIVE:
            // Since we are testing expensive flag, we should always turn the expensive flag on and off.
            require_action( context->isExpensivePrev ^ context->isExpensiveNow, test_failed,
                            SNPrintF( buffer, sizeof( buffer ), "The current expensive status should be different with the previous one: %d -> %d\n", context->isExpensivePrev, context->isExpensiveNow);
                            errorDescription = buffer );
            // constrained flag is always turned off when testing expensive
            require_action( context->isConstrainedNow == false, test_failed,
                            SNPrintF( buffer, sizeof( buffer ), "The interface %s should be unconstrained when testing \"expensive\"\n", context->ifName );
                            errorDescription = buffer );
            require_action( context->expectedOperation == context->operation, test_failed, errorDescription = "Operation is not expected" );

            context->counter++; // one test repetition has passed
            if ( context->counter == TEST_REPETITION ) // expensive test finished
            {
                // prepare to test constrained flag
                context->state = TEST_CONSTRAINED_PREPARE;

                // reset the interface
                err = systemf( NULL, "ifconfig %s -expensive && ifconfig %s -constrained", context->ifName, context->ifName );
                require_noerr_action( err, test_failed, errorDescription = "systemf failed" );

                context->isExpensiveNow = false;
                context->isConstrainedNow = false;
                gettimeofday( &context->updateTime, NULL );
            }
            else
            {
                context->subtestProgress_startTime  = NanoTimeGetCurrent();
                context->isExpensivePrev            = context->isExpensiveNow;
                context->isExpensiveNow             = !context->isExpensiveNow; // flip the expensive status
                if ( gExpensiveConstrainedTest_DenyExpensive )
                    context->expectedOperation      = context->isExpensiveNow ? RESULT_RMV : RESULT_ADD;
                else
                    context->expectedOperation      = NO_UPDATE;
                context->operation                  = NO_UPDATE;
                context->subtestProgress_callBack   = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
                require_action( context->subtestProgress_callBack != NULL, test_failed, errorDescription = "CFArrayCreateMutable failed" );

                err = systemf( NULL, "ifconfig %s %sexpensive", context->ifName, context->isExpensiveNow ? "" : "-" );
                require_noerr_action( err, test_failed, errorDescription = "systemf failed" );

                gettimeofday( &context->updateTime, NULL );
            }
            break;
        case TEST_CONSTRAINED_PREPARE:
            // The interface should be inexpensive and unconstrained when the constrained test starts
            require_action( context->isExpensiveNow == false, test_failed, SNPrintF( buffer, sizeof( buffer ), "Interface %s should be inexpensive.", context->ifName );
                            errorDescription = buffer );
            require_action( context->isConstrainedNow == false, test_failed, SNPrintF( buffer, sizeof( buffer ), "Interface %s should be unconstrained.\n", context->ifName );
                            errorDescription = buffer );

            context->subtestProgress_startTime  = NanoTimeGetCurrent();
            context->state                      = TEST_CONSTRAINED; // constrained interface is now under testing
            context->counter                    = 0;
            context->isExpensivePrev            = false;
            context->isExpensiveNow             = false;
            context->isConstrainedPrev          = false;
            context->isConstrainedNow           = true;             // will set constrained flag on the interface
            if ( gExpensiveConstrainedTest_DenyConstrained )
                context->expectedOperation      = RESULT_RMV;
            else
                context->expectedOperation      = NO_UPDATE;
            context->operation                  = NO_UPDATE;
            context->subtestProgress_callBack   = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
            require_action( context->subtestProgress_callBack != NULL, test_failed, errorDescription = "CFArrayCreateMutable failed" );

            // change interface to the constrained one
            err = systemf( NULL, "ifconfig %s -expensive && ifconfig %s constrained", context->ifName, context->ifName );
            require_noerr_action( err, test_failed, errorDescription = "systemf failed" );

            gettimeofday( &context->updateTime, NULL );
            break;
        case TEST_CONSTRAINED:
            // Since we are testing constrained flag, we should always turn the constrained flag on and off.
            require_action( context->isConstrainedPrev ^ context->isConstrainedNow, test_failed,
                            SNPrintF( buffer, sizeof( buffer ), "The current constrained status should be different with the previous one: %d -> %d\n", context->isConstrainedPrev, context->isConstrainedNow );
                            errorDescription = buffer );
            require_action( context->isExpensiveNow == false, test_failed,
                            SNPrintF( buffer, sizeof( buffer ), "The interface %s should be inexpensive when testing \"constrained\"\n", context->ifName );
                            errorDescription = buffer );
            require_action( context->expectedOperation == context->operation, test_failed, errorDescription = "Operation is not expected");

            context->counter++;
            if (context->counter == TEST_REPETITION)
            {
                // test changing expensive and constrained flags at the same time
                context->state = TEST_EXPENSIVE_CONSTRAINED_PREPARE;

                // reset interface
                err = systemf( NULL, "ifconfig %s -expensive && ifconfig %s -constrained", context->ifName, context->ifName );
                require_noerr_action( err, test_failed, errorDescription = "systemf failed" );

                context->isExpensiveNow = false;
                context->isConstrainedNow = false;
                gettimeofday( &context->updateTime, NULL );
            }
            else
            {
                context->subtestProgress_startTime  = NanoTimeGetCurrent();
                context->isConstrainedPrev          = context->isConstrainedNow;
                context->isConstrainedNow           = !context->isConstrainedNow; // flip constrained flag
                if ( gExpensiveConstrainedTest_DenyConstrained )
                    context->expectedOperation      = context->isConstrainedNow ? RESULT_RMV : RESULT_ADD;
                else
                    context->expectedOperation      = NO_UPDATE;
                context->operation                  = NO_UPDATE;
                context->subtestProgress_callBack   = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
                require_action( context->subtestProgress_callBack != NULL, test_failed, errorDescription = "CFArrayCreateMutable failed" );

                err = systemf( NULL, "ifconfig %s %sconstrained", context->ifName, context->isConstrainedNow ? "" : "-" );
                require_noerr_action( err, test_failed, errorDescription = "systemf failed" );

                gettimeofday(&context->updateTime, NULL);
            }
            break;
        case TEST_EXPENSIVE_CONSTRAINED_PREPARE:
            // The interface should be inexpensive and unconstrained when the constrained test starts
            require_action( context->isExpensiveNow == false,   test_failed,
                            SNPrintF( buffer, sizeof( buffer ), "Interface %s should be inexpensive.\n", context->ifName );
                            errorDescription = buffer );
            require_action( context->isConstrainedNow == false, test_failed,
                            SNPrintF(buffer, sizeof( buffer ), "Interface %s should be unconstrained.\n", context->ifName );
                            errorDescription = buffer );

            // now flip expensive and constrained at the same time
            context->subtestProgress_startTime  = NanoTimeGetCurrent();
            context->state                      = TEST_EXPENSIVE_CONSTRAINED;
            context->counter                    = 0;
            context->isExpensivePrev            = false;
            context->isExpensiveNow             = true;
            context->isConstrainedPrev          = false;
            context->isConstrainedNow           = true;
            if (gExpensiveConstrainedTest_DenyConstrained || gExpensiveConstrainedTest_DenyExpensive)
                context->expectedOperation      = RESULT_RMV;
            else
                context->expectedOperation      = NO_UPDATE;
            context->operation                  = NO_UPDATE;
            context->subtestProgress_callBack   = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
            require_action( context->subtestProgress_callBack != NULL, test_failed, errorDescription = "CFArrayCreateMutable failed" );

            err = systemf(NULL, "ifconfig %s expensive && ifconfig %s constrained", context->ifName, context->ifName );
            require_noerr_action( err, test_failed, errorDescription = "systemf failed" );

            gettimeofday( &context->updateTime, NULL );
            break;
        case TEST_EXPENSIVE_CONSTRAINED:
            // expensive and constrained flag should always be changed
            require_action( ( context->isExpensivePrev ^ context->isExpensiveNow ) && ( context->isConstrainedPrev ^ context->isConstrainedNow ), test_failed,
                            SNPrintF( buffer, sizeof( buffer ), "Both expensive and constrained status need to be changed" );
                            errorDescription = buffer );
            require_action( context->isExpensiveNow == context->isConstrainedNow, test_failed, errorDescription = "context->isExpensiveNow != context->isConstrainedNow" );
            require_action( context->expectedOperation == context->operation, test_failed, errorDescription = "Operation is not expected" );

            context->counter++;
            if ( context->counter == TEST_REPETITION )
            {
                context->state = TEST_SUCCEEDED;
            }
            else
            {
                context->subtestProgress_startTime  = NanoTimeGetCurrent();
                context->isExpensivePrev            = context->isExpensiveNow;
                context->isExpensiveNow             = !context->isExpensiveNow;
                context->isConstrainedPrev          = context->isConstrainedNow;
                context->isConstrainedNow           = !context->isConstrainedNow;
                if (gExpensiveConstrainedTest_DenyConstrained || gExpensiveConstrainedTest_DenyExpensive)
                    context->expectedOperation      = context->isExpensiveNow ? RESULT_RMV : RESULT_ADD;
                else
                    context->expectedOperation      = NO_UPDATE;
                context->operation                  = NO_UPDATE;
                context->subtestProgress_callBack   = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
                require_action( context->subtestProgress_callBack != NULL, test_failed, errorDescription = "CFArrayCreateMutable failed" );

                err = systemf( NULL, "ifconfig %s %sexpensive && ifconfig %s %sconstrained", context->ifName, context->isExpensiveNow ? "" : "-", context->ifName, context->isConstrainedNow ? "" : "-" );
                require_noerr_action( err, test_failed, errorDescription = "systemf failed" );

                gettimeofday( &context->updateTime, NULL );
            }
            break;
        case TEST_FAILED:
        test_failed:
            ExpensiveConstrainedSubtestReport( context, errorDescription );
            ExpensiveConstrainedStopAndCleanTheTest( context );
            if ( context->numOfRetries > 0 )
            {
                context->state = TEST_BEGIN;
                context->numOfRetries--;
                break;
            }
            ExpensiveConstrainedSubtestParams[context->subtestIndex++].test_passed = 0;
            if (context->subtestIndex == (int) countof( ExpensiveConstrainedSubtestParams ))
            {
                ExpensiveConstrainedFinalResultReport( context, false );
                exit( 2 );
            }
            if (context->timer == NULL)
            {
                // If timer is NULL, it means that we encounter error before we set up the test handler, which is unrecoverable.
                ExpensiveConstrainedFinalResultReport( context, false );
                exit( 1 );
            }
            context->state = TEST_BEGIN;
            break;
        case TEST_SUCCEEDED:
            ExpensiveConstrainedSubtestReport( context, NULL );
            ExpensiveConstrainedStopAndCleanTheTest( context );
            ExpensiveConstrainedSubtestParams[context->subtestIndex++].test_passed = 1;
            if (context->subtestIndex == (int) countof( ExpensiveConstrainedSubtestParams ))
            {
                // all the subtests have been run
                Boolean hasFailed = false;
                for ( int i = 0; i < (int) countof( ExpensiveConstrainedSubtestParams ) && !hasFailed; i++ )
                    hasFailed = ( ExpensiveConstrainedSubtestParams[i].test_passed != 1 );

                ExpensiveConstrainedFinalResultReport( context, !hasFailed );
                exit( hasFailed ? 2 : 0 );
            }
            context->state = TEST_BEGIN;
            break;
        default:
            FPrintF( stdout, "unknown error\n" );
            exit( 1 );
    }
}

//===========================================================================================================================
//    ExpensiveConstrainedCallback
//===========================================================================================================================

static void DNSSD_API
    ExpensiveConstrainedCallback(
        __unused DNSServiceRef  inSDRef,
        DNSServiceFlags         inFlags,
        uint32_t                inInterfaceIndex,
        DNSServiceErrorType     inError,
        const char *            inHostname,
        const struct sockaddr * inSockAddr,
        __unused uint32_t       inTTL,
        void *                  inContext )
{
    ExpensiveConstrainedContext * const   context = (ExpensiveConstrainedContext *)inContext;
    OSStatus                                    err;
    const char *                                addrStr;
    char                                        addrStrBuf[ kSockAddrStringMaxSize ];
    char                                        inFlagsDescription[ 128 ];
    NanoTime64                                  now;
    char                                        nowTimestamp[ 32 ];

    switch ( inError ) {
        case kDNSServiceErr_NoError:
        case kDNSServiceErr_NoSuchRecord:
            break;

        case kDNSServiceErr_Timeout:
            Exit( kExitReason_Timeout );

        default:
            err = inError;
            goto exit;
    }

    if( ( inSockAddr->sa_family != AF_INET ) && ( inSockAddr->sa_family != AF_INET6 ) )
    {
        dlogassert( "Unexpected address family: %d", inSockAddr->sa_family );
        err = kTypeErr;
        goto exit;
    }

    if( !inError )
    {
        err = SockAddrToString( inSockAddr, kSockAddrStringFlagsNone, addrStrBuf );
        require_noerr( err, exit );
        addrStr = addrStrBuf;
    }
    else
    {
        addrStr = ( inSockAddr->sa_family == AF_INET ) ? kNoSuchRecordAStr : kNoSuchRecordAAAAStr;
    }

    now = NanoTimeGetCurrent();
    _NanoTime64ToTimestamp( now, nowTimestamp, sizeof( nowTimestamp ) );
    SNPrintF( inFlagsDescription, sizeof( inFlagsDescription ), "%{du:cbflags}", inFlags );
    err = CFPropertyListAppendFormatted( kCFAllocatorDefault,  context->subtestProgress_callBack,
        "{"
            "%kO=%s"
            "%kO=%s"
            "%kO=%s"
            "%kO=%lli"
            "%kO=%s"
        "}",
        EXPENSIVE_CONSTRAINED_SUBTEST_ACTUAL_RESULT_KEY_TIMESTAMP,  nowTimestamp,
        EXPENSIVE_CONSTRAINED_SUBTEST_ACTUAL_RESULT_KEY_NAME,       inHostname,
        EXPENSIVE_CONSTRAINED_SUBTEST_ACTUAL_RESULT_KEY_FLAGS,      inFlagsDescription,
        EXPENSIVE_CONSTRAINED_SUBTEST_ACTUAL_RESULT_KEY_INTERFACE,  (int64_t) inInterfaceIndex,
        EXPENSIVE_CONSTRAINED_SUBTEST_ACTUAL_RESULT_KEY_ADDRESS,    addrStr
    );
    require_noerr_quiet( err, exit );

    if ( inFlags & kDNSServiceFlagsMoreComing )
        return;

    if ( inFlags & kDNSServiceFlagsAdd )
        context->operation = RESULT_ADD;
    else
        context->operation = RESULT_RMV;

    gettimeofday(&context->notificationTime, NULL);
exit:
    if( err ) exit( 1 );
}

//===========================================================================================================================
//    ExpensiveConstrainedInitializeContext
//===========================================================================================================================

static void ExpensiveConstrainedInitializeContext( ExpensiveConstrainedContext *context )
{
    // clear the flags of the previous subtest
    context->flags      = 0;
    context->protocols  = 0;

    // get the parameter for the current subtest
    const ExpensiveConstrainedTestParams *param     = &ExpensiveConstrainedSubtestParams[context->subtestIndex];
    gExpensiveConstrainedTest_Name                  = param->qname;
    gExpensiveConstrainedTest_DenyExpensive         = param->deny_expensive;
    gExpensiveConstrainedTest_DenyConstrained       = param->deny_constrained;
    gExpensiveConstrainedTest_StartFromExpensive    = param->start_from_expensive;
    gExpensiveConstrainedTest_ProtocolIPv4          = param->ipv4_query;
    gExpensiveConstrainedTest_ProtocolIPv6          = param->ipv6_query;
}

//===========================================================================================================================
//    ExpensiveConstrainedStopAndCleanTheTest
//===========================================================================================================================

static void ExpensiveConstrainedStopAndCleanTheTest( ExpensiveConstrainedContext *context )
{
    // Stop the ongoing query
    if ( context->opRef != NULL )
        DNSServiceRefDeallocate( context->opRef );

    context->opRef      = NULL;
    context->flags      = 0;
    context->protocols  = 0;
}

//===========================================================================================================================
//    ExpensiveConstrainedSubtestProgressReport
//===========================================================================================================================

static void ExpensiveConstrainedSubtestProgressReport( ExpensiveConstrainedContext *context )
{
    OSStatus            err;
    NanoTime64          now;
    char                startTime[ 32 ];
    char                endTime[ 32 ];
    char                expensive[ 32 ];
    char                constrained[ 32 ];

    now = NanoTimeGetCurrent();
    _NanoTime64ToTimestamp( context->subtestProgress_startTime, startTime, sizeof( startTime ) );
    _NanoTime64ToTimestamp( now, endTime, sizeof( endTime ) );

    snprintf( expensive, sizeof( expensive ), "%s -> %s", context->isExpensivePrev ? "True" : "False", context->isExpensiveNow ? "True" : "False" );
    snprintf( constrained, sizeof( constrained ), "%s -> %s", context->isConstrainedPrev ? "True" : "False", context->isConstrainedNow ? "True" : "False" );

    err = CFPropertyListAppendFormatted( kCFAllocatorDefault, context->subtestProgress,
        "{"
            "%kO=%s"
            "%kO=%s"
            "%kO=%s"
            "%kO=%s"
            "%kO=%s"
            "%kO=%s"
            "%kO=%s"
            "%kO=%O"
        "}",
        EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_START_TIME,              startTime,
        EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_END_TIME,                endTime,
        EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_STATE,                   ExpensiveConstrainedStateString(context->state),
        EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_EXPECT_RESULT,           ExpensiveConstrainedOperationString(context->expectedOperation),
        EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_ACTUAL_RESULT,           ExpensiveConstrainedOperationString(context->operation),
        EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_EXPENSIVE_PREV_NOW,      expensive,
        EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_CONSTRAINED_PREV_NOW,    constrained,
        EXPENSIVE_CONSTRAINED_SUBTEST_PROGRESS_KEY_CALL_BACK,               context->subtestProgress_callBack
    );
    require_noerr( err, exit );
    ForgetCF( &context->subtestProgress_callBack );
    return;

exit:
    ErrQuit( 1, "error: %#m\n", err );
}

//===========================================================================================================================
//    ExpensiveConstrainedFinalSubtestReport
//===========================================================================================================================

static void ExpensiveConstrainedSubtestReport( ExpensiveConstrainedContext *context, const char *error_description )
{
    OSStatus            err;
    NanoTime64          now;
    char                startTime[ 32 ];
    char                endTime[ 32 ];
    char                flagDescription[ 1024 ];

    now = NanoTimeGetCurrent();
    _NanoTime64ToTimestamp( context->subtestReport_startTime, startTime, sizeof( startTime ) );
    _NanoTime64ToTimestamp( now, endTime, sizeof( endTime ) );
    SNPrintF( flagDescription, sizeof( flagDescription ), "%#{flags}", context->flags, kDNSServiceFlagsDescriptors );

    if (error_description != NULL)
    {
        err = CFPropertyListAppendFormatted( kCFAllocatorDefault, context->subtestReport,
            "{"
                "%kO=%s"
                "%kO=%s"
                "%kO=%s"
                "%kO=%s"
                "%kO=%s"
                "%kO=%lli"
                "%kO=%s"
                "%kO=%O"
                "%kO=%s"
                "%kO=%O"
            "}",
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_START_TIME,        startTime,
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_END_TIME,          endTime,
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_QNAME,             context->name,
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_FLAGS,             flagDescription,
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_PROTOCOLS,         ExpensiveConstrainedProtocolString( context->protocols ),
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_INTERFACE_INDEX,   (int64_t) context->ifIndex,
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_INTERFACE_NAME,    context->ifName,
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_RESULT,            CFSTR( "Fail" ),
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_ERROR,             error_description,
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_TEST_PROGRESS,     context->subtestProgress
        );
    }
    else
    {
        err = CFPropertyListAppendFormatted( kCFAllocatorDefault, context->subtestReport,
            "{"
                "%kO=%s"
                "%kO=%s"
                "%kO=%s"
                "%kO=%s"
                "%kO=%s"
                "%kO=%lli"
                "%kO=%s"
                "%kO=%O"
                "%kO=%O"
            "}",
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_START_TIME,        startTime,
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_END_TIME,          endTime,
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_QNAME,             context->name,
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_FLAGS,             flagDescription,
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_PROTOCOLS,         ExpensiveConstrainedProtocolString( context->protocols ),
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_INTERFACE_INDEX,   (int64_t) context->ifIndex,
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_INTERFACE_NAME,    context->ifName,
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_RESULT,            CFSTR( "Pass" ),
            EXPENSIVE_CONSTRAINED_SUBTEST_REPORT_KEY_TEST_PROGRESS,     context->subtestProgress
        );
    }

    require_noerr( err, exit );
    ForgetCF( &context->subtestProgress );
    return;
exit:
    ErrQuit( 1, "error: %#m\n", err );
}

//===========================================================================================================================
//    ExpensiveConstrainedFinalResultReport
//===========================================================================================================================

static void ExpensiveConstrainedFinalResultReport( ExpensiveConstrainedContext *context, Boolean allPassed )
{
    OSStatus            err;
    CFPropertyListRef   plist;
    NanoTime64          now;
    char                startTime[ 32 ];
    char                endTime[ 32 ];

    now = NanoTimeGetCurrent();
    _NanoTime64ToTimestamp( context->testReport_startTime, startTime, sizeof( startTime ) );
    _NanoTime64ToTimestamp( now, endTime, sizeof( endTime ) );

    err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &plist,
        "{"
            "%kO=%s"
            "%kO=%s"
            "%kO=%b"
            "%kO=%O"
        "}",
        EXPENSIVE_CONSTRAINED_TEST_REPORT_KEY_START_TIME,       startTime,
        EXPENSIVE_CONSTRAINED_TEST_REPORT_KEY_END_TIME,         endTime,
        EXPENSIVE_CONSTRAINED_TEST_REPORT_KEY_ALL_PASSED,       allPassed,
        EXPENSIVE_CONSTRAINED_TEST_REPORT_KEY_SUBTEST_RESULT,   context->subtestReport
    );
    require_noerr( err, exit );
    ForgetCF( &context->subtestReport );

    err = OutputPropertyList( plist, context->outputFormat, context->outputFilePath );
    CFRelease( plist );
    require_noerr( err, exit );

    return;
exit:
    ErrQuit( 1, "error: %#m\n", err );
}

//===========================================================================================================================
//    ExpensiveConstrainedProtocolString
//===========================================================================================================================

static const char *ExpensiveConstrainedProtocolString( DNSServiceProtocol protocol )
{
    const char *str = NULL;
    switch ( protocol ) {
        case kDNSServiceProtocol_IPv4:
            str = "IPv4";
            break;
        case kDNSServiceProtocol_IPv6:
            str = "IPv6";
            break;
        case kDNSServiceProtocol_IPv4 | kDNSServiceProtocol_IPv6:
            str = "IPv4 & IPv6";
            break;
        default:
            break;
    }
    return str;
}

//===========================================================================================================================
//    ExpensiveConstrainedStateString
//===========================================================================================================================

static const char *ExpensiveConstrainedStateString( enum ExpensiveConstrainedTestState state )
{
    const char *str = NULL;
    switch ( state ) {
        case TEST_BEGIN:
            str = "TEST_BEGIN";
            break;
        case TEST_EXPENSIVE_PREPARE:
            str = "TEST_EXPENSIVE_PREPARE";
            break;
        case TEST_EXPENSIVE:
            str = "TEST_EXPENSIVE";
            break;
        case TEST_CONSTRAINED_PREPARE:
            str = "TEST_CONSTRAINED_PREPARE";
            break;
        case TEST_CONSTRAINED:
            str = "TEST_CONSTRAINED";
            break;
        case TEST_EXPENSIVE_CONSTRAINED_PREPARE:
            str = "TEST_EXPENSIVE_CONSTRAINED_PREPARE";
            break;
        case TEST_EXPENSIVE_CONSTRAINED:
            str = "TEST_EXPENSIVE_CONSTRAINED";
            break;
        case TEST_FAILED:
            str = "TEST_FAILED";
            break;
        case TEST_SUCCEEDED:
            str = "TEST_SUCCEEDED";
            break;
        default:
            str = "UNKNOWN";
            break;
    }

    return str;
}

//===========================================================================================================================
//    ExpensiveConstrainedOperationString
//===========================================================================================================================

static const char *ExpensiveConstrainedOperationString( enum ExpensiveConstrainedTestOperation operation )
{
    const char *str = NULL;
    switch ( operation ) {
        case RESULT_ADD:
            str = "RESULT_ADD";
            break;
        case RESULT_RMV:
            str = "RESULT_RMV";
            break;
        case NO_UPDATE:
            str = "NO_UPDATE";
            break;
        default:
            str = "UNKNOWN";
            break;
    }
    return str;
}

//===========================================================================================================================
//    expensiveConstrainedEndsWith
//===========================================================================================================================
static Boolean expensiveConstrainedEndsWith( const char *str, const char *suffix )
{
    if ( !str || !suffix )
        return false;
    size_t lenstr = strlen( str );
    size_t lensuffix = strlen( suffix );
    if ( lensuffix > lenstr )
        return false;
    return strncmp( str + lenstr - lensuffix, suffix, lensuffix ) == 0;
}

//===========================================================================================================================
//	DNSProxyTestCmd
//===========================================================================================================================

// DNS Proxy Test Mode Parameters

typedef enum
{
	kDNSProxyTestMode_Normal = 0,
	kDNSProxyTestMode_ForceAAAASynthesis,
	kDNSProxyTestMode_Count
	
}	DNSProxyTestMode;

check_compile_time( kDNSProxyTestMode_Count > 0 );

// DNS Proxy Test DNS64 Prefix Parameters
// See <https://tools.ietf.org/html/rfc6052#section-2.2>.

static const char * const		kDNSProxyTestParams_DNS64Prefixes[] =
{
	NULL,								// No prefix.
	"3ffe:ffff::/32",					// 32-bit prefix. Note: Prefix is from deprecated 3ffe::/16 block (see RFC 3701).
	"2001:db8:ff00::/40",				// 40-bit prefix.
	"2001:db8:ffff::/48",				// 48-bit prefix.
	"2001:db8:ffff:ff00::/56",			// 56-bit prefix.
	"2001:db8:ffff:ffff::/64",			// 64-bit prefix.
	"2001:db8:ffff:ff00:ffff:ffff::/96"	// 96-bit prefix. Note: bits 64 - 71 MUST be zero.
};

// DNS Proxy Test Transport Parameters

typedef enum
{
	kDNSProxyTestTransport_UDPv4 = 0,
	kDNSProxyTestTransport_TCPv4,
	kDNSProxyTestTransport_UDPv6,
	kDNSProxyTestTransport_TCPv6,
	kDNSProxyTestTransport_Count
	
}	DNSProxyTestTransport;

check_compile_time( kDNSProxyTestTransport_Count > 0 );

// DNS Proxy Test Query Parameters

typedef enum
{
	kDNSProxyTestQuery_A = 0,
	kDNSProxyTestQuery_AAAA,
	kDNSProxyTestQuery_IPv6OnlyA,
	kDNSProxyTestQuery_IPv6OnlyAAAA,
	kDNSProxyTestQuery_IPv4OnlyAAAA,
	kDNSProxyTestQuery_AliasA,
	kDNSProxyTestQuery_AliasAAAA,
	kDNSProxyTestQuery_AliasIPv6OnlyA,
	kDNSProxyTestQuery_AliasIPv6OnlyAAAA,
	kDNSProxyTestQuery_AliasIPv4OnlyAAAA,
	kDNSProxyTestQuery_NXDomainA,
	kDNSProxyTestQuery_NXDomainAAAA,
	kDNSProxyTestQuery_ReverseIPv6,
	kDNSProxyTestQuery_ReverseIPv6NXDomain,
	kDNSProxyTestQuery_ReverseIPv6DNS64,
	kDNSProxyTestQuery_ReverseIPv6DNS64NXDomain,
	kDNSProxyTestQuery_Count
	
}	DNSProxyTestQuery;

check_compile_time( kDNSProxyTestQuery_Count > 0 );

#define kDNSProxyTestQueryIterationCount		2

typedef struct DNSProxyTest *		DNSProxyTestRef;
struct DNSProxyTest
{
	dispatch_queue_t			queue;				// Serial queue for test events.
	dispatch_semaphore_t		doneSem;			// Semaphore to signal when the test is done.
	DNSServiceRef				probeGAI;			// Probe GAI for DNS server.
	char *						probeHostname;		// Probe hostname.
	mrc_dns_proxy_t				dnsProxy;			// DNS proxy reference.
	dispatch_source_t			timer;				// Timer to put time limit on queries.
	mdns_resolver_t				resolver;			// Resolver to represent the DNS proxy as a DNS service.
	CFMutableDictionaryRef		report;				// Test's report.
	CFMutableArrayRef			modeResults;		// "Weak" pointer to the 1st-level array of mode results.
	CFMutableArrayRef			prefixResults;		// "Weak" pointer to current 2nd-level array of DNS64 prefix results.
	CFMutableArrayRef			transportResults;	// "Weak" pointer to current 3rd-level array of transport results.
	CFMutableArrayRef			queryResults;		// "Weak" pointer to current 4th-level array of query results.
	DNSProxyTestMode			modeParam;			// Current mode parameter.
	unsigned int				prefixParamIdx;		// Current DNS64 prefix parameter index.
	DNSProxyTestTransport		transportParam;		// Current transport parameter.
	DNSProxyTestQuery			queryParam;			// Current query parameter.
	unsigned int				queryParamIter;		// Current query iteration.
	uint32_t					loopbackIndex;		// Loopback interface's index.
	mdns_querier_t				querier;			// Subtest's querier to send queries to DNS proxy.
	NanoTime64					startTime;			// Subtest's start time.
	char *						subtestDesc;		// Subtest's description.
	char *						qnameStr;			// Subtest's query QNAME as a C string.
	uint8_t *					qname;				// Subtest's query QNAME in label format.
	uint16_t					qtype;				// Subtest's query QTYPE.
	unsigned int				aliasCount;			// Subtest's expected number of CNAMEs in response answer section.
	unsigned int				answerCount;		// Subtest's expected number of QTYPE records.
	int							responseCode;		// Subtest's expected response code.
	uint8_t *					canonicalName;		// Subtest's expected CNAME rdata for reverse IPv6 queries.
	uint8_t *					answerName;			// Subtest's expected PTR rdata for reverse IPv6 queries.
	pid_t						serverPID;			// PID of spawned DNS server.
	int							subtestCount;		// Number of subtests that have completed or are in progress.
	int							subtestPassCount;	// Number of subtests that have passed so far.
	int32_t						refCount;			// Test object's reference count.
	OSStatus					error;				// Overall test's error.
	int							dns64PrefixBitLen;	// Current DNS64 prefix length (valid if > 0).
	uint8_t						dns64Prefix[ 16 ];	// Current DNS64 prefix (valid if dns64PrefixBitLen > 0).
	char						tag[ 6 + 1 ];		// Current subtest's random tag to uniquify QNAMEs.
	Boolean						synthesizedAAAA;	// True if the current subtest expects DNS64 synthesized AAAA records.
	Boolean						startedSubtests;	// True if the test has started running subtests.
};

ulog_define_ex( kDNSSDUtilIdentifier, DNSProxyTest, kLogLevelInfo, kLogFlags_None, "DNSProxyTest", NULL );
#define dpt_ulog( LEVEL, ... )		ulog( &log_category_from_name( DNSProxyTest ), (LEVEL), __VA_ARGS__ )

static OSStatus	_DNSProxyTestCreate( DNSProxyTestRef *outTest );
static OSStatus	_DNSProxyTestRun( DNSProxyTestRef inTest, Boolean *outPassed );
static void		_DNSProxyTestRetain( DNSProxyTestRef inTest );
static void		_DNSProxyTestRelease( DNSProxyTestRef inTest );

static void	DNSProxyTestCmd( void )
{
	OSStatus				err;
	OutputFormatType		outputFormat;
	DNSProxyTestRef			test	= NULL;
	Boolean					passed	= false;
	
	err = OutputFormatFromArgString( gDNSProxyTest_OutputFormat, &outputFormat );
	require_noerr_quiet( err, exit );
	
	err = _DNSProxyTestCreate( &test );
	require_noerr( err, exit );
	
	err = _DNSProxyTestRun( test, &passed );
	require_noerr( err, exit );
	
	err = OutputPropertyList( test->report, outputFormat, gDNSProxyTest_OutputFilePath );
	require_noerr( err, exit );
	
exit:
	if( test ) _DNSProxyTestRelease( test );
    gExitCode = err ? 1 : ( passed ? 0 : 2 );
}

//===========================================================================================================================

static void				_DNSProxyTestStart( void *inCtx );
static void				_DNSProxyTestStop( DNSProxyTestRef inTest, OSStatus inError );
static OSStatus			_DNSProxyTestContinue( DNSProxyTestRef inTest, OSStatus inSubtestError, Boolean *outDone );
static const char *		_DNSProxyTestGetCurrentDNS64PrefixParam( DNSProxyTestRef inTest );
static OSStatus			_DNSProxyTestPrepareMode( DNSProxyTestRef inTest );
static OSStatus			_DNSProxyTestPrepareDNSProxy( DNSProxyTestRef inTest );
static OSStatus			_DNSProxyTestPrepareResolver( DNSProxyTestRef inTest );
static OSStatus			_DNSProxyTestStartQuery( DNSProxyTestRef inTest, Boolean *outSkipQuery );
static OSStatus
	_DNSProxyTestSynthesizeIPv6(
		const uint8_t *	inIPv6Prefix,
		int				inIPv6PrefixBitLen,
		uint32_t		inIPv4Addr,
		uint8_t			outIPv6Addr[ STATIC_PARAM 16 ] );
static void	_DNSProxyTestHandleQuerierResult( DNSProxyTestRef inTest );
static OSStatus
	_DNSProxyTestVerifyAddressResponse(
		const uint8_t *	inMsgPtr,
		size_t			inMsgLen,
		const uint8_t *	inQName,
		uint16_t		inQType,
		int				inResponseCode,
		unsigned int	inAliasCount,
		unsigned int	inAnswerCount,
		const uint8_t *	inDNS64Prefix,
		int				inDNS64PrefixBitLen );
static OSStatus
	_DNSProxyTestVerifyReverseIPv6Response(
		const uint8_t *	inMsgPtr,
		size_t			inMsgLen,
		const uint8_t *	inQName,
		int				inResponseCode,
		const uint8_t *	inCanonicalName,
		const uint8_t *	inAnswerName );
static void DNSSD_API
	_DNSProxyTestProbeGAICallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inCtx );
static void			_DNSProxyTestProbeTimerHandler( void *inCtx );

static OSStatus	_DNSProxyTestCreate( DNSProxyTestRef *outTest )
{
	OSStatus			err;
	DNSProxyTestRef		obj;
	
	obj = (DNSProxyTestRef) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->refCount	= 1;
	obj->error		= kInProgressErr;
	obj->serverPID	= -1;
	
	obj->queue = dispatch_queue_create( "com.apple.dnssdutil.dns-proxy-test", DISPATCH_QUEUE_SERIAL );
	require_action( obj->queue, exit, err = kNoResourcesErr );
	
	obj->doneSem = dispatch_semaphore_create( 0 );
	require_action( obj->doneSem, exit, err = kNoResourcesErr );
	
	*outTest = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) _DNSProxyTestRelease( obj );
	return( err );
}

//===========================================================================================================================

static OSStatus	_DNSProxyTestRun( DNSProxyTestRef me, Boolean *outPassed )
{
	Boolean		passed;
	
	dispatch_async_f( me->queue, me, _DNSProxyTestStart );
	dispatch_semaphore_wait( me->doneSem, DISPATCH_TIME_FOREVER );
	
	passed = ( !me->error && ( me->subtestPassCount == me->subtestCount ) ) ? true : false;
	CFDictionarySetBoolean( me->report, CFSTR( "pass" ), passed );
	dpt_ulog( kLogLevelInfo, "Test result: %s\n", passed ? "pass" : "fail" );
	
	if( outPassed ) *outPassed = passed;
	return( me->error );
}

//===========================================================================================================================

static void	_DNSProxyTestRetain( DNSProxyTestRef me )
{
	atomic_add_32( &me->refCount, 1 );
}

//===========================================================================================================================

static void	_DNSProxyTestRelease( DNSProxyTestRef me )
{
	if( atomic_add_and_fetch_32( &me->refCount, -1 ) == 0 )
	{
		check( !me->probeGAI );
		check( !me->probeHostname );
		check( !me->dnsProxy );
		check( !me->timer );
		check( !me->resolver );
		check( !me->modeResults );
		check( !me->prefixResults );
		check( !me->transportResults );
		check( !me->queryResults );
		check( !me->querier );
		check( !me->subtestDesc );
		check( !me->qnameStr );
		check( !me->qname );
		check( !me->canonicalName );
		check( !me->answerName );
		check( me->serverPID < 0 );
		dispatch_forget( &me->queue );
		dispatch_forget( &me->doneSem );
		ForgetCF( &me->report );
		free( me );
	}
}

//===========================================================================================================================

#define kDNSProxyTestProbeQueryTimeoutSecs		5

static void _DNSProxyTestStart( void *inCtx )
{
	OSStatus					err;
	const DNSProxyTestRef		me			= (DNSProxyTestRef) inCtx;
	char *						serverCmd	= NULL;
	NanoTime64					startTime;
	char						startTimeStr[ 32 ];
	char						tag[ 6 + 1 ];
	
	startTime = NanoTimeGetCurrent();
	
	dpt_ulog( kLogLevelInfo, "Starting test\n" );
	
	me->error			= kInProgressErr;
	me->loopbackIndex	= if_nametoindex( "lo0" );
	err = map_global_value_errno( me->loopbackIndex != 0, me->loopbackIndex );
	require_noerr_action_quiet( err, exit, dpt_ulog( kLogLevelError, "Failed to get interface index for lo0: %#m", err ) );
	
	// The test DNS server uses an ephemeral port because the DNS proxy will use port 53 for itself.
	
	serverCmd = NULL;
	ASPrintF( &serverCmd, "dnssdutil server --loopback --follow %lld --port 0 --defaultTTL 300 --responseDelay 10",
		(int64_t) getpid() );
	require_action_quiet( serverCmd, exit, err = kUnknownErr );
	
	err = _SpawnCommand( &me->serverPID, "/dev/null", "/dev/null", "%s", serverCmd );
	require_noerr( err, exit );
	
	check( !me->probeHostname );
	ASPrintF( &me->probeHostname, "tag-dns-proxy-test-probe-%s.ipv4.d.test.",
		_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
	require_action( me->probeHostname, exit, err = kNoMemoryErr );
	
	err = DNSServiceGetAddrInfo( &me->probeGAI, 0, kDNSServiceInterfaceIndexAny, kDNSServiceProtocol_IPv4,
		me->probeHostname, _DNSProxyTestProbeGAICallback, me );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( me->probeGAI, me->queue );
	require_noerr( err, exit );
	
	check( !me->timer );
	err = DispatchTimerOneShotCreate( dispatch_time_seconds( kDNSProxyTestProbeQueryTimeoutSecs ),
		kDNSProxyTestProbeQueryTimeoutSecs * ( UINT64_C_safe( kNanosecondsPerSecond ) / 10 ),
		me->queue, _DNSProxyTestProbeTimerHandler, me, &me->timer );
	require_noerr( err, exit );
	dispatch_resume( me->timer );
	
	_NanoTime64ToTimestamp( startTime, startTimeStr, sizeof( startTimeStr ) );
	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &me->report,
		"{"
			"%kO=%s"	// startTime
			"%kO=%s"	// serverCmd
			"%kO=%s"	// probeHostname
			"%kO=[%@]"	// results
		"}",
		CFSTR( "startTime" ),		startTimeStr,
		CFSTR( "serverCmd" ),		serverCmd,
		CFSTR( "probeHostname" ),	me->probeHostname,
		CFSTR( "results" ),			&me->modeResults );
	require_noerr( err, exit );
	
exit:
	FreeNullSafe( serverCmd );
	if( err ) _DNSProxyTestStop( me, err );
}

//===========================================================================================================================

static void		_DNSProxyTestSubtestCleanup( DNSProxyTestRef inTest );

#define _mrc_dns_proxy_forget( X )	ForgetCustomEx( X, mrc_dns_proxy_invalidate, mrc_release )

static void	_DNSProxyTestStop( DNSProxyTestRef me, OSStatus inError )
{
	OSStatus		err;
	NanoTime64		endTime;
	char			endTimeStr[ 32 ];
	
	endTime = NanoTimeGetCurrent();
	me->error = inError;
	dpt_ulog( kLogLevelInfo, "Stopping test with error: %#m\n", me->error );
	
	DNSServiceForget( &me->probeGAI );
	ForgetMem( &me->probeHostname );
	_mrc_dns_proxy_forget( &me->dnsProxy );
	dispatch_source_forget( &me->timer );
	mdns_resolver_forget( &me->resolver );
	me->prefixResults		= NULL;
	me->modeResults			= NULL;
	me->transportResults	= NULL;
	me->queryResults		= NULL;
	_DNSProxyTestSubtestCleanup( me );
	if( me->serverPID >= 0 )
	{
		OSStatus		killErr;
		
		killErr = kill( me->serverPID, SIGTERM );
		killErr = map_global_noerr_errno( killErr );
		check_noerr( killErr );
		me->serverPID = -1;
	}
	_NanoTime64ToTimestamp( endTime, endTimeStr, sizeof( endTimeStr ) );
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, me->report,
		"%kO=%s"		// endTime
		"%kO=%lli"		// subtestCount
		"%kO=%lli",		// subtestPassCount
		CFSTR( "endTime" ),				endTimeStr,
		CFSTR( "subtestCount" ),		(int64_t) me->subtestCount,
		CFSTR( "subtestPassCount" ),	(int64_t) me->subtestPassCount );
	check_noerr( err );
	if( err && !me->error ) me->error = err;
	dispatch_semaphore_signal( me->doneSem );
}

//===========================================================================================================================

static void	_DNSProxyTestSubtestCleanup( DNSProxyTestRef me )
{
	dispatch_source_forget( &me->timer );
	mdns_querier_forget( &me->querier );
	ForgetMem( &me->subtestDesc );
	ForgetMem( &me->qnameStr );
	ForgetMem( &me->qname );
	ForgetMem( &me->canonicalName );
	ForgetMem( &me->answerName );
}

//===========================================================================================================================

static OSStatus		_DNSProxyTestHandleSubtestCompletion( DNSProxyTestRef inTest, OSStatus inSubtestError );
static char *		_DNSProxyTestCreateSubtestDescription( DNSProxyTestRef inTest );
static const char *	_DNSProxyTestQueryToString( DNSProxyTestQuery inQuery );
static const char *	_DNSProxyTestTransportToString( DNSProxyTestTransport inTransport );
static const char *	_DNSProxyTestModeToString( DNSProxyTestMode inMode );

static OSStatus	_DNSProxyTestContinue( DNSProxyTestRef me, OSStatus inSubtestError, Boolean *outDone )
{
	OSStatus		err;
	Boolean			skipQueries	= false;
	Boolean			done		= false;
	
	do
	{
		if( me->startedSubtests )
		{
			if( !skipQueries )
			{
				err = _DNSProxyTestHandleSubtestCompletion( me, inSubtestError );
				require_noerr( err, exit );
			}
			else
			{
				dpt_ulog( kLogLevelInfo, "Skipped subtest: %s\n", me->subtestDesc );
			}
			_DNSProxyTestSubtestCleanup( me );
			if( skipQueries || ( ++me->queryParamIter == kDNSProxyTestQueryIterationCount ) )
			{
				me->queryParamIter = 0;
				if( ++me->queryParam == kDNSProxyTestQuery_Count )
				{
					me->queryParam		= 0;
					me->queryResults	= NULL;
					dpt_ulog( kLogLevelInfo, "Invalidating resolver: %@\n", me->resolver );
					mdns_resolver_forget( &me->resolver );
					if( ++me->transportParam == kDNSProxyTestTransport_Count )
					{
						me->transportParam		= 0;
						me->transportResults	= NULL;
						dpt_ulog( kLogLevelInfo, "Disabling DNS proxy\n" );
						_mrc_dns_proxy_forget( &me->dnsProxy );
						if( ++me->prefixParamIdx == countof( kDNSProxyTestParams_DNS64Prefixes ) )
						{
							me->prefixParamIdx	= 0;
							me->prefixResults	= NULL;
							if( ++me->modeParam == kDNSProxyTestMode_Count )
							{
								me->modeParam	= 0;
								me->modeResults	= NULL;
								done			= true;
								err				= kNoErr;
								goto exit;
							}
						}
					}
				}
			}
		}
		else
		{
			me->startedSubtests = true;
		}
		if( ( me->queryParamIter == 0 ) && ( me->queryParam == 0 ) )
		{
			if( me->transportParam == 0 )
			{
				if( me->prefixParamIdx == 0 )
				{
					err = _DNSProxyTestPrepareMode( me );
					require_noerr( err, exit );
				}
				err = _DNSProxyTestPrepareDNSProxy( me );
				require_noerr( err, exit );
			}
			err = _DNSProxyTestPrepareResolver( me );
			require_noerr( err, exit );
		}
		err = _DNSProxyTestStartQuery( me, &skipQueries );
		require_noerr( err, exit );
		
		check( !me->subtestDesc );
		me->subtestDesc = _DNSProxyTestCreateSubtestDescription( me );
		require_action( me->subtestDesc, exit, err = kNoMemoryErr );
		
	}	while( skipQueries );
	
	++me->subtestCount;
	dpt_ulog( kLogLevelInfo, "Started subtest #%d: %s\n", me->subtestCount, me->subtestDesc );
	
exit:
	if( outDone ) *outDone = done;
	return( err );
}

static OSStatus	_DNSProxyTestHandleSubtestCompletion( DNSProxyTestRef me, OSStatus inSubtestError )
{
	OSStatus		err;
	NanoTime64		endTime;
	char			errorStr[ 128 ];
	char			startTimeStr[ 32 ];
	char			endTimeStr[ 32 ];
	
	endTime = NanoTimeGetCurrent();
	
	if( !inSubtestError ) ++me->subtestPassCount;
	
	dpt_ulog( kLogLevelInfo, "Subtest #%d result: %s (pass rate: %d/%d)\n",
		me->subtestCount, inSubtestError ? "fail" : "pass", me->subtestPassCount, me->subtestCount );
	
	_NanoTime64ToTimestamp( me->startTime, startTimeStr, sizeof( startTimeStr ) );
	_NanoTime64ToTimestamp( endTime, endTimeStr, sizeof( endTimeStr ) );
	SNPrintF( errorStr, sizeof( errorStr ), "%m", inSubtestError );
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, me->queryResults,
		"{"
			"%kO=%s"		// description
			"%kO=%s"		// startTime
			"%kO=%s"		// endTime
			"%kO=%s"		// qname
			"%kO=%s"		// qtype
			"%kO=%b"		// pass
			"%kO="			// error
			"{"
				"%kO=%lli"	// code
				"%kO=%s"	// description
			"}"
		"}",
		CFSTR( "description" ),	me->subtestDesc,
		CFSTR( "startTime" ),	startTimeStr,
		CFSTR( "endTime" ),		endTimeStr,
		CFSTR( "qname" ),		me->qnameStr,
		CFSTR( "qtype" ),		DNSRecordTypeValueToString( me->qtype ),
		CFSTR( "pass" ),		inSubtestError ? false : true,
		CFSTR( "error" ),
		CFSTR( "code" ),		(int64_t) inSubtestError,
		CFSTR( "description" ),	errorStr );
	return( err );
}

static char *	_DNSProxyTestCreateSubtestDescription( DNSProxyTestRef me )
{
	const char *		queryStr;
	const char *		transportStr;
	const char *		dns64PrefixStr;
	const char *		modeStr;
	char *				description;
	
	queryStr		= _DNSProxyTestQueryToString( me->queryParam );
	transportStr	= _DNSProxyTestTransportToString( me->transportParam );
	dns64PrefixStr	= _DNSProxyTestGetCurrentDNS64PrefixParam( me );
	modeStr			= _DNSProxyTestModeToString( me->modeParam );
	description		= NULL;
	if( dns64PrefixStr )
	{
		ASPrintF( &description, "%s over %s to DNS proxy using DNS64 prefix %s in %s mode (%u of %d)",
			queryStr, transportStr, dns64PrefixStr, modeStr, me->queryParamIter + 1, kDNSProxyTestQueryIterationCount );
	}
	else
	{
		ASPrintF( &description, "%s over %s to DNS proxy in %s mode (%u of %d)",
			queryStr, transportStr, modeStr, me->queryParamIter + 1, kDNSProxyTestQueryIterationCount );
	}
	return( description );
}

//===========================================================================================================================

static const char *	_DNSProxyTestQueryToString( DNSProxyTestQuery inQuery )
{
	switch( inQuery )
	{
		case kDNSProxyTestQuery_A:							return( "A record query" );
		case kDNSProxyTestQuery_AAAA:						return( "AAAA record query" );
		case kDNSProxyTestQuery_IPv6OnlyA:					return( "IPv6-only A record query" );
		case kDNSProxyTestQuery_IPv6OnlyAAAA:				return( "IPv6-only AAAA record query" );
		case kDNSProxyTestQuery_IPv4OnlyAAAA:				return( "IPv4-only AAAA record query" );
		case kDNSProxyTestQuery_AliasA:						return( "A record query with CNAMEs" );
		case kDNSProxyTestQuery_AliasAAAA:					return( "AAAA record query with CNAMEs" );
		case kDNSProxyTestQuery_AliasIPv6OnlyA:				return( "IPv6-only A record query with CNAMEs" );
		case kDNSProxyTestQuery_AliasIPv6OnlyAAAA:			return( "IPv6-only AAAA record query with CNAMEs" );
		case kDNSProxyTestQuery_AliasIPv4OnlyAAAA:			return( "IPv4-only AAAA record query with CNAMEs" );
		case kDNSProxyTestQuery_NXDomainA:					return( "A record query (NXDomain)" );
		case kDNSProxyTestQuery_NXDomainAAAA:				return( "AAAA record query (NXDomain)" );
		case kDNSProxyTestQuery_ReverseIPv6:				return( "Reverse IPv6 query" );
		case kDNSProxyTestQuery_ReverseIPv6NXDomain:		return( "Reverse IPv6 query (NXDomain)" );
		case kDNSProxyTestQuery_ReverseIPv6DNS64:			return( "Reverse IPv6 query with DNS64 prefix" );
		case kDNSProxyTestQuery_ReverseIPv6DNS64NXDomain:	return( "Reverse IPv6 query with DNS64 prefix (NXDomain)" );
		default:											return( "<INVALID QUERY>" );
    }
}

//===========================================================================================================================

static const char *	_DNSProxyTestTransportToString( DNSProxyTestTransport inTransport )
{
    switch( inTransport )
	{
		case kDNSProxyTestTransport_UDPv4:	return( "IPv4-UDP" );
		case kDNSProxyTestTransport_TCPv4:	return( "IPv4-TCP" );
		case kDNSProxyTestTransport_UDPv6:	return( "IPv6-UDP" );
		case kDNSProxyTestTransport_TCPv6:	return( "IPv6-TCP" );
		default:							return( "<INVALID TRANSPORT>" );
	}
}

//===========================================================================================================================

static const char *	_DNSProxyTestModeToString( DNSProxyTestMode inMode )
{
    switch( inMode )
	{
		case kDNSProxyTestMode_Normal:				return( "normal" );
		case kDNSProxyTestMode_ForceAAAASynthesis:	return( "force-AAAA-synthesis" );
		default:									return( "<INVALID MODE>" );
	}
}

//===========================================================================================================================

static const char *	_DNSProxyTestGetCurrentDNS64PrefixParam( DNSProxyTestRef me )
{
	const unsigned int		index = me->prefixParamIdx;
	const unsigned int		count = countof( kDNSProxyTestParams_DNS64Prefixes );
	
	require_fatal( index < count, "DNSProxyTest DNS Proxy DNS64 prefix parameter index %u out-of-range.", index );
	return( kDNSProxyTestParams_DNS64Prefixes[ index ] );
}

//===========================================================================================================================

static OSStatus	_DNSProxyTestPrepareMode( DNSProxyTestRef me )
{
	OSStatus		err;
	
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, me->modeResults,
		"{"
			"%kO=%s"	// mode
			"%kO=[%@]"	// results
		"}",
		CFSTR( "mode" ),	_DNSProxyTestModeToString( me->modeParam ),
		CFSTR( "results" ),	&me->prefixResults );
	return( err );
}

//===========================================================================================================================

static OSStatus	_DNSProxyTestPrepareDNSProxy( DNSProxyTestRef me )
{
	OSStatus			err;
	const char *		dns64PrefixStr;
	Boolean				forceAAAASynthesis;
	
	me->dns64PrefixBitLen = 0;
	memset( me->dns64Prefix, 0, sizeof( me->dns64Prefix ) );
	dns64PrefixStr = _DNSProxyTestGetCurrentDNS64PrefixParam( me );
	if( dns64PrefixStr )
	{
		const char *		end;
		
		err = _StringToIPv6Address( dns64PrefixStr, kStringToIPAddressFlagsNoPort | kStringToIPAddressFlagsNoScope,
			me->dns64Prefix, NULL, NULL, &me->dns64PrefixBitLen, &end );
		if( !err && ( *end != '\0' ) ) err = kMalformedErr;
		require_noerr_quiet( err, exit );
	}
	switch( me->modeParam )
	{
		case kDNSProxyTestMode_Normal:
			forceAAAASynthesis = false;
			break;
		
		case kDNSProxyTestMode_ForceAAAASynthesis:
			forceAAAASynthesis = true;
			break;
		
		default:
			FatalErrorF( "Unhandled DNSProxyTestMode value %ld", (long) me->modeParam );
	}
	mrc_dns_proxy_parameters_t params = mrc_dns_proxy_parameters_create( &err );
	require_noerr( err, exit );
	
	mrc_dns_proxy_parameters_add_input_interface( params, me->loopbackIndex );
	mrc_dns_proxy_parameters_set_output_interface( params, 0 );
	if( dns64PrefixStr )
	{
		mrc_dns_proxy_parameters_set_nat64_prefix( params, me->dns64Prefix, (size_t) me->dns64PrefixBitLen );
		if( forceAAAASynthesis ) mrc_dns_proxy_parameters_set_force_aaaa_synthesis( params, true );
		dpt_ulog( kLogLevelInfo, "Starting DNS proxy with DNS64 prefix %.16a/%d\n",
			me->dns64Prefix, me->dns64PrefixBitLen );
	}
	else
	{
		dpt_ulog( kLogLevelInfo, "Starting DNS proxy (without a DNS64 prefix)\n" );
	}
	check( !me->dnsProxy );
	me->dnsProxy = mrc_dns_proxy_create( params, &err );
	mrc_forget( &params );
	require_noerr_quiet( err, exit );
	
	mrc_dns_proxy_set_queue( me->dnsProxy, me->queue );
	mrc_dns_proxy_set_event_handler( me->dnsProxy,
	^( const mrc_dns_proxy_event_t inEvent, const OSStatus inError )
	{
		switch( inEvent )
		{
			case mrc_dns_proxy_event_started:
				dpt_ulog( kLogLevelInfo, "DNS proxy was started\n" );
				break;
			
			case mrc_dns_proxy_event_interruption:
				dpt_ulog( kLogLevelInfo, "DNS proxy was interrupted\n" );
				break;
			
			case mrc_dns_proxy_event_invalidation:
				if( inError )	dpt_ulog( kLogLevelError, "DNS proxy invalidated with error: %#m\n", inError );
				else			dpt_ulog( kLogLevelInfo, "DNS proxy invalidated\n" );
				break;
			
			default:
			case mrc_dns_proxy_event_none:
				FatalErrorF( "Unhandled DNS proxy event value: %d", inEvent );
		}
	} );
	mrc_dns_proxy_activate( me->dnsProxy );
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, me->prefixResults,
		"{"
			"%kO=%s"	// dns64Prefix
			"%kO=[%@]"	// results
		"}",
		CFSTR( "dns64Prefix" ),	dns64PrefixStr ? dns64PrefixStr : "",
		CFSTR( "results" ),		&me->transportResults );
	require_noerr( err, exit );
	
exit:
	return( err );
}

//===========================================================================================================================

#define kDNSProxyTestDNSProxyAddrStr_IPv4		"127.0.0.1:53"
#define kDNSProxyTestDNSProxyAddrStr_IPv6		"[::1]:53"

static OSStatus	_DNSProxyTestPrepareResolver( DNSProxyTestRef me )
{
	OSStatus					err;
	const char *				addrStr;
	mdns_address_t				addr = NULL;
	mdns_resolver_type_t		resolverType;
	
	switch( me->transportParam )
	{
		case kDNSProxyTestTransport_UDPv4:
			addrStr			= kDNSProxyTestDNSProxyAddrStr_IPv4;
			resolverType	= mdns_resolver_type_normal;
			break;
		
		case kDNSProxyTestTransport_TCPv4:
			addrStr			= kDNSProxyTestDNSProxyAddrStr_IPv4;
			resolverType	= mdns_resolver_type_tcp;
			break;
		
		case kDNSProxyTestTransport_UDPv6:
			addrStr			= kDNSProxyTestDNSProxyAddrStr_IPv6;
			resolverType	= mdns_resolver_type_normal;
			break;
		
		case kDNSProxyTestTransport_TCPv6:
			addrStr			= kDNSProxyTestDNSProxyAddrStr_IPv6;
			resolverType	= mdns_resolver_type_tcp;
			break;
		
		default:
			FatalErrorF( "Unhandled DNSProxyTestTransport value %ld", (long) me->transportParam );
	}
	check( !me->resolver );
	me->resolver = mdns_resolver_create( resolverType, 0, &err );
	require_noerr( err, exit );
	
	addr = mdns_address_create_from_ip_address_string( addrStr );
	require_action( addr, exit, err = kUnknownErr );
	
	err = mdns_resolver_add_server_address( me->resolver, addr );
	mdns_forget( &addr );
	require_noerr( err, exit );
	
	dpt_ulog( kLogLevelInfo, "Activating resolver: %@\n", me->resolver );
	mdns_resolver_activate( me->resolver );
	
	_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( me->tag ) - 1, me->tag );
	
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, me->transportResults,
		"{"
			"%kO=%s"	// transport
			"%kO=[%@]"	// results
		"}",
		CFSTR( "transport" ),	_DNSProxyTestTransportToString( me->transportParam ),
		CFSTR( "results" ),		&me->queryResults );
	require_noerr( err, exit );
	
exit:
	mdns_release_null_safe( addr );
	return( err );
}

//===========================================================================================================================

#define kDNSProxyTestQuerierTimeLimitSecs		5
#define kDNSProxyTestRecordTTL					( 5 * kSecondsPerMinute )
#define kDNSProxyTestAddressCount				4
#define kDNSProxyTestAliasCount					4

static void	_DNSProxyTestQuerierTimerHandler( void *inCtx );

static OSStatus	_DNSProxyTestStartQuery( DNSProxyTestRef me, Boolean *outSkipQuery )
{
	OSStatus			err;
	mdns_querier_t		querier;
	uint8_t				tmpName[ kDomainNameLengthMax ];
	Boolean				skipQuery = false;
	
	me->startTime		= NanoTimeGetCurrent();
	me->qtype			= 0;
	me->aliasCount		= 0;
	me->answerCount		= 0;
	me->responseCode	= 0;
	me->canonicalName	= NULL;
	me->answerName		= NULL;
	me->synthesizedAAAA	= false;
	
	check( !me->qnameStr );
	switch( me->queryParam )
	{
		case kDNSProxyTestQuery_A:
			me->qtype			= kDNSRecordType_A;
			me->answerCount		= kDNSProxyTestAddressCount;
			me->responseCode	= kDNSRCode_NoError;
			ASPrintF( &me->qnameStr, "count-%u.ttl-%u.tag-a-%s.d.test.",
				kDNSProxyTestAddressCount, kDNSProxyTestRecordTTL, me->tag );
			require_action( me->qnameStr, exit, err = kNoMemoryErr );
			break;
		
		case kDNSProxyTestQuery_AAAA:
			me->qtype			= kDNSRecordType_AAAA;
			me->answerCount		= kDNSProxyTestAddressCount;
			me->responseCode	= kDNSRCode_NoError;
			if( ( me->dns64PrefixBitLen > 0 ) && ( me->modeParam == kDNSProxyTestMode_ForceAAAASynthesis ) )
			{
				me->synthesizedAAAA = true;
			}
			else
			{
				me->synthesizedAAAA = false;
			}
			ASPrintF( &me->qnameStr, "count-%u.ttl-%u.tag-aaaa-%s.d.test.",
				kDNSProxyTestAddressCount, kDNSProxyTestRecordTTL, me->tag );
			require_action( me->qnameStr, exit, err = kNoMemoryErr );
			break;
		
		case kDNSProxyTestQuery_IPv6OnlyA:
			me->qtype			= kDNSRecordType_A;
			me->answerCount		= 0;
			me->responseCode	= kDNSRCode_NoError;
			ASPrintF( &me->qnameStr, "ipv6.ttl-%u.tag-ipv6-only-a-%s.d.test.", kDNSProxyTestRecordTTL, me->tag );
			require_action( me->qnameStr, exit, err = kNoMemoryErr );
			break;
		
		case kDNSProxyTestQuery_IPv6OnlyAAAA:
			me->qtype			= kDNSRecordType_AAAA;
			me->responseCode	= kDNSRCode_NoError;
			if( ( me->dns64PrefixBitLen > 0 ) && ( me->modeParam == kDNSProxyTestMode_ForceAAAASynthesis ) )
			{
				me->answerCount = 0;
			}
			else
			{
				me->answerCount = kDNSProxyTestAddressCount;
			}
			ASPrintF( &me->qnameStr, "count-%u.ipv6.ttl-%u.tag-ipv6-only-aaaa-%s.d.test.",
				kDNSProxyTestAddressCount, kDNSProxyTestRecordTTL, me->tag );
			require_action( me->qnameStr, exit, err = kNoMemoryErr );
			break;
		
		case kDNSProxyTestQuery_IPv4OnlyAAAA:
			me->qtype			= kDNSRecordType_AAAA;
			me->responseCode	= kDNSRCode_NoError;
			if( me->dns64PrefixBitLen > 0 )
			{
				me->answerCount		= kDNSProxyTestAddressCount;
				me->synthesizedAAAA	= true;
			}
			else
			{
				me->answerCount		= 0;
				me->synthesizedAAAA	= false;
			}
			ASPrintF( &me->qnameStr, "count-%u.ipv4.ttl-%u.tag-ipv4-only-aaaa-%s.d.test.",
				kDNSProxyTestAddressCount, kDNSProxyTestRecordTTL, me->tag );
			require_action( me->qnameStr, exit, err = kNoMemoryErr );
			break;
		
		case kDNSProxyTestQuery_AliasA:
			me->qtype			= kDNSRecordType_A;
			me->aliasCount		= kDNSProxyTestAliasCount;
			me->answerCount		= kDNSProxyTestAddressCount;
			me->responseCode	= kDNSRCode_NoError;
			ASPrintF( &me->qnameStr, "alias-%u.count-%u.ttl-%u.tag-alias-a-%s.d.test.",
				kDNSProxyTestAliasCount, kDNSProxyTestAddressCount, kDNSProxyTestRecordTTL, me->tag );
			require_action( me->qnameStr, exit, err = kNoMemoryErr );
			break;
		
		case kDNSProxyTestQuery_AliasAAAA:
			me->qtype			= kDNSRecordType_AAAA;
			me->aliasCount		= kDNSProxyTestAliasCount;
			me->answerCount		= kDNSProxyTestAddressCount;
			me->responseCode	= kDNSRCode_NoError;
			if( ( me->dns64PrefixBitLen > 0 ) && ( me->modeParam == kDNSProxyTestMode_ForceAAAASynthesis ) )
			{
				me->synthesizedAAAA = true;
			}
			else
			{
				me->synthesizedAAAA = false;
			}
			ASPrintF( &me->qnameStr, "alias-%u.count-%u.ttl-%u.tag-alias-aaaa-%s.d.test.",
				kDNSProxyTestAliasCount, kDNSProxyTestAddressCount, kDNSProxyTestRecordTTL, me->tag );
			require_action( me->qnameStr, exit, err = kNoMemoryErr );
			break;
		
		case kDNSProxyTestQuery_AliasIPv6OnlyA:
			me->qtype			= kDNSRecordType_A;
			me->aliasCount		= kDNSProxyTestAliasCount;
			me->answerCount		= 0;
			me->responseCode	= kDNSRCode_NoError;
			ASPrintF( &me->qnameStr, "alias-%u.ipv6.ttl-%u.tag-alias-ipv6-only-a-%s.d.test.",
				kDNSProxyTestAliasCount, kDNSProxyTestRecordTTL, me->tag );
			require_action( me->qnameStr, exit, err = kNoMemoryErr );
			break;
		
		case kDNSProxyTestQuery_AliasIPv6OnlyAAAA:
			me->qtype			= kDNSRecordType_AAAA;
			me->aliasCount		= kDNSProxyTestAliasCount;
			me->responseCode	= kDNSRCode_NoError;
			if( ( me->dns64PrefixBitLen > 0 ) && ( me->modeParam == kDNSProxyTestMode_ForceAAAASynthesis ) )
			{
				me->answerCount	= 0;
			}
			else
			{
				me->answerCount	= kDNSProxyTestAddressCount;
			}
			ASPrintF( &me->qnameStr, "alias-%u.ipv6.count-%u.ttl-%u.tag-alias-ipv6-only-aaaa-%s.d.test.",
				kDNSProxyTestAliasCount, kDNSProxyTestAddressCount, kDNSProxyTestRecordTTL, me->tag );
			require_action( me->qnameStr, exit, err = kNoMemoryErr );
			break;
		
		case kDNSProxyTestQuery_AliasIPv4OnlyAAAA:
			me->qtype			= kDNSRecordType_AAAA;
			me->aliasCount		= kDNSProxyTestAliasCount;
			me->responseCode	= kDNSRCode_NoError;
			if( me->dns64PrefixBitLen > 0 )
			{
				me->answerCount		= kDNSProxyTestAddressCount;
				me->synthesizedAAAA	= true;
			}
			else
			{
				me->answerCount		= 0;
				me->synthesizedAAAA	= false;
			}
			ASPrintF( &me->qnameStr, "alias-%u.count-%u.ipv4.ttl-%u.tag-alias-ipv4-only-aaaa-%s.d.test.",
				kDNSProxyTestAliasCount, kDNSProxyTestAddressCount, kDNSProxyTestRecordTTL, me->tag );
			require_action( me->qnameStr, exit, err = kNoMemoryErr );
			break;
		
		case kDNSProxyTestQuery_NXDomainA:
			me->qtype			= kDNSRecordType_A;
			me->answerCount		= 0;
			me->responseCode	= kDNSRCode_NXDomain;
			ASPrintF( &me->qnameStr, "does-not-exist.tag-nx-domain-a-%s.d.test.", me->tag );
			require_action( me->qnameStr, exit, err = kNoMemoryErr );
			break;
		
		case kDNSProxyTestQuery_NXDomainAAAA:
			me->qtype			= kDNSRecordType_AAAA;
			me->answerCount		= 0;
			me->responseCode	= kDNSRCode_NXDomain;
			ASPrintF( &me->qnameStr, "does-not-exist.tag-nx-domain-aaaa-%s.d.test.", me->tag );
			require_action( me->qnameStr, exit, err = kNoMemoryErr );
			break;
		
		case kDNSProxyTestQuery_ReverseIPv6:
		case kDNSProxyTestQuery_ReverseIPv6NXDomain:
		{
			unsigned int		hostID;
			uint8_t				ipv6Addr[ 16 ];
			char				reverseIPv6NameStr[ kReverseIPv6DomainNameBufLen ];
			
			// To force mDNSResponder to have to send a query on the first query iteration, use a different reverse IPv6
			// PTR query for each mode/DNS64 prefix/transport combination.
			
			check_compile_time_code( kDNSProxyTestTransport_Count <= 4 );
			check_compile_time_code( countof( kDNSProxyTestParams_DNS64Prefixes ) <= 8 );
			check_compile_time_code( kDNSProxyTestMode_Count <= 4 );
			hostID  =   ( (unsigned int) me->transportParam ) & 0x03;			// Set bits 1 - 0 to transport param.
			hostID |= (   me->prefixParamIdx                  & 0x07 ) << 2;	// Set bits 4 - 2 to prefix param index.
			hostID |= ( ( (unsigned int) me->modeParam )      & 0x03 ) << 5;	// Set bits 6 - 5 to mode param.
			hostID |= 1U << 7;													// Set bit 7 to ensure a non-zero hostID.
			check( ( hostID >= 1 ) && ( hostID <= 255 ) );
			
			memcpy( ipv6Addr, kDNSServerBaseAddrV6, 16 );
			ipv6Addr[ 15 ] = (uint8_t) hostID;
			
			me->qtype = kDNSRecordType_PTR;
			if( me->queryParam == kDNSProxyTestQuery_ReverseIPv6 )
			{
				char				answerNameStr[ 128 ];
				char *				dst = answerNameStr;
				char * const		lim = &answerNameStr[ countof( answerNameStr ) ];
				int					i;
				
				me->responseCode = kDNSRCode_NoError;
				
				// Create the expected RDATA.
				
				SNPrintF_Add( &dst, lim, "ipv6" );
				for( i = 0; i < 8; ++i )
				{
					SNPrintF_Add( &dst, lim, "-%04x", ReadBig16( &ipv6Addr[ i * 2 ] ) );
				}
				SNPrintF_Add( &dst, lim, ".d.test." );
				err = DomainNameFromString( tmpName, answerNameStr, NULL );
				require_noerr_quiet( err, exit );
				
				err = DomainNameDup( tmpName, &me->answerName, NULL );
				require_noerr( err, exit );
			}
			else
			{
				me->responseCode	= kDNSRCode_NXDomain;
				me->answerName		= NULL;
				
				// Make the IPv6 address invalid by setting bits 15-8. This makes the host identifier bogus.
				// The DNS server's IPv6 prefix is 96 bits, but only host identifiers in [1, 255] are recognized.
				
				ipv6Addr[ 14 ] = 0xFF;
			}
			_WriteReverseIPv6DomainNameString( ipv6Addr, reverseIPv6NameStr );
			me->qnameStr = strdup( reverseIPv6NameStr );
			require_action( me->qnameStr, exit, err = kNoMemoryErr );
			break;
		}
		case kDNSProxyTestQuery_ReverseIPv6DNS64:
		case kDNSProxyTestQuery_ReverseIPv6DNS64NXDomain:
		{
			uint32_t	ipv4Addr;
			uint8_t		ipv6Addr[ 16 ];
			char		reverseIPNameStr[ kReverseIPv6DomainNameBufLen ];
			
			if( me->dns64PrefixBitLen <= 0 )
			{
				skipQuery = true;
				err = kNoErr;
				goto exit;
			}
			me->qtype = kDNSRecordType_PTR;
			if( me->queryParam == kDNSProxyTestQuery_ReverseIPv6DNS64 )
			{
				char		answerNameStr[ 64 ];
				
				me->responseCode = kDNSRCode_NoError;
				
				ipv4Addr = kDNSServerBaseAddrV4 + 1;
				_WriteReverseIPv4DomainNameString( ipv4Addr, reverseIPNameStr );
				
				err = DomainNameFromString( tmpName, reverseIPNameStr, NULL );
				require_noerr_quiet( err, exit );
				
				err = DomainNameDup( tmpName, &me->canonicalName, NULL );
				require_noerr( err, exit );
				
				SNPrintF( answerNameStr, sizeof( answerNameStr ), "ipv4-%u-%u-%u-%u.d.test.",
					( ipv4Addr >> 24 ) & 0xFF,
					( ipv4Addr >> 16 ) & 0xFF,
					( ipv4Addr >>  8 ) & 0xFF,
					  ipv4Addr         & 0xFF );
				err = DomainNameFromString( tmpName, answerNameStr, NULL );
				require_noerr_quiet( err, exit );
				
				err = DomainNameDup( tmpName, &me->answerName, NULL );
				require_noerr( err, exit );
			}
			else
			{
				ipv4Addr = kDNSServerBaseAddrV4 + 0;
				
				me->responseCode	= kDNSRCode_NXDomain;
				me->canonicalName	= NULL;
				me->answerName		= NULL;
			}
			err = _DNSProxyTestSynthesizeIPv6( me->dns64Prefix, me->dns64PrefixBitLen, ipv4Addr, ipv6Addr );
			require_noerr( err, exit );
			
			_WriteReverseIPv6DomainNameString( ipv6Addr, reverseIPNameStr );
			me->qnameStr = strdup( reverseIPNameStr );
			require_action( me->qnameStr, exit, err = kNoMemoryErr );
			break;
		}
		default:
			FatalErrorF( "Unhandled DNSProxyTestQuery value %ld", (long) me->queryParam );
	}
	check( !me->qname );
	err = DomainNameFromString( tmpName, me->qnameStr, NULL );
	require_noerr_quiet( err, exit );
	
	err = DomainNameDup( tmpName, &me->qname, NULL );
	require_noerr( err, exit );
	
	check( !me->querier );
	me->querier = mdns_resolver_create_querier( me->resolver, &err );
	require_noerr( err, exit );
	
	err = mdns_querier_set_query( me->querier, me->qname, me->qtype, kDNSClassType_IN );
	require_noerr( err, exit );
	
	mdns_querier_set_queue( me->querier, me->queue );
	_DNSProxyTestRetain( me );
	querier = me->querier;
	mdns_retain( querier );
	mdns_querier_set_result_handler( me->querier,
	^{
		if( me->querier == querier ) _DNSProxyTestHandleQuerierResult( me );
		_DNSProxyTestRelease( me );
		mdns_release( querier );
	} );
	mdns_querier_activate( me->querier );
	
	check( !me->timer );
	err = DispatchTimerOneShotCreate( dispatch_time_seconds( kDNSProxyTestQuerierTimeLimitSecs ),
		kDNSProxyTestQuerierTimeLimitSecs * ( UINT64_C_safe( kNanosecondsPerSecond ) / 10 ),
		me->queue, _DNSProxyTestQuerierTimerHandler, me, &me->timer );
	require_noerr( err, exit );
	dispatch_resume( me->timer );
	
exit:
	if( outSkipQuery ) *outSkipQuery = skipQuery;
	return( err );
}

static void	_DNSProxyTestQuerierTimerHandler( void *inCtx )
{
	OSStatus					err;
	const DNSProxyTestRef		me = (DNSProxyTestRef) inCtx;
	Boolean						done;
	
	dpt_ulog( kLogLevelInfo, "Query for '%{du:dname}' timed out.\n", me->qname );
	
	err = _DNSProxyTestContinue( me, kTimeoutErr, &done );
	check_noerr( err );
	if( err || done ) _DNSProxyTestStop( me, err );
}

//===========================================================================================================================

static OSStatus
	_DNSProxyTestSynthesizeIPv6(
		const uint8_t *	inIPv6Prefix,
		int				inIPv6PrefixBitLen,
		uint32_t		inIPv4Addr,
		uint8_t			outIPv6Addr[ STATIC_PARAM 16 ] )
{
	// From <https://tools.ietf.org/html/rfc6052#section-2.2>:
	//
	// 2.2.  IPv4-Embedded IPv6 Address Format
	//
	//   IPv4-converted IPv6 addresses and IPv4-translatable IPv6 addresses
	//   follow the same format, described here as the IPv4-embedded IPv6
	//   address Format.  IPv4-embedded IPv6 addresses are composed of a
	//   variable-length prefix, the embedded IPv4 address, and a variable-
	//   length suffix, as presented in the following diagram, in which PL
	//   designates the prefix length:
	//
	//    +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
	//    |PL| 0-------------32--40--48--56--64--72--80--88--96--104---------|
	//    +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
	//    |32|     prefix    |v4(32)         | u | suffix                    |
	//    +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
	//    |40|     prefix        |v4(24)     | u |(8)| suffix                |
	//    +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
	//    |48|     prefix            |v4(16) | u | (16)  | suffix            |
	//    +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
	//    |56|     prefix                |(8)| u |  v4(24)   | suffix        |
	//    +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
	//    |64|     prefix                    | u |   v4(32)      | suffix    |
	//    +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
	//    |96|     prefix                                    |    v4(32)     |
	//    +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
	//
	//                                 Figure 1
	switch( inIPv6PrefixBitLen )
	{
		case 32:
		case 40:
		case 48:
		case 56:
		case 64:
		case 96:
		{
			const int		prefixLen = inIPv6PrefixBitLen / 8;
			int				i, j;
			uint8_t			v4Addr[ 4 ];
			
			memcpy( outIPv6Addr, inIPv6Prefix, (size_t) prefixLen );
			WriteBig32Typed( v4Addr, inIPv4Addr );
			
			// 1. Bits 64 - 71, i.e., reserved octet "u", MUST be zero.
			// 2. Except for bits 64 - 71, the 32 bits following the prefix are the bits of the embedded IPv4 address.
			// 3. The remaining bits, if any, are the suffix bits, which SHOULD be zero.
			
			j = 0;
			for( i = prefixLen; i < 16; ++i )
			{
				if( ( j < 4 ) && ( i != 8 ) )	outIPv6Addr[ i ] = v4Addr[ j++ ];
				else							outIPv6Addr[ i ] = 0;
			}
			return( kNoErr );
		}
		default:
			return( kSizeErr );
	}
}

//===========================================================================================================================

static void	_DNSProxyTestHandleQuerierResult( DNSProxyTestRef me )
{
	OSStatus								err, verifyErr;
	const uint8_t *							msgPtr;
	size_t									msgLen;
	const mdns_querier_result_type_t		resultType	= mdns_querier_get_result_type( me->querier );
	Boolean									done		= false;
	
	if( resultType == mdns_querier_result_type_response )
	{
		msgPtr = mdns_querier_get_response_ptr( me->querier );
		msgLen = mdns_querier_get_response_length( me->querier );
		dpt_ulog( kLogLevelInfo, "Querier response: %.1{du:dnsmsg}\n", msgPtr, msgLen );
	}
	else
	{
		if( resultType == mdns_querier_result_type_error )
		{
			err = mdns_querier_get_error( me->querier );
			if( !err ) err = kUnknownErr;
		}
		else
		{
			err = kUnexpectedErr;
		}
		dpt_ulog( kLogLevelError, "Querier result: %s, error: %#m\n", mdns_querier_get_result_type( me->querier ), err );
		goto exit;
	}
	switch( me->queryParam )
	{
		case kDNSProxyTestQuery_A:
		case kDNSProxyTestQuery_AAAA:
		case kDNSProxyTestQuery_IPv6OnlyA:
		case kDNSProxyTestQuery_IPv6OnlyAAAA:
		case kDNSProxyTestQuery_IPv4OnlyAAAA:
		case kDNSProxyTestQuery_AliasA:
		case kDNSProxyTestQuery_AliasAAAA:
		case kDNSProxyTestQuery_AliasIPv6OnlyA:
		case kDNSProxyTestQuery_AliasIPv6OnlyAAAA:
		case kDNSProxyTestQuery_AliasIPv4OnlyAAAA:
		case kDNSProxyTestQuery_NXDomainA:
		case kDNSProxyTestQuery_NXDomainAAAA:
			verifyErr = _DNSProxyTestVerifyAddressResponse( msgPtr, msgLen, me->qname, me->qtype, me->responseCode,
				me->aliasCount, me->answerCount,
				me->synthesizedAAAA ? me->dns64Prefix : NULL,
				me->synthesizedAAAA ? me->dns64PrefixBitLen : 0 );
			break;
		
		case kDNSProxyTestQuery_ReverseIPv6:
		case kDNSProxyTestQuery_ReverseIPv6NXDomain:
		case kDNSProxyTestQuery_ReverseIPv6DNS64:
		case kDNSProxyTestQuery_ReverseIPv6DNS64NXDomain:
			verifyErr = _DNSProxyTestVerifyReverseIPv6Response( msgPtr, msgLen, me->qname, me->responseCode,
				me->canonicalName, me->answerName );
			break;
		
		default:
			FatalErrorF( "Unhandled DNSProxyTestQuery value %ld", (long) me->queryParam );
	}
	err = _DNSProxyTestContinue( me, verifyErr, &done );
	require_noerr( err, exit );
	
exit:
	if( err || done ) _DNSProxyTestStop( me, err );
}

//===========================================================================================================================

static OSStatus
	_DNSProxyTestVerifyAddressResponse(
		const uint8_t *	inMsgPtr,
		size_t			inMsgLen,
		const uint8_t *	inQName,
		uint16_t		inQType,
		int				inResponseCode,
		unsigned int	inAliasCount,
		unsigned int	inAnswerCount,
		const uint8_t *	inDNS64Prefix,
		int				inDNS64PrefixBitLen )
{
	OSStatus				err;
	const DNSHeader *		hdr;
	const uint8_t *			ptr;
	const uint8_t *			answerSection;
	unsigned int			flags, qCount, answerCount;
	int						rcode;
	uint16_t				qtype, qclass;
	uint8_t					qname[ kDomainNameLengthMax ];
	
	require_action_quiet( inMsgLen >= kDNSHeaderLength, exit, err = kMalformedErr );
	
	hdr		= (const DNSHeader *) inMsgPtr;
	flags	= DNSHeaderGetFlags( hdr );
	rcode	= DNSFlagsGetRCode( flags );
	require_action_quiet( rcode == inResponseCode, exit, err = kValueErr );
	
	qCount = DNSHeaderGetQuestionCount( hdr );
	require_action_quiet( qCount == 1, exit, err = kCountErr );
	
	ptr = (const uint8_t *) &hdr[ 1 ];
	err = DNSMessageExtractQuestion( inMsgPtr, inMsgLen, ptr, qname, &qtype, &qclass, &ptr );
	require_noerr_quiet( err, exit );
	require_action_quiet( DomainNameEqual( qname, inQName ), exit, err = kNameErr );
	require_action_quiet( qtype  == inQType, exit, err = kTypeErr );
	require_action_quiet( qclass == kDNSClassType_IN, exit, kTypeErr );
	
	answerCount = DNSHeaderGetAnswerCount( hdr );
	require_action_quiet( answerCount == ( inAliasCount + inAnswerCount ), exit, err = kCountErr );
	
	answerSection = ptr;
	if( inAliasCount > 0 )
	{
		unsigned int		i;
		const uint8_t *		parentDomain;
		uint8_t				target[ kDomainNameLengthMax ];
		
		parentDomain = DomainNameGetNextLabel( inQName );
		require_fatal( parentDomain, "Invalid qname '%{du:dname}' for non-zero alias count.", inQName );
		
		target[ 0 ] = 0;
		err = DomainNameAppendDomainName( target, inQName, NULL );
		require_noerr( err, exit );
		
		for( i = 0; i < inAliasCount; ++i )
		{
			unsigned int			j;
			const unsigned int		aliasNumber	= inAliasCount - i;
			Boolean					foundCNAME	= false;
			
			ptr = answerSection;
			for( j = 0; j < answerCount; ++j )
			{
				const uint8_t *		rdataPtr;
				unsigned int		nextAliasNumber;
				uint16_t			type;
				uint16_t			class;
				uint8_t				name[ kDomainNameLengthMax ];
				char				aliasLabelStr[ 32 ];
				
				err = DNSMessageExtractRecord( inMsgPtr, inMsgLen, ptr, name, &type, &class, NULL, &rdataPtr, NULL, &ptr );
				require_noerr( err, exit );
				
				if( type  != kDNSRecordType_CNAME )		continue;
				if( class != kDNSClassType_IN )		continue;
				if( !DomainNameEqual( name, target ) )	continue;
				
				target[ 0 ] = 0;
				nextAliasNumber = aliasNumber - 1;
				if( nextAliasNumber >= 2 )
				{
					SNPrintF( aliasLabelStr, sizeof( aliasLabelStr ), "alias-%u", nextAliasNumber );
					err = DomainNameAppendString( target, aliasLabelStr, NULL );
					require_noerr( err, exit );
				}
				else if( nextAliasNumber == 1 )
				{
					SNPrintF( aliasLabelStr, sizeof( aliasLabelStr ), "alias" );
					err = DomainNameAppendString( target, aliasLabelStr, NULL );
					require_noerr( err, exit );
				}
				err = DomainNameAppendDomainName( target, parentDomain, NULL );
				require_noerr( err, exit );
				
				err = DNSMessageExtractDomainName( inMsgPtr, inMsgLen, rdataPtr, name, NULL );
				require_noerr( err, exit );
				
				if( !DomainNameEqual( name, target ) ) continue;
				foundCNAME = true;
				break;
			}
			require_action_quiet( foundCNAME, exit, err = kNotFoundErr );
		}
	}
	if( inAnswerCount > 0 )
	{
		const uint8_t *		target;
		unsigned int		i;
		
		if( inAliasCount > 0 )
		{
			target = DomainNameGetNextLabel( inQName );
			require_fatal( target, "Invalid qname '%{du:dname}' for non-zero alias count.", inQName );
		}
		else
		{
			target = inQName;
		}
		for( i = 0; i < inAnswerCount; ++i )
		{
			unsigned int		j;
			size_t				expectedLen;
			uint8_t				expectedData[ 16 ];
			Boolean				foundRecord = false;
			
			if( inQType == kDNSRecordType_A )
			{
				const uint32_t		ipv4Addr = kDNSServerBaseAddrV4 + ( i + 1 );
				
				WriteBig32Typed( expectedData, ipv4Addr );
				expectedLen = 4;
			}
			else
			{
				if( inDNS64PrefixBitLen != 0 )
				{
					const uint32_t		ipv4Addr = kDNSServerBaseAddrV4 + ( i + 1 );
					
					err = _DNSProxyTestSynthesizeIPv6( inDNS64Prefix, inDNS64PrefixBitLen, ipv4Addr, expectedData );
					require_noerr( err, exit );
				}
				else
				{
					memcpy( expectedData, kDNSServerBaseAddrV6, 16 );
					expectedData[ 15 ] = (uint8_t)( i + 1 );
				}
				expectedLen = 16;
			}
			ptr = answerSection;
			for( j = 0; j < answerCount; ++j )
			{
				const uint8_t *		rdataPtr;
				size_t				rdataLen;
				uint16_t			type;
				uint16_t			class;
				uint8_t				name[ kDomainNameLengthMax ];
				
				err = DNSMessageExtractRecord( inMsgPtr, inMsgLen, ptr, name, &type, &class, NULL, &rdataPtr, &rdataLen,
					&ptr );
				require_noerr( err, exit );
				
				if( type  != inQType )												continue;
				if( class != kDNSClassType_IN )										continue;
				if( !DomainNameEqual( name, target ) )								continue;
				if( !MemEqual( rdataPtr, rdataLen, expectedData, expectedLen ) )	continue;
				foundRecord = true;
				break;
			}
			require_action_quiet( foundRecord, exit, err = kNotFoundErr );
		}
	}
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus
	_DNSProxyTestFindDomainNameRecord(
		const uint8_t *	inMsgPtr,
		size_t			inMsgLen,
		const uint8_t *	inRecordSection,
		unsigned int	inRecordCount,
		const uint8_t *	inRecordName,
		uint16_t		inRecordType,
		const uint8_t *	inRDataName );

static OSStatus
	_DNSProxyTestVerifyReverseIPv6Response(
		const uint8_t *	inMsgPtr,
		size_t			inMsgLen,
		const uint8_t *	inQName,
		int				inResponseCode,
		const uint8_t *	inCanonicalName,
		const uint8_t *	inAnswerName )
{
	OSStatus				err;
	const DNSHeader *		hdr;
	const uint8_t *			ptr;
	const uint8_t *			answerSection;
	const uint8_t *			ownerName;
	unsigned int			flags, qCount, answerCount, answerCountExpected;
	int						rcode;
	uint16_t				qtype, qclass;
	uint8_t					qname[ kDomainNameLengthMax ];
	
	require_action_quiet( inMsgLen >= kDNSHeaderLength, exit, err = kMalformedErr );
	
	hdr		= (const DNSHeader *) inMsgPtr;
	flags	= DNSHeaderGetFlags( hdr );
	rcode	= DNSFlagsGetRCode( flags );
	require_action_quiet( rcode == inResponseCode, exit, err = kValueErr );
	
	qCount = DNSHeaderGetQuestionCount( hdr );
	require_action_quiet( qCount == 1, exit, err = kCountErr );
	
	ptr = (const uint8_t *) &hdr[ 1 ];
	err = DNSMessageExtractQuestion( inMsgPtr, inMsgLen, ptr, qname, &qtype, &qclass, &ptr );
	require_noerr_quiet( err, exit );
	require_action_quiet( DomainNameEqual( qname, inQName ), exit, err = kNameErr );
	require_action_quiet( qtype  == kDNSRecordType_PTR, exit, err = kTypeErr );
	require_action_quiet( qclass == kDNSClassType_IN, exit, kTypeErr );
	
	answerCountExpected	= ( inCanonicalName ? 1 : 0 ) + ( inAnswerName ? 1 : 0 );
	answerCount			= DNSHeaderGetAnswerCount( hdr );
	require_action_quiet( answerCount == answerCountExpected, exit, err = kCountErr );
	
	answerSection	= ptr;
	ownerName		= inQName;
	if( inCanonicalName )
	{
		err = _DNSProxyTestFindDomainNameRecord( inMsgPtr, inMsgLen, answerSection, answerCount, ownerName,
			kDNSRecordType_CNAME, inCanonicalName );
		require_noerr( err, exit );
		
		ownerName = inCanonicalName;
	}
	if( inAnswerName )
	{
		err = _DNSProxyTestFindDomainNameRecord( inMsgPtr, inMsgLen, answerSection, answerCount, ownerName,
			kDNSRecordType_PTR, inAnswerName );
		require_noerr( err, exit );
	}
	
exit:
	return( err );
}

static OSStatus
	_DNSProxyTestFindDomainNameRecord(
		const uint8_t *	inMsgPtr,
		size_t			inMsgLen,
		const uint8_t *	inRecordSection,
		unsigned int	inRecordCount,
		const uint8_t *	inRecordName,
		uint16_t		inRecordType,
		const uint8_t *	inRDataName )
{
	OSStatus			err;
	const uint8_t *		ptr;
	unsigned int		i;
	Boolean				found = false;
	
	ptr = inRecordSection;
	for( i = 0; i < inRecordCount; ++i )
	{
		const uint8_t *		rdataPtr;
		uint16_t			type;
		uint16_t			class;
		uint8_t				name[ kDomainNameLengthMax ];
		
		err = DNSMessageExtractRecord( inMsgPtr, inMsgLen, ptr, name, &type, &class, NULL, &rdataPtr, NULL, &ptr );
		require_noerr( err, exit );
		
		if( type  != inRecordType )						continue;
		if( class != kDNSClassType_IN )					continue;
		if( !DomainNameEqual( name, inRecordName ) )	continue;
		
		err = DNSMessageExtractDomainName( inMsgPtr, inMsgLen, rdataPtr, name, NULL );
		require_noerr( err, exit );
		
		if( !DomainNameEqual( name, inRDataName ) ) continue;
		found = true;
		break;
	}
	err = found ? kNoErr : kNotFoundErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static void DNSSD_API
	_DNSProxyTestProbeGAICallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inCtx )
{
	OSStatus					err;
	const DNSProxyTestRef		me		= (DNSProxyTestRef) inCtx;
	Boolean						done	= false;
	
	Unused( inSDRef );
	Unused( inInterfaceIndex );
	Unused( inHostname );
	Unused( inTTL );
	
	if( ( inFlags & kDNSServiceFlagsAdd ) && !inError )
	{
		DNSServiceForget( &me->probeGAI );
		dispatch_source_forget( &me->timer );
		
		dpt_ulog( kLogLevelInfo, "Probe: Got GAI address %##a for %s\n", inSockAddr, me->probeHostname );
		
		err = _DNSProxyTestContinue( me, kNoErr, &done );
		require_noerr( err, exit );
	}
	err = kNoErr;
	
exit:
	if( err || done ) _DNSProxyTestStop( me, err );
}

//===========================================================================================================================

static void	_DNSProxyTestProbeTimerHandler( void *inCtx )
{
	const DNSProxyTestRef		me = (DNSProxyTestRef) inCtx;
	
	dpt_ulog( kLogLevelInfo, "Probe: GAI request for '%s' timed out.\n", me->probeHostname );
	_DNSProxyTestStop( me, kNotPreparedErr );
}

//===========================================================================================================================
//	RCodeTestCmd
//===========================================================================================================================

#define kRCodeTestMaxRCodeValue		15

typedef struct RCodeTest *		RCodeTestRef;
struct RCodeTest
{
	dispatch_queue_t			queue;				// Serial queue for test events.
	dispatch_semaphore_t		doneSem;			// Semaphore to signal when the test is done.
	dnssd_getaddrinfo_t			gai;				// Current subtest's GAI object. (Also used for probing test DNS server.)
	dispatch_source_t			timer;				// Timer for enforcing time limit on current dnssd_getaddrinfo.
	CFMutableDictionaryRef		report;				// Test's report, as a plist.
	CFMutableArrayRef			subtestResults;		// Pointer to report's subtest results.
	CFMutableArrayRef			gaiResults;			// Array of dnssd_getaddrinfo_result objects for the current GAI.
	char *						hostname;			// Current subtest's hostname. (Also used for probing test DNS server.)
	char *						description;		// Current subtest description.
	NanoTime64					startTime;			// Current subtest's start time.
	pid_t						serverPID;			// PID of spawned test DNS server.
	OSStatus					error;				// Current test error.
	int32_t						refCount;			// Test's reference count.
	int							rcode;				// Argument to use in current subtest's hostname's RCODE label.
	int							indexIdx;			// Index of argument to use in current subtest's hostname's Index label.
	int							subtestCount;		// Number of subtests that have completed or are in progress.
	int							subtestPassCount;	// Number of subtests that have passed so far.
	Boolean						hostnameIsAlias;	// True if current subtest's hostname is an alias.
	Boolean						hostnameHasAddr;	// True if current subtest's hostname that has an IPv4 address.
	Boolean						done;				// True if all subtests have completed.
};

ulog_define_ex( kDNSSDUtilIdentifier, RCodeTest, kLogLevelInfo, kLogFlags_None, "RCodeTest", NULL );
#define rct_ulog( LEVEL, ... )		ulog( &log_category_from_name( RCodeTest ), (LEVEL), __VA_ARGS__ )

static OSStatus	_RCodeTestCreate( RCodeTestRef *outTest );
static OSStatus	_RCodeTestRun( RCodeTestRef inTest, Boolean *outPassed );
static void		_RCodeTestRetain( RCodeTestRef inTest );
static void		_RCodeTestRelease( RCodeTestRef inTest );

static void	RCodeTestCmd( void )
{
	OSStatus				err;
	OutputFormatType		outputFormat;
	RCodeTestRef			test	= NULL;
	Boolean					passed	= false;
	
	err = CheckRootUser();
	require_noerr_quiet( err, exit );
	
	err = OutputFormatFromArgString( gRCodeTest_OutputFormat, &outputFormat );
	require_noerr_quiet( err, exit );
	
	err = _RCodeTestCreate( &test );
	require_noerr( err, exit );
	
	err = _RCodeTestRun( test, &passed );
	require_noerr( err, exit );
	
	err = OutputPropertyList( test->report, outputFormat, gRCodeTest_OutputFilePath );
	require_noerr( err, exit );
	
exit:
	if( test ) _RCodeTestRelease( test );
    gExitCode = err ? 1 : ( passed ? 0 : 2 );
}

//===========================================================================================================================

static OSStatus	_RCodeTestCreate( RCodeTestRef *outTest )
{
	OSStatus			err;
	RCodeTestRef		obj;
	
	obj = (RCodeTestRef) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->refCount	= 1;
	obj->error		= kInProgressErr;
	obj->serverPID	= -1;
	
	obj->queue = dispatch_queue_create( "com.apple.dnssdutil.rcode-test", DISPATCH_QUEUE_SERIAL );
	require_action( obj->queue, exit, err = kNoResourcesErr );
	
	obj->doneSem = dispatch_semaphore_create( 0 );
	require_action( obj->doneSem, exit, err = kNoResourcesErr );
	
	obj->report = CFDictionaryCreateMutable( NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks );
	require_action( obj->report, exit, err = kNoMemoryErr );
	
	obj->indexIdx = -1;
	*outTest = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) _RCodeTestRelease( obj );
	return( err );
}

//===========================================================================================================================

static void		_RCodeTestStart( void *inCtx );
static void		_RCodeTestStop( RCodeTestRef inTest, OSStatus inError );
static OSStatus	_RCodeTestStartSubtest( RCodeTestRef inTest );
static OSStatus	_RCodeTestContinue( RCodeTestRef inType, OSStatus inSubtestError, Boolean *outDone );
static Boolean	_RCodeTestRCodeIsGood( const int inRCode );

static OSStatus	_RCodeTestRun( RCodeTestRef me, Boolean *outPassed )
{
	Boolean		passed;
	
	dispatch_async_f( me->queue, me, _RCodeTestStart );
	dispatch_semaphore_wait( me->doneSem, DISPATCH_TIME_FOREVER );
	
	passed = ( !me->error && ( me->subtestPassCount == me->subtestCount ) ) ? true : false;
	CFDictionarySetBoolean( me->report, CFSTR( "pass" ), passed );
	rct_ulog( kLogLevelInfo, "Test result: %s\n", passed ? "pass" : "fail" );
	
	if( outPassed ) *outPassed = passed;
	return( me->error );
}

//===========================================================================================================================

static void	_RCodeTestRetain( const RCodeTestRef me )
{
	atomic_add_32( &me->refCount, 1 );
}

//===========================================================================================================================

static void	_RCodeTestRelease( const RCodeTestRef me )
{
	if( atomic_add_and_fetch_32( &me->refCount, -1 ) == 0 )
	{
		check( !me->gai );
		check( !me->timer );
		check( !me->subtestResults );
		check( !me->gaiResults );
		check( !me->hostname );
		check( !me->description );
		check( me->serverPID < 0 );
		dispatch_forget( &me->queue );
		dispatch_forget( &me->doneSem );
		ForgetCF( &me->report );
		free( me );
	}
}

//===========================================================================================================================

#define kRCodeTestProbeQueryTimeoutSecs		5

static void	_RCodeTestHandleGAIProbeResults( RCodeTestRef inTest, dnssd_getaddrinfo_result_t *inResults, size_t inCount );
static void	_RCodeTestProbeQueryTimerHandler( void *inCtx );

static void	_RCodeTestStart( void * const inCtx )
{
	OSStatus				err;
	const RCodeTestRef		me			= (RCodeTestRef) inCtx;
	char *					serverCmd	= NULL;
	dnssd_getaddrinfo_t		gai;
	NanoTime64				startTime;
	char					startTimeStr[ 32 ];
	char					tag[ 6 + 1 ];
	
	startTime = NanoTimeGetCurrent();
	rct_ulog( kLogLevelInfo, "Starting test\n" );
	
	// The "dnssdutil server" command will create a resolver entry for the server's "d.test." domain containing an array
	// of the server's IP addresses. Because configd favors IPv6 addresses, when there's a mix of IPv4 and IPv6
	// addresses, configd may rearrange the array in order to ensure that IPv6 addresses come before the IPv4 addresses.
	// To preserve the original address order, the server is specified to run in IPv6-only mode. This way,
	// mDNSResponder's view of the address will be such that address with index value 1 is first, address with index
	// value 2 is second, etc.
	
	serverCmd = NULL;
	ASPrintF( &serverCmd,
		DNSSDUTIL_TEST_DNS_SERVER_COMMAND_PREAMBLE " --loopback --follow %lld --responseDelay 20 --ipv6 --extraIPv6 3",
		(int64_t) getpid() );
	require_action_quiet( serverCmd, exit, err = kNoMemoryErr );
	
	err = _SpawnCommand( &me->serverPID, "/dev/null", "/dev/null", "%s", serverCmd );
	require_noerr( err, exit );
	
	check( !me->hostname );
	me->hostname = NULL;
	ASPrintF( &me->hostname, "tag-rcode-test-probe-%s.ipv4.d.test.",
		_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
	require_action( me->hostname, exit, err = kNoMemoryErr );
	
	check( !me->gai );
	me->gai = dnssd_getaddrinfo_create();
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	dnssd_getaddrinfo_set_hostname( me->gai, me->hostname );
	dnssd_getaddrinfo_set_flags( me->gai, 0 );
	dnssd_getaddrinfo_set_interface_index( me->gai, kDNSServiceInterfaceIndexAny );
	dnssd_getaddrinfo_set_protocols( me->gai, kDNSServiceProtocol_IPv4 );
	dnssd_getaddrinfo_set_queue( me->gai, me->queue );
	gai = me->gai;
	dnssd_retain( gai );
	_RCodeTestRetain( me );
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		_RCodeTestHandleGAIProbeResults( me, inResults, inCount );
	} );
	dnssd_getaddrinfo_set_event_handler( me->gai,
	^( const dnssd_event_t inEvent, const DNSServiceErrorType inGAIError )
	{
		if( inEvent == dnssd_event_invalidated )
		{
			dnssd_release( gai );
			_RCodeTestRelease( me );
		}
		else if( inEvent == dnssd_event_error )
		{
			require_return( me->gai == gai );
			rct_ulog( kLogLevelError, "dnssd_getaddrinfo error: %#m\n", inGAIError );
			_RCodeTestStop( me, inGAIError );
		}
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	check( !me->timer );
	err = DispatchTimerOneShotCreate( dispatch_time_seconds( kRCodeTestProbeQueryTimeoutSecs ),
		kRCodeTestProbeQueryTimeoutSecs * ( UINT64_C_safe( kNanosecondsPerSecond ) / 10 ),
		me->queue, _RCodeTestProbeQueryTimerHandler, me, &me->timer );
	require_noerr( err, exit );
	dispatch_resume( me->timer );
	
	_NanoTime64ToTimestamp( startTime, startTimeStr, sizeof( startTimeStr ) );
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, me->report,
		"%kO=%s"	// startTime
		"%kO=%s"	// serverCmd
		"%kO=%s"	// probeHostname
		"%kO=[%@]",	// results
		CFSTR( "startTime" ),		startTimeStr,
		CFSTR( "serverCmd" ),		serverCmd,
		CFSTR( "probeHostname" ),	me->hostname,
		CFSTR( "results" ),			&me->subtestResults );
	require_noerr( err, exit );
	
exit:
	FreeNullSafe( serverCmd );
	if( err ) _RCodeTestStop( me, err );
}

static void
	_RCodeTestHandleGAIProbeResults(
		const RCodeTestRef					me,
		dnssd_getaddrinfo_result_t * const	inResults,
		const size_t						inCount )
{
	size_t		i;
	Boolean		startSubtests = false;
	
	for( i = 0; i < inCount; ++i )
	{
		const dnssd_getaddrinfo_result_t		result = inResults[ i ];
		
		if( dnssd_getaddrinfo_result_get_type( result ) == dnssd_getaddrinfo_result_type_add )
		{
			rct_ulog( kLogLevelInfo, "Probe GAI got %##a for %s\n",
				dnssd_getaddrinfo_result_get_address( result ), me->hostname );
			startSubtests = true;
			break;
		}
	}
	if( startSubtests )
	{
		OSStatus		err;
		
		dnssd_getaddrinfo_forget( &me->gai );
		dispatch_source_forget( &me->timer );
		err = _RCodeTestStartSubtest( me );
		if( err ) _RCodeTestStop( me, err );
	}
}

static void	_RCodeTestProbeQueryTimerHandler( void * const inCtx )
{
	const RCodeTestRef		me = (RCodeTestRef) inCtx;
	
	rct_ulog( kLogLevelInfo, "Probe GAI request for '%s' timed out.\n", me->hostname );
	_RCodeTestStop( me, kNotPreparedErr );
}

//===========================================================================================================================

static void	_RCodeTestStop( RCodeTestRef me, OSStatus inError )
{
	OSStatus		err;
	NanoTime64		endTime;
	char			endTimeStr[ 32 ];
	
	endTime = NanoTimeGetCurrent();
	me->error = inError;
	rct_ulog( kLogLevelInfo, "Stopping test with error: %#m\n", me->error );
	
	dnssd_getaddrinfo_forget( &me->gai );
	dispatch_source_forget( &me->timer );
	me->subtestResults = NULL;
	ForgetCF( &me->gaiResults );
	ForgetMem( &me->hostname );
	ForgetMem( &me->description );
	if( me->serverPID >= 0 )
	{
		OSStatus		killErr;
		
		killErr = kill( me->serverPID, SIGTERM );
		killErr = map_global_noerr_errno( killErr );
		check_noerr( killErr );
		me->serverPID = -1;
	}
	_NanoTime64ToTimestamp( endTime, endTimeStr, sizeof( endTimeStr ) );
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, me->report,
		"%kO=%s"		// endTime
		"%kO=%lli"		// subtestCount
		"%kO=%lli",		// subtestPassCount
		CFSTR( "endTime" ),				endTimeStr,
		CFSTR( "subtestCount" ),		(int64_t) me->subtestCount,
		CFSTR( "subtestPassCount" ),	(int64_t) me->subtestPassCount );
	check_noerr( err );
	if( err && !me->error ) me->error = err;
	dispatch_semaphore_signal( me->doneSem );
}

//===========================================================================================================================

#define kRCodeSubtestRegularTimeLimitSecs		4
#define kRCodeSubtestExtendedTimeLimitSecs		12	// Allow three seconds for each of the four server addresses.

static void	_RCodeSubtestHandleGAIResults( RCodeTestRef inTest, dnssd_getaddrinfo_result_t *inResults, size_t inCount );
static void	_RCodeSubtestTimerHandler( void *inCtx );

static const void *	_DNSSDObjectCFArrayCallbackRetain( CFAllocatorRef inAllocator, const void *inObject );
static void			_DNSSDObjectCFArrayCallbackRelease( CFAllocatorRef inAllocator, const void *inObject );

static const CFArrayCallBacks kDNSSDObjectArrayCallbacks =
{
	.version	= 0,
	.retain		= _DNSSDObjectCFArrayCallbackRetain,
	.release	= _DNSSDObjectCFArrayCallbackRelease
};

// Arguments to use for Index labels. Since the test uses a test DNS server with four IPv6 addresses, which
// mDNSResponder views as four different servers, this is an arbitrary mix of the four possible index values. An Index
// label makes it so that only the server specified by the Index label's argument responds with the correct response.
// The point of the mix is to force mDNSResponder to have to send queries to more than one server before it gets the
// right response.

static const int		kRCodeTestIndexArguments[] = { 2, 4, 1, 3, 2, 1, 4, 3 };
#define kRCodeTestMaxIndexArgumentIndex		( countof( kRCodeTestIndexArguments ) - 1 )

static OSStatus	_RCodeTestStartSubtest( const RCodeTestRef me )
{
	OSStatus				err;
	dnssd_getaddrinfo_t		gai;
	const char *			rcodeStr;
	int						index;
	unsigned int			timeLimitSecs;
	char					rcodeStrBuf[ 32 ];
	char					indexLabelStr[ 32 ];
	char					rcodeLabelStr[ 32 ];
	char					tag[ 6 + 1 ];
	
	require_action_quiet( !me->done, exit, err = kInternalErr );
	
	check( !me->gai );
	check( !me->timer );
	
	me->startTime = NanoTimeGetCurrent();
	
	if( me->indexIdx < 0 )
	{
		index = -1;
		indexLabelStr[ 0 ] = '\0';
		require_fatal( ( me->rcode >= 0 ) && ( me->rcode <= kRCodeTestMaxRCodeValue ),
			"Unexpected subtest rcode value %d.", me->rcode );
	}
	else
	{
		index = kRCodeTestIndexArguments[ me->indexIdx ];
		SNPrintF( indexLabelStr, sizeof( indexLabelStr ), "index-%d.", index );
		me->hostnameHasAddr = true;
		me->hostnameIsAlias = true;
		require_fatal( ( me->rcode >= -1 ) && ( me->rcode <= kRCodeTestMaxRCodeValue ),
			"Unexpected subtest rcode value %d.", me->rcode );
	}
	if( me->rcode > 0 )
	{
		SNPrintF( rcodeLabelStr, sizeof( rcodeLabelStr ), "rcode-%d.", me->rcode );
	}
	else
	{
		rcodeLabelStr[ 0 ] = '\0';
	}
	ForgetMem( &me->hostname );
	ASPrintF( &me->hostname, "%s%s%scount-%d.tag-rcode-test-%s.d.test.",
		me->hostnameIsAlias ? "alias." : "", indexLabelStr, rcodeLabelStr, me->hostnameHasAddr ? 1 : 0,
		_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
	require_action( me->hostname, exit, err = kNoMemoryErr );
	
	CFReleaseNullSafe( me->gaiResults );
	me->gaiResults = CFArrayCreateMutable( NULL, 0, &kDNSSDObjectArrayCallbacks );
	require_action( me->gaiResults, exit, err = kNoMemoryErr );
	
	me->gai = dnssd_getaddrinfo_create();
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	dnssd_getaddrinfo_set_hostname( me->gai, me->hostname );
	dnssd_getaddrinfo_set_flags( me->gai, kDNSServiceFlagsReturnIntermediates );
	dnssd_getaddrinfo_set_interface_index( me->gai, kDNSServiceInterfaceIndexAny );
	dnssd_getaddrinfo_set_protocols( me->gai, kDNSServiceProtocol_IPv4 );
	dnssd_getaddrinfo_set_queue( me->gai, me->queue );
	gai = me->gai;
	dnssd_retain( gai );
	_RCodeTestRetain( me );
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		_RCodeSubtestHandleGAIResults( me, inResults, inCount );
	} );
	dnssd_getaddrinfo_set_event_handler( gai,
	^( const dnssd_event_t inEvent, const DNSServiceErrorType inGAIError )
	{
		if( inEvent == dnssd_event_invalidated )
		{
			dnssd_release( gai );
			_RCodeTestRelease( me );
		}
		else if( inEvent == dnssd_event_error )
		{
			OSStatus		testErr;
			Boolean			done;
			
			require_return( me->gai == gai );
			rct_ulog( kLogLevelError, "dnssd_getaddrinfo error: %#m\n", inGAIError );
			testErr = _RCodeTestContinue( me, inGAIError, &done );
			if( testErr || done ) _RCodeTestStop( me, testErr );
		}
	} );
	
	// If an Index label is being used without an RCode label, then only the server specified by the index label will
	// respond to the query, so we need more time to allow for query retries due to unresponsive servers.
	
	if( ( index > 0 ) && ( me->rcode < 0 ) )
	{
		timeLimitSecs = kRCodeSubtestExtendedTimeLimitSecs;
	}
	else
	{
		timeLimitSecs = kRCodeSubtestRegularTimeLimitSecs;
	}
	check( !me->timer );
	err = DispatchTimerOneShotCreate( dispatch_time_seconds( timeLimitSecs ),
		timeLimitSecs * ( UINT64_C_safe( kNanosecondsPerSecond ) / 20 ),
		me->queue, _RCodeSubtestTimerHandler, me, &me->timer );
	require_noerr( err, exit );
	
	rcodeStr = DNSRCodeToString( me->rcode );
	if( !rcodeStr )
	{
		SNPrintF( rcodeStrBuf, sizeof( rcodeStrBuf ), "RCODE%d", me->rcode );
		rcodeStr = rcodeStrBuf;
	}
	ForgetMem( &me->description );
	if( me->indexIdx < 0 )
	{
		ASPrintF( &me->description, "DNS response with RCODE %s (%d), %s CNAME record, and %s A record from all servers",
			rcodeStr, me->rcode, me->hostnameIsAlias ? "one" : "no", me->hostnameHasAddr ? "one" : "no" );
		require_action( me->description, exit, err = kNoMemoryErr );
	}
	else
	{
		int		n;
		
		ASPrintF( &me->description, "DNS response with RCODE NoError (0), %s CNAME record, and %s A record from server #%d.",
			me->hostnameIsAlias ? "one" : "no", me->hostnameHasAddr ? "one" : "no", index );
		require_action( me->description, exit, err = kNoMemoryErr );
		
		if( me->rcode < 0 )
		{
			n = AppendPrintF( &me->description, " No DNS respones from all other servers." );
			require_action( n >= 0, exit, err = kNoMemoryErr );
		}
		else
		{
			n = AppendPrintF( &me->description, " DNS responses with RCODE %s (%d) and no records from all other servers.",
				rcodeStr, me->rcode );
			require_action( n >= 0, exit, err = kNoMemoryErr );
		}
	}
	++me->subtestCount;
	rct_ulog( kLogLevelInfo, "Starting subtest #%d: %s\n", me->subtestCount, me->description );
	
	dnssd_getaddrinfo_activate( me->gai );
	dispatch_resume( me->timer );
	
exit:
	return( err );
}

static void
	_RCodeSubtestHandleGAIResults(
		const RCodeTestRef					me,
		dnssd_getaddrinfo_result_t * const	inResults,
		const size_t						inCount )
{
	size_t			i;
	
	for( i = 0; i < inCount; ++i )
	{
		const dnssd_getaddrinfo_result_t		result = inResults[ i ];
		
		rct_ulog( kLogLevelInfo, "GAI result -- %@\n", result );
		CFArrayAppendValue( me->gaiResults, result );
	}
}

static void	_RCodeSubtestTimerHandler( void * const inCtx )
{
	OSStatus							err;
	const RCodeTestRef					me = (RCodeTestRef) inCtx;
	dnssd_getaddrinfo_result_t			result;
	const sockaddr_ip *					sip;
	const uint8_t *						targetName;
	CFIndex								n;
	dnssd_getaddrinfo_result_type_t		resultType;
	int									expectedResultCount;
	uint8_t								hostname[ kDomainNameLengthMax ];
	uint8_t								actualHostname[ kDomainNameLengthMax ];
	Boolean								expectAddress, expectCanonName, done;
	
	// mDNSResponder has traditionally ignored responses with RCODEs not equal to NoError, NXDomain, or NotAuth.
	// Such responses result in a kDNSServiceErr_NoSuchRecord error, or NoAddress in the case of dnssd_getaddrinfo.
	
	if( ( me->indexIdx >= 0 ) || _RCodeTestRCodeIsGood( me->rcode ) )
	{
		expectAddress		= me->hostnameHasAddr;
		expectCanonName		= me->hostnameIsAlias;
		expectedResultCount	= 1;
	}
	else
	{
		expectAddress		= false;
		expectCanonName		= false;
		expectedResultCount	= 1;
	}
	n = CFArrayGetCount( me->gaiResults );
	require_action_quiet( n == expectedResultCount, exit, err = kCountErr );
	
	if( n != 0 )
	{
		result = (dnssd_getaddrinfo_result_t) CFArrayGetValueAtIndex( me->gaiResults, 0 );
		resultType = dnssd_getaddrinfo_result_get_type( result );
		
		sip = (const sockaddr_ip *) dnssd_getaddrinfo_result_get_address( result );
		require_action_quiet( sip->sa.sa_family == AF_INET, exit, err = kAddressErr );
		
		if( expectAddress )
		{
			require_action_quiet( resultType == dnssd_getaddrinfo_result_type_add, exit, err = kTypeErr );
			require_action_quiet( ntohl( sip->v4.sin_addr.s_addr ) == ( kDNSServerBaseAddrV4 + 1 ), exit, err = kAddressErr );
		}
		else
		{
			require_action_quiet( resultType == dnssd_getaddrinfo_result_type_no_address, exit, err = kTypeErr );
		}
		err = DomainNameFromString( hostname, me->hostname, NULL );
		require_noerr_fatal( err, "Failed to convert hostname to DNS wire format -- hostname: %s, error: %#m",
			me->hostname, err );
		
		err = DomainNameFromString( actualHostname, dnssd_getaddrinfo_result_get_actual_hostname( result ), NULL );
		require_noerr_quiet( err, exit );
		
		if( expectCanonName )
		{
			size_t		firstLabelLen;
			
			firstLabelLen = hostname[ 0 ];
			require_fatal( firstLabelLen > 0, "Hostname's first label length is zero -- hostname: %s", me->hostname );
			
			targetName = &hostname[ 1 + firstLabelLen ];
		}
		else
		{
			targetName = hostname;
		}
		require_action_quiet( DomainNameEqual( actualHostname, targetName ), exit, err = kNameErr );
	}
	err = kNoErr;
	
exit:
	err = _RCodeTestContinue( me, err, &done );
	if( err || done ) _RCodeTestStop( me, err );
}

static const void *	_DNSSDObjectCFArrayCallbackRetain( __unused const CFAllocatorRef inAllocator, const void *inObject )
{
	dnssd_retain( (dnssd_object_t) inObject );
	return inObject;
}

static void	_DNSSDObjectCFArrayCallbackRelease( __unused const CFAllocatorRef inAllocator, const void *inObject )
{
	dnssd_release( (dnssd_object_t) inObject );
}

//===========================================================================================================================

static OSStatus	_RCodeTestStopSubtest( RCodeTestRef inTest, OSStatus inError );

static OSStatus	_RCodeTestContinue( const RCodeTestRef me, OSStatus inSubtestError, Boolean *outDone )
{
	OSStatus		err;
	
	require_action_quiet( !me->done, exit, err = kNoErr );
	
	err = _RCodeTestStopSubtest( me, inSubtestError );
	require_noerr( err, exit );
	require_action_quiet( !me->done, exit, err = kNoErr );
	
	err = _RCodeTestStartSubtest( me );
	require_noerr( err, exit );
	
exit:
	if( outDone ) *outDone = me->done;
	return( err );
}

//===========================================================================================================================

static OSStatus	_RCodeTestStopSubtest( const RCodeTestRef me, const OSStatus inError )
{
	OSStatus				err;
	NanoTime64				endTime;
	CFMutableArrayRef		gaiResultDescs;
	char					errorStr[ 128 ];
	char					startTimeStr[ 32 ];
	char					endTimeStr[ 32 ];
	CFIndex					i, n;
	
	dnssd_getaddrinfo_forget( &me->gai );
	dispatch_source_forget( &me->timer );
	
	endTime = NanoTimeGetCurrent();
	
	if( !inError ) ++me->subtestPassCount;
	
	rct_ulog( kLogLevelInfo, "Subtest #%d result: %s (pass rate: %d/%d)\n",
		me->subtestCount, inError ? "fail" : "pass", me->subtestPassCount, me->subtestCount );
	
	_NanoTime64ToTimestamp( me->startTime, startTimeStr, sizeof( startTimeStr ) );
	_NanoTime64ToTimestamp( endTime, endTimeStr, sizeof( endTimeStr ) );
	SNPrintF( errorStr, sizeof( errorStr ), "%m", inError );
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, me->subtestResults,
		"{"
			"%kO=%s"		// description
			"%kO=%s"		// startTime
			"%kO=%s"		// endTime
			"%kO=%s"		// hostname
			"%kO=%b"		// pass
			"%kO="			// error
			"{"
				"%kO=%lli"	// code
				"%kO=%s"	// description
			"}"
			"%kO=[%@]"		// GAIResults
		"}",
		CFSTR( "description" ),	me->description,
		CFSTR( "startTime" ),	startTimeStr,
		CFSTR( "endTime" ),		endTimeStr,
		CFSTR( "hostname" ),	me->hostname,
		CFSTR( "pass" ),		!inError ? true : false,
		CFSTR( "error" ),
		CFSTR( "code" ),		(int64_t) inError,
		CFSTR( "description" ),	errorStr,
		CFSTR( "GAIResults" ),	&gaiResultDescs );
	require_noerr( err, exit );
	
	n = CFArrayGetCount( me->gaiResults );
	for( i = 0; i < n; ++ i )
	{
		dnssd_getaddrinfo_result_t		result = (dnssd_getaddrinfo_result_t) CFArrayGetValueAtIndex( me->gaiResults, i );
		char *							resultDesc;
		
		resultDesc = dnssd_copy_description( result );
		require_action_quiet( resultDesc, exit, err = kNoMemoryErr );
		
		err = CFPropertyListAppendFormatted( NULL, gaiResultDescs, "%s", resultDesc );
		ForgetMem( &resultDesc );
		require_noerr( err, exit );
	}
	
	if( me->indexIdx < 0 )
	{
		// Each subtest is one of the 64 possible combinations of
		// rcode ∈ {0, 1, 2, …, 15}, hostnameIsAlias ∈ {false, true}, hostnameHasAddr ∈ {false, true}.
		
		if( !me->hostnameHasAddr )
		{
			me->hostnameHasAddr = true;
		}
		else
		{
			me->hostnameHasAddr = false;
			if( !me->hostnameIsAlias )
			{
				me->hostnameIsAlias = true;
			}
			else
			{
				me->hostnameIsAlias = false;
				if( me->rcode < kRCodeTestMaxRCodeValue )
				{
					++me->rcode;
				}
				else
				{
					me->indexIdx	= 0;	// At this point we move on to using Index labels.
					me->rcode		= -1;	// Start with rcode set to -1 to indicate no RCode label.
				}
			}
		}
	}
	else
	{
		if( me->indexIdx < (int) kRCodeTestMaxIndexArgumentIndex )
		{
			++me->indexIdx;
		}
		else
		{
			me->indexIdx = 0;
			
			// When using Index labels to limit responses to one server, we want the other servers to respond
			// with bad RCODEs, so if the current RCODE value is good, try incrementing again.
			
			do
			{
				if( me->rcode < kRCodeTestMaxRCodeValue )
				{
					++me->rcode;
				}
				else
				{
					me->done = true;
				}
				
			}	while( !me->done && _RCodeTestRCodeIsGood( me->rcode ) );
		}
	}
	
exit:
	return( err );
}

//===========================================================================================================================

static Boolean	_RCodeTestRCodeIsGood( const int inRCode )
{
	return( ( inRCode == kDNSRCode_NoError ) || ( inRCode == kDNSRCode_NXDomain ) || ( inRCode == kDNSRCode_NotAuth ) );
}

//===========================================================================================================================
//	DNSQueryTestCmd
//===========================================================================================================================

typedef uint32_t DNSQueryTestGAIOpts;
#define kDNSQueryTestGAIOpts_None			0
#define kDNSQueryTestGAIOpts_WantIPv4		( 1U << 0 )
#define kDNSQueryTestGAIOpts_WantIPv6		( 1U << 1 )

typedef struct
{
	unsigned int			cnameCount;
	unsigned int			addrCount;
	DNSQueryTestGAIOpts		gaiOpts;
	
}	DNSQuerySubtestParams;

const DNSQuerySubtestParams		kDNSQuerySubtestParams[] =
{
	{ .cnameCount = 0, .addrCount = 1, .gaiOpts = kDNSQueryTestGAIOpts_WantIPv4 },
	{ .cnameCount = 0, .addrCount = 1, .gaiOpts = kDNSQueryTestGAIOpts_WantIPv6 },
	{ .cnameCount = 0, .addrCount = 1, .gaiOpts = kDNSQueryTestGAIOpts_WantIPv4 | kDNSQueryTestGAIOpts_WantIPv6 },
	{ .cnameCount = 3, .addrCount = 1, .gaiOpts = kDNSQueryTestGAIOpts_WantIPv4 },
	{ .cnameCount = 3, .addrCount = 1, .gaiOpts = kDNSQueryTestGAIOpts_WantIPv6 },
	{ .cnameCount = 3, .addrCount = 1, .gaiOpts = kDNSQueryTestGAIOpts_WantIPv4 | kDNSQueryTestGAIOpts_WantIPv6 },
	{ .cnameCount = 0, .addrCount = 3, .gaiOpts = kDNSQueryTestGAIOpts_WantIPv4 },
	{ .cnameCount = 0, .addrCount = 3, .gaiOpts = kDNSQueryTestGAIOpts_WantIPv6 },
	{ .cnameCount = 0, .addrCount = 3, .gaiOpts = kDNSQueryTestGAIOpts_WantIPv4 | kDNSQueryTestGAIOpts_WantIPv6 },
	{ .cnameCount = 3, .addrCount = 3, .gaiOpts = kDNSQueryTestGAIOpts_WantIPv4 },
	{ .cnameCount = 3, .addrCount = 3, .gaiOpts = kDNSQueryTestGAIOpts_WantIPv6 },
	{ .cnameCount = 3, .addrCount = 3, .gaiOpts = kDNSQueryTestGAIOpts_WantIPv4 | kDNSQueryTestGAIOpts_WantIPv6 }
};

typedef struct DNSQueryTest *		DNSQueryTestRef;
struct DNSQueryTest
{
	dispatch_queue_t			queue;				// Serial queue for test events.
	dispatch_semaphore_t		doneSem;			// Semaphore to signal when the test is done.
	dnssd_getaddrinfo_t			gai;				// Current subtest's GAI object. (Also used for probing test DNS server.)
	dispatch_source_t			timer;				// Timer for enforcing time limit on current dnssd_getaddrinfo.
	CFMutableDictionaryRef		report;				// Test's report, as a plist.
	CFMutableArrayRef			subtestResults;		// Pointer to report's subtest results.
	CFMutableArrayRef			gaiResults;			// All dnssd_getaddrinfo_result objects from the current GAI.
	CFMutableArrayRef			unexpectedResults;	// Unexpected dnssd_getaddrinfo_result from the current GAI.
	CFMutableArrayRef			missingAddrs;		// Address results missing from the current GAI.
	CFMutableArrayRef			queryCountStats;	// Stats regarding a subtests query counts.
	char *						hostname;			// Current subtest's hostname. (Also used for probing test DNS server.)
	char *						domain;				// High-level domain of current hostname.
	char *						description;		// Current subtest description.
	pcap_t *					pcap;				// Captures traffic between mDNSResponder and test DNS server.
	NanoTime64					startTime;			// Current subtest's start time.
	size_t						subtestIndex;		// Index of the current subtest.
	pid_t						serverPID;			// PID of spawned test DNS server.
	uint32_t					addressOffset;		// Current subtest's address offset for hostname addresses.
	int32_t						refCount;			// Test's reference count.
	OSStatus					error;				// Current test error.
	int							subtestCount;		// Number of subtests that have completed or are in progress.
	int							subtestPassCount;	// Number of subtests that have passed so far.
	uint16_t					serverPort;			// Port number used by DNS server.
	Boolean						haveBadQueryCounts;	// True if the current subtest's query counts are incorrect.
	Boolean						done;				// True if all subtests have completed.
};

ulog_define_ex( kDNSSDUtilIdentifier, DNSQueryTest, kLogLevelInfo, kLogFlags_None, "DNSQueryTest", NULL );
#define dqt_ulog( LEVEL, ... )		ulog( &log_category_from_name( DNSQueryTest ), (LEVEL), __VA_ARGS__ )

static OSStatus	_DNSQueryTestCreate( DNSQueryTestRef *outTest );
static OSStatus	_DNSQueryTestRun( DNSQueryTestRef inTest, Boolean *outPassed );
static void		_DNSQueryTestRetain( DNSQueryTestRef inTest );
static void		_DNSQueryTestRelease( DNSQueryTestRef inTest );

static void	DNSQueryTestCmd( void )
{
	OSStatus				err;
	OutputFormatType		outputFormat;
	DNSQueryTestRef			test	= NULL;
	Boolean					passed	= false;
	
	err = CheckRootUser();
	require_noerr_quiet( err, exit );
	
	err = OutputFormatFromArgString( gDNSQueryTest_OutputFormat, &outputFormat );
	require_noerr_quiet( err, exit );
	
	err = _DNSQueryTestCreate( &test );
	require_noerr( err, exit );
	
	err = _DNSQueryTestRun( test, &passed );
	require_noerr( err, exit );
	
	err = OutputPropertyList( test->report, outputFormat, gDNSQueryTest_OutputFilePath );
	require_noerr( err, exit );
	
exit:
	if( test ) _DNSQueryTestRelease( test );
    gExitCode = err ? 1 : ( passed ? 0 : 2 );
}

//===========================================================================================================================

static OSStatus	_DNSQueryTestCreate( DNSQueryTestRef * const outTest )
{
	OSStatus			err;
	DNSQueryTestRef		obj;
	
	obj = (DNSQueryTestRef) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->refCount	= 1;
	obj->error		= kInProgressErr;
	obj->serverPID	= -1;
	
	obj->queue = dispatch_queue_create( "com.apple.dnssdutil.dns-query-test", DISPATCH_QUEUE_SERIAL );
	require_action( obj->queue, exit, err = kNoResourcesErr );
	
	obj->doneSem = dispatch_semaphore_create( 0 );
	require_action( obj->doneSem, exit, err = kNoResourcesErr );
	
	obj->report = CFDictionaryCreateMutable( NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks );
	require_action( obj->report, exit, err = kNoMemoryErr );
	
	*outTest = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) _DNSQueryTestRelease( obj );
	return( err );
}

//===========================================================================================================================

static void		_DNSQueryTestStart( void *inCtx );
static void		_DNSQueryTestStop( DNSQueryTestRef inTest, OSStatus inError );
static OSStatus	_DNSQueryTestStartSubtest( DNSQueryTestRef inTest );
static OSStatus	_DNSQueryTestContinue( DNSQueryTestRef inType, OSStatus inSubtestError );

static OSStatus	_DNSQueryTestRun( const DNSQueryTestRef me, Boolean * const outPassed )
{
	Boolean		passed;
	
	dispatch_async_f( me->queue, me, _DNSQueryTestStart );
	dispatch_semaphore_wait( me->doneSem, DISPATCH_TIME_FOREVER );
	
	passed = ( !me->error && ( me->subtestPassCount == me->subtestCount ) ) ? true : false;
	CFDictionarySetBoolean( me->report, CFSTR( "pass" ), passed );
	dqt_ulog( kLogLevelInfo, "Test result: %s\n", passed ? "pass" : "fail" );
	
	if( outPassed ) *outPassed = passed;
	return( me->error );
}

//===========================================================================================================================

static void	_DNSQueryTestRetain( const DNSQueryTestRef me )
{
	atomic_add_32( &me->refCount, 1 );
}

//===========================================================================================================================

static void	_DNSQueryTestRelease( const DNSQueryTestRef me )
{
	if( atomic_add_and_fetch_32( &me->refCount, -1 ) == 0 )
	{
		check( !me->gai );
		check( !me->timer );
		check( !me->subtestResults );
		check( !me->gaiResults );
		check( !me->unexpectedResults );
		check( !me->missingAddrs );
		check( !me->queryCountStats );
		check( !me->hostname );
		check( !me->domain );
		check( !me->description );
		check( !me->pcap );
		check( me->serverPID < 0 );
		dispatch_forget( &me->queue );
		dispatch_forget( &me->doneSem );
		ForgetCF( &me->report );
		free( me );
	}
}

//===========================================================================================================================

#define kDNSQueryTestProbeQueryTimeoutSecs		5

static void
	_DNSQueryTestHandleGAIProbeResults(
		DNSQueryTestRef					inTest,
		dnssd_getaddrinfo_result_t *	inResults,
		size_t							inCount );
static void	_DNSQueryTestProbeQueryTimerHandler( void *inCtx );
static void	_DNSQueryTestSubtestCleanup( DNSQueryTestRef test );

static void	_DNSQueryTestStart( void * const inCtx )
{
	OSStatus					err;
	const DNSQueryTestRef		me			= (DNSQueryTestRef) inCtx;
	char *						serverCmd	= NULL;
	dnssd_getaddrinfo_t			gai;
	NanoTime64					startTime;
	char						startTimeStr[ 32 ];
	char						tag[ 6 + 1 ];
	
	startTime = NanoTimeGetCurrent();
	dqt_ulog( kLogLevelInfo, "Starting test\n" );
	
	serverCmd = NULL;
#if( DNSSDUTIL_TEST_USE_ALTERNATE_SERVER_PORT_FOR_DO53 )
	me->serverPort = kDNSPort_Do53Alt;
#else
	me->serverPort = kDNSPort_Do53;
#endif
	ASPrintF( &serverCmd, "dnssdutil server --loopback --follow %lld --port %u --responseDelay 10",
		(int64_t) getpid(), me->serverPort );
	require_action_quiet( serverCmd, exit, err = kNoMemoryErr );
	
	err = _SpawnCommand( &me->serverPID, "/dev/null", "/dev/null", "%s", serverCmd );
	require_noerr( err, exit );
	
	check( !me->hostname );
	ASPrintF( &me->hostname, "tag-dns-query-test-probe-%s.ipv4.d.test.",
		_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
	require_action( me->hostname, exit, err = kNoMemoryErr );
	
	check( !me->gai );
	me->gai = dnssd_getaddrinfo_create();
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	dnssd_getaddrinfo_set_hostname( me->gai, me->hostname );
	dnssd_getaddrinfo_set_flags( me->gai, 0 );
	dnssd_getaddrinfo_set_interface_index( me->gai, kDNSServiceInterfaceIndexAny );
	dnssd_getaddrinfo_set_protocols( me->gai, kDNSServiceProtocol_IPv4 );
	dnssd_getaddrinfo_set_queue( me->gai, me->queue );
	gai = me->gai;
	dnssd_retain( gai );
	_DNSQueryTestRetain( me );
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		_DNSQueryTestHandleGAIProbeResults( me, inResults, inCount );
	} );
	dnssd_getaddrinfo_set_event_handler( me->gai,
	^( const dnssd_event_t inEvent, const DNSServiceErrorType inGAIError )
	{
		if( inEvent == dnssd_event_invalidated )
		{
			dnssd_release( gai );
			_DNSQueryTestRelease( me );
		}
		else if( inEvent == dnssd_event_error )
		{
			require_return( me->gai == gai );
			dqt_ulog( kLogLevelError, "dnssd_getaddrinfo error: %#m\n", inGAIError );
			_DNSQueryTestStop( me, inGAIError );
		}
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	check( !me->timer );
	err = DispatchTimerOneShotCreate( dispatch_time_seconds( kDNSQueryTestProbeQueryTimeoutSecs ),
		kDNSQueryTestProbeQueryTimeoutSecs * ( UINT64_C_safe( kNanosecondsPerSecond ) / 10 ),
		me->queue, _DNSQueryTestProbeQueryTimerHandler, me, &me->timer );
	require_noerr( err, exit );
	dispatch_resume( me->timer );
	
	_NanoTime64ToTimestamp( startTime, startTimeStr, sizeof( startTimeStr ) );
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, me->report,
		"%kO=%s"	// startTime
		"%kO=%s"	// serverCmd
		"%kO=%s"	// probeHostname
		"%kO=[%@]",	// results
		CFSTR( "startTime" ),		startTimeStr,
		CFSTR( "serverCmd" ),		serverCmd,
		CFSTR( "probeHostname" ),	me->hostname,
		CFSTR( "results" ),			&me->subtestResults );
	require_noerr( err, exit );
	
exit:
	FreeNullSafe( serverCmd );
	if( err ) _DNSQueryTestStop( me, err );
}

static void
	_DNSQueryTestHandleGAIProbeResults(
		const DNSQueryTestRef				me,
		dnssd_getaddrinfo_result_t * const	inResults,
		const size_t						inCount )
{
	size_t		i;
	Boolean		startSubtests = false;
	
	for( i = 0; i < inCount; ++i )
	{
		const dnssd_getaddrinfo_result_t		result = inResults[ i ];
		
		if( dnssd_getaddrinfo_result_get_type( result ) == dnssd_getaddrinfo_result_type_add )
		{
			dqt_ulog( kLogLevelInfo, "Probe GAI got %##a for %s\n",
				dnssd_getaddrinfo_result_get_address( result ), me->hostname );
			startSubtests = true;
			break;
		}
	}
	if( startSubtests )
	{
		OSStatus		err;
		
		_DNSQueryTestSubtestCleanup( me );
		err = _DNSQueryTestStartSubtest( me );
		if( err ) _DNSQueryTestStop( me, err );
	}
}

static void	_DNSQueryTestProbeQueryTimerHandler( void * const inCtx )
{
	const DNSQueryTestRef		me = (DNSQueryTestRef) inCtx;
	
	dqt_ulog( kLogLevelInfo, "Probe GAI request for '%s' timed out.\n", me->hostname );
	_DNSQueryTestStop( me, kNotPreparedErr );
}

//===========================================================================================================================

static void	_DNSQueryTestStop( const DNSQueryTestRef me, const OSStatus inError )
{
	OSStatus		err;
	NanoTime64		endTime;
	char			endTimeStr[ 32 ];
	
	endTime = NanoTimeGetCurrent();
	me->error = inError;
	dqt_ulog( kLogLevelInfo, "Stopping test with error: %#m\n", me->error );
	
	_DNSQueryTestSubtestCleanup( me );
	me->subtestResults = NULL;
	if( me->serverPID >= 0 )
	{
		OSStatus		killErr;
		
		killErr = kill( me->serverPID, SIGTERM );
		killErr = map_global_noerr_errno( killErr );
		check_noerr( killErr );
		me->serverPID = -1;
	}
	_NanoTime64ToTimestamp( endTime, endTimeStr, sizeof( endTimeStr ) );
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, me->report,
		"%kO=%s"	// endTime
		"%kO=%i"	// subtestCount
		"%kO=%i",	// subtestPassCount
		CFSTR( "endTime" ),				endTimeStr,
		CFSTR( "subtestCount" ),		me->subtestCount,
		CFSTR( "subtestPassCount" ),	me->subtestPassCount );
	check_noerr( err );
	if( err && !me->error ) me->error = err;
	dispatch_semaphore_signal( me->doneSem );
}

//===========================================================================================================================

typedef struct
{
	size_t		queryCountA;
	size_t		queryCountAAAA;
	size_t		queryCountOther;
	
}	DNSQueryPCapStats;

static void
	_DNSQuerySubtestHandleGAIResults(
		DNSQueryTestRef					inTest,
		dnssd_getaddrinfo_result_t *	inResults,
		size_t							inCount );
static void		_DNSQuerySubtestTimerHandler( void *inCtx );
static OSStatus	_DNSQuerySubtestProcessResults( DNSQueryTestRef inTest );
static OSStatus	_DNSQueryTestProcessPacketCapture( pcap_t *inPCap, const char *inDomain, DNSQueryPCapStats *outStats );

#define kDNSQuerySubtestTimeSecs		5

static OSStatus	_DNSQueryTestStartSubtest( const DNSQueryTestRef me )
{
	OSStatus								err;
	dnssd_getaddrinfo_t						gai;
	const DNSQuerySubtestParams * const		params = &kDNSQuerySubtestParams[ me->subtestIndex ];
	DNSServiceProtocol						protocols;
	char									aliasLabelStr[ 32 ];
	char									tag[ 6 + 1 ];
	const Boolean							wantIPv4 = ( params->gaiOpts & kDNSQueryTestGAIOpts_WantIPv4 ) != 0;
	const Boolean							wantIPv6 = ( params->gaiOpts & kDNSQueryTestGAIOpts_WantIPv6 ) != 0;
	
	require_action_quiet( !me->done, exit, err = kInternalErr );
	
	check( !me->gai );
	check( !me->timer );
	
	me->startTime = NanoTimeGetCurrent();
	
	switch( params->cnameCount )
	{
		case 0:
			aliasLabelStr[ 0 ] = '\0';
			break;
		
		case 1:
			SNPrintF( aliasLabelStr, sizeof( aliasLabelStr ), "alias." );
			break;
		
		default:
			SNPrintF( aliasLabelStr, sizeof( aliasLabelStr ), "alias-%u.", params->cnameCount );
			break;
	}
	check( !me->domain );
	ASPrintF( &me->domain, "tag-dns-query-test-%s.d.test.",
		_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
	require_action( me->domain, exit, err = kNoMemoryErr );
	
	me->addressOffset = RandomRange( 0, 255 );
	check( !me->hostname );
	ASPrintF( &me->hostname, "%scount-%u.offset-%u.pdelay-100.%s",
		aliasLabelStr, params->addrCount, me->addressOffset, me->domain );
	require_action( me->hostname, exit, err = kNoMemoryErr );
	
	check( !me->gaiResults );
	me->gaiResults = CFArrayCreateMutable( NULL, 0, &kDNSSDObjectArrayCallbacks );
	require_action( me->gaiResults, exit, err = kNoMemoryErr );
	
	check( !me->gai );
	me->gai = dnssd_getaddrinfo_create();
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	dnssd_getaddrinfo_set_hostname( me->gai, me->hostname );
	dnssd_getaddrinfo_set_flags( me->gai, kDNSServiceFlagsReturnIntermediates );
	dnssd_getaddrinfo_set_interface_index( me->gai, kDNSServiceInterfaceIndexAny );
	protocols = 0;
	if( params->gaiOpts & kDNSQueryTestGAIOpts_WantIPv4 ) protocols |= kDNSServiceProtocol_IPv4;
	if( params->gaiOpts & kDNSQueryTestGAIOpts_WantIPv6 ) protocols |= kDNSServiceProtocol_IPv6;
	dnssd_getaddrinfo_set_protocols( me->gai, protocols );
	dnssd_getaddrinfo_set_queue( me->gai, me->queue );
	gai = me->gai;
	dnssd_retain( gai );
	_DNSQueryTestRetain( me );
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		_DNSQuerySubtestHandleGAIResults( me, inResults, inCount );
	} );
	dnssd_getaddrinfo_set_event_handler( gai,
	^( const dnssd_event_t inEvent, const DNSServiceErrorType inGAIError )
	{
		if( inEvent == dnssd_event_invalidated )
		{
			dnssd_release( gai );
			_DNSQueryTestRelease( me );
		}
		else if( inEvent == dnssd_event_error )
		{
			OSStatus		testErr;
			
			require_return( me->gai == gai );
			dqt_ulog( kLogLevelError, "dnssd_getaddrinfo error: %#m\n", inGAIError );
			testErr = _DNSQueryTestContinue( me, inGAIError );
			if( testErr || me->done ) _DNSQueryTestStop( me, testErr );
		}
	} );
	
	check( !me->pcap );
	err = _GAITesterCreatePacketCapture( me->serverPort, &me->pcap );
	require_noerr( err, exit );
	
	check( !me->timer );
	err = DispatchTimerOneShotCreate( dispatch_time_seconds( kDNSQuerySubtestTimeSecs ),
		kDNSQuerySubtestTimeSecs * ( UINT64_C_safe( kNanosecondsPerSecond ) / 20 ),
		me->queue, _DNSQuerySubtestTimerHandler, me, &me->timer );
	require_noerr( err, exit );
	
	check( !me->description );
	ASPrintF( &me->description, "%s%s%s GAI request for hostname with %u CNAME records and %u address records",
		wantIPv4 ? "IPv4" : "", ( wantIPv4 && wantIPv6 ) ? "+" : "", wantIPv6 ? "IPv6" : "", params->cnameCount,
		params->addrCount );
	require_action( me->description, exit, err = kNoMemoryErr );
	
	++me->subtestCount;
	dqt_ulog( kLogLevelInfo, "Starting subtest #%d: %s\n", me->subtestCount, me->description );
	
	dnssd_getaddrinfo_activate( me->gai );
	dispatch_resume( me->timer );
	
exit:
	return( err );
}

static void
	_DNSQuerySubtestHandleGAIResults(
		const DNSQueryTestRef				me,
		dnssd_getaddrinfo_result_t * const	inResults,
		const size_t						inCount )
{
	size_t		i;
	
	for( i = 0; i < inCount; ++i )
	{
		const dnssd_getaddrinfo_result_t		result = inResults[ i ];
		
		dqt_ulog( kLogLevelInfo, "GAI result -- %@\n", result );
		CFArrayAppendValue( me->gaiResults, result );
	}
}

static void	_DNSQuerySubtestTimerHandler( void * const inCtx )
{
	OSStatus					err;
	const DNSQueryTestRef		me = (DNSQueryTestRef) inCtx;
	
	err = _DNSQuerySubtestProcessResults( me );
	require_noerr( err, exit );
	
	err = _DNSQueryTestContinue( me, kNoErr );
	require_noerr( err, exit );
	
exit:
	if( err || me->done ) _DNSQueryTestStop( me, err );
}

static OSStatus	_DNSQuerySubtestProcessResults( const DNSQueryTestRef me )
{
	OSStatus								err;
	DNSQueryPCapStats						stats;
	const DNSQuerySubtestParams * const		params = &kDNSQuerySubtestParams[ me->subtestIndex ];
	uint64_t								addrBitmapV4, addrBitmapV6;
	CFIndex									i, n;
	unsigned int							expectedQueryCountA, expectedQueryCountAAAA, j;
	const Boolean							wantIPv4 = ( params->gaiOpts & kDNSQueryTestGAIOpts_WantIPv4 ) != 0;
	const Boolean							wantIPv6 = ( params->gaiOpts & kDNSQueryTestGAIOpts_WantIPv6 ) != 0;
	
	err = _DNSQueryTestProcessPacketCapture( me->pcap, me->domain, &stats );
	ForgetPacketCapture( &me->pcap );
	require_noerr( err, exit );
	
	expectedQueryCountA		= wantIPv4 ? 1 : 0;
	expectedQueryCountAAAA	= wantIPv6 ? 1 : 0;
	me->haveBadQueryCounts	= ( stats.queryCountA != expectedQueryCountA ) ||
		( stats.queryCountAAAA != expectedQueryCountAAAA ) || ( stats.queryCountOther != 0 );
	
	check( !me->queryCountStats );
	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &me->queryCountStats,
		"{"
			"%kO=%lli"	// expectedQueryCountA
			"%kO=%lli"	// expectedQueryCountAAAA
			"%kO=%lli"	// expectedQueryCountOther
			"%kO=%lli"	// actualQueryCountA
			"%kO=%lli"	// actualQueryCountAAAA
			"%kO=%lli"	// actualQueryCountOther
		"}",
		CFSTR( "expectedQueryCountA" ),		(int64_t) expectedQueryCountA,
		CFSTR( "expectedQueryCountAAAA" ),	(int64_t) expectedQueryCountAAAA,
		CFSTR( "expectedQueryCountOther" ),	(int64_t) 0,
		CFSTR( "actualQueryCountA" ),		(int64_t) stats.queryCountA,
		CFSTR( "actualQueryCountAAAA" ),	(int64_t) stats.queryCountAAAA,
		CFSTR( "actualQueryCountOther" ),	(int64_t) stats.queryCountOther );
	require_noerr( err, exit );
	require_noerr( err, exit );
	
	check( ( params->addrCount >= 0 ) && ( params->addrCount <= 64 ) );
	addrBitmapV4 = 0;
	if( wantIPv4 )
	{
		if( params->addrCount < 64 )	addrBitmapV4 = ( UINT64_C( 1 ) << params->addrCount ) - 1;
		else							addrBitmapV4 =  ~UINT64_C( 0 );
	}
	addrBitmapV6 = 0;
	if( wantIPv6 )
	{
		if( params->addrCount < 64 )	addrBitmapV6 = ( UINT64_C( 1 ) << params->addrCount ) - 1;
		else							addrBitmapV6 =  ~UINT64_C( 0 );
	}
	n = CFArrayGetCount( me->gaiResults );
	for( i = 0; i < n; ++i )
	{
		dnssd_getaddrinfo_result_t			result;
		const sockaddr_ip *					sip;
		dnssd_getaddrinfo_result_type_t		resultType;
		Boolean								validResult	= false;
		
		result = (dnssd_getaddrinfo_result_t) CFArrayGetValueAtIndex( me->gaiResults, i );
		resultType = dnssd_getaddrinfo_result_get_type( result );
		if( resultType == dnssd_getaddrinfo_result_type_add )
		{
			uint64_t *		bitmapPtr = NULL;
			uint32_t		addrValue = 0;
			
			sip = (const sockaddr_ip *) dnssd_getaddrinfo_result_get_address( result );
			switch( sip->sa.sa_family )
			{
				case AF_INET:
					if( params->gaiOpts & kDNSQueryTestGAIOpts_WantIPv4 )
					{
						const uint32_t		addr = ntohl( sip->v4.sin_addr.s_addr );
						
						if( ( addr & UINT32_C( 0xFFFFFF00 ) ) == kDNSServerBaseAddrV4 )
						{
							addrValue = addr & 0xFFU;
							bitmapPtr = &addrBitmapV4;
						}
					}
					break;
				
				case AF_INET6:
					if( params->gaiOpts & kDNSQueryTestGAIOpts_WantIPv6 )
					{
						const uint8_t * const		addr = sip->v6.sin6_addr.s6_addr;
						
						if( memcmp( addr, kDNSServerBaseAddrV6, 15 ) == 0 )
						{
							addrValue = addr[ 15 ];
							bitmapPtr = &addrBitmapV6;
						}
					}
					break;
			}
			if( bitmapPtr )
			{
				addrValue = ( addrValue - me->addressOffset ) % 256;
				if( ( addrValue >= 1 ) && ( addrValue <= params->addrCount ) )
				{
					const uint64_t		bitmask = UINT64_C( 1 ) << ( addrValue - 1 );
					
					if( *bitmapPtr & bitmask )
					{
						*bitmapPtr &= ~bitmask;
						validResult = true;
					}
				}
			}
		}
		if( !validResult )
		{
			if( !me->unexpectedResults )
			{
				me->unexpectedResults = CFArrayCreateMutable( NULL, 0, &kDNSSDObjectArrayCallbacks );
				require_action( me->unexpectedResults, exit, err = kNoMemoryErr );
			}
			CFArrayAppendValue( me->unexpectedResults, result );
		}
	}
	if( ( addrBitmapV4 != 0 ) || ( addrBitmapV6 != 0 ) )
	{
		check( !me->missingAddrs );
		me->missingAddrs = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
		require_action( me->missingAddrs, exit, err = kNoMemoryErr );
	}
	for( j = 1; ( j <= 64 ) && ( addrBitmapV4 != 0 ); ++j )
	{
		const uint64_t		bitmask = UINT64_C( 1 ) << ( j - 1 );
		
		if( addrBitmapV4 & bitmask )
		{
			uint8_t		missingAddr[ 4 ];
			
			addrBitmapV4 &= ~bitmask;
			WriteBig32Typed( missingAddr, kDNSServerBaseAddrV4 );
			missingAddr[ 3 ] = (uint8_t)( ( me->addressOffset + j ) % 256 );
			err = CFPropertyListAppendFormatted( kCFAllocatorDefault, me->missingAddrs, "%.4a", missingAddr );
			require_noerr( err, exit );
		}
	}
	for( j = 1; ( j <= 64 ) && ( addrBitmapV6 != 0 ); ++j )
	{
		const uint64_t		bitmask = UINT64_C( 1 ) << ( j - 1 );
		
		if( addrBitmapV6 & bitmask )
		{
			uint8_t		missingAddr[ 16 ];
			
			addrBitmapV6 &= ~bitmask;
			memcpy( missingAddr, kDNSServerBaseAddrV6, 16 );
			missingAddr[ 15 ] = (uint8_t)( ( me->addressOffset + j ) % 256 );
			err = CFPropertyListAppendFormatted( kCFAllocatorDefault, me->missingAddrs, "%.16a", missingAddr );
			require_noerr( err, exit );
		}
	}
	err = kNoErr;
	
exit:
	return( err );
}

static OSStatus
	_DNSQueryTestProcessPacketCapture(
		pcap_t * const				inPCap,
		const char * const			inDomain,
		DNSQueryPCapStats * const	outStats )
{
	OSStatus		err;
	int				domainLabelCount;
	uint8_t			domain[ kDomainNameLengthMax ];
	
	err = DomainNameFromString(	domain, inDomain, NULL );
	require_noerr( err, exit );
	
	domainLabelCount = DomainNameLabelCount( domain );
	if( outStats ) memset( outStats, 0, sizeof( *outStats ) );
	for( ;; )
	{
		int							status;
		struct pcap_pkthdr *		pktHdr;
		const uint8_t *				packet;
		const uint8_t *				msgPtr;
		size_t						msgLen;
		const DNSHeader *			hdr;
		unsigned int				flags;
		int							labelDiff;
		const uint8_t *				questionSection;
		const uint8_t *				qnameAncestor;
		uint16_t					qtype, qclass;
		uint8_t						qname[ kDomainNameLengthMax ];
		
		status = pcap_next_ex( inPCap, &pktHdr, &packet );
		if( status != 1 ) break;
		if( _GAITesterGetDNSMessageFromPacket( packet, pktHdr->caplen, &msgPtr, &msgLen ) != kNoErr ) continue;
		if( msgLen < kDNSHeaderLength ) continue;
		
		hdr = (const DNSHeader *) msgPtr;
		flags = DNSHeaderGetFlags( hdr );
		if( DNSFlagsGetOpCode( flags ) != kDNSOpCode_Query ) continue;
		if( DNSHeaderGetQuestionCount( hdr ) < 1 ) continue;
		
		questionSection = (const uint8_t *) &hdr[ 1 ];
		if( DNSMessageExtractQuestion( msgPtr, msgLen, questionSection, qname, &qtype, &qclass, NULL ) != kNoErr ) continue;
		if( qclass != kDNSServiceClass_IN ) continue;
		
		labelDiff = DomainNameLabelCount( qname ) - domainLabelCount;
		if( labelDiff < 0 ) continue;
		
		qnameAncestor = qname;
		while( labelDiff-- > 0 )
		{
			qnameAncestor = DomainNameGetNextLabel( qnameAncestor );
		}
		if( !DomainNameEqual( qnameAncestor, domain ) ) continue;
		
		if( outStats && ( flags & kDNSHeaderFlag_Response ) )
		{
			switch( qtype )
			{
				case kDNSRecordType_A:		++outStats->queryCountA;		break;
				case kDNSRecordType_AAAA:	++outStats->queryCountAAAA;		break;
				default:					++outStats->queryCountOther;	break;
			}
		}
	}
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_DNSQueryTestStopSubtest( DNSQueryTestRef inTest, OSStatus inError );

static OSStatus	_DNSQueryTestContinue( const DNSQueryTestRef me, const OSStatus inSubtestError )
{
	OSStatus		err;
	
	require_action_quiet( !me->done, exit, err = kNoErr );
	
	err = _DNSQueryTestStopSubtest( me, inSubtestError );
	require_noerr( err, exit );
	require_action_quiet( !me->done, exit, err = kNoErr );
	
	err = _DNSQueryTestStartSubtest( me );
	require_noerr( err, exit );
	
exit:
	return( err );
}

//===========================================================================================================================

static void	_DNSQueryTestSubtestCleanup( const DNSQueryTestRef me )
{
	dnssd_getaddrinfo_forget( &me->gai );
	dispatch_source_forget( &me->timer );
	CFForget( &me->gaiResults );
	CFForget( &me->unexpectedResults );
	CFForget( &me->missingAddrs );
	CFForget( &me->queryCountStats );
	ForgetMem( &me->hostname );
	ForgetMem( &me->domain );
	ForgetMem( &me->description );
	ForgetPacketCapture( &me->pcap );
	me->haveBadQueryCounts = false;
}

static OSStatus	_DNSQueryTestStopSubtest( const DNSQueryTestRef me, const OSStatus inError )
{
	OSStatus				err, subtestErr;
	NanoTime64				endTime;
	CFMutableArrayRef		gaiResultStrings, unexpectedResultStrings;
	CFIndex					i, n;
	char					errorStr[ 128 ];
	char					startTimeStr[ 32 ];
	char					endTimeStr[ 32 ];
	
	endTime = NanoTimeGetCurrent();
	
	if( inError )						subtestErr = inError;
	else if( me->missingAddrs )			subtestErr = kAddressErr;
	else if( me->unexpectedResults )	subtestErr = kUnexpectedErr;
	else if( me->haveBadQueryCounts )	subtestErr = kCountErr;
	else								subtestErr = kNoErr;
	if( !subtestErr ) ++me->subtestPassCount;
	dqt_ulog( kLogLevelInfo, "Subtest #%d result: %s (pass rate: %d/%d)\n",
		me->subtestCount, subtestErr ? "fail" : "pass", me->subtestPassCount, me->subtestCount );
	
	_NanoTime64ToTimestamp( me->startTime, startTimeStr, sizeof( startTimeStr ) );
	_NanoTime64ToTimestamp( endTime, endTimeStr, sizeof( endTimeStr ) );
	SNPrintF( errorStr, sizeof( errorStr ), "%m", subtestErr );
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, me->subtestResults,
		"{"
			"%kO=%s"		// description
			"%kO=%s"		// startTime
			"%kO=%s"		// endTime
			"%kO=%s"		// hostname
			"%kO=%b"		// pass
			"%kO=[%@]"		// GAIResults
			"%kO=%O"		// queryCountStats
			"%kO=[%@]"		// unexpectedResults
			"%kO=%O"		// missingAddrs
			"%kO="			// error
			"{"
				"%kO=%lli"	// code
				"%kO=%s"	// description
			"}"
		"}",
		CFSTR( "description" ),			me->description,
		CFSTR( "startTime" ),			startTimeStr,
		CFSTR( "endTime" ),				endTimeStr,
		CFSTR( "hostname" ),			me->hostname,
		CFSTR( "pass" ),				!subtestErr,
		CFSTR( "GAIResults" ),			&gaiResultStrings,
		CFSTR( "queryCountStats" ),		me->queryCountStats,
		CFSTR( "unexpectedResults" ),	&unexpectedResultStrings,
		CFSTR( "missingAddrs" ),		me->missingAddrs,
		CFSTR( "error" ),
		CFSTR( "code" ),				(int64_t) subtestErr,
		CFSTR( "description" ),			errorStr );
	require_noerr( err, exit );
	
	n = CFArrayGetCount( me->gaiResults );
	for( i = 0; i < n; ++ i )
	{
		CFStringRef		resultStr;
		
		resultStr = CFStringCreateF( &err, "%@", CFArrayGetValueAtIndex( me->gaiResults, i ) );
		require( resultStr, exit );
		
		CFArrayAppendValue( gaiResultStrings, resultStr );
		CFForget( &resultStr );
	}
	n = CFArrayGetCountNullSafe( me->unexpectedResults );
	for( i = 0; i < n; ++ i )
	{
		CFStringRef		resultStr;
		
		resultStr = CFStringCreateF( &err, "%@", CFArrayGetValueAtIndex( me->unexpectedResults, i ) );
		require( resultStr, exit );
		
		CFArrayAppendValue( unexpectedResultStrings, resultStr );
		CFForget( &resultStr );
	}
	if( me->subtestIndex < ( countof( kDNSQuerySubtestParams ) - 1 ) )
	{
		++me->subtestIndex;
	}
	else
	{
		me->done = true;
	}
	
exit:
	_DNSQueryTestSubtestCleanup( me );
	return( err );
}

//===========================================================================================================================
//	FastRecoveryTestCmd
//===========================================================================================================================

typedef struct
{
	dnssd_getaddrinfo_t		gai;			// GAI object.
	char *					hostname;		// Hostname to be resolved.
	Boolean					gotResultIPv4;	// True if the IPv4 GAI result was received.
	Boolean					gotResultIPv6;	// True if the IPv6 GAI result was received.
	
}	FastRecoveryGAI;

typedef struct FastRecoveryTest *		FastRecoveryTestRef;
struct FastRecoveryTest
{
	dispatch_queue_t			queue;			// Serial queue for test events.
	dispatch_semaphore_t		doneSem;		// Semaphore to signal when the test is done.
	dnssd_getaddrinfo_t			gai;			// GAI object for probing/suspending and resuming the test DNS server.
	FastRecoveryGAI				gaiArray[ 5 ];	// Array of regular GAI operations to test fast recovery.
	size_t						gaiCount;		// Number of regular GAI operations that are in progress.
	dispatch_source_t			timer;			// Timer for enforcing time limit on current dnssd_getaddrinfo.
	CFMutableDictionaryRef		report;			// Test's report, as a plist.
	CFMutableArrayRef			gaiResults;		// Pointer to report's GAI results.
	char *						domain;			// High-level domain for test's hostnames.
	char *						probeQNAME;		// QNAME used in probe query for test DNS server.
	char *						resumeQNAME;	// QNAME used in query to resume the test DNS server.
	NanoTime64					startTime;		// Current subtest's start time.
	pid_t						serverPID;		// PID of spawned test DNS server.
	int32_t						refCount;		// Test's reference count.
	OSStatus					error;			// Current test error.
	Boolean						suspended;		// True if the test DNS server is currently suspended.
	Boolean						done;			// True if all subtests have completed.
};

ulog_define_ex( kDNSSDUtilIdentifier, FastRecoveryTest, kLogLevelInfo, kLogFlags_None, "FastRecoveryTest", NULL );
#define frt_ulog( LEVEL, ... )		ulog( &log_category_from_name( FastRecoveryTest ), (LEVEL), __VA_ARGS__ )

static OSStatus	_FastRecoveryTestCreate( FastRecoveryTestRef *outTest );
static OSStatus	_FastRecoveryTestRun( FastRecoveryTestRef inTest, Boolean *outPassed );
static void		_FastRecoveryTestRetain( FastRecoveryTestRef inTest );
static void		_FastRecoveryTestRelease( FastRecoveryTestRef inTest );

static void	FastRecoveryTestCmd( void )
{
	OSStatus				err;
	OutputFormatType		outputFormat;
	FastRecoveryTestRef		test	= NULL;
	Boolean					passed	= false;
	
	err = OutputFormatFromArgString( gFastRecoveryTest_OutputFormat, &outputFormat );
	require_noerr_quiet( err, exit );
	
	err = _FastRecoveryTestCreate( &test );
	require_noerr( err, exit );
	
	err = _FastRecoveryTestRun( test, &passed );
	require_noerr( err, exit );
	
	err = OutputPropertyList( test->report, outputFormat, gFastRecoveryTest_OutputFilePath );
	require_noerr( err, exit );
	
exit:
	if( test ) _FastRecoveryTestRelease( test );
    gExitCode = err ? 1 : ( passed ? 0 : 2 );
}

//===========================================================================================================================

static OSStatus	_FastRecoveryTestCreate( FastRecoveryTestRef * const outTest )
{
	OSStatus				err;
	FastRecoveryTestRef		obj;
	
	obj = (FastRecoveryTestRef) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->refCount	= 1;
	obj->error		= kInProgressErr;
	obj->serverPID	= -1;
	
	obj->queue = dispatch_queue_create( "com.apple.dnssdutil.fast-recovery-test", DISPATCH_QUEUE_SERIAL );
	require_action( obj->queue, exit, err = kNoResourcesErr );
	
	obj->doneSem = dispatch_semaphore_create( 0 );
	require_action( obj->doneSem, exit, err = kNoResourcesErr );
	
	obj->report = CFDictionaryCreateMutable( NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks );
	require_action( obj->report, exit, err = kNoMemoryErr );
	
	*outTest = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) _FastRecoveryTestRelease( obj );
	return( err );
}

//===========================================================================================================================

static void		_FastRecoveryTestStart( void *inCtx );
static void		_FastRecoveryTestStop( FastRecoveryTestRef inTest, OSStatus inError );

static OSStatus	_FastRecoveryTestRun( const FastRecoveryTestRef me, Boolean * const outPassed )
{
	Boolean		passed;
	
	dispatch_async_f( me->queue, me, _FastRecoveryTestStart );
	dispatch_semaphore_wait( me->doneSem, DISPATCH_TIME_FOREVER );
	
	passed = !me->error;
	CFDictionarySetBoolean( me->report, CFSTR( "pass" ), passed );
	frt_ulog( kLogLevelInfo, "Test result: %s\n", passed ? "pass" : "fail" );
	
	if( outPassed ) *outPassed = passed;
	return( me->error );
}

//===========================================================================================================================

static void	_FastRecoveryTestRetain( const FastRecoveryTestRef me )
{
	atomic_add_32( &me->refCount, 1 );
}

//===========================================================================================================================

static void	_FastRecoveryTestRelease( const FastRecoveryTestRef me )
{
	if( atomic_add_and_fetch_32( &me->refCount, -1 ) == 0 )
	{
		check( !me->gai );
		check( !me->timer );
		check( !me->gaiResults );
		check( !me->domain );
		check( !me->probeQNAME );
		check( !me->resumeQNAME );
		check( me->serverPID < 0 );
		dispatch_forget( &me->queue );
		dispatch_forget( &me->doneSem );
		ForgetCF( &me->report );
		free( me );
	}
}

//===========================================================================================================================

#define kFastRecoveryTestProbeQueryTimeoutSecs		5

static void
	_FastRecoveryTestHandleGAIProbeResults(
		FastRecoveryTestRef				inTest,
		dnssd_getaddrinfo_result_t *	inResults,
		size_t							inCount );
static void		_FastRecoveryTestProbeQueryTimerHandler( void *inCtx );
static OSStatus	_FastRecoveryTestStartGAIRequests( FastRecoveryTestRef inTest );

static void	_FastRecoveryTestStart( void * const inCtx )
{
	OSStatus						err;
	const FastRecoveryTestRef		me			= (FastRecoveryTestRef) inCtx;
	char *							serverCmd	= NULL;
	dnssd_getaddrinfo_t				gai;
	NanoTime64						startTime;
	char							startTimeStr[ 32 ];
	char							tag[ 6 + 1 ];
	
	startTime = NanoTimeGetCurrent();
	frt_ulog( kLogLevelInfo, "Starting test\n" );
	
	serverCmd = NULL;
	ASPrintF( &serverCmd, DNSSDUTIL_TEST_DNS_SERVER_COMMAND_PREAMBLE " --loopback --follow %lld --responseDelay 10",
		(int64_t) getpid() );
	require_action_quiet( serverCmd, exit, err = kNoMemoryErr );
	
	err = _SpawnCommand( &me->serverPID, "/dev/null", "/dev/null", "%s", serverCmd );
	require_noerr( err, exit );
	
	check( !me->domain );
	ASPrintF( &me->domain, "tag-fast-recovery-test-%s.d.test.",
		_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
	require_action( me->domain, exit, err = kNoMemoryErr );
	
	check( !me->probeQNAME );
	ASPrintF( &me->probeQNAME, "tag-fast-recovery-test-probe.ttl-300.command-suspend.%s", me->domain );
	require_action( me->probeQNAME, exit, err = kNoMemoryErr );
	
	check( !me->gai );
	me->gai = dnssd_getaddrinfo_create();
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	dnssd_getaddrinfo_set_hostname( me->gai, me->probeQNAME );
	dnssd_getaddrinfo_set_flags( me->gai, 0 );
	dnssd_getaddrinfo_set_interface_index( me->gai, kDNSServiceInterfaceIndexAny );
	dnssd_getaddrinfo_set_protocols( me->gai, kDNSServiceProtocol_IPv4 );
	dnssd_getaddrinfo_set_queue( me->gai, me->queue );
	gai = me->gai;
	dnssd_retain( gai );
	_FastRecoveryTestRetain( me );
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		_FastRecoveryTestHandleGAIProbeResults( me, inResults, inCount );
	} );
	dnssd_getaddrinfo_set_event_handler( me->gai,
	^( const dnssd_event_t inEvent, const DNSServiceErrorType inGAIError )
	{
		if( inEvent == dnssd_event_invalidated )
		{
			dnssd_release( gai );
			_FastRecoveryTestRelease( me );
		}
		else if( inEvent == dnssd_event_error )
		{
			require_return( me->gai == gai );
			frt_ulog( kLogLevelError, "dnssd_getaddrinfo error: %#m\n", inGAIError );
			_FastRecoveryTestStop( me, inGAIError );
		}
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	check( !me->timer );
	err = DispatchTimerOneShotCreate( dispatch_time_seconds( kFastRecoveryTestProbeQueryTimeoutSecs ),
		kFastRecoveryTestProbeQueryTimeoutSecs * ( UINT64_C_safe( kNanosecondsPerSecond ) / 10 ),
		me->queue, _FastRecoveryTestProbeQueryTimerHandler, me, &me->timer );
	require_noerr( err, exit );
	dispatch_resume( me->timer );
	
	_NanoTime64ToTimestamp( startTime, startTimeStr, sizeof( startTimeStr ) );
	err = CFPropertyListAppendFormatted( NULL, me->report,
		"%kO=%s"	// startTime
		"%kO=%s"	// serverCmd
		"%kO=%s"	// probeQNAME
		"%kO=[%@]",	// results
		CFSTR( "startTime" ),	startTimeStr,
		CFSTR( "serverCmd" ),	serverCmd,
		CFSTR( "probeQNAME" ),	me->probeQNAME,
		CFSTR( "results" ),		&me->gaiResults );
	require_noerr( err, exit );
	
exit:
	FreeNullSafe( serverCmd );
	if( err ) _FastRecoveryTestStop( me, err );
}

static void
	_FastRecoveryTestHandleGAIProbeResults(
		const FastRecoveryTestRef			me,
		dnssd_getaddrinfo_result_t * const	inResults,
		const size_t						inCount )
{
	size_t		i;
	Boolean		startGAIRequests = false;
	
	for( i = 0; i < inCount; ++i )
	{
		const dnssd_getaddrinfo_result_t		result = inResults[ i ];
		
		if( dnssd_getaddrinfo_result_get_type( result ) == dnssd_getaddrinfo_result_type_add )
		{
			frt_ulog( kLogLevelInfo,
				"Probe GAI got %##a for %s\n", dnssd_getaddrinfo_result_get_address( result ), me->probeQNAME );
			startGAIRequests = true;
			break;
		}
	}
	if( startGAIRequests )
	{
		OSStatus		err;
		
		me->suspended = true;
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		err = _FastRecoveryTestStartGAIRequests( me );
		if( err ) _FastRecoveryTestStop( me, err );
	}
}

static void	_FastRecoveryTestProbeQueryTimerHandler( void * const inCtx )
{
	const FastRecoveryTestRef		me = (FastRecoveryTestRef) inCtx;
	
	frt_ulog( kLogLevelInfo, "Probe GAI request for '%s' timed out.\n", me->probeQNAME );
	_FastRecoveryTestStop( me, kNotPreparedErr );
}

//===========================================================================================================================

static void	_FastRecoveryTestStop( const FastRecoveryTestRef me, const OSStatus inError )
{
	OSStatus		err;
	NanoTime64		endTime;
	size_t			i;
	char			endTimeStr[ 32 ];
	
	endTime = NanoTimeGetCurrent();
	me->error = inError;
	frt_ulog( kLogLevelInfo, "Stopping test with error: %#m\n", me->error );
	
	dnssd_getaddrinfo_forget( &me->gai );
	dispatch_source_forget( &me->timer );
	for( i = 0; i < countof( me->gaiArray ); ++i )
	{
		FastRecoveryGAI * const		frGAI = &me->gaiArray[ i ];
		
		ForgetMem( &frGAI->hostname );
		dnssd_getaddrinfo_forget( &frGAI->gai );
	}
	ForgetMem( &me->domain );
	ForgetMem( &me->probeQNAME );
	ForgetMem( &me->resumeQNAME );
	me->gaiResults = NULL;
	if( me->serverPID >= 0 )
	{
		OSStatus		killErr;
		
		killErr = kill( me->serverPID, SIGTERM );
		killErr = map_global_noerr_errno( killErr );
		check_noerr( killErr );
		me->serverPID = -1;
	}
	_NanoTime64ToTimestamp( endTime, endTimeStr, sizeof( endTimeStr ) );
	err = CFPropertyListAppendFormatted( NULL, me->report, "%kO=%s", CFSTR( "endTime" ), endTimeStr );
	check_noerr( err );
	if( err && !me->error ) me->error = err;
	dispatch_semaphore_signal( me->doneSem );
}

//===========================================================================================================================

static OSStatus	_FastRecoveryTestStartQuerier( FastRecoveryTestRef inTest );

#define kFastRecoveryTestQuerierStartIntervalSecs		5

static OSStatus	_FastRecoveryTestStartGAIRequests( const FastRecoveryTestRef me )
{
	OSStatus		err;
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, _dispatch_monotonictime_after_sec( kFastRecoveryTestQuerierStartIntervalSecs ),
		kFastRecoveryTestQuerierStartIntervalSecs * UINT64_C_safe( kNanosecondsPerSecond ),
		kFastRecoveryTestQuerierStartIntervalSecs * ( UINT64_C_safe( kNanosecondsPerSecond ) / 20 ) );
	dispatch_set_context( me->timer, me );
	dispatch_source_set_event_handler( me->timer,
	^{
		OSStatus		startErr;
		
		startErr = _FastRecoveryTestStartQuerier( me );
		if( startErr ) _FastRecoveryTestStop( me, startErr );
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

#define kFastRecoveryTestFinalTimeoutSecs		2

static void
	_FastRecoveryTestHandleGAIResuts(
		FastRecoveryTestRef				inTest,
		FastRecoveryGAI *				inGAI,
		dnssd_getaddrinfo_result_t *	inResults,
		size_t							inCount );
static void	_FastRecoveryTestComplete( FastRecoveryTestRef inTest );

static OSStatus	_FastRecoveryTestStartQuerier( const FastRecoveryTestRef me )
{
	OSStatus		err;
	
	if( me->gaiCount < countof( me->gaiArray ) )
	{
		dnssd_getaddrinfo_t			gai;
		FastRecoveryGAI * const		frGAI = &me->gaiArray[ me->gaiCount++ ];
		char						tag[ 6 + 1 ];
		
		check( !frGAI->hostname );
		ASPrintF( &frGAI->hostname, "count-1.tag-%s.%s",
			_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ),
			me->domain );
		require_action( frGAI->hostname, exit, err = kNoMemoryErr );
		
		gai = dnssd_getaddrinfo_create();
		require_action( gai, exit, err = kNoResourcesErr );
		
		dnssd_getaddrinfo_set_hostname( gai, frGAI->hostname );
		dnssd_getaddrinfo_set_flags( gai, 0 );
		dnssd_getaddrinfo_set_interface_index( gai, kDNSServiceInterfaceIndexAny );
		dnssd_getaddrinfo_set_protocols( gai, kDNSServiceProtocol_IPv4 | kDNSServiceProtocol_IPv6 );
		dnssd_getaddrinfo_set_queue( gai, me->queue );
		dnssd_retain( gai );
		_FastRecoveryTestRetain( me );
		dnssd_getaddrinfo_set_result_handler( gai,
		^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
		{
			require_return( frGAI->gai == gai );
			_FastRecoveryTestHandleGAIResuts( me, frGAI, inResults, inCount );
		} );
		dnssd_getaddrinfo_set_event_handler( gai,
		^( const dnssd_event_t inEvent, const DNSServiceErrorType inGAIError )
		{
			switch( inEvent )
			{
				case dnssd_event_invalidated:
					dnssd_release( gai );
					_FastRecoveryTestRelease( me );
					break;
				
				case dnssd_event_error:
					require_return( frGAI->gai == gai );
					frt_ulog( kLogLevelError, "dnssd_getaddrinfo error: %#m\n", inGAIError );
					_FastRecoveryTestStop( me, inGAIError );
					break;
				
				default:
					break;
			}
		} );
		frGAI->gai = gai;
		gai = NULL;
		dnssd_getaddrinfo_activate( frGAI->gai );
	}
	else
	{
		dnssd_getaddrinfo_t		gai;
		
		dispatch_source_forget( &me->timer );
		me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
		require_action( me->timer, exit, err = kNoResourcesErr );
		
		dispatch_source_set_timer( me->timer, _dispatch_monotonictime_after_sec( kFastRecoveryTestFinalTimeoutSecs ),
			kFastRecoveryTestFinalTimeoutSecs * UINT64_C_safe( kNanosecondsPerSecond ),
			kFastRecoveryTestFinalTimeoutSecs * ( UINT64_C_safe( kNanosecondsPerSecond ) / 20 ) );
		dispatch_set_context( me->timer, me );
		dispatch_source_set_event_handler( me->timer,
		^{
			_FastRecoveryTestComplete( me );
		} );
		dispatch_activate( me->timer );
		
		check( !me->resumeQNAME );
		ASPrintF( &me->resumeQNAME, "tag-fast-recovery-test-probe.command-resume.%s", me->domain );
		require_action( me->resumeQNAME, exit, err = kNoMemoryErr );
		
		err = CFPropertyListAppendFormatted( NULL, me->report, "%kO=%s", CFSTR( "resumeQNAME" ), me->resumeQNAME );
		require_noerr( err, exit );
		
		gai = dnssd_getaddrinfo_create();
		require_action( gai, exit, err = kNoResourcesErr );
		
		dnssd_getaddrinfo_set_hostname( gai, me->resumeQNAME );
		dnssd_getaddrinfo_set_flags( gai, 0 );
		dnssd_getaddrinfo_set_interface_index( gai, kDNSServiceInterfaceIndexAny );
		dnssd_getaddrinfo_set_protocols( gai, kDNSServiceProtocol_IPv4 );
		dnssd_getaddrinfo_set_queue( gai, me->queue );
		dnssd_retain( gai );
		_FastRecoveryTestRetain( me );
		dnssd_getaddrinfo_set_result_handler( gai,
		^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
		{
			size_t		i;
			
			require_return( me->gai == gai );
			
			for( i = 0; i < inCount; ++i )
			{
				frt_ulog( kLogLevelInfo, "GAI result -- %@\n", inResults[ i ] );
			}
		} );
		dnssd_getaddrinfo_set_event_handler( gai,
		^( const dnssd_event_t inEvent, const DNSServiceErrorType inGAIError )
		{
			switch( inEvent )
			{
				case dnssd_event_invalidated:
					dnssd_release( gai );
					_FastRecoveryTestRelease( me );
					break;
				
				case dnssd_event_error:
					require_return( me->gai == gai );
					frt_ulog( kLogLevelError, "dnssd_getaddrinfo error: %#m\n", inGAIError );
					_FastRecoveryTestStop( me, inGAIError );
					break;
				
				default:
					break;
			}
		} );
		me->gai = gai;
		gai = NULL;
		dnssd_getaddrinfo_activate( me->gai );
		me->suspended = false; // The resume command query will resume the DNS server.
	}
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static void
	_FastRecoveryTestHandleGAIResuts(
		const FastRecoveryTestRef			me,
		FastRecoveryGAI * const				inGAI,
		dnssd_getaddrinfo_result_t * const	inResults,
		const size_t						inCount )
{
	OSStatus		err;
	size_t			i;
	
	for( i = 0; i < inCount; ++i )
	{
		const dnssd_getaddrinfo_result_t		result = inResults[ i ];
		
		frt_ulog( kLogLevelInfo, "GAI result -- %@\n", result );
		if( !me->suspended )
		{
			const sockaddr_ip *							sip;
			const dnssd_getaddrinfo_result_type_t		type = dnssd_getaddrinfo_result_get_type( result );
			
			require_action_quiet( type == dnssd_getaddrinfo_result_type_add, exit, err = kTypeErr );
			sip = (const sockaddr_ip *) dnssd_getaddrinfo_result_get_address( result );
			switch( sip->sa.sa_family )
			{
				case AF_INET:
					require_action_quiet( !inGAI->gotResultIPv4, exit, err = kUnexpectedErr );
					inGAI->gotResultIPv4 = true;
					break;
				
				case AF_INET6:
					require_action_quiet( !inGAI->gotResultIPv6, exit, err = kUnexpectedErr );
					inGAI->gotResultIPv6 = true;
					break;
				
				default:
					err = kTypeErr;
					goto exit;
			}
			if( inGAI->gotResultIPv4 && inGAI->gotResultIPv6 ) dnssd_getaddrinfo_forget( &inGAI->gai );
		}
	}
	err = me->suspended ? kUnexpectedErr : kNoErr;
	
exit:
	if( err ) _FastRecoveryTestStop( me, err );
}

//===========================================================================================================================

static void	_FastRecoveryTestComplete( const FastRecoveryTestRef me )
{
	OSStatus		err;
	size_t			i;
	Boolean			resultsMissing = false;
	
	for( i = 0; i < countof( me->gaiArray ); ++i )
	{
		FastRecoveryGAI * const		frGAI = &me->gaiArray[ i ];
		
		err = CFPropertyListAppendFormatted( NULL, me->gaiResults,
			"{"
				"%kO=%s"	// hostname
				"%kO=%b"	// gotResultIPv4
				"%kO=%b"	// gotResultIPv6
			"}",
			CFSTR( "hostname" ),		frGAI->hostname,
			CFSTR( "gotResultIPv4" ),	frGAI->gotResultIPv4,
			CFSTR( "gotResultIPv6" ),	frGAI->gotResultIPv6 );
		require_noerr( err, exit );
		
		if( !frGAI->gotResultIPv4 || !frGAI->gotResultIPv6 ) resultsMissing = true;
	}
	err = resultsMissing ? kUnderrunErr : kNoErr;
	
exit:
	_FastRecoveryTestStop( me, err );
}
#endif	// MDNSRESPONDER_PROJECT

//===========================================================================================================================
//	RegistrationTestCmd
//===========================================================================================================================

typedef struct RegistrationSubtest		RegistrationSubtest;

typedef struct
{
	CFMutableArrayRef			subtestReports;				// Array of subtest reports.
	dispatch_source_t			timer;						// Timer to enforce subtest durations.
	dispatch_source_t			sigSourceINT;				// SIGINT signal handler for a clean test exit.
	dispatch_source_t			sigSourceTERM;				// SIGTERM signal handler for a clean test exit.
	RegistrationSubtest *		subtest;					// Current subtest.
	char *						outputFilePath;				// Path of test result output file. If NULL, stdout will be used.
	OutputFormatType			outputFormat;				// Format of test results output.
	CFStringRef					computerNamePrev;			// Previous ComputerName.
	CFStringRef					localHostNamePrev;			// Previous LocalHostName.
	NanoTime64					startTime;					// Test's start time.
	char *						computerName;				// Temporary ComputerName to set during testing. (malloc'd)
	char *						localHostName;				// Temporary LocalHostName to set during testing. (malloc'd)
	CFStringEncoding			computerNamePrevEncoding;	// Previous ComputerName's encoding.
	int							subtestIndex;				// Index of current subtest.
	Boolean						computerNameSet;			// True if a temporary ComputerName was set.
	Boolean						localHostNameSet;			// True if a temporary LocalHostName was set.
	Boolean						failed;						// True if at least one non-skipped subtest failed.
	Boolean						forBATS;					// True if the test is running in a BATS environment.
	
}	RegistrationTest;

typedef enum
{
	kRegistrationInterfaceSet_Null			= 0,
	kRegistrationInterfaceSet_All			= 1,
	kRegistrationInterfaceSet_AllPlusAWDL	= 2,
	kRegistrationInterfaceSet_LoopbackOnly	= 3,
	kRegistrationInterfaceSet_AWDLOnly		= 4
	
}	RegistrationInterfaceSet;

typedef struct
{
	RegistrationInterfaceSet		interfaceSet;	// Interfaces to register the service over.
	Boolean							useDefaultName;	// True if registration is to use the default service name.
	Boolean							useLODiscovery;	// True if discovery is to use kDNSServiceInterfaceIndexLocalOnly.
	
}	RegistrationSubtestParams;

static const RegistrationSubtestParams		kRegistrationSubtestParams[] =
{
	{ kRegistrationInterfaceSet_All,			true,	false },
	{ kRegistrationInterfaceSet_All,			false,	false },
	{ kRegistrationInterfaceSet_AllPlusAWDL,	true,	false },
	{ kRegistrationInterfaceSet_AllPlusAWDL,	false,	false },
	{ kRegistrationInterfaceSet_LoopbackOnly,	true,	false },
	{ kRegistrationInterfaceSet_LoopbackOnly,	false,	false },
	{ kRegistrationInterfaceSet_AWDLOnly,		true,	false },
	{ kRegistrationInterfaceSet_AWDLOnly,		false,	false },
	{ kRegistrationInterfaceSet_All,			true,	true  },
	{ kRegistrationInterfaceSet_All,			false,	true  },
	{ kRegistrationInterfaceSet_AllPlusAWDL,	true,	true  },
	{ kRegistrationInterfaceSet_AllPlusAWDL,	false,	true  },
	{ kRegistrationInterfaceSet_LoopbackOnly,	true,	true  },
	{ kRegistrationInterfaceSet_LoopbackOnly,	false,	true  },
	{ kRegistrationInterfaceSet_AWDLOnly,		true,	true  },
	{ kRegistrationInterfaceSet_AWDLOnly,		false,	true  }
};

typedef struct
{
	NanoTime64		browseResultTime;	// Per-interface browse result time.
	NanoTime64		querySRVResultTime;	// Per-interface SRV record query result time.
	NanoTime64		queryTXTResultTime;	// Per-interface TXT record query result time.
	
}	RegistrationResultTimes;

typedef struct
{
	MDNSInterfaceItem			base;	// Underlying MDNSInterface linked-list item.
	RegistrationResultTimes		times;	// Per-interface result times.
	
}	RegistrationInterfaceItem;

struct RegistrationSubtest
{
	DNSServiceRef					registration;		// DNS-SD service registration.
	DNSServiceRef					connection;			// Shared DNS-SD connection.
	DNSServiceRef					browse;				// DNS-SD browse for service's type.
	DNSServiceRef					querySRV;			// DNS-SD query request for service's SRV record.
	DNSServiceRef					queryTXT;			// DNS-SD query request for service's TXT record.
	CFMutableArrayRef				unexpected;			// Array of unexpected registration, browse, and query results.
#if( TARGET_OS_WATCH )
	CFMutableArrayRef				ignored;			// Array of unexpected, but ignored, browse and query results.
#endif
	const char *					serviceName;		// Service's name.
	char *							serviceNameCustom;	// Service's name if using a custom name. (malloc'd)
	char *							serviceType;		// Service's service type. (malloc'd)
	size_t							serviceTypeLen;		// C string length of service's service type.
	char *							serviceFQDN;		// Service's FQDN, i.e., name of its SRV and TXT records.
	uint8_t *						txtPtr;				// Pointer to service's TXT record data. (malloc'd)
	size_t							txtLen;				// Length of service's TXT record data.
	RegistrationInterfaceItem *		ifList;				// If ifIndex == 0, interfaces that service should register over.
	RegistrationResultTimes			ifTimes;			// If ifIndex != 0, result times for interface with that index.
	RegistrationTest *				test;				// Pointer to parent test.
	NanoTime64						startTime;			// Subtest's start time.
	char *							description;		// Subtest's description. (malloc'd)
	uint32_t						ifIndex;			// Interface index used for service registration.
	uint16_t						port;				// Service's port number.
	Boolean							useLODiscovery;		// True if discovery is to use kDNSServiceInterfaceIndexLocalOnly.
	Boolean							includeAWDL;		// True if the IncludeAWDL flag was used during registration.
	Boolean							ifIsAWDL;			// True if ifIndex is the index of an AWDL interface.
	Boolean							skipped;			// True if this subtest is to be skipped.
	Boolean							registered;			// True if the test service was successfully registered.
	Boolean							useDefaultName;		// True if the service is to use the default service name.
};

static OSStatus	_RegistrationTestCreate( RegistrationTest **outTest );
static void		_RegistrationTestFree( RegistrationTest *inTest );
static void		_RegistrationTestBegin( void *inContext );
static void		_RegistrationTestProceed( RegistrationTest *inTest );
static OSStatus	_RegistrationTestStart( RegistrationTest *inTest );
static void		_RegistrationTestStop( RegistrationTest *inTest );
#define _RegistrationTestForget( X )		ForgetCustomEx( X, _RegistrationTestStop, _RegistrationTestFree )
static OSStatus
	_RegistrationTestStartSubtest(
		RegistrationTest *					inTest,
		const RegistrationSubtestParams *	inParams,
		Boolean *							outSkipped );
static OSStatus	_RegistrationTestEndSubtest( RegistrationTest *inTest );
static void		_RegistrationTestEnd( RegistrationTest *inTest ) ATTRIBUTE_NORETURN;
static void		_RegistrationTestExit( RegistrationTest *inTest, OSStatus inError ) ATTRIBUTE_NORETURN;
static OSStatus	_RegistrationSubtestCreate( RegistrationSubtest **outSubtest );
static void		_RegistrationSubtestStop( RegistrationSubtest *inSubtest );
static void		_RegistrationSubtestFree( RegistrationSubtest *inSubtest );
#define _RegistrationSubtestForget( X )		ForgetCustomEx( X, _RegistrationSubtestStop, _RegistrationSubtestFree )
static OSStatus	_RegistrationTestInterfaceListCreate( Boolean inIncludeAWDL, RegistrationInterfaceItem **outList );
static OSStatus
	_RegistrationTestCreateRandomTXTRecord(
		size_t		inMinLen,
		size_t		inMaxLen,
		uint8_t **	outTXTPtr,
		size_t *	outTXTLen );
static void DNSSD_API
	_RegistrationSubtestRegisterCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		DNSServiceErrorType	inError,
		const char *		inName,
		const char *		inType,
		const char *		inDomain,
		void *				inContext );
static void DNSSD_API
	_RegistrationSubtestBrowseCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		uint32_t			inIfIndex,
		DNSServiceErrorType	inError,
		const char *		inServiceName,
		const char *		inServiceType,
		const char *		inDomain,
		void *				inContext );
static void DNSSD_API
	_RegistrationSubtestQueryCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inIfIndex,
		DNSServiceErrorType		inError,
		const char *			inName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext );
static Boolean	_RegistrationSubtestValidServiceType( const RegistrationSubtest *inSubtest, const char *inServiceType );
static RegistrationResultTimes *
	_RegistrationSubtestGetInterfaceResultTimes(
		RegistrationSubtest *	inSubtest,
		uint32_t				inIfIndex,
		Boolean *				outIsAWDL );
static void		_RegistrationTestTimerHandler( void *inContext );
#if( TARGET_OS_WATCH )
static Boolean	_RegistrationTestInterfaceIsWiFi( const char *inIfName );
#endif

static void	RegistrationTestCmd( void )
{
	OSStatus				err;
	RegistrationTest *		test = NULL;
	
	err = _RegistrationTestCreate( &test );
	require_noerr( err, exit );
	
	if( gRegistrationTest_BATSEnvironment ) test->forBATS = true;
	if( gRegistrationTest_OutputFilePath )
	{
		test->outputFilePath = strdup( gRegistrationTest_OutputFilePath );
		require_action( test->outputFilePath, exit, err = kNoMemoryErr );
	}
	
	err = OutputFormatFromArgString( gRegistrationTest_OutputFormat, &test->outputFormat );
	require_noerr_quiet( err, exit );
	
	dispatch_async_f( dispatch_get_main_queue(), test, _RegistrationTestBegin );
	dispatch_main();
	
exit:
	if( test ) _RegistrationTestFree( test );
	ErrQuit( 1, "error: %#m\n", err );
}

//===========================================================================================================================

static OSStatus	_RegistrationTestCreate( RegistrationTest **outTest )
{
	OSStatus				err;
	RegistrationTest *		obj;
	
	obj = (RegistrationTest *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->subtestReports = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
	require_action( obj->subtestReports, exit, err = kNoMemoryErr );
	
	*outTest = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) _RegistrationTestFree( obj );
	return( err );
}

//===========================================================================================================================

static void	_RegistrationTestFree( RegistrationTest *inTest )
{
	check( !inTest->timer );
	check( !inTest->sigSourceINT );
	check( !inTest->sigSourceTERM );
	check( !inTest->computerNameSet );
	check( !inTest->localHostNameSet );
	check( !inTest->subtest );
	ForgetCF( &inTest->subtestReports );
	ForgetMem( &inTest->outputFilePath );
	ForgetCF( &inTest->computerNamePrev );
	ForgetCF( &inTest->localHostNamePrev );
	ForgetMem( &inTest->computerName );
	ForgetMem( &inTest->localHostName );
}

//===========================================================================================================================

static void	_RegistrationTestBegin( void *inContext )
{
	_RegistrationTestProceed( (RegistrationTest *) inContext );
}

//===========================================================================================================================

static void	_RegistrationTestProceed( RegistrationTest *inTest )
{
	OSStatus		err;
	Boolean			skippedSubtest;
	
	do
	{
		int		subtestIndex;
		
		if( !inTest->startTime )
		{
			err = _RegistrationTestStart( inTest );
			require_noerr_quiet( err, exit );
			
			inTest->startTime = NanoTimeGetCurrent();
		}
		else
		{
			err = _RegistrationTestEndSubtest( inTest );
			require_noerr( err, exit );
			
			++inTest->subtestIndex;
		}
		
		subtestIndex = inTest->subtestIndex;
		if( subtestIndex < (int) countof( kRegistrationSubtestParams ) )
		{
			err = _RegistrationTestStartSubtest( inTest, &kRegistrationSubtestParams[ subtestIndex ], &skippedSubtest );
			require_noerr_quiet( err, exit );
		}
		else
		{
			_RegistrationTestEnd( inTest );
		}
		
	}	while( skippedSubtest );
	
exit:
	if( err ) _RegistrationTestExit( inTest, err );
}

//===========================================================================================================================

static void	_RegistrationTestSignalHandler( void *inContext );

static OSStatus	_RegistrationTestStart( RegistrationTest *inTest )
{
	OSStatus		err;
	char			tag[ 6 + 1 ];
	
	// Save original ComputerName and LocalHostName.
	
	check( !inTest->computerNamePrev );
	inTest->computerNamePrev = SCDynamicStoreCopyComputerName( NULL, &inTest->computerNamePrevEncoding );
	err = map_scerror( inTest->computerNamePrev );
	require_noerr( err, exit );
	
	check( !inTest->localHostNamePrev );
	inTest->localHostNamePrev = SCDynamicStoreCopyLocalHostName( NULL );
	err = map_scerror( inTest->localHostNamePrev );
	require_noerr( err, exit );
	
	// Generate a unique test ComputerName.
	
	check( !inTest->computerName ); 
	ASPrintF( &inTest->computerName, "dnssdutil-regtest-computer-name-%s",
		_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
	require_action( inTest->computerName, exit, err = kNoMemoryErr );
	
	// Generate a unique test LocalHostName.
	
	check( !inTest->localHostName ); 
	ASPrintF( &inTest->localHostName, "dnssdutil-regtest-local-hostname-%s",
		_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
	require_action( inTest->localHostName, exit, err = kNoMemoryErr );
	
	// Set up SIGINT signal handler.
	
	signal( SIGINT, SIG_IGN );
	check( !inTest->sigSourceINT ); 
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), _RegistrationTestSignalHandler, inTest,
		&inTest->sigSourceINT );
	require_noerr( err, exit );
	dispatch_resume( inTest->sigSourceINT );
	
	// Set up SIGTERM signal handler.
	
	signal( SIGTERM, SIG_IGN );
	check( !inTest->sigSourceTERM ); 
	err = DispatchSignalSourceCreate( SIGTERM, dispatch_get_main_queue(), _RegistrationTestSignalHandler, inTest,
		&inTest->sigSourceTERM );
	require_noerr( err, exit );
	dispatch_resume( inTest->sigSourceTERM );
	
	// Set test ComputerName.
	
	check( !inTest->computerNameSet );
	err = _SetComputerNameWithUTF8CString( inTest->computerName );
	require_noerr( err, exit );
	inTest->computerNameSet = true;
	
	// Set test LocalHostName.
	
	check( !inTest->localHostNameSet );
	err = _SetLocalHostNameWithUTF8CString( inTest->localHostName );
	require_noerr( err, exit );
	inTest->localHostNameSet = true;
	
exit:
	if( err ) _RegistrationTestStop( inTest );
	return( err );
}

static void	_RegistrationTestSignalHandler( void *inContext )
{
	RegistrationTest * const		test = (RegistrationTest *) inContext;
	
	FPrintF( stderr, "Registration test got a SIGINT or SIGTERM signal, exiting..." );
	
	_RegistrationTestExit( test, kCanceledErr );
}

//===========================================================================================================================

static void	_RegistrationTestStop( RegistrationTest *inTest )
{
	OSStatus		err;
	
	dispatch_source_forget( &inTest->timer );
	dispatch_source_forget( &inTest->sigSourceINT );
	dispatch_source_forget( &inTest->sigSourceTERM );
	_RegistrationSubtestForget( &inTest->subtest );
	if( inTest->computerNameSet )
	{
		err = _SetComputerName( inTest->computerNamePrev, inTest->computerNamePrevEncoding );
		check_noerr( err );
		if( !err ) inTest->computerNameSet = false;
	}
	if( inTest->localHostNameSet )
	{
		err = _SetLocalHostName( inTest->localHostNamePrev );
		check_noerr( err );
		if( !err ) inTest->localHostNameSet = false;
	}
}

//===========================================================================================================================

#define kRegistrationTestSubtestDurationSecs		5

static OSStatus
	_RegistrationTestStartSubtest(
		RegistrationTest *					inTest,
		const RegistrationSubtestParams *	inParams,
		Boolean *							outSkipped )
{
	OSStatus					err;
	RegistrationSubtest *		subtest;
	const char *				interfaceDesc;
	DNSServiceFlags				flags;
	char						tag[ 6 + 1 ];
	
	subtest	= NULL;
	err = _RegistrationSubtestCreate( &subtest );
	require_noerr( err, exit );
	
	subtest->test			= inTest;
	subtest->useDefaultName	= inParams->useDefaultName;
	subtest->useLODiscovery	= inParams->useLODiscovery;
	
	// Determine registration interfaces.
	
	switch( inParams->interfaceSet )
	{
		case kRegistrationInterfaceSet_All:
			subtest->ifIndex = kDNSServiceInterfaceIndexAny;
			
			if( !subtest->useLODiscovery )
			{
				err = _RegistrationTestInterfaceListCreate( false, &subtest->ifList );
				require_noerr( err, exit );
			}
			interfaceDesc = "all interfaces (excluding AWDL)";
			break;
		
		case kRegistrationInterfaceSet_AllPlusAWDL:
			subtest->ifIndex		= kDNSServiceInterfaceIndexAny;
			subtest->includeAWDL	= true;
			
			if( !subtest->useLODiscovery )
			{
				err = _RegistrationTestInterfaceListCreate( true, &subtest->ifList );
				require_noerr( err, exit );
			}
			interfaceDesc = "all interfaces (including AWDL)";
			break;
		
		case kRegistrationInterfaceSet_LoopbackOnly:
			subtest->ifIndex = if_nametoindex( "lo0" );
			if( subtest->ifIndex == 0 )
			{
				FPrintF( stderr, "Failed to get index for loopback interface lo0.\n" );
				err = kNoResourcesErr;
				goto exit;
			}
			interfaceDesc = "loopback interface";
			break;
		
		case kRegistrationInterfaceSet_AWDLOnly:
			err = _MDNSInterfaceGetAny( kMDNSInterfaceSubset_AWDL, NULL, &subtest->ifIndex );
			if( err == kNotFoundErr )
			{
				FPrintF( stderr, "Warning: No mDNS-capable AWDL interface is available.\n" );
				subtest->skipped = true;
				err = kNoErr;
			}
			require_noerr( err, exit );
			
			subtest->ifIsAWDL = true;
			interfaceDesc = "AWDL interface";
			break;
		
		default:
			err = kParamErr;
			goto exit;
	}
	
	// Create description.
	
	ASPrintF( &subtest->description, "Service registration over %s using %s service name.%s",
		interfaceDesc, subtest->useDefaultName ? "default" : "custom",
		subtest->useLODiscovery ? " (LocalOnly discovery)" : "" );
	require_action( subtest->description, exit, err = kNoMemoryErr );
	
	if( subtest->skipped )
	{
		subtest->startTime = NanoTimeGetCurrent();
	}
	else
	{
		// Generate a service name.
		
		if( subtest->useDefaultName )
		{
			subtest->serviceName = inTest->computerName;
		}
		else
		{
			ASPrintF( &subtest->serviceNameCustom, "dnssdutil-regtest-service-name-%s",
				_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
			require_action( subtest->serviceNameCustom, exit, err = kNoMemoryErr );
			
			subtest->serviceName = subtest->serviceNameCustom;
		}
		
		// Generate a service type.
		
		ASPrintF( &subtest->serviceType, "_regtest-%s._udp",
			_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
		require_action( subtest->serviceType, exit, err = kNoMemoryErr );
		
		subtest->serviceTypeLen = strlen( subtest->serviceType );
		
		// Create SRV and TXT record name FQDN.
		
		ASPrintF( &subtest->serviceFQDN, "%s.%s.local.", subtest->serviceName, subtest->serviceType );
		require_action( subtest->serviceFQDN, exit, err = kNoMemoryErr );
		
		// Generate a port number.
		
		subtest->port = (uint16_t) RandomRange( 60000, 65535 );
		
		// Generate TXT record data.
		
		err = _RegistrationTestCreateRandomTXTRecord( 100, 1000, &subtest->txtPtr, &subtest->txtLen );
		require_noerr( err, exit );
		
		// Register service.
		
		subtest->startTime = NanoTimeGetCurrent();
		
		flags = kDNSServiceFlagsNoAutoRename;
		if( subtest->includeAWDL ) flags |= kDNSServiceFlagsIncludeAWDL;
		err = DNSServiceRegister( &subtest->registration, flags, subtest->ifIndex,
			subtest->useDefaultName ? NULL : subtest->serviceNameCustom, subtest->serviceType, "local.",
			NULL, htons( subtest->port ), (uint16_t) subtest->txtLen, subtest->txtPtr,
			_RegistrationSubtestRegisterCallback, subtest );
		require_noerr( err, exit );
		
		err = DNSServiceSetDispatchQueue( subtest->registration, dispatch_get_main_queue() );
		require_noerr( err, exit );
		
		// Start timer.
		
		check( !inTest->timer );
		err = DispatchTimerOneShotCreate( dispatch_time_seconds( kRegistrationTestSubtestDurationSecs ),
			INT64_C_safe( kRegistrationTestSubtestDurationSecs ) * kNanosecondsPerSecond / 10, dispatch_get_main_queue(),
			_RegistrationTestTimerHandler, inTest, &inTest->timer );
		require_noerr( err, exit );
		dispatch_resume( inTest->timer );
	}
	
	*outSkipped = subtest->skipped;
	
	check( !inTest->subtest );
	inTest->subtest = subtest;
	subtest = NULL;
	
exit:
	_RegistrationSubtestForget( &subtest );
	return( err );
}

//===========================================================================================================================

#define kRegistrationTestReportKey_ComputerName			CFSTR( "computerName" )			// String
#define kRegistrationTestReportKey_Description			CFSTR( "description" )			// String
#define kRegistrationTestReportKey_Domain				CFSTR( "domain" )				// String
#define kRegistrationTestReportKey_EndTime				CFSTR( "endTime" )				// String
#define kRegistrationTestReportKey_Error				CFSTR( "error" )				// Integer
#define kRegistrationTestReportKey_Flags				CFSTR( "flags" )				// Integer
#define kRegistrationTestReportKey_IgnoredResults		CFSTR( "ignoredResults" )		// Array of dictionaries
#define kRegistrationTestReportKey_InterfaceIndex		CFSTR( "ifIndex" )				// Integer
#define kRegistrationTestReportKey_InterfaceName		CFSTR( "ifName" )				// String
#define kRegistrationTestReportKey_LocalHostName		CFSTR( "localHostName" )		// String
#define kRegistrationTestReportKey_MissingResults		CFSTR( "missingResults" )		// Array of dictionaries
#define kRegistrationTestReportKey_Pass					CFSTR( "pass" )					// Boolean
#define kRegistrationTestReportKey_Port					CFSTR( "port" )					// Integer
#define kRegistrationTestReportKey_RDataFormatted		CFSTR( "rdataFormatted" )		// String
#define kRegistrationTestReportKey_RDataHexString		CFSTR( "rdataHexString" )		// String
#define kRegistrationTestReportKey_RecordClass			CFSTR( "recordClass" )			// Integer
#define kRegistrationTestReportKey_RecordType			CFSTR( "recordType" )			// Integer
#define kRegistrationTestReportKey_Registered			CFSTR( "registered" )			// Boolean
#define kRegistrationTestReportKey_ResultType			CFSTR( "resultType" )			// String
#define kRegistrationTestReportKey_ServiceFQDN			CFSTR( "serviceFQDN" )			// String
#define kRegistrationTestReportKey_ServiceName			CFSTR( "serviceName" )			// String
#define kRegistrationTestReportKey_ServiceType			CFSTR( "serviceType" )			// String
#define kRegistrationTestReportKey_Skipped				CFSTR( "skipped" )				// Boolean
#define kRegistrationTestReportKey_StartTime			CFSTR( "startTime" )			// String
#define kRegistrationTestReportKey_Subtests				CFSTR( "subtests" )				// Array of dictionaries
#define kRegistrationTestReportKey_Timestamp			CFSTR( "timestamp" )			// String
#define kRegistrationTestReportKey_TXT					CFSTR( "txt" )					// String
#define kRegistrationTestReportKey_UnexpectedResults	CFSTR( "unexpectedResults" )	// Array of dictionaries
#define kRegistrationTestReportKey_UsedDefaultName		CFSTR( "usedDefaultName" )		// Boolean
#define kRegistrationTestReportKey_UsedLODiscovery		CFSTR( "usedLODiscovery" )		// Boolean

#define kRegistrationTestResultType_Browse				CFSTR( "browse" )
#define kRegistrationTestResultType_Query				CFSTR( "query" )
#define kRegistrationTestResultType_QuerySRV			CFSTR( "querySRV" )
#define kRegistrationTestResultType_QueryTXT			CFSTR( "queryTXT" )
#define kRegistrationTestResultType_Registration		CFSTR( "registration" )

static OSStatus
	_RegistrationTestAppendMissingResults(
		CFMutableArrayRef				inMissingResults,
		const RegistrationResultTimes *	inTimes,
		uint32_t						inIfIndex,
		const char *					inIfName );

static OSStatus	_RegistrationTestEndSubtest( RegistrationTest *inTest )
{
	OSStatus					err;
	RegistrationSubtest *		subtest;
	CFMutableDictionaryRef		subtestReport;
	CFMutableArrayRef			missing;
	char *						txtStr;
	NanoTime64					now;
	Boolean						subtestFailed;
	char						startTime[ 32 ];
	char						endTime[ 32 ];
	char						ifNameBuf[ IF_NAMESIZE + 1 ];
	
	now = NanoTimeGetCurrent();
	
	subtest = inTest->subtest;
	inTest->subtest = NULL;
	_RegistrationSubtestStop( subtest );
	
	missing			= NULL;
	subtestReport	= NULL;
	txtStr			= NULL;
	if( subtest->txtPtr )
	{
		err = DNSRecordDataToString( subtest->txtPtr, subtest->txtLen, kDNSServiceType_TXT, &txtStr );
		require_noerr( err, exit );
	}
	_NanoTime64ToTimestamp( subtest->startTime, startTime, sizeof( startTime ) );
	_NanoTime64ToTimestamp( now, endTime, sizeof( endTime ) );
	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &subtestReport,
		"{"
			"%kO=%s"	// description
			"%kO=%s"	// startTime
			"%kO=%s"	// endTime
			"%kO=%s"	// serviceFQDN
			"%kO=%lli"	// ifIndex
			"%kO=%s"	// ifName
			"%kO=%lli"	// port
			"%kO=%s"	// txt
			"%kO=%b"	// registered
			"%kO=%b"	// usedDefaultName
			"%kO=%b"	// usedLODiscovery
		"}",
		kRegistrationTestReportKey_Description,		subtest->description,
		kRegistrationTestReportKey_StartTime,		startTime,
		kRegistrationTestReportKey_EndTime,			endTime,
		kRegistrationTestReportKey_ServiceFQDN,		subtest->serviceFQDN,
		kRegistrationTestReportKey_InterfaceIndex,	(int64_t) subtest->ifIndex,
		kRegistrationTestReportKey_InterfaceName,	if_indextoname( subtest->ifIndex, ifNameBuf ),
		kRegistrationTestReportKey_Port,			(int64_t) subtest->port,
		kRegistrationTestReportKey_TXT,				txtStr,
		kRegistrationTestReportKey_Registered,		(int) subtest->registered,
		kRegistrationTestReportKey_UsedDefaultName,	(int) subtest->useDefaultName,
		kRegistrationTestReportKey_UsedLODiscovery,	(int) subtest->useLODiscovery );
	ForgetMem( &txtStr );
	require_noerr( err, exit );
	
	if( !subtest->skipped && subtest->registered )
	{
		missing = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
		require_action( missing, exit, err = kNoMemoryErr );
		
		if( subtest->ifList )
		{
			RegistrationInterfaceItem *		item;
			
			for( item = subtest->ifList; item; item = (RegistrationInterfaceItem *) item->base.next )
			{
			#if( TARGET_OS_WATCH )
				if( inTest->forBATS && item->base.isWiFi ) continue;
			#endif
				err = _RegistrationTestAppendMissingResults( missing, &item->times, item->base.ifIndex, item->base.ifName );
				require_noerr( err, exit );
			}
		}
		else
		{
			err = _RegistrationTestAppendMissingResults( missing, &subtest->ifTimes, subtest->ifIndex, NULL );
			require_noerr( err, exit );
		}
		
		subtestFailed = false;
		if( CFArrayGetCount( missing ) > 0 )
		{
			subtestFailed = true;
			CFDictionarySetValue( subtestReport, kRegistrationTestReportKey_MissingResults, missing );
		}
		if( CFArrayGetCount( subtest->unexpected ) > 0 )
		{
			subtestFailed = true;
			CFDictionarySetValue( subtestReport, kRegistrationTestReportKey_UnexpectedResults, subtest->unexpected );
		}
	#if( TARGET_OS_WATCH )
		if( CFArrayGetCount( subtest->ignored ) > 0 )
		{
			CFDictionarySetValue( subtestReport, kRegistrationTestReportKey_IgnoredResults, subtest->ignored );
		}
	#endif
	}
	else
	{
		subtestFailed = true;
	}
	
	CFDictionarySetBoolean( subtestReport, kRegistrationTestReportKey_Pass, subtestFailed ? false : true );
	if( subtestFailed )
	{
		CFDictionarySetBoolean( subtestReport, kRegistrationTestReportKey_Skipped, subtest->skipped );
		if( !subtest->skipped ) inTest->failed = true;
	}
	CFArrayAppendValue( inTest->subtestReports, subtestReport );
	
exit:
	CFReleaseNullSafe( missing );
	CFReleaseNullSafe( subtestReport );
	_RegistrationSubtestFree( subtest );
	return( err );
}

static OSStatus
	_RegistrationTestAppendMissingResult(
		CFMutableArrayRef	inMissingResults,
		CFStringRef			inType,
		uint32_t			inIfIndex,
		const char *		inIfName );

static OSStatus
	_RegistrationTestAppendMissingResults(
		CFMutableArrayRef				inMissingResults,
		const RegistrationResultTimes *	inTimes,
		uint32_t						inIfIndex,
		const char *					inIfName )
{
	OSStatus		err;
	
	if( !inTimes->browseResultTime )
	{
		err = _RegistrationTestAppendMissingResult( inMissingResults, kRegistrationTestResultType_Browse,
			inIfIndex, inIfName );
		require_noerr( err, exit );
	}
	if( !inTimes->querySRVResultTime )
	{
		err = _RegistrationTestAppendMissingResult( inMissingResults, kRegistrationTestResultType_QuerySRV,
			inIfIndex, inIfName );
		require_noerr( err, exit );
	}
	if( !inTimes->queryTXTResultTime )
	{
		err = _RegistrationTestAppendMissingResult( inMissingResults, kRegistrationTestResultType_QueryTXT,
			inIfIndex, inIfName );
		require_noerr( err, exit );
	}
	err = kNoErr;
	
exit:
	return( err );
}

static OSStatus
	_RegistrationTestAppendMissingResult(
		CFMutableArrayRef	inMissingResults,
		CFStringRef			inType,
		uint32_t			inIfIndex,
		const char *		inIfName )
{
	OSStatus		err;
	char			ifName[ IF_NAMESIZE + 1 ];
	
	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, inMissingResults,
		"{"
			"%kO=%O"	// resultType
			"%kO=%lli"	// ifIndex
			"%kO=%s"	// ifName
		"}",
		kRegistrationTestReportKey_ResultType,		inType,
		kRegistrationTestReportKey_InterfaceIndex,	(int64_t) inIfIndex,
		kRegistrationTestReportKey_InterfaceName,	inIfName ? inIfName : if_indextoname( inIfIndex, ifName ) );
	return( err );
}

//===========================================================================================================================

static void	_RegistrationTestEnd( RegistrationTest *inTest )
{
	OSStatus				err;
	NanoTime64				now;
	CFPropertyListRef		plist;
	char					startTime[ 32 ];
	char					endTime[ 32 ];
	
	now = NanoTimeGetCurrent();
	_NanoTime64ToTimestamp( inTest->startTime, startTime, sizeof( startTime ) );
	_NanoTime64ToTimestamp( now, endTime, sizeof( endTime ) );
	
	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &plist,
		"{"
			"%kO=%s"	// startTime
			"%kO=%s"	// endTime
			"%kO=%s"	// computerName
			"%kO=%s"	// localHostName
			"%kO=%O"	// subtests
			"%kO=%b"	// pass
		"}",
		kRegistrationTestReportKey_StartTime,		startTime,
		kRegistrationTestReportKey_EndTime,			endTime,
		kRegistrationTestReportKey_ComputerName,	inTest->computerName,
		kRegistrationTestReportKey_LocalHostName,	inTest->localHostName,
		kRegistrationTestReportKey_Subtests,		inTest->subtestReports,
		kRegistrationTestReportKey_Pass,			inTest->failed ? false : true );
	require_noerr( err, exit );
	
	err = OutputPropertyList( plist, inTest->outputFormat, inTest->outputFilePath );
	CFRelease( plist );
	require_noerr( err, exit );
	
exit:
	_RegistrationTestExit( inTest, err );
}

//===========================================================================================================================

static void	_RegistrationTestExit( RegistrationTest *inTest, OSStatus inError )
{
	int		exitCode;
	
	if( inError )
	{
		FPrintF( stderr, "error: %#m\n", inError );
		exitCode = 1;
	}
	else
	{
		exitCode = inTest->failed ? 2 : 0;
	}
	_RegistrationTestForget( &inTest );
	exit( exitCode );
}

//===========================================================================================================================

static OSStatus	_RegistrationSubtestCreate( RegistrationSubtest **outSubtest )
{
	OSStatus					err;
	RegistrationSubtest *		obj;
	
	obj = (RegistrationSubtest *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->unexpected = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
	require_action( obj->unexpected, exit, err = kNoMemoryErr );
	
#if( TARGET_OS_WATCH )
	obj->ignored = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
	require_action( obj->ignored, exit, err = kNoMemoryErr );
#endif
	
	*outSubtest = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) _RegistrationSubtestFree( obj );
	return( err );
}

//===========================================================================================================================

static void	_RegistrationSubtestFree( RegistrationSubtest *inSubtest )
{
	check( !inSubtest->registration );
	check( !inSubtest->browse );
	check( !inSubtest->querySRV );
	check( !inSubtest->queryTXT );
	check( !inSubtest->connection );
	ForgetMem( &inSubtest->serviceNameCustom );
	ForgetMem( &inSubtest->serviceType );
	ForgetMem( &inSubtest->serviceFQDN );
	ForgetMem( &inSubtest->txtPtr );
	ForgetCF( &inSubtest->unexpected );
#if( TARGET_OS_WATCH )
	ForgetCF( &inSubtest->ignored );
#endif
	_MDNSInterfaceListForget( (MDNSInterfaceItem **) &inSubtest->ifList );
	ForgetMem( &inSubtest->description );
	free( inSubtest );
}

//===========================================================================================================================

static void	_RegistrationSubtestStop( RegistrationSubtest *inSubtest )
{
	DNSServiceForget( &inSubtest->registration );
	DNSServiceForget( &inSubtest->browse );
	DNSServiceForget( &inSubtest->querySRV );
	DNSServiceForget( &inSubtest->queryTXT );
	DNSServiceForget( &inSubtest->connection );
}

//===========================================================================================================================

static OSStatus	_RegistrationTestInterfaceListCreate( Boolean inIncludeAWDL, RegistrationInterfaceItem **outList )
{
	OSStatus						err;
	RegistrationInterfaceItem *		list;
	const MDNSInterfaceSubset		subset = inIncludeAWDL ? kMDNSInterfaceSubset_All : kMDNSInterfaceSubset_NonAWDL;
	
	err = _MDNSInterfaceListCreate( subset, sizeof( *list ), (MDNSInterfaceItem **) &list );
	require_noerr( err, exit );
	
	*outList = list;
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus
	_RegistrationTestCreateRandomTXTRecord(
		size_t		inMinLen,
		size_t		inMaxLen,
		uint8_t **	outTXTPtr,
		size_t *	outTXTLen )
{
	OSStatus			err;
	uint8_t *			ptr;
	const uint8_t *		txtEnd;
	uint8_t *			txtPtr = NULL;
	size_t				txtLen;
	
	require_action_quiet( inMinLen <= inMaxLen, exit, err = kSizeErr );
	
	txtLen = RandomRange( inMinLen, inMaxLen );
	txtPtr = (uint8_t *) malloc( txtLen + 1 );
	require_action( txtPtr, exit, err = kNoMemoryErr );
	
	_RandomStringExact( kAlphaNumericCharSet, sizeof_string( kAlphaNumericCharSet ), txtLen, (char *)txtPtr );
	
	ptr		= txtPtr;
	txtEnd	= &txtPtr[ txtLen ];
	while( ptr < txtEnd )
	{
		size_t		maxLen, len;
		
		maxLen = ( (size_t)( txtEnd - ptr ) ) - 1;
		len = RandomRange( 1, 255 );
		if( len > maxLen ) len = maxLen;
		
		*ptr = (uint8_t) len;
		ptr += ( 1 + len );
	}
	check( ptr == txtEnd );
	
	if( outTXTPtr )
	{
		*outTXTPtr = txtPtr;
		txtPtr = NULL;
	}
	if( outTXTLen ) *outTXTLen = txtLen;
	err = kNoErr;
	
exit:
	FreeNullSafe( txtPtr );
	return( err );
}

//===========================================================================================================================

static void DNSSD_API
	_RegistrationSubtestRegisterCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		DNSServiceErrorType	inError,
		const char *		inServiceName,
		const char *		inServiceType,
		const char *		inDomain,
		void *				inContext )
{
	OSStatus						err;
	const NanoTime64				now		= NanoTimeGetCurrent();
	RegistrationSubtest * const		subtest	= (RegistrationSubtest *) inContext;
	
	Unused( inSDRef );
	
	if( ( inFlags & kDNSServiceFlagsAdd ) && !inError &&
		( strcasecmp( inServiceName, subtest->serviceName ) == 0 ) &&
		_RegistrationSubtestValidServiceType( subtest, inServiceType ) &&
		( strcasecmp( inDomain, "local." ) == 0 ) )
	{
		if( !subtest->registered )
		{
			DNSServiceRef				sdRef;
			const DNSServiceFlags		flags = kDNSServiceFlagsShareConnection | kDNSServiceFlagsIncludeAWDL;
			
			subtest->registered = true;
			
			// Create shared connection.
			
			check( !subtest->connection );
			err = DNSServiceCreateConnection( &subtest->connection );
			require_noerr( err, exit );
			
			err = DNSServiceSetDispatchQueue( subtest->connection, dispatch_get_main_queue() );
			require_noerr( err, exit );
			
			// Start browse.
			
			check( !subtest->browse );
			sdRef = subtest->connection;
			err = DNSServiceBrowse( &sdRef, flags,
				subtest->useLODiscovery ? kDNSServiceInterfaceIndexLocalOnly : kDNSServiceInterfaceIndexAny,
				subtest->serviceType, "local.", _RegistrationSubtestBrowseCallback, subtest );
			require_noerr( err, exit );
			
			subtest->browse = sdRef;
		}
	}
	else
	{
		char		timestamp[ 32 ];
		
		err = CFPropertyListAppendFormatted( kCFAllocatorDefault, subtest->unexpected,
			"{"
				"%kO=%O"	// resultType
				"%kO=%s"	// timestamp
				"%kO=%lli"	// flags
				"%kO=%lli"	// error
				"%kO=%s"	// serviceName
				"%kO=%s"	// serviceType
				"%kO=%s"	// domain
			"}",
			kRegistrationTestReportKey_ResultType,	kRegistrationTestResultType_Registration,
			kRegistrationTestReportKey_Timestamp,	_NanoTime64ToTimestamp( now, timestamp, sizeof( timestamp ) ),
			kRegistrationTestReportKey_Flags,		(int64_t) inFlags,
			kRegistrationTestReportKey_Error,		(int64_t) inError,
			kRegistrationTestReportKey_ServiceName,	inServiceName,
			kRegistrationTestReportKey_ServiceType,	inServiceType,
			kRegistrationTestReportKey_Domain,		inDomain );
		require_noerr( err, exit );
	}
	err = kNoErr;
	
exit:
	if( err ) _RegistrationTestExit( subtest->test, err );
}

//===========================================================================================================================

static void DNSSD_API
	_RegistrationSubtestBrowseCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		uint32_t			inIfIndex,
		DNSServiceErrorType	inError,
		const char *		inServiceName,
		const char *		inServiceType,
		const char *		inDomain,
		void *				inContext )
{
	OSStatus						err;
	NanoTime64						now;
	RegistrationSubtest * const		subtest = (RegistrationSubtest *) inContext;
	Boolean							serviceIsCorrect, resultIsExpected;
	
	Unused( inSDRef );
	
	now = NanoTimeGetCurrent();
	if( !inError && ( strcasecmp( inServiceName, subtest->serviceName ) == 0 ) &&
		_RegistrationSubtestValidServiceType( subtest, inServiceType ) && ( strcasecmp( inDomain, "local." ) == 0 ) )
	{
		serviceIsCorrect = true;
	}
	else
	{
		serviceIsCorrect = false;
	}
	
	resultIsExpected = false;
	if( serviceIsCorrect && ( inFlags & kDNSServiceFlagsAdd ) )
	{
		RegistrationResultTimes *		times;
		
		times = _RegistrationSubtestGetInterfaceResultTimes( subtest, inIfIndex, NULL );
		if( times )
		{
			DNSServiceRef				sdRef;
			const DNSServiceFlags		flags = kDNSServiceFlagsShareConnection | kDNSServiceFlagsIncludeAWDL;
			uint32_t					ifIndex;
			
			resultIsExpected = true;
			if( !times->browseResultTime ) times->browseResultTime = now;
			
			ifIndex = subtest->useLODiscovery ? kDNSServiceInterfaceIndexLocalOnly : kDNSServiceInterfaceIndexAny;
			if( !subtest->querySRV )
			{
				// Start SRV record query.
				
				sdRef = subtest->connection;
				err = DNSServiceQueryRecord( &sdRef, flags, ifIndex, subtest->serviceFQDN, kDNSServiceType_SRV,
					kDNSServiceClass_IN, _RegistrationSubtestQueryCallback, subtest );
				require_noerr( err, exit );
				
				subtest->querySRV = sdRef;
			}
			if( !subtest->queryTXT )
			{
				// Start TXT record query.
				
				sdRef = subtest->connection;
				err = DNSServiceQueryRecord( &sdRef, flags, ifIndex, subtest->serviceFQDN, kDNSServiceType_TXT,
					kDNSServiceClass_IN, _RegistrationSubtestQueryCallback, subtest );
				require_noerr( err, exit );
				
				subtest->queryTXT = sdRef;
			}
		}
	}
	
	if( !resultIsExpected )
	{
		CFMutableArrayRef		resultArray;
		char					timestamp[ 32 ];
		const char *			ifNamePtr;
		char					ifNameBuf[ IF_NAMESIZE + 1 ];
		
		ifNamePtr = if_indextoname( inIfIndex, ifNameBuf );
		resultArray = subtest->unexpected;
	#if( TARGET_OS_WATCH )
		if( subtest->test->forBATS && ( subtest->ifIndex == kDNSServiceInterfaceIndexAny ) &&
			ifNamePtr && _RegistrationTestInterfaceIsWiFi( ifNamePtr ) && serviceIsCorrect )
		{
			resultArray = subtest->ignored;
		}
	#endif
		err = CFPropertyListAppendFormatted( kCFAllocatorDefault, resultArray,
			"{"
				"%kO=%O"	// resultType
				"%kO=%s"	// timestamp
				"%kO=%lli"	// flags
				"%kO=%lli"	// ifIndex
				"%kO=%s"	// ifName
				"%kO=%lli"	// error
				"%kO=%s"	// serviceName
				"%kO=%s"	// serviceType
				"%kO=%s"	// domain
			"}",
			kRegistrationTestReportKey_ResultType,		kRegistrationTestResultType_Browse,
			kRegistrationTestReportKey_Timestamp,		_NanoTime64ToTimestamp( now, timestamp, sizeof( timestamp ) ),
			kRegistrationTestReportKey_Flags,			(int64_t) inFlags,
			kRegistrationTestReportKey_InterfaceIndex,	(int64_t) inIfIndex,
			kRegistrationTestReportKey_InterfaceName,	ifNamePtr,
			kRegistrationTestReportKey_Error,			(int64_t) inError,
			kRegistrationTestReportKey_ServiceName,		inServiceName,
			kRegistrationTestReportKey_ServiceType,		inServiceType,
			kRegistrationTestReportKey_Domain,			inDomain );
		require_noerr( err, exit );
	}
	err = kNoErr;
	
exit:
	if( err ) _RegistrationTestExit( subtest->test, err );
}

//===========================================================================================================================

static Boolean
	_RegistrationSubtestIsSRVRecordDataValid(
		RegistrationSubtest *	inSubtest,
		const uint8_t *			inRDataPtr,
		size_t					inRDataLen,
		Boolean					inExpectRandHostname );

static void DNSSD_API
	_RegistrationSubtestQueryCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inIfIndex,
		DNSServiceErrorType		inError,
		const char *			inName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext )
{
	OSStatus						err;
	NanoTime64						now;
	RegistrationSubtest * const		subtest = (RegistrationSubtest *) inContext;
	Boolean							resultIsExpected;
	
	Unused( inSDRef );
	Unused( inTTL );
	
	now = NanoTimeGetCurrent();
	resultIsExpected = false;
	if( ( inFlags & kDNSServiceFlagsAdd ) && !inError && ( strcasecmp( inName, subtest->serviceFQDN ) == 0 ) &&
		( inClass == kDNSServiceClass_IN ) )
	{
		RegistrationResultTimes *		times;
		Boolean							isAWDL;
		
		times = _RegistrationSubtestGetInterfaceResultTimes( subtest, inIfIndex, &isAWDL );
		if( times )
		{
			if( inType == kDNSServiceType_SRV )
			{
				Boolean		expectRandHostname;
				
				if( isAWDL || ( ( subtest->ifIndex == kDNSServiceInterfaceIndexAny ) && subtest->includeAWDL ) )
				{
					expectRandHostname = true;
				}
				else
				{
					expectRandHostname = false;
				}
				if( _RegistrationSubtestIsSRVRecordDataValid( subtest, inRDataPtr, inRDataLen, expectRandHostname ) )
				{
					resultIsExpected = true;
					if( !times->querySRVResultTime ) times->querySRVResultTime = now;
				}
			}
			else if( inType == kDNSServiceType_TXT )
			{
				if( MemEqual( inRDataPtr, inRDataLen, subtest->txtPtr, subtest->txtLen ) )
				{
					resultIsExpected = true;
					if( !times->queryTXTResultTime ) times->queryTXTResultTime = now;
				}
			}
		}
	}
	
	if( !resultIsExpected )
	{
		CFMutableArrayRef			resultArray;
		CFMutableDictionaryRef		resultDict;
		CFStringRef					rdataKey;
		char *						rdataStr;
		const char *				ifNamePtr;
		char						timestamp[ 32 ];
		char						ifNameBuf[ IF_NAMESIZE + 1 ];
		
		ifNamePtr = if_indextoname( inIfIndex, ifNameBuf );
		resultArray = subtest->unexpected;
	#if( TARGET_OS_WATCH )
		if( subtest->test->forBATS && ( subtest->ifIndex == kDNSServiceInterfaceIndexAny ) &&
			ifNamePtr && _RegistrationTestInterfaceIsWiFi( ifNamePtr ) && !inError &&
			( strcasecmp( inName, subtest->serviceFQDN ) == 0 ) )
		{
			if( inType == kDNSServiceType_SRV )
			{
				const Boolean		expectRandHostname = subtest->includeAWDL ? true : false;
				
				if( _RegistrationSubtestIsSRVRecordDataValid( subtest, inRDataPtr, inRDataLen, expectRandHostname ) )
				{
					resultArray = subtest->ignored;
				}
			}
			else if( inType == kDNSServiceType_TXT )
			{
				if( MemEqual( inRDataPtr, inRDataLen, subtest->txtPtr, subtest->txtLen ) )
				{
					resultArray = subtest->ignored;
				}
			}
		}
	#endif
		err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &resultDict,
			"{"
				"%kO=%O"	// resultType
				"%kO=%s"	// timestamp
				"%kO=%lli"	// flags
				"%kO=%lli"	// ifIndex
				"%kO=%s"	// ifName
				"%kO=%lli"	// error
				"%kO=%s"	// serviceFQDN
				"%kO=%lli"	// recordType
				"%kO=%lli"	// class
			"}",
			kRegistrationTestReportKey_ResultType,		kRegistrationTestResultType_Query,
			kRegistrationTestReportKey_Timestamp,		_NanoTime64ToTimestamp( now, timestamp, sizeof( timestamp ) ),
			kRegistrationTestReportKey_Flags,			(int64_t) inFlags,
			kRegistrationTestReportKey_InterfaceIndex,	(int64_t) inIfIndex,
			kRegistrationTestReportKey_InterfaceName,	ifNamePtr,
			kRegistrationTestReportKey_Error,			(int64_t) inError,
			kRegistrationTestReportKey_ServiceFQDN,		inName,
			kRegistrationTestReportKey_RecordType,		(int64_t) inType,
			kRegistrationTestReportKey_RecordClass,		(int64_t) inClass );
		require_noerr( err, exit );
		
		rdataStr = NULL;
		DNSRecordDataToString( inRDataPtr, inRDataLen, inType, &rdataStr );
		if( rdataStr )
		{
			rdataKey = kRegistrationTestReportKey_RDataFormatted;
		}
		else
		{
			ASPrintF( &rdataStr, "%#H", inRDataPtr, inRDataLen, inRDataLen );
			require_action( rdataStr, exit, err = kNoMemoryErr );
			
			rdataKey = kRegistrationTestReportKey_RDataHexString;
		}
		err = CFDictionarySetCString( resultDict, rdataKey, rdataStr, kSizeCString );
		ForgetMem( &rdataStr );
		if( err ) CFRelease( resultDict );
		require_noerr( err, exit );
		
		CFArrayAppendValue( resultArray, resultDict );
		CFRelease( resultDict );
	}
	err = kNoErr;
	
exit:
	if( err ) _RegistrationTestExit( subtest->test, err );
}

static Boolean
	_RegistrationSubtestIsSRVRecordDataValid(
		RegistrationSubtest *	inSubtest,
		const uint8_t *			inRDataPtr,
		size_t					inRDataLen,
		Boolean					inExpectRandHostname )
{
	const dns_fixed_fields_srv *		fields;
	const uint8_t * const				end		= &inRDataPtr[ inRDataLen ];
	const uint8_t *						label;
	size_t								len;
	uint16_t							port;
	Boolean								isValid;
	
	isValid = false;
	require_quiet( inRDataLen >= sizeof( dns_fixed_fields_srv ), exit );
	
	fields	= (const dns_fixed_fields_srv *) inRDataPtr;
	port	= dns_fixed_fields_srv_get_port( fields );
	require_quiet( port == inSubtest->port, exit );
	
	// First target label should be a UUID string for the AWDL interface.
	
	label = (const uint8_t *) &fields[ 1 ];
	require_quiet( ( end - label ) >= 1, exit );
	
	len = label[ 0 ];
	require_quiet( ( (size_t)( end - label ) ) >= ( 1 + len ), exit );
	
	if( inExpectRandHostname )
	{
		if( StringToUUID( (const char *) &label[ 1 ], len, false, NULL ) != kNoErr ) goto exit;
	}
	else
	{
		if( strnicmpx( &label[ 1 ], len, inSubtest->test->localHostName ) != 0 ) goto exit;
	}
	
	// Second target label should be "local".
	
	label = &label[ 1 + len ];
	require_quiet( ( end - label ) >= 1, exit );
	
	len = label[ 0 ];
	require_quiet( ( (size_t)( end - label ) ) >= ( 1 + len ), exit );
	
	if( ( len != kLocalLabel[ 0 ] ) || ( _memicmp( &label[ 1 ], &kLocalLabel[ 1 ], kLocalLabel[ 0 ] ) != 0 ) ) goto exit;
	
	// Third target label should be the root label.
	
	label = &label[ 1 + len ];
	require_quiet( ( end - label ) >= 1, exit );
	
	len = label[ 0 ];
	if( len != 0 ) goto exit;
	
	isValid = true;
	
exit:
	return( isValid );
}

//===========================================================================================================================

static Boolean	_RegistrationSubtestValidServiceType( const RegistrationSubtest *inSubtest, const char *inServiceType )
{
	if( stricmp_prefix( inServiceType, inSubtest->serviceType ) == 0 )
	{
		const char * const		ptr = &inServiceType[ inSubtest->serviceTypeLen ];
		
		if( ( ptr[ 0 ] == '\0' ) || ( ( ptr[ 0 ] == '.' ) && ( ptr[ 1 ] == '\0' ) ) ) return( true );
	}
	return( false );
}

//===========================================================================================================================

static RegistrationResultTimes *
	_RegistrationSubtestGetInterfaceResultTimes(
		RegistrationSubtest *	inSubtest,
		uint32_t				inIfIndex,
		Boolean *				outIsAWDL )
{
	if( inSubtest->ifList )
	{
		RegistrationInterfaceItem *		item;
		
		for( item = inSubtest->ifList; item; item = (RegistrationInterfaceItem *) item->base.next )
		{
			if( inIfIndex == item->base.ifIndex )
			{
				if( outIsAWDL ) *outIsAWDL = item->base.isAWDL ? true : false;
				return( &item->times );
			}
		}
	}
	else
	{
		if( inIfIndex == inSubtest->ifIndex )
		{
			if( outIsAWDL ) *outIsAWDL = inSubtest->ifIsAWDL ? true : false;
			return( &inSubtest->ifTimes );
		}
	}
	return( NULL );
}

//===========================================================================================================================

static void	_RegistrationTestTimerHandler( void *inContext )
{
	RegistrationTest * const		test = (RegistrationTest *) inContext;
	
	dispatch_source_forget( &test->timer );
	_RegistrationTestProceed( test );
}

//===========================================================================================================================

#if( TARGET_OS_WATCH )
static Boolean	_RegistrationTestInterfaceIsWiFi( const char *inIfName )
{
	NetTransportType		type = kNetTransportType_Undefined;
	
	SocketGetInterfaceInfo( kInvalidSocketRef, inIfName, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &type );
	return( ( type == kNetTransportType_WiFi ) ? true : false );
}
#endif

#if( TARGET_OS_DARWIN )
//===========================================================================================================================
//	KeepAliveTestCmd
//===========================================================================================================================

typedef enum
{
	kKeepAliveCallVariant_Null				= 0,
	kKeepAliveCallVariant_TakesSocket		= 1, // DNSServiceSleepKeepalive(), which takes a connected socket.
	kKeepAliveCallVariant_TakesSockAddrs	= 2, // DNSServiceSleepKeepalive_sockaddr(), which takes connection's sockaddrs.
	
}	KeepAliveCallVariant;

typedef struct
{
	int							family;			// TCP connection's address family.
	KeepAliveCallVariant		callVariant;	// Describes which DNSServiceSleepKeepalive* call to use.
	const char *				description;	// Subtest description.
	
}	KeepAliveSubtestParams;

const KeepAliveSubtestParams		kKeepAliveSubtestParams[] =
{
	{ AF_INET,  kKeepAliveCallVariant_TakesSocket,    "Calls DNSServiceSleepKeepalive() for IPv4 TCP connection." },
	{ AF_INET,  kKeepAliveCallVariant_TakesSockAddrs, "Calls DNSServiceSleepKeepalive_sockaddr() for IPv4 TCP connection." },
	{ AF_INET6, kKeepAliveCallVariant_TakesSocket,    "Calls DNSServiceSleepKeepalive() for IPv6 TCP connection." },
	{ AF_INET6, kKeepAliveCallVariant_TakesSockAddrs, "Calls DNSServiceSleepKeepalive_sockaddr() for IPv6 TCP connection." }
};

typedef struct
{
	sockaddr_ip			local;			// TCP connection's local address and port.
	sockaddr_ip			remote;			// TCP connection's remote address and port.
	NanoTime64			startTime;		// Subtest's start time.
	NanoTime64			endTime;		// Subtest's end time.
	SocketRef			clientSock;		// Socket for client-side of TCP connection.
	SocketRef			serverSock;		// Socket for server-side of TCP connection.
	char *				recordName;		// Keepalive record's name.
	char *				dataStr;		// Data expected to be contained in keepalive record's data.
	const char *		description;	// Subtests's description.
	unsigned int		timeoutKA;		// Randomly-generated timeout value that gets put in keepalive record's rdata.
	OSStatus			error;			// Subtest's error.
	
}	KeepAliveSubtest;

typedef struct KeepAliveTest *		KeepAliveTestRef;

typedef struct
{
	KeepAliveTestRef		test;	// Weak back pointer to test.
	
}	KeepAliveTestConnectionContext;

struct KeepAliveTest
{
	dispatch_queue_t						queue;			// Serial queue for test events.
	dispatch_semaphore_t					doneSem;		// Semaphore to signal when the test is done.
	dispatch_source_t						readSource;		// Read source for TCP listener socket.
	DNSServiceRef							keepalive;		// DNSServiceSleepKeepalive{,sockaddr} operation.
	DNSServiceRef							query;			// Query to verify registered keepalive record.
	dispatch_source_t						timer;			// Timer to put time limit on query.
	AsyncConnectionRef						connection;		// Establishes current subtest's TCP connection.
	KeepAliveTestConnectionContext *		connectionCtx;	// Weak pointer to connection's context.
	NanoTime64								startTime;		// Test's start time.
	NanoTime64								endTime;		// Test's end time.
	OSStatus								error;			// Test's error.
	size_t									subtestIdx;		// Index of current subtest.
	KeepAliveSubtest						subtests[ 4 ];	// Subtest array.
};
check_compile_time( countof_field( struct KeepAliveTest, subtests ) == countof( kKeepAliveSubtestParams ) );

ulog_define_ex( kDNSSDUtilIdentifier, KeepAliveTest, kLogLevelInfo, kLogFlags_None, "KeepAliveTest", NULL );
#define kat_ulog( LEVEL, ... )		ulog( &log_category_from_name( KeepAliveTest ), (LEVEL), __VA_ARGS__ )

static OSStatus	_KeepAliveTestCreate( KeepAliveTestRef *outTest );
static OSStatus	_KeepAliveTestRun( KeepAliveTestRef inTest );
static void		_KeepAliveTestFree( KeepAliveTestRef inTest );

static void	KeepAliveTestCmd( void )
{
	OSStatus				err;
	OutputFormatType		outputFormat;
	KeepAliveTestRef		test		= NULL;
	CFPropertyListRef		plist		= NULL;
	CFMutableArrayRef		subtests;
	size_t					i;
	size_t					subtestFailCount;
	Boolean					testPassed	= false;
	char					startTime[ 32 ];
	char					endTime[ 32 ];
	
	err = OutputFormatFromArgString( gKeepAliveTest_OutputFormat, &outputFormat );
	require_noerr_quiet( err, exit );
	
	err = _KeepAliveTestCreate( &test );
	require_noerr( err, exit );
	
	err = _KeepAliveTestRun( test );
	require_noerr( err, exit );
	
	_NanoTime64ToTimestamp( test->startTime, startTime, sizeof( startTime ) );
	_NanoTime64ToTimestamp( test->endTime, endTime, sizeof( endTime ) );
	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &plist,
		"{"
			"%kO=%s"	// startTime
			"%kO=%s"	// endTime
			"%kO=[%@]"	// subtests
		"}",
		CFSTR( "startTime" ),	startTime,
		CFSTR( "endTime" ),		endTime,
		CFSTR( "subtests" ),	&subtests );
	require_noerr( err, exit );
	
	subtestFailCount = 0;
	check( test->subtestIdx == countof( test->subtests ) );
	for( i = 0; i < countof( test->subtests ); ++i )
	{
		KeepAliveSubtest * const		subtest = &test->subtests[ i ];
		char							errorDesc[ 128 ];
		
		_NanoTime64ToTimestamp( subtest->startTime, startTime, sizeof( startTime ) );
		_NanoTime64ToTimestamp( subtest->endTime, endTime, sizeof( endTime ) );
		SNPrintF( errorDesc, sizeof( errorDesc ), "%m", subtest->error );
		err = CFPropertyListAppendFormatted( kCFAllocatorDefault, subtests,
			"{"
				"%kO=%s"		// startTime
				"%kO=%s"		// endTime
				"%kO=%s"		// description
				"%kO=%##a"		// localAddr
				"%kO=%##a"		// remoteAddr
				"%kO=%s"		// recordName
				"%kO=%s"		// expectedRData
				"%kO="			// error
				"{"
					"%kO=%lli"	// code
					"%kO=%s"	// description
				"}"
			"}",
			CFSTR( "startTime" ),		startTime,
			CFSTR( "endTime" ),			endTime,
			CFSTR( "description" ),		subtest->description,
			CFSTR( "localAddr" ),		&subtest->local.sa,
			CFSTR( "remoteAddr" ),		&subtest->remote.sa,
			CFSTR( "recordName" ),		subtest->recordName,
			CFSTR( "expectedRData" ),	subtest->dataStr,
			CFSTR( "error" ),
			CFSTR( "code" ),			(int64_t) subtest->error,
			CFSTR( "description" ),		errorDesc
		);
		require_noerr( err, exit );
		if( subtest->error ) ++subtestFailCount;
	}
	if( subtestFailCount == 0 ) testPassed = true;
	CFPropertyListAppendFormatted( kCFAllocatorDefault, plist, "%kO=%b", CFSTR( "pass" ), testPassed );
	
	err = OutputPropertyList( plist, outputFormat, gKeepAliveTest_OutputFilePath );
	require_noerr( err, exit );
	
exit:
	if( test ) _KeepAliveTestFree( test );
	CFReleaseNullSafe( plist );
    gExitCode = err ? 1 : ( testPassed ? 0 : 2 );
}

//===========================================================================================================================

static void					_KeepAliveTestStart( void *inCtx );
static void					_KeepAliveTestStop( KeepAliveTestRef inTest, OSStatus inError );
static OSStatus				_KeepAliveTestStartSubtest( KeepAliveTestRef inTest );
static void					_KeepAliveTestStopSubtest( KeepAliveTestRef inTest );
static KeepAliveSubtest *	_KeepAliveTestGetSubtest( KeepAliveTestRef inTest );
static const char *			_KeepAliveTestGetSubtestLogPrefix( KeepAliveTestRef inTest, char *inBufPtr, size_t inBufLen );
static OSStatus				_KeepAliveTestContinue( KeepAliveTestRef inTest, OSStatus inSubtestError, Boolean *outDone );
static void					_KeepAliveTestTCPAcceptHandler( void *inCtx );
static void					_KeepAliveTestConnectionHandler( SocketRef inSock, OSStatus inError, void *inArg );
static void					_KeepAliveTestHandleConnection( KeepAliveTestRef inTest, SocketRef inSock, OSStatus inError );
static void					_KeepAliveTestForgetConnection( KeepAliveTestRef inTest );
static void DNSSD_API		_KeepAliveTestKeepaliveCallback( DNSServiceRef inSDRef, DNSServiceErrorType inErr, void *inCtx );
static void					_KeepAliveTestQueryTimerHandler( void *inCtx );
static void DNSSD_API
	_KeepAliveTestQueryRecordCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		uint32_t			inInterfaceIndex,
		DNSServiceErrorType	inError,
		const char *		inFullName,
		uint16_t			inType,
		uint16_t			inClass,
		uint16_t			inRDataLen,
		const void *		inRDataPtr,
		uint32_t			inTTL,
		void *				inCtx );

static OSStatus	_KeepAliveTestCreate( KeepAliveTestRef *outTest )
{
	OSStatus				err;
	KeepAliveTestRef		test;
	size_t					i;
	
	test = (KeepAliveTestRef) calloc( 1, sizeof( *test ) );
	require_action( test, exit, err = kNoMemoryErr );
	
	test->error = kInProgressErr;
	for( i = 0; i < countof( test->subtests ); ++i )
	{
		KeepAliveSubtest * const		subtest = &test->subtests[ i ];
		
		subtest->local.sa.sa_family		= AF_UNSPEC;
		subtest->remote.sa.sa_family	= AF_UNSPEC;
		subtest->clientSock				= kInvalidSocketRef;
		subtest->serverSock				= kInvalidSocketRef;
	}
	test->queue = dispatch_queue_create( "com.apple.dnssdutil.keepalive-test", DISPATCH_QUEUE_SERIAL );
	require_action( test->queue, exit, err = kNoResourcesErr );
	
	test->doneSem = dispatch_semaphore_create( 0 );
	require_action( test->doneSem, exit, err = kNoResourcesErr );
	
	*outTest = test;
	test = NULL;
	err = kNoErr;
	
exit:
	if( test ) _KeepAliveTestFree( test );
	return( err );
}

//===========================================================================================================================

static OSStatus	_KeepAliveTestRun( KeepAliveTestRef inTest )
{
	dispatch_async_f( inTest->queue, inTest, _KeepAliveTestStart );
	dispatch_semaphore_wait( inTest->doneSem, DISPATCH_TIME_FOREVER );
	return( inTest->error );
}

//===========================================================================================================================

static void	_KeepAliveTestFree( KeepAliveTestRef inTest )
{
	size_t		i;
	
	check( !inTest->readSource );
	check( !inTest->query );
	check( !inTest->timer );
	check( !inTest->keepalive );
	check( !inTest->connection );
	check( !inTest->connectionCtx );
	dispatch_forget( &inTest->queue );
	dispatch_forget( &inTest->doneSem );
	for( i = 0; i < countof( inTest->subtests ); ++i )
	{
		KeepAliveSubtest * const		subtest = &inTest->subtests[ i ];
		
		check( !IsValidSocket( subtest->clientSock ) );
		check( !IsValidSocket( subtest->serverSock ) );
		ForgetMem( &subtest->recordName );
		ForgetMem( &subtest->dataStr );
	}
	free( inTest );
}

//===========================================================================================================================

static void _KeepAliveTestStart( void *inCtx )
{
	OSStatus					err;
	const KeepAliveTestRef		test = (KeepAliveTestRef) inCtx;
	
	test->error		= kInProgressErr;
	test->startTime	= NanoTimeGetCurrent();
	err = _KeepAliveTestStartSubtest( test );
	require_noerr( err, exit );
	
exit:
	if( err ) _KeepAliveTestStop( test, err );
}

//===========================================================================================================================

static void	_KeepAliveTestStop( KeepAliveTestRef inTest, OSStatus inError )
{
	size_t		i;
	
	inTest->error	= inError;
	inTest->endTime	= NanoTimeGetCurrent();
	_KeepAliveTestStopSubtest( inTest );
	for( i = 0; i < countof( inTest->subtests ); ++i )
	{
		KeepAliveSubtest * const		subtest = &inTest->subtests[ i ];
		
		ForgetSocket( &subtest->clientSock );
		ForgetSocket( &subtest->serverSock );
	}
	dispatch_semaphore_signal( inTest->doneSem );
}

//===========================================================================================================================

static OSStatus	_KeepAliveTestStartSubtest( KeepAliveTestRef inTest )
{
	OSStatus									err;
	KeepAliveSubtest * const					subtest		= _KeepAliveTestGetSubtest( inTest );
	const KeepAliveSubtestParams * const		params		= &kKeepAliveSubtestParams[ inTest->subtestIdx ];
	int											port;
	SocketRef									sock		= kInvalidSocketRef;
	const uint32_t								loopbackV4	= htonl( INADDR_LOOPBACK );
	SocketContext *								sockCtx		= NULL;
	KeepAliveTestConnectionContext *			cnxCtx		= NULL;
	Boolean										useIPv4;
	char										serverAddrStr[ 64 ];
	char										prefix[ 64 ];
	
	subtest->error			= kInProgressErr;
	subtest->startTime		= NanoTimeGetCurrent();
	subtest->description	= params->description;
	
	require_action( ( params->family == AF_INET ) || ( params->family == AF_INET6 ), exit, err = kInternalErr );
	
	// Create TCP listener socket.
	
	useIPv4 = ( params->family == AF_INET ) ? true : false;
	err = ServerSocketOpenEx( params->family, SOCK_STREAM, IPPROTO_TCP,
		useIPv4 ? ( (const void *) &loopbackV4 ) : ( (const void *) &in6addr_loopback ), kSocketPort_Auto, &port,
		kSocketBufferSize_DontSet, &sock );
	require_noerr( err, exit );
	
	if( useIPv4 )	SNPrintF( serverAddrStr, sizeof( serverAddrStr ), "%.4a:%d", &loopbackV4, port );
	else			SNPrintF( serverAddrStr, sizeof( serverAddrStr ), "[%.16a]:%d", in6addr_loopback.s6_addr, port );
	_KeepAliveTestGetSubtestLogPrefix( inTest, prefix, sizeof( prefix ) );
	kat_ulog( kLogLevelInfo, "%s: Will listen for connections on %s\n", prefix, serverAddrStr );
	
	sockCtx = SocketContextCreate( sock, inTest, &err );
	require_noerr( err, exit );
	sock = kInvalidSocketRef;
	
	// Create read source for TCP listener socket.
	
	check( !inTest->readSource );
	err = DispatchReadSourceCreate( sockCtx->sock, inTest->queue, _KeepAliveTestTCPAcceptHandler,
		SocketContextCancelHandler, sockCtx, &inTest->readSource );
	require_noerr( err, exit );
	sockCtx = NULL;
	dispatch_resume( inTest->readSource );
	
	cnxCtx = (KeepAliveTestConnectionContext *) calloc( 1, sizeof( *cnxCtx ) );
	require_action( cnxCtx, exit, err = kNoMemoryErr );
	
	// Start asynchronous connection to listener socket.
	
	kat_ulog( kLogLevelInfo, "%s: Will connect to %s\n", prefix, serverAddrStr );
	
	check( !inTest->connection );
	err = AsyncConnection_Connect( &inTest->connection, serverAddrStr, 0, kAsyncConnectionFlags_None,
		5 * UINT64_C_safe( kNanosecondsPerSecond ), kSocketBufferSize_DontSet, kSocketBufferSize_DontSet,
		NULL, NULL, _KeepAliveTestConnectionHandler, cnxCtx, inTest->queue );
	require_noerr( err, exit );
	
	cnxCtx->test = inTest;
	check( !inTest->connectionCtx );
	inTest->connectionCtx = cnxCtx;
	cnxCtx = NULL;
	
exit:
	ForgetSocket( &sock );
	if( sockCtx ) SocketContextRelease( sockCtx );
	FreeNullSafe( cnxCtx );
	return( err );
}

//===========================================================================================================================

static void	_KeepAliveTestStopSubtest( KeepAliveTestRef inTest )
{
	dispatch_source_forget( &inTest->readSource );
	DNSServiceForget( &inTest->keepalive );
	DNSServiceForget( &inTest->query );
	dispatch_source_forget( &inTest->timer );
	_KeepAliveTestForgetConnection( inTest );
}

//===========================================================================================================================

static KeepAliveSubtest *	_KeepAliveTestGetSubtest( KeepAliveTestRef inTest )
{
	return( ( inTest->subtestIdx < countof( inTest->subtests ) ) ? &inTest->subtests[ inTest->subtestIdx ] : NULL );
}

//===========================================================================================================================

static const char *	_KeepAliveTestGetSubtestLogPrefix( KeepAliveTestRef inTest, char *inBufPtr, size_t inBufLen )
{
	SNPrintF( inBufPtr, inBufLen, "Subtest %zu/%zu", inTest->subtestIdx + 1, countof( inTest->subtests ) );
	return( inBufPtr );
}

//===========================================================================================================================

static OSStatus	_KeepAliveTestContinue( KeepAliveTestRef inTest, OSStatus inSubtestError, Boolean *outDone )
{
	OSStatus				err;
	KeepAliveSubtest *		subtest;
	
	require_action( inTest->subtestIdx <= countof( inTest->subtests ), exit, err = kInternalErr );
	
	if( inTest->subtestIdx < countof( inTest->subtests ) )
	{
		subtest = _KeepAliveTestGetSubtest( inTest );
		_KeepAliveTestStopSubtest( inTest );
		subtest->endTime	= NanoTimeGetCurrent();
		subtest->error		= inSubtestError;
		if( ++inTest->subtestIdx < countof( inTest->subtests ) )
		{
			err = _KeepAliveTestStartSubtest( inTest );
			require_noerr_quiet( err, exit );
		}
	}
	err = kNoErr;
	
exit:
	if( outDone ) *outDone = ( !err && ( inTest->subtestIdx == countof( inTest->subtests ) ) ) ? true : false;
	return( err );
}

//===========================================================================================================================

static void	_KeepAliveTestTCPAcceptHandler( void *inCtx )
{
	OSStatus						err;
	const SocketContext * const		sockCtx	= (SocketContext *) inCtx;
	const KeepAliveTestRef			test	= (KeepAliveTestRef) sockCtx->userContext;
	KeepAliveSubtest * const		subtest	= _KeepAliveTestGetSubtest( test );
	sockaddr_ip						peer;
	socklen_t						len;
	char							prefix[ 64 ];
	
	check( !IsValidSocket( subtest->serverSock ) );
	len = (socklen_t) sizeof( peer );
	subtest->serverSock = accept( sockCtx->sock, &peer.sa, &len );
	err = map_socket_creation_errno( subtest->serverSock );
	require_noerr( err, exit );
	
	_KeepAliveTestGetSubtestLogPrefix( test, prefix, sizeof( prefix ) );
	kat_ulog( kLogLevelInfo, "%s: Accepted connection from %##a\n", prefix, &peer.sa );
	
	dispatch_source_forget( &test->readSource );
	
exit:
	if( err ) _KeepAliveTestStop( test, err );
}

//===========================================================================================================================

static void	_KeepAliveTestConnectionHandler( SocketRef inSock, OSStatus inError, void *inArg )
{
	KeepAliveTestConnectionContext *		ctx		= (KeepAliveTestConnectionContext *) inArg;
	const KeepAliveTestRef					test	= ctx->test;
	
	if( test )
	{
		_KeepAliveTestForgetConnection( test );
		_KeepAliveTestHandleConnection( test, inSock, inError );
		inSock = kInvalidSocketRef;
	}
	ForgetSocket( &inSock );
	free( ctx );
}

//===========================================================================================================================

#define kKeepAliveTestQueryTimeoutSecs		5

static void	_KeepAliveTestHandleConnection( KeepAliveTestRef inTest, SocketRef inSock, OSStatus inError )
{
	OSStatus								err;
	KeepAliveSubtest * const				subtest			= _KeepAliveTestGetSubtest( inTest );
	const KeepAliveSubtestParams * const	params			= &kKeepAliveSubtestParams[ inTest->subtestIdx ];
	socklen_t								len;
    uint32_t								value;
	int										family, i;
	Boolean									subtestFailed	= false;
	Boolean									done;
	char									prefix[ 64 ];
	
	require_noerr_action( inError, exit, err = inError );
	
	check( !IsValidSocket( subtest->clientSock ) );
	subtest->clientSock = inSock;
	inSock = kInvalidSocketRef;
	
	// Get local and remote IP addresses.
	
	len = (socklen_t) sizeof( subtest->local );
	err = getsockname( subtest->clientSock, &subtest->local.sa, &len );
	err = map_global_noerr_errno( err );
	require_noerr( err, exit );
	
	len = (socklen_t) sizeof( subtest->remote );
	err = getpeername( subtest->clientSock, &subtest->remote.sa, &len );
	err = map_global_noerr_errno( err );
	require_noerr( err, exit );
	
	_KeepAliveTestGetSubtestLogPrefix( inTest, prefix, sizeof( prefix ) );
	kat_ulog( kLogLevelInfo, "%s: Connection established: %##a <-> %##a\n",
		prefix, &subtest->local.sa, &subtest->remote.sa );
	
	// Call either DNSServiceSleepKeepalive() or DNSServiceSleepKeepalive_sockaddr().
	
	check( subtest->timeoutKA == 0 );
	subtest->timeoutKA = (unsigned int) RandomRange( 1, UINT_MAX );
	
    switch( params->callVariant )
	{
		case kKeepAliveCallVariant_TakesSocket:
			kat_ulog( kLogLevelInfo, "%s: Will call DNSServiceSleepKeepalive() for client-side socket\n", prefix );
			check( !inTest->keepalive );
			err = DNSServiceSleepKeepalive( &inTest->keepalive, 0, subtest->clientSock,
				subtest->timeoutKA, _KeepAliveTestKeepaliveCallback, inTest );
			require_noerr( err, exit );
			
			err = DNSServiceSetDispatchQueue( inTest->keepalive, inTest->queue );
			require_noerr( err, exit );
			break;
		
		case kKeepAliveCallVariant_TakesSockAddrs:
			kat_ulog( kLogLevelInfo,
				"%s: Will call DNSServiceSleepKeepalive_sockaddr() for local and remote sockaddrs\n", prefix );
			check( !inTest->keepalive );
			err = DNSServiceSleepKeepalive_sockaddr( &inTest->keepalive, 0, &subtest->local.sa, &subtest->remote.sa,
				subtest->timeoutKA, _KeepAliveTestKeepaliveCallback, inTest );
			require_noerr( err, exit );
			
			err = DNSServiceSetDispatchQueue( inTest->keepalive, inTest->queue );
			require_noerr( err, exit );
			break;
		
		default:
			kat_ulog( kLogLevelError, "%s: Invalid KeepAliveCallVariant value %d\n", prefix, (int) params->callVariant );
			err = kInternalErr;
			goto exit;
	}
	// Use the same logic that the DNSServiceSleepKeepalive functions use to derive a record name and rdata.
	
	value = 0;
	family = subtest->local.sa.sa_family;
	if( family == AF_INET )
	{
		const struct sockaddr_in * const		sin = &subtest->local.v4;
		const uint8_t *							ptr;
		
		check_compile_time_code( sizeof( sin->sin_addr.s_addr ) == 4 );
		ptr = (const uint8_t *) &sin->sin_addr.s_addr;
		for( i = 0; i < 4; ++i ) value += ptr[ i ];
		value += sin->sin_port;	// Note: No ntohl(). This is what DNSServiceSleepKeepalive does.
		
		check( subtest->remote.sa.sa_family == AF_INET );
		ASPrintF( &subtest->dataStr, "t=%u h=%.4a d=%.4a l=%u r=%u",
			subtest->timeoutKA, &subtest->local.v4.sin_addr.s_addr, &subtest->remote.v4.sin_addr.s_addr,
			ntohs( subtest->local.v4.sin_port ), ntohs( subtest->remote.v4.sin_port ) );
		require_action( subtest->dataStr, exit, err = kNoMemoryErr );
	}
	else if( family == AF_INET6 )
	{
		const struct sockaddr_in6 * const		sin6 = &subtest->local.v6;
		
		check_compile_time_code( countof( sin6->sin6_addr.s6_addr ) == 16 );
		for( i = 0; i < 16; ++i ) value += sin6->sin6_addr.s6_addr[ i ];
		value += sin6->sin6_port; // Note: No ntohl(). This is what DNSServiceSleepKeepalive does.
		
		check( subtest->remote.sa.sa_family == AF_INET6 );
		ASPrintF( &subtest->dataStr, "t=%u H=%.16a D=%.16a l=%u r=%u",
			subtest->timeoutKA, subtest->local.v6.sin6_addr.s6_addr, subtest->remote.v6.sin6_addr.s6_addr,
			ntohs( subtest->local.v6.sin6_port ), ntohs( subtest->remote.v6.sin6_port ) );
		require_action( subtest->dataStr, exit, err = kNoMemoryErr );
	}
	else
	{
		kat_ulog( kLogLevelError, "%s: Unexpected local address family %d\n", prefix, family );
		err = kInternalErr;
		goto exit;
	}
	
	// Start query for the new keepalive record.
	
	check( !subtest->recordName );
	ASPrintF( &subtest->recordName, "%u._keepalive._dns-sd._udp.local.", value );
	require_action( subtest->recordName, exit, err = kNoMemoryErr );
	
	kat_ulog( kLogLevelInfo, "%s: Will query for %s NULL record\n", prefix, subtest->recordName );
	check( !inTest->query );
	err = DNSServiceQueryRecord( &inTest->query, 0, kDNSServiceInterfaceIndexLocalOnly, subtest->recordName,
		kDNSServiceType_NULL, kDNSServiceClass_IN, _KeepAliveTestQueryRecordCallback, inTest );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( inTest->query, inTest->queue );
	require_noerr( err, exit );
	
	// Start timer to enforce a time limit on the query.
	
	check( !inTest->timer );
	err = DispatchTimerOneShotCreate( dispatch_time_seconds( kKeepAliveTestQueryTimeoutSecs ),
		kKeepAliveTestQueryTimeoutSecs * ( INT64_C_safe( kNanosecondsPerSecond ) / 20 ), inTest->queue,
		_KeepAliveTestQueryTimerHandler, inTest, &inTest->timer );
	require_noerr( err, exit );
	dispatch_resume( inTest->timer );
	
exit:
	ForgetSocket( &inSock );
	if( subtestFailed )
	{
		err = _KeepAliveTestContinue( inTest, err, &done );
		check_noerr( err );
	}
	else
	{
		done = false;
	}
	if( err || done ) _KeepAliveTestStop( inTest, err );
}

//===========================================================================================================================

static void	_KeepAliveTestForgetConnection( KeepAliveTestRef inTest )
{
	if( inTest->connection )
	{
		check( inTest->connectionCtx );
		inTest->connectionCtx->test	= NULL; // Unset the connection's back pointer to test.
		inTest->connectionCtx		= NULL; // Context will be freed by the connection's handler.
		AsyncConnection_Forget( &inTest->connection );
	}
}

//===========================================================================================================================

static void DNSSD_API	_KeepAliveTestKeepaliveCallback( DNSServiceRef inSDRef, DNSServiceErrorType inError, void *inCtx )
{
	OSStatus					err;
	const KeepAliveTestRef		test	= (KeepAliveTestRef) inCtx;
	char						prefix[ 64 ];
	
	Unused( inSDRef );
	
	_KeepAliveTestGetSubtestLogPrefix( test, prefix, sizeof( prefix ) );
	kat_ulog( kLogLevelInfo, "%s: Keepalive callback error: %#m\n", prefix, inError );
	
	if( inError )
	{
		Boolean		done;
		
		err = _KeepAliveTestContinue( test, inError, &done );
		check_noerr( err );
		if( err || done ) _KeepAliveTestStop( test, err );
	}
}

//===========================================================================================================================

static void	_KeepAliveTestQueryTimerHandler( void *inCtx )
{
	OSStatus						err;
	const KeepAliveTestRef			test	= (KeepAliveTestRef) inCtx;
	KeepAliveSubtest * const		subtest	= _KeepAliveTestGetSubtest( test );
	Boolean							done;
	char							prefix[ 64 ];
	
	_KeepAliveTestGetSubtestLogPrefix( test, prefix, sizeof( prefix ) );
	kat_ulog( kLogLevelInfo, "%s: Query for \"%s\" timed out.\n", prefix, subtest->recordName );
	
	err = _KeepAliveTestContinue( test, kTimeoutErr, &done );
	check_noerr( err );
	if( err || done ) _KeepAliveTestStop( test, err );
}

//===========================================================================================================================

static void DNSSD_API
	_KeepAliveTestQueryRecordCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		uint32_t			inInterfaceIndex,
		DNSServiceErrorType	inError,
		const char *		inFullName,
		uint16_t			inType,
		uint16_t			inClass,
		uint16_t			inRDataLen,
		const void *		inRDataPtr,
		uint32_t			inTTL,
		void *				inCtx )
{
	OSStatus						err;
	const KeepAliveTestRef			test	= (KeepAliveTestRef) inCtx;
	KeepAliveSubtest * const		subtest	= _KeepAliveTestGetSubtest( test );
	const uint8_t *					ptr;
	size_t							dataStrLen, minLen;
	Boolean							done;
	char							prefix[ 64 ];
	
	Unused( inSDRef );
	Unused( inInterfaceIndex );
	Unused( inTTL );
	
	_KeepAliveTestGetSubtestLogPrefix( test, prefix, sizeof( prefix ) );
	if( strcasecmp( inFullName, subtest->recordName ) != 0 )
	{
		kat_ulog( kLogLevelError, "%s: QueryRecord(%s) result: Got unexpected record name \"%s\".\n",
			prefix, subtest->recordName, inFullName );
		err = kUnexpectedErr;
		goto exit;
	}
	if( inType != kDNSServiceType_NULL )
	{
		kat_ulog( kLogLevelError, "%s: QueryRecord(%s) result: Got unexpected record type %d (%s) != %d (NULL).\n",
			prefix, subtest->recordName, inType, RecordTypeToString( inType ), kDNSServiceType_NULL );
		err = kUnexpectedErr;
		goto exit;
	}
	if( inClass != kDNSServiceClass_IN )
	{
		kat_ulog( kLogLevelError, "%s: QueryRecord(%s) result: Got unexpected record class %d != %d (IN).\n",
			prefix, subtest->recordName, inClass, kDNSServiceClass_IN );
		err = kUnexpectedErr;
		goto exit;
	}
	if( inError )
	{
		kat_ulog( kLogLevelError, "%s: QueryRecord(%s) result: Got unexpected error %#m.\n",
			prefix, subtest->recordName, inError );
		err = inError;
		goto exit;
	}
	if( ( inFlags & kDNSServiceFlagsAdd ) == 0 )
	{
		kat_ulog( kLogLevelError, "%s: QueryRecord(%s) result: Missing Add flag.\n", prefix, subtest->recordName );
		err = kUnexpectedErr;
		goto exit;
	}
	kat_ulog( kLogLevelInfo, "%s: QueryRecord(%s) result rdata: %#H\n",
		prefix, subtest->recordName, inRDataPtr, inRDataLen, inRDataLen );
	
	dataStrLen	= strlen( subtest->dataStr ) + 1;	// There's a NUL terminator at the end of the rdata.
	minLen		= 1 + dataStrLen;					// The first byte of the rdata is a length byte.
	if( inRDataLen < minLen )
	{
		kat_ulog( kLogLevelError, "%s: QueryRecord(%s) result: rdata length (%d) is less than expected minimum (%zu).\n",
			prefix, subtest->recordName, inRDataLen, minLen );
		err = kUnexpectedErr;
		goto exit;
	}
	ptr = (const uint8_t *) inRDataPtr;
	if( ptr[ 0 ] < dataStrLen )
	{
		kat_ulog( kLogLevelError,
			"%s: QueryRecord(%s) result: rdata length byte value (%d) is less than expected minimum (%zu).\n",
			prefix, subtest->recordName, ptr[ 0 ], dataStrLen );
		err = kUnexpectedErr;
		goto exit;
	}
	if( memcmp( &ptr[ 1 ], subtest->dataStr, dataStrLen - 1 ) != 0 )
	{
		kat_ulog( kLogLevelError, "%s: QueryRecord(%s) result: rdata body doesn't contain '%s'.\n",
			prefix, subtest->recordName, subtest->dataStr );
	}
	err = kNoErr;
	
exit:
	err = _KeepAliveTestContinue( test, err, &done );
	check_noerr( err );
	if( err || done ) _KeepAliveTestStop( test, kNoErr );
}
#endif	// TARGET_OS_DARWIN

#if ( ENABLE_DNSSDUTIL_DNSSEC_TEST == 1 )
//===========================================================================================================================
//	DNSSECTestCmd
//===========================================================================================================================

#define kDNSSECTestQueryTimeoutSecs 4

typedef struct DNSSECTest_BasicValidationContext
{
	uint32_t number_of_answers;		// The number of answers expected to receive in the "basic validation" test

}	DNSSECTest_BasicValidationContext;

//===========================================================================================================================

typedef struct
{
	DNSServiceRef		query;
	dispatch_source_t	queryTimer;			// Used to setup timeout timer, to prevent from waiting forever.
	Boolean				testStarted;
	const char *		testName;			// The name of the curreent running test case.
	const char *		testCaseName;		// The query name in the subtest of the test case.
	union {
		DNSSECTest_BasicValidationContext	basicValidation;
	} testCaseContext;						// Contains different customized context pointer, which can be used by different test cases.
	const char *		subtestQueryName;	// The query name that is passed to mDNSResponder API
	int					subtestIndex;		// The index of the current case in the test input array.
	pid_t				localServerPID;		// The pid of the dnssdutil server
	NanoTime64			startTime;			// The time when the DNSSEC test case starts.
	CFMutableArrayRef	subtestReport;		// The reference to the CFMutableArrayRef, which contains subtest reports for different subtests
	NanoTime64			subtestStartTime;	// The time when the subtest starts
	Boolean				subtestFailed;		// Indicate if any subtest failed before.

	char *				outputFilePath;		// File to write test results to. If NULL, then write to stdout. (malloced)
	OutputFormatType	outputFormat;		// Format of test report output.
} DNSSECTestContext;

//===========================================================================================================================

typedef struct
{
	const char *	testCaseName;										// The name of the test case that will be run.
	void			(*testCaseHandler)( DNSSECTestContext * context );	// The main function of the test case.
} DNSSECTestTestCase;

//===========================================================================================================================

static void DNSSECTestSetupLocalDNSServer( DNSSECTestContext *context );
static void DNSSECTestStartTestCase( DNSSECTestContext *context );

//===========================================================================================================================

// DNSSEC test cases
static void DNSSECTest_BasicValidation( DNSSECTestContext *context );

static DNSSECTestTestCase DNSSECTestTestCases[] =
{
	{ "basic validation", DNSSECTest_BasicValidation }
};

//===========================================================================================================================

// The main function to start the DNSSEC test
static void
	DNSSECTestCmd( void )
{
	OSStatus			err;
	dispatch_source_t	signalSource	= NULL;
	DNSSECTestContext *	context			= NULL;

	// Set up SIGINT handler.
    signal( SIGINT, SIG_IGN );
    err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), Exit, kExitReason_SIGINT, &signalSource );
    require_noerr( err, exit );
    dispatch_resume( signalSource );

	// Create the test context.
	context = (DNSSECTestContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );

	context->testStarted = false;

	// Get the command line option.
	context->testCaseName = strdup( gDNSSECTest_TestCaseName );
	require( context->testCaseName , exit );

	// Start the subtest from index 0
	context->subtestIndex = 0;

	// Get the output format.
	err = OutputFormatFromArgString( gDNSSECTest_OutputFormat, &context->outputFormat );
	require_noerr_quiet( err, exit );

	// Get the output file path.
	if( gDNSSECTest_OutputFilePath )
	{
		context->outputFilePath = strdup( gDNSSECTest_OutputFilePath );
		require_noerr_quiet( context->outputFilePath , exit );
	}

	// Initialize the CFArray to store the test result.
	context->subtestReport	= CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
	context->startTime 		= NanoTimeGetCurrent();

	// Start the local dnssdutil server.
	DNSSECTestSetupLocalDNSServer( context );

	// Start the test.
	DNSSECTestStartTestCase( context );

exit:
	exit( 1 );
}

//===========================================================================================================================

// Start the local DNS server with dnssdutil server command.
static void
	DNSSECTestSetupLocalDNSServer( DNSSECTestContext *context )
{
	Unused( context );
	pid_t pid = getpid();

	OSStatus err = _SpawnCommand( &context->localServerPID, NULL, NULL,
		DNSSDUTIL_TEST_DNS_SERVER_COMMAND_PREAMBLE " --loopback --follow %lld", (int64_t) pid );
	require_noerr_action( err, exit, FPrintF( stderr,
		DNSSDUTIL_TEST_DNS_SERVER_COMMAND_PREAMBLE " --loopback --follow %lld failed, error: %d\n", (int64_t) pid, err ) );

	// Wait long enough to allow the DNS server being setup.
	sleep( 2 );

	return;
exit:
	exit( 1 );
}

//===========================================================================================================================

// Start the test case that user specifies.
static void
	DNSSECTestStartTestCase( DNSSECTestContext *context )
{
	Boolean findTestCase = false;

	for ( int i = 0; i < (int)countof( DNSSECTestTestCases ); i++ )
	{
		if( strcmp( context->testCaseName, DNSSECTestTestCases[i].testCaseName ) == 0 )
		{
			DNSSECTestTestCases[ i ].testCaseHandler( context );
			findTestCase = true;
		}
	}

	if( !findTestCase )
	{
		FPrintF( stdout, "Unknown test case \"%s\"\n", context->testCaseName );
		exit( 1 );
	}
}

//===========================================================================================================================

// Write the current subtest status into the report list. When this function is called, either some error occurs or the
// subtest finishes.
static void
	_DNSSECTestQueryWriteSubtestReport( DNSSECTestContext *inContext, const char *errorDescription )
{
	OSStatus	err;
	NanoTime64	now;
	char		startTime[ 32 ];
	char		endTime[ 32 ];

	now = NanoTimeGetCurrent();
	_NanoTime64ToTimestamp( inContext->subtestStartTime, startTime, sizeof( startTime ) );
	_NanoTime64ToTimestamp( now, endTime, sizeof( endTime ) );

	err = CFPropertyListAppendFormatted( kCFAllocatorDefault, inContext->subtestReport,
		"{"
			"%kO=%s"
			"%kO=%s"
			"%kO=%s"
			"%kO=%s"
			"%kO=%s"
		"}",
		CFSTR( "Start Time" ),			startTime,
		CFSTR( "End Time" ),			endTime,
		CFSTR( "Subtest Query Name" ),	inContext->subtestQueryName,
		CFSTR( "Result" ),				errorDescription ? "Fail" : "Pass",
		CFSTR( "Error Description" ),	errorDescription ? errorDescription : "No Error"
	);

	require_noerr( err, exit );
	return;

exit:
	ErrQuit( 1, "error: %#m\n", err );
}

//===========================================================================================================================

// Output the final test result to the file (path specified by the user) in the disk.
static void
	_DNSSECTestQueryOutputFinalReport( DNSSECTestContext *inContext )
{
	OSStatus			err;
	CFPropertyListRef	plist;
	NanoTime64			now;
	char				startTime[ 32 ];
	char				endTime[ 32 ];

	now = NanoTimeGetCurrent();
	_NanoTime64ToTimestamp( inContext->startTime, startTime, sizeof( startTime ) );
	_NanoTime64ToTimestamp( now, endTime, sizeof( endTime ) );

	err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &plist,
		"{"
			"%kO=%s"
			"%kO=%s"
			"%kO=%s"
			"%kO=%b"
			"%kO=%O"
		"}",
		CFSTR( "Start Time" ),		startTime,
		CFSTR( "End Time" ),		endTime,
		CFSTR( "Test Case Name" ),	inContext->testCaseName,
		CFSTR( "All Passed" ),		!inContext->subtestFailed,
		CFSTR( "Subtest Reports" ),	inContext->subtestReport
	);
	require_noerr( err, exit );
	ForgetCF( &inContext->subtestReport );

	err = OutputPropertyList( plist, inContext->outputFormat, inContext->outputFilePath );
	CFRelease( plist );
	require_noerr( err, exit );

	return;
exit:
	ErrQuit( 1, "error: %#m\n", err );
}

//===========================================================================================================================

// The handler that will be called when timeout happens. When timeout happens it always means something bad happen, and
// it might be possible that all the remaning tests timeout too, so the function exits directly instead of continuing.
static void
	_DNSSECTestQueryTimeoutHandler( void *inContext )
{
	DNSSECTestContext * const context = (DNSSECTestContext *) inContext;

	DNSServiceForget( &context->query );
	dispatch_source_forget( &context->queryTimer );

	context->subtestFailed = true;
	_DNSSECTestQueryWriteSubtestReport( context, "Query for DNSSEC-related records timed out" );
	_DNSSECTestQueryOutputFinalReport( context );

	exit( 1 );
}

//===========================================================================================================================
#pragma mark - Test case "basic validation"

//===========================================================================================================================

typedef struct DNSSECTest_BasicValidationTestCase
{
	const char *	queryName;		// The query name that controls the behavior of the local DNS server.
	uint16_t		queryType;		// The DNS record being queried.
	uint32_t		num_of_answers;	// How many anwers are expected to be returned.

}	DNSSECTest_BasicValidationTestCase;

//===========================================================================================================================

DNSSECTest_BasicValidationTestCase DNSSECTest_BasicValidationTestCases[] =
{
	// kDNSSECAlgorithm_RSASHA1 5
	{ "count-1.z-5-1.z-5-2.z-5-3.dnssec.test.",		kDNSServiceType_A,		1 },
	{ "count-2.z-5-1.z-5-2.z-5-3.dnssec.test.",		kDNSServiceType_A,		2 },
	{ "count-1.z-5-1.z-5-2.z-5-3.dnssec.test.",		kDNSServiceType_AAAA,	1 },
	{ "count-2.z-5-1.z-5-2.z-5-3.dnssec.test.",		kDNSServiceType_AAAA,	2 },
	{ "count-10.z-5-1.z-5-2.z-5-3.dnssec.test.",	kDNSServiceType_A,		10 },
	// kDNSSECAlgorithm_RSASHA256 8
	{ "count-1.z-8-1.z-8-2.z-8-3.dnssec.test.",		kDNSServiceType_A,		1 },
	{ "count-2.z-8-1.z-8-2.z-8-3.dnssec.test.",		kDNSServiceType_A,		2 },
	{ "count-1.z-8-1.z-8-2.z-8-3.dnssec.test.",		kDNSServiceType_AAAA,	1 },
	{ "count-2.z-8-1.z-8-2.z-8-3.dnssec.test.",		kDNSServiceType_AAAA,	2 },
	{ "count-10.z-8-1.z-8-2.z-8-3.dnssec.test.",	kDNSServiceType_A,		10 },
	// kDNSSECAlgorithm_RSASHA512 10
	{ "count-1.z-10-1.z-10-2.z-10-3.dnssec.test.",	kDNSServiceType_A,		1 },
	{ "count-2.z-10-1.z-10-2.z-10-3.dnssec.test.",	kDNSServiceType_A,		2 },
	{ "count-1.z-10-1.z-10-2.z-10-3.dnssec.test.",	kDNSServiceType_AAAA,	1 },
	{ "count-2.z-10-1.z-10-2.z-10-3.dnssec.test.",	kDNSServiceType_AAAA,	2 },
	{ "count-10.z-10-1.z-10-2.z-10-3.dnssec.test.",	kDNSServiceType_A,		10 },
	// kDNSSECAlgorithm_ECDSAP256SHA256 13
	{ "count-1.z-13-1.z-13-2.z-13-3.dnssec.test.",	kDNSServiceType_A,		1 },
	{ "count-2.z-13-1.z-13-2.z-13-3.dnssec.test.",	kDNSServiceType_A,		2 },
	{ "count-1.z-13-1.z-13-2.z-13-3.dnssec.test.",	kDNSServiceType_AAAA,	1 },
	{ "count-2.z-13-1.z-13-2.z-13-3.dnssec.test.",	kDNSServiceType_AAAA,	2 },
	{ "count-10.z-13-1.z-13-2.z-13-3.dnssec.test.",	kDNSServiceType_A,		10 },
	// kDNSSECAlgorithm_ECDSAP384SHA384 14
	{ "count-1.z-14-1.z-14-2.z-14-3.dnssec.test.",	kDNSServiceType_A,		1 },
	{ "count-2.z-14-1.z-14-2.z-14-3.dnssec.test.",	kDNSServiceType_A,		2 },
	{ "count-1.z-14-1.z-14-2.z-14-3.dnssec.test.",	kDNSServiceType_AAAA,	1 },
	{ "count-2.z-14-1.z-14-2.z-14-3.dnssec.test.",	kDNSServiceType_AAAA,	2 },
	{ "count-10.z-14-1.z-14-2.z-14-3.dnssec.test.",	kDNSServiceType_A,		10 },
	// Mixed use of mutiple DNSKEY algorithms
	{ "count-1.z-5-1.z-8-2.z-10-3.dnssec.test.",	kDNSServiceType_A,		1 },
	{ "count-2.z-5-1.z-5-2.z-8-3.dnssec.test.",		kDNSServiceType_A,		2 },
	{ "count-1.z-8-1.z-5-2.z-5-3.dnssec.test.",		kDNSServiceType_AAAA,	1 },
	{ "count-2.z-10-1.z-8-2.z-8-3.dnssec.test.",	kDNSServiceType_AAAA,	2 },
	{ "count-10.z-10-1.z-8-2.z-5-3.dnssec.test.",	kDNSServiceType_A,		10 },
	{ "count-1.z-13-1.z-14-2.z-14-3.dnssec.test.",	kDNSServiceType_A,		1 },
	{ "count-2.z-14-1.z-13-2.z-8-3.dnssec.test.",	kDNSServiceType_A,		2 },
	{ "count-1.z-5-1.z-14-2.z-8-3.dnssec.test.",	kDNSServiceType_AAAA,	1 },
	{ "count-2.z-10-1.z-8-2.z-13-3.dnssec.test.",	kDNSServiceType_AAAA,	2 },
	{ "count-10.z-14-1.z-13-2.z-5-3.dnssec.test.",	kDNSServiceType_A,		10 }
};

//===========================================================================================================================

static void DNSSD_API
	_DNSSECTest_BasicValidationCallBack(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext );

//===========================================================================================================================

static Boolean
	_DNSSECTest_BasicValidationVerifyTheResponse(
		DNSSECTestContext *	inContext,
		DNSServiceFlags		inFlags,
		uint32_t			inInterfaceIndex,
		DNSServiceErrorType	inError,
		const char *		inFullName,
		uint16_t			inType,
		uint16_t			inClass,
		uint16_t			inRDataLen,
		const void *		inRDataPtr,
		uint32_t			inTTL,
		char *				inStrBuffer,
		uint32_t			inBufferLen,
		Boolean *			outExpectMore );

//===========================================================================================================================

static void DNSSECTest_BasicValidation( DNSSECTestContext *inContext )
{
	OSStatus		err;
	const char *	error_description = NULL;

	// Get the current subtest
	DNSSECTest_BasicValidationTestCase * testCases = &DNSSECTest_BasicValidationTestCases[ inContext->subtestIndex ];
	inContext->subtestQueryName = testCases->queryName;
	inContext->testCaseContext.basicValidation.number_of_answers = testCases->num_of_answers;

	// Issue the query.
	err = DNSServiceQueryRecord( &inContext->query,
		kDNSServiceFlagsEnableDNSSEC, 0, testCases->queryName, testCases->queryType, kDNSServiceClass_IN,
		_DNSSECTest_BasicValidationCallBack, inContext );
	require_noerr_action( err, exit, error_description = "DNSServiceQueryRecord failed" );

	// Set the dispatch queue.
	err = DNSServiceSetDispatchQueue( inContext->query,
		dispatch_get_main_queue() );
	require_noerr_action( err, exit, error_description = "DNSServiceSetDispatchQueue failed" );

	// Create a timer to handle timeout.
	err = DispatchTimerCreate( dispatch_time_seconds( kDNSSECTestQueryTimeoutSecs ), DISPATCH_TIME_FOREVER,
		UINT64_C_safe( kDNSSECTestQueryTimeoutSecs ) * kNanosecondsPerSecond / 10, NULL, _DNSSECTestQueryTimeoutHandler,
		NULL, inContext, &inContext->queryTimer );
	require_noerr_action( err, exit, error_description = "DispatchTimerCreate failed" );
	dispatch_resume( inContext->queryTimer );

	// start the current subtest, and record the start time.
	inContext->subtestStartTime	= NanoTimeGetCurrent();
	if( !inContext->testStarted )
	{
		// Only call dispatch_main once, when we first start the test.
		inContext->testStarted = true;
		dispatch_main();
	}

	return;
exit:
	_DNSSECTestQueryWriteSubtestReport( inContext, error_description );
	_DNSSECTestQueryOutputFinalReport( inContext );
	exit( 1 );
}

//===========================================================================================================================

static void DNSSD_API
	_DNSSECTest_BasicValidationCallBack(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext )
{
	char						err_desp_str[1024];
	Boolean						valid;
	Boolean						expectMore;
	DNSSECTestContext * const	context = (DNSSECTestContext *)inContext;

	Unused( inSDRef );

	err_desp_str[0] = '\0';

	// Verify if the response is expected
	valid = _DNSSECTest_BasicValidationVerifyTheResponse(context, inFlags, inInterfaceIndex, inError, inFullName, inType,
		inClass, inRDataLen, inRDataPtr, inTTL, err_desp_str, sizeof( err_desp_str ), &expectMore);

	// If more answers are expected to be returned.
	if( expectMore )
	{
		return;
	}

	// Cancel the current request and its corresponding timer.
	DNSServiceForget( &context->query );
	dispatch_source_forget( &context->queryTimer );

	// Check if the test fails.
	if( !valid )
	{
		context->subtestFailed = true;
	}
	_DNSSECTestQueryWriteSubtestReport( context, valid ? NULL : err_desp_str );

	// Increment the index.
	context->subtestIndex++;
	if( context->subtestIndex == countof( DNSSECTest_BasicValidationTestCases ) )
	{
		// All test cases have been run
		_DNSSECTestQueryOutputFinalReport( context );
		exit( context->subtestFailed ? 2 : 0 );
	}

	// Start the next subtest.
	DNSSECTest_BasicValidation( context );
}

//===========================================================================================================================

// Check if the response is expected. Return true if the response is expected, otherwise return false.
static Boolean
_DNSSECTest_BasicValidationVerifyTheResponse(
	DNSSECTestContext *	inContext,
	DNSServiceFlags		inFlags,
	uint32_t			inInterfaceIndex,
	DNSServiceErrorType	inError,
	const char *		inFullName,
	uint16_t			inType,
	uint16_t			inClass,
	uint16_t			inRDataLen,
	const void *		inRDataPtr,
	uint32_t			inTTL,
	char *				inStrBuffer,
	uint32_t			inBufferLen,
	Boolean *			outExpectMore )
{
	const char * const						queryName	= inContext->subtestQueryName;
	DNSSECTest_BasicValidationTestCase *	testCases 	= &DNSSECTest_BasicValidationTestCases[ inContext->subtestIndex ];
	DNSSECTest_BasicValidationContext *		subContext	= &inContext->testCaseContext.basicValidation;
	Boolean									expectMore	= false;

	Unused( inClass );
	Unused( inRDataLen );
	Unused( inRDataPtr );
	Unused( inTTL );

	require_noerr_action( inError, exit, SNPrintF( inStrBuffer, inBufferLen, "Unexpected DNSServiceErrorType: %d", inError ) );

	require_action( inFlags & kDNSServiceFlagsAdd, exit,
		SNPrintF( inStrBuffer, inBufferLen, "Unexpected add/remove event: %#{flags}", inFlags, kDNSServiceFlagsDescriptors )
	);

	require_action( inInterfaceIndex == kDNSServiceInterfaceIndexAny, exit,
		SNPrintF( inStrBuffer, inBufferLen, "Non-local-only interface is used: %u", inInterfaceIndex ) );

	require_action( strcmp( queryName, inFullName ) == 0, exit,
		SNPrintF( inStrBuffer, inBufferLen, "Mismatched name: %s", inFullName ) );

	require_action( inType == testCases->queryType, exit,
		SNPrintF( inStrBuffer, inBufferLen, "Mismatched query type: %s", RecordTypeToString( inType ) ) );

	require_action( (inFlags & kDNSServiceFlagsSecure) == kDNSServiceFlagsSecure, exit,
		SNPrintF( inStrBuffer, inBufferLen, "Unexpected DNSSEC result: %#{flags}", (inFlags & kDNSServiceFlagsSecure),
			kDNSServiceFlagsDescriptors )
	);

	if( --subContext->number_of_answers != 0 )
	{
		expectMore = true;
	}


exit:
	if( outExpectMore )
	{
		*outExpectMore = expectMore;
	}

	return inStrBuffer[0] != '\0' ? false : true;
}
#else
static void DNSSECTestCmd( void )
{
	exit( 0 );
}
#endif // #if ( ENABLE_DNSSDUTIL_DNSSEC_TEST == 1 )

//===========================================================================================================================
//	OptimisticDNSTestCommand
//===========================================================================================================================

typedef struct OptimisticDNSTest *		OptimisticDNSTestRef;
struct OptimisticDNSTest
{
	dispatch_queue_t			queue;			// Serial queue for test events.
	dispatch_semaphore_t		doneSem;		// Semaphore to signal when the test is done.
	dnssd_getaddrinfo_t			gai;			// For GAI operations.
	dispatch_source_t			timer;			// Timer for enforcing time limits.
	mdns_domain_name_t			domain;			// High-level domain for test's hostnames.
	char *						hostname;		// Hostname used in current GAI operation.
	CFPropertyListRef			savedAASDPref;	// The saved AlwaysAppendSearchDomains preference value. [1]
	pid_t						serverPID;		// PID of spawned test DNS server.
	int32_t						refCount;		// Test's reference count.
	OSStatus					error;			// Test's error code.
	sockaddr_ip					expectedAddr;	// The expected IP address from a GAI operation.
	Boolean						modifiedDaemon;	// True if the test needs to undo mDNSResponder modifications. [2]
	Boolean						fullTest;		// Run full test, even if mDNSResponder needs to be killed.
	uint8_t						expectedHostname[ kDomainNameLengthMax ];
};

// Notes:
// 1. The saved AlwaysAppendSearchDomains preference will be restored after the test is done testing with its own
//    AlwaysAppendSearchDomains value.
// 2. For example, the test might have set a preference and restarted mDNSResponder for the preference to take effect.
//    Cleanup would consist of restoring the preference and restarting mDNSResponder again.

static OptimisticDNSTestRef	_OptimisticDNSTestCreate( OSStatus *outError, Boolean inFullTest );
static OSStatus				_OptimisticDNSTestRun( OptimisticDNSTestRef inTest );
static void					_OptimisticDNSTestRelease( OptimisticDNSTestRef inTest );

static void	OptimisticDNSTestCommand( void )
{
	OSStatus err;
	OptimisticDNSTestRef test = NULL;
	const Boolean fullTest = ( gOptimisticDNSTest_FullTest != 0 );
	if( fullTest )
	{
		err = CheckRootUser();
		require_noerr_quiet( err, exit );
	}
	test = _OptimisticDNSTestCreate( &err, fullTest );
	require_noerr( err, exit );
	
	err = _OptimisticDNSTestRun( test );
	require_noerr( err, exit );
	
exit:
	if( test ) _OptimisticDNSTestRelease( test );
    gExitCode = err ? 1 : 0;
}

//===========================================================================================================================

static OptimisticDNSTestRef	_OptimisticDNSTestCreate( OSStatus * const outError, const Boolean inFullTest )
{
	OSStatus err;
	OptimisticDNSTestRef test = NULL;
	OptimisticDNSTestRef obj = (OptimisticDNSTestRef) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->refCount	= 1;
	obj->error		= kInProgressErr;
	obj->serverPID	= -1;
	obj->fullTest	= inFullTest;
	
	obj->queue = dispatch_queue_create( "com.apple.dnssdutil.optimistic-dns-test", DISPATCH_QUEUE_SERIAL );
	require_action( obj->queue, exit, err = kNoResourcesErr );
	
	obj->doneSem = dispatch_semaphore_create( 0 );
	require_action( obj->doneSem, exit, err = kNoResourcesErr );
	
	test = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( outError ) *outError = err;
	if( obj ) _OptimisticDNSTestRelease( obj );
	return( test );
}

//===========================================================================================================================

static void	_OptimisticDNSTestRetain( const OptimisticDNSTestRef me )
{
	atomic_add_32( &me->refCount, 1 );
}

//===========================================================================================================================

static void	_OptimisticDNSTestStop( const OptimisticDNSTestRef me, const OSStatus inError )
{
	me->error = inError;
	if( !me->error )
	{
		FPrintF( stdout, "%{du:time} Test PASSED\n", NULL );
	}
	else
	{
		FPrintF( stdout, "%{du:time} Test FAILED: %#m\n", NULL, me->error );
	}
	dnssd_getaddrinfo_forget( &me->gai );
	dispatch_source_forget( &me->timer );
	mdns_forget( &me->domain );
	ForgetMem( &me->hostname );
	if( me->serverPID >= 0 )
	{
		OSStatus		killErr;
		
		killErr = kill( me->serverPID, SIGTERM );
		killErr = map_global_noerr_errno( killErr );
		check_noerr( killErr );
		me->serverPID = -1;
	}
	if( me->modifiedDaemon )
	{
		const CFStringRef keyAASD = CFSTR( kMDNSResponderPrefStr_AlwaysAppendSearchDomains );
		FPrintF( stdout, "\n%{du:time} Cleanup:\n", NULL );
		FPrintF( stdout, "%{du:time} ⚒ Restoring %@ preference to '%@'\n", NULL, keyAASD, me->savedAASDPref );
		const CFStringRef appID = CFSTR( kMDNSResponderPrefAppIDStr );
		CFPreferencesSetValue( keyAASD, me->savedAASDPref, appID, kCFPreferencesAnyUser, kCFPreferencesCurrentHost );
		FPrintF( stdout, "%{du:time} ↻ Restarting mDNSResponder\n", NULL );
		const OSStatus killErr = systemf( NULL, "killall -KILL mDNSResponder" );
		if( killErr )
		{
			FPrintF( stdout, "%{du:time} x Error killing mDNSResponder: %#m\n", NULL, killErr );
		}
	}
	CFForget( &me->savedAASDPref );
	dispatch_semaphore_signal( me->doneSem );
}

//===========================================================================================================================

static dnssd_getaddrinfo_t
	_OptimisticDNSTestCreateGAI(
		const OptimisticDNSTestRef	me,
		const char * const			inHostname,
		const DNSServiceFlags		inFlags )
{
	const dnssd_getaddrinfo_t gai = dnssd_getaddrinfo_create();
	require_quiet( gai, exit );
	
	dnssd_getaddrinfo_set_hostname( gai, inHostname );
	dnssd_getaddrinfo_set_flags( gai, inFlags );
	dnssd_getaddrinfo_set_interface_index( gai, kDNSServiceInterfaceIndexAny );
	dnssd_getaddrinfo_set_protocols( gai, kDNSServiceProtocol_IPv4 );
	dnssd_getaddrinfo_set_queue( gai, me->queue );
	dnssd_retain( gai );
	_OptimisticDNSTestRetain( me );
	dnssd_getaddrinfo_set_event_handler( gai,
	^( const dnssd_event_t inEvent, const DNSServiceErrorType inGAIError )
	{
		switch( inEvent )
		{
			case dnssd_event_invalidated:
				dnssd_release( gai );
				_OptimisticDNSTestRelease( me );
				break;
			
			case dnssd_event_error:
				require_return( me->gai == gai );
				
				FPrintF( stdout, "dnssd_getaddrinfo error: %#m\n", inGAIError );
				_OptimisticDNSTestStop( me, inGAIError );
				break;
			
			case dnssd_event_remove_all:
				break;
		}
	} );
	
exit:
	return( gai );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestStartSubtest7_2( const OptimisticDNSTestRef me )
{
	OSStatus err;
	__block Boolean gotExpiredResult = false;
	__block Boolean gotAddResult = false;
	FPrintF( stdout,
		"%{du:time} Subtest 7.2: Second multi-label PQDN GAI for hostname that has one CNAME record and that doesn't"
		" depend on a search domain (CNAME record expired, AlwaysAppendSearchDomains enabled)\n",
		NULL );
	
	// Start the GAI operation.
	
	check( me->hostname );
	const unsigned int timeLimitSecs = 5;
	FPrintF( stdout, "%{du:time} * Starting GAI for multi-label PQDN '%s' with %u second time limit\n",
		NULL, me->hostname, timeLimitSecs );
	const DNSServiceFlags flags = kDNSServiceFlagsReturnIntermediates | kDNSServiceFlagsAllowExpiredAnswers;
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, flags );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		size_t unexpectedResultCount = 0;
		for( size_t i = 0; i < inCount; ++i )
		{
			Boolean resultIsExpected = false;
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			const dnssd_getaddrinfo_result_type_t resultType = dnssd_getaddrinfo_result_get_type( result );
			const char * const actualHostnameStr = dnssd_getaddrinfo_result_get_actual_hostname( result );
			const bool fromCache = dnssd_getaddrinfo_result_is_from_cache( result );
			if( SockAddrCompareAddr( dnssd_getaddrinfo_result_get_address( result ), &me->expectedAddr.sa ) == 0 )
			{
				// Since this is a GAI for a single-label instead of a FQDN, to make sure that appropriate search domain
				// was appended, check the actual hostname.
				
				uint8_t actualHostname[ kDomainNameLengthMax ];
				OSStatus nameErr = DomainNameFromString( actualHostname, actualHostnameStr, NULL );
				if( !nameErr && DomainNameEqual( actualHostname, me->expectedHostname ) )
				{
					if( !gotExpiredResult )
					{
						if( ( resultType == dnssd_getaddrinfo_result_type_expired ) && fromCache )
						{
							gotExpiredResult = true;
							resultIsExpected = true;
						}
					}
					else if ( !gotAddResult )
					{
						if( ( resultType == dnssd_getaddrinfo_result_type_add ) && !fromCache )
						{
							gotAddResult     = true;
							resultIsExpected = true;
						}
					}
				}
			}
			if( !resultIsExpected ) ++unexpectedResultCount;
			FPrintF( stdout,
				"%{du:time} %s Got %sexpected result -- type: %s, hostname: %s, address: %##a, actual hostname: %s, "
				"from cache: %s\n",
				NULL, resultIsExpected ? "✓" : "x", resultIsExpected ? "" : "un",
				dnssd_getaddrinfo_result_type_to_string( resultType ), dnssd_getaddrinfo_result_get_hostname( result ),
				dnssd_getaddrinfo_result_get_address( result ), actualHostnameStr, YesNoStr( fromCache ) );
		}
		if( unexpectedResultCount > 0 ) _OptimisticDNSTestStop( me, kUnexpectedErr );
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	// Start the time limit timer.
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		OSStatus finalErr;
		if( !gotExpiredResult || !gotAddResult )
		{
			if( !gotExpiredResult )
			{
				FPrintF( stdout, "%{du:time} Failed to get EXPIRED GAI result for '%s' after %u seconds.\n",
					NULL, me->hostname, timeLimitSecs );
			}
			if( !gotAddResult )
			{
				FPrintF( stdout, "%{du:time} Failed to get ADD GAI result for '%s' after %u seconds.\n",
					NULL, me->hostname, timeLimitSecs );
			}
			finalErr = kTimeoutErr;
		}
		else
		{
			finalErr = kNoErr;
		}
		_OptimisticDNSTestStop( me, finalErr );
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestStartSubtest7_1( const OptimisticDNSTestRef me )
{
	OSStatus err;
	__block Boolean gotAddResult = false;
	FPrintF( stdout,
		"%{du:time} Subtest 7.1: First multi-label PQDN GAI for hostname that has one CNAME record and that doesn't"
		" depend on a search domain (AlwaysAppendSearchDomains enabled)\n",
		NULL );
	
	// Create the hostname.
	
	const uint8_t offset = 70;
	char middleLabels[ 512 ];
	SNPrintF( middleLabels, sizeof( middleLabels ), "ttl-600.offset-%u", offset );
	ForgetMem( &me->hostname );
	const char * const domainStr = mdns_domain_name_get_presentation( me->domain );
	ASPrintF( &me->hostname, "alias-ttl-1.%s.%s", middleLabels, domainStr );
	require_action( me->hostname, exit, err = kNoMemoryErr );
	
	// Make sure to remove the trailing dot to make the hostname a PQDN.
	
	const size_t len = strlen( me->hostname );
	me->hostname[ len - 1 ] = '\0';
	
	// Set the expected IPv4 address.
	
	char ipv4AddrStr[ kSockAddrStringMaxSize ];
	SNPrintF( ipv4AddrStr, sizeof( ipv4AddrStr ), "203.0.113.%u", offset + 1 );
	err = StringToSockAddr( ipv4AddrStr, &me->expectedAddr, sizeof( me->expectedAddr ), NULL );
	require_noerr( err, exit );
	
	// Since this is a GAI for a PQDN instead of a FQDN, and it doesn't depend on a search domain, to make sure that no
	// search domain was appended, check the actual hostname, which should be a child of the domain that we used for the
	// test DNS server. Specifically, the actual hostname should be of the form ttl-600.offset-60.<domain>.
	//
	// Note: We don't want results from another of the DNS server's search domains, such as dnssec.test. The domain
	// specified with --domain should come first in the list of search domains.
	
	err = DomainNameFromString( me->expectedHostname, middleLabels, NULL );
	require_noerr( err, exit );
	
	err = DomainNameAppendString( me->expectedHostname, domainStr, NULL );
	require_noerr( err, exit );
	
	// Start the GAI operation.
	
	const unsigned int timeLimitSecs = 5;
	FPrintF( stdout,
		"%{du:time} * Starting GAI for multi-label PQDN '%s' with %u second time limit\n", NULL,
		me->hostname, timeLimitSecs );
	const DNSServiceFlags flags = kDNSServiceFlagsReturnIntermediates | kDNSServiceFlagsAllowExpiredAnswers;
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, flags );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		size_t unexpectedResultCount = 0;
		for( size_t i = 0; i < inCount; ++i )
		{
			Boolean resultIsExpected = false;
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			const dnssd_getaddrinfo_result_type_t resultType = dnssd_getaddrinfo_result_get_type( result );
			const char * const actualHostnameStr = dnssd_getaddrinfo_result_get_actual_hostname( result );
			const bool fromCache = dnssd_getaddrinfo_result_is_from_cache( result );
			if( SockAddrCompareAddr( dnssd_getaddrinfo_result_get_address( result ), &me->expectedAddr.sa ) == 0 )
			{
				// Since this is a GAI for a single-label instead of a FQDN, to make sure that appropriate search domain
				// was appended, check the actual hostname, which should be equal to the domain that we used for the
				// test DNS server: alias-ttl-1.<domain> → <domain> → 203.0.113.1
				//
				// Note: We don't want results from another of the DNS server's search domains, such as dnssec.test. The
				// domain specified with --domain should come first in the list of search domains.
				
				uint8_t actualHostname[ kDomainNameLengthMax ];
				OSStatus nameErr = DomainNameFromString( actualHostname, actualHostnameStr, NULL );
				if( !nameErr && DomainNameEqual( actualHostname, me->expectedHostname ) )
				{
					if( !gotAddResult )
					{
						if( ( resultType == dnssd_getaddrinfo_result_type_add ) && !fromCache )
						{
							gotAddResult     = true;
							resultIsExpected = true;
						}
					}
				}
			}
			if( !resultIsExpected ) ++unexpectedResultCount;
			FPrintF( stdout,
				"%{du:time} %s Got %sexpected result -- type: %s, hostname: %s, address: %##a, actual hostname: %s, "
				"from cache: %s\n",
				NULL, resultIsExpected ? "✓" : "x", resultIsExpected ? "" : "un",
				dnssd_getaddrinfo_result_type_to_string( resultType ), dnssd_getaddrinfo_result_get_hostname( result ),
				dnssd_getaddrinfo_result_get_address( result ), actualHostnameStr, YesNoStr( fromCache ) );
		}
		if( unexpectedResultCount > 0 ) _OptimisticDNSTestStop( me, kUnexpectedErr );
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	// Start the time limit timer.
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		if( !gotAddResult )
		{
			FPrintF( stdout, "%{du:time} Failed to get ADD GAI result for '%s' after %u seconds.\n",
				NULL, me->hostname, timeLimitSecs );
			_OptimisticDNSTestStop( me, kTimeoutErr );
		}
		else
		{
			const unsigned int waitSecs = 15;
			FPrintF( stdout, "%{du:time} ⧖ Waiting %u seconds for CNAME record to expire\n", NULL, waitSecs );
			usleep( waitSecs * kMicrosecondsPerSecond );
			const OSStatus startErr = _OptimisticDNSTestStartSubtest7_2( me );
			if( startErr ) _OptimisticDNSTestStop( me, startErr );
		}
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestStartSubtest6_2( const OptimisticDNSTestRef me )
{
	OSStatus err;
	__block Boolean gotExpiredResult = false;
	__block Boolean gotAddResult = false;
	FPrintF( stdout,
		"%{du:time} Subtest 6.2: Second multi-label PQDN GAI for hostname that has one CNAME record and that depends on a"
		" search domain (CNAME record expired, AlwaysAppendSearchDomains enabled)\n",
		NULL );
	
	// Start the GAI operation.
	
	check( me->hostname );
	const unsigned int timeLimitSecs = 5;
	FPrintF( stdout, "%{du:time} * Starting GAI for multi-label PQDN '%s' with %u second time limit\n",
		NULL, me->hostname, timeLimitSecs );
	const DNSServiceFlags flags = kDNSServiceFlagsReturnIntermediates | kDNSServiceFlagsAllowExpiredAnswers;
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, flags );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		size_t unexpectedResultCount = 0;
		for( size_t i = 0; i < inCount; ++i )
		{
			Boolean resultIsExpected = false;
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			const dnssd_getaddrinfo_result_type_t resultType = dnssd_getaddrinfo_result_get_type( result );
			const char * const actualHostnameStr = dnssd_getaddrinfo_result_get_actual_hostname( result );
			const bool fromCache = dnssd_getaddrinfo_result_is_from_cache( result );
			if( SockAddrCompareAddr( dnssd_getaddrinfo_result_get_address( result ), &me->expectedAddr.sa ) == 0 )
			{
				uint8_t actualHostname[ kDomainNameLengthMax ];
				OSStatus nameErr = DomainNameFromString( actualHostname, actualHostnameStr, NULL );
				if( !nameErr && DomainNameEqual( actualHostname, me->expectedHostname ) )
				{
					if( !gotExpiredResult )
					{
						if( ( resultType == dnssd_getaddrinfo_result_type_expired ) && fromCache )
						{
							gotExpiredResult = true;
							resultIsExpected = true;
						}
					}
					else if ( !gotAddResult )
					{
						if( ( resultType == dnssd_getaddrinfo_result_type_add ) && !fromCache )
						{
							gotAddResult     = true;
							resultIsExpected = true;
						}
					}
				}
			}
			if( !resultIsExpected ) ++unexpectedResultCount;
			FPrintF( stdout,
				"%{du:time} %s Got %sexpected result -- type: %s, hostname: %s, address: %##a, actual hostname: %s, "
				"from cache: %s\n",
				NULL, resultIsExpected ? "✓" : "x", resultIsExpected ? "" : "un",
				dnssd_getaddrinfo_result_type_to_string( resultType ), dnssd_getaddrinfo_result_get_hostname( result ),
				dnssd_getaddrinfo_result_get_address( result ), actualHostnameStr, YesNoStr( fromCache ) );
		}
		if( unexpectedResultCount > 0 ) _OptimisticDNSTestStop( me, kUnexpectedErr );
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	// Start the time limit timer.
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		if( !gotExpiredResult || !gotAddResult )
		{
			if( !gotExpiredResult )
			{
				FPrintF( stdout, "%{du:time} Failed to get EXPIRED GAI result for '%s' after %u seconds.\n",
					NULL, me->hostname, timeLimitSecs );
			}
			if( !gotAddResult )
			{
				FPrintF( stdout, "%{du:time} Failed to get ADD GAI result for '%s' after %u seconds.\n",
					NULL, me->hostname, timeLimitSecs );
			}
			_OptimisticDNSTestStop( me, kTimeoutErr );
		}
		else
		{
			const OSStatus startErr = _OptimisticDNSTestStartSubtest7_1( me );
			if( startErr ) _OptimisticDNSTestStop( me, startErr );
		}
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestStartSubtest6_1( const OptimisticDNSTestRef me )
{
	OSStatus err;
	__block Boolean gotAddResult = false;
	
	// Check if the test should proceed with subtests that require that mDNSResponder be killed.
	
	if( !me->fullTest )
	{
		FPrintF( stdout, "%{du:time} ⚠ Not proceeding with subtests that require mDNSResponder to be killed\n", NULL );
		_OptimisticDNSTestRetain( me );
		dispatch_async( me->queue,
		^{
			_OptimisticDNSTestStop( me, kNoErr );
			_OptimisticDNSTestRelease( me );
		} );
		err = kNoErr;
		goto exit;
	}
	FPrintF( stdout,
		"%{du:time} Subtest 6.1: First multi-label PQDN GAI for hostname that has one CNAME record and that depends on a"
		" search domain (AlwaysAppendSearchDomains enabled)\n",
		NULL );
	
	// Set AlwaysAppendSearchDomains to true.
	
	const CFStringRef appID = CFSTR( kMDNSResponderPrefAppIDStr );
	const CFStringRef keyAASD = CFSTR( kMDNSResponderPrefStr_AlwaysAppendSearchDomains );
	const CFPropertyListRef valueAASD = kCFBooleanTrue;
	me->savedAASDPref = CFPreferencesCopyValue( keyAASD, appID, kCFPreferencesAnyUser, kCFPreferencesCurrentHost );
	FPrintF( stdout, "%{du:time} ⚒ Changing %@ preference: '%@' → '%@'\n", NULL, keyAASD, me->savedAASDPref, valueAASD );
	CFPreferencesSetValue( keyAASD, valueAASD, appID, kCFPreferencesAnyUser, kCFPreferencesCurrentHost );
	me->modifiedDaemon = true;
	
	// Restart mDNSResponder.
	
	FPrintF( stdout, "%{du:time} ↻ Restarting mDNSResponder\n", NULL );
	err = systemf( NULL, "killall -KILL mDNSResponder" );
	require_noerr( err, exit );
	
	// Give mDNSResponder some time to respawn.
	
	const unsigned int delaySecs = 15;
	FPrintF( stdout, "%{du:time} ⧖ Waiting %u seconds to allow mDNSResponder to respawn\n", NULL, delaySecs );
	usleep( delaySecs * kMicrosecondsPerSecond );
	
	// Create the hostname.
	
	const uint8_t offset = 60;
	char offsetLabel[ 512 ];
	SNPrintF( offsetLabel, sizeof( offsetLabel ), "offset-%u", offset );
	ForgetMem( &me->hostname );
	ASPrintF( &me->hostname, "alias-ttl-1.%s", offsetLabel );
	require_action( me->hostname, exit, err = kNoMemoryErr );
	
	// Set the expected IPv4 address.
	
	char ipv4AddrStr[ kSockAddrStringMaxSize ];
	SNPrintF( ipv4AddrStr, sizeof( ipv4AddrStr ), "203.0.113.%u", offset + 1 );
	err = StringToSockAddr( ipv4AddrStr, &me->expectedAddr, sizeof( me->expectedAddr ), NULL );
	require_noerr( err, exit );
	
	// Since this is a GAI for a PQDN instead of a FQDN, and it depends on a search domain, to make sure that
	// the appropriate search domain was appended, check the actual hostname, which should be a child of the domain
	// that we used for the test DNS server. Specifically, the actual hostname should be of the form offset-50.<domain>.
	//
	// Note: We don't want results from another of the DNS server's search domains, such as dnssec.test. The domain
	// specified with --domain should come first in the list of search domains.
	
	err = DomainNameFromString( me->expectedHostname, offsetLabel, NULL );
	require_noerr( err, exit );
	
	err = DomainNameAppendString( me->expectedHostname, mdns_domain_name_get_presentation( me->domain ), NULL );
	require_noerr( err, exit );
	
	// Start the GAI operation.
	
	const unsigned int timeLimitSecs = 5;
	FPrintF( stdout,
		"%{du:time} * Starting GAI for multi-label PQDN '%s' with %u second time limit\n", NULL,
		me->hostname, timeLimitSecs );
	const DNSServiceFlags flags = kDNSServiceFlagsReturnIntermediates | kDNSServiceFlagsAllowExpiredAnswers;
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, flags );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		size_t unexpectedResultCount = 0;
		for( size_t i = 0; i < inCount; ++i )
		{
			Boolean resultIsExpected = false;
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			const dnssd_getaddrinfo_result_type_t resultType = dnssd_getaddrinfo_result_get_type( result );
			const char * const actualHostnameStr = dnssd_getaddrinfo_result_get_actual_hostname( result );
			const bool fromCache = dnssd_getaddrinfo_result_is_from_cache( result );
			if( SockAddrCompareAddr( dnssd_getaddrinfo_result_get_address( result ), &me->expectedAddr.sa ) == 0 )
			{
				uint8_t actualHostname[ kDomainNameLengthMax ];
				OSStatus nameErr = DomainNameFromString( actualHostname, actualHostnameStr, NULL );
				if( !nameErr && DomainNameEqual( actualHostname, me->expectedHostname ) )
				{
					if( !gotAddResult )
					{
						if( ( resultType == dnssd_getaddrinfo_result_type_add ) && !fromCache )
						{
							gotAddResult     = true;
							resultIsExpected = true;
						}
					}
				}
			}
			if( !resultIsExpected ) ++unexpectedResultCount;
			FPrintF( stdout,
				"%{du:time} %s Got %sexpected result -- type: %s, hostname: %s, address: %##a, actual hostname: %s, "
				"from cache: %s\n",
				NULL, resultIsExpected ? "✓" : "x", resultIsExpected ? "" : "un",
				dnssd_getaddrinfo_result_type_to_string( resultType ), dnssd_getaddrinfo_result_get_hostname( result ),
				dnssd_getaddrinfo_result_get_address( result ), actualHostnameStr, YesNoStr( fromCache ) );
		}
		if( unexpectedResultCount > 0 ) _OptimisticDNSTestStop( me, kUnexpectedErr );
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	// Start the time limit timer.
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		if( !gotAddResult )
		{
			FPrintF( stdout, "%{du:time} Failed to get ADD GAI result for '%s' after %u seconds.\n",
				NULL, me->hostname, timeLimitSecs );
			_OptimisticDNSTestStop( me, kTimeoutErr );
		}
		else
		{
			const unsigned int waitSecs = 15;
			FPrintF( stdout, "%{du:time} ⧖ Waiting %u seconds for CNAME record to expire\n", NULL, waitSecs );
			usleep( waitSecs * kMicrosecondsPerSecond );
			const OSStatus startErr = _OptimisticDNSTestStartSubtest6_2( me );
			if( startErr ) _OptimisticDNSTestStop( me, startErr );
		}
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestStartSubtest5_2( const OptimisticDNSTestRef me )
{
	OSStatus err;
	__block Boolean gotExpiredResult = false;
	__block Boolean gotAddResult = false;
	FPrintF( stdout,
		"%{du:time} Subtest 5.2: Second single-label GAI for hostname with CNAME record"
		" (appends search domain, CNAME record expired)\n",
		NULL );
	check( me->hostname );
	const unsigned int timeLimitSecs = 5;
	FPrintF( stdout, "%{du:time} * Starting GAI for single-label '%s' with %u second time limit\n",
		NULL, me->hostname, timeLimitSecs );
	const DNSServiceFlags flags = kDNSServiceFlagsReturnIntermediates | kDNSServiceFlagsAllowExpiredAnswers;
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, flags );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		size_t unexpectedResultCount = 0;
		for( size_t i = 0; i < inCount; ++i )
		{
			Boolean resultIsExpected = false;
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			const dnssd_getaddrinfo_result_type_t resultType = dnssd_getaddrinfo_result_get_type( result );
			const char * const actualHostnameStr = dnssd_getaddrinfo_result_get_actual_hostname( result );
			const bool fromCache = dnssd_getaddrinfo_result_is_from_cache( result );
			if( SockAddrCompareAddr( dnssd_getaddrinfo_result_get_address( result ), &me->expectedAddr.sa ) == 0 )
			{
				uint8_t actualHostname[ kDomainNameLengthMax ];
				OSStatus nameErr = DomainNameFromString( actualHostname, actualHostnameStr, NULL );
				if( !nameErr && DomainNameEqual( actualHostname, me->expectedHostname ) )
				{
					if( !gotExpiredResult )
					{
						if( ( resultType == dnssd_getaddrinfo_result_type_expired ) && fromCache )
						{
							gotExpiredResult = true;
							resultIsExpected = true;
						}
					}
					else if ( !gotAddResult )
					{
						if( ( resultType == dnssd_getaddrinfo_result_type_add ) && !fromCache )
						{
							gotAddResult     = true;
							resultIsExpected = true;
						}
					}
				}
			}
			if( !resultIsExpected ) ++unexpectedResultCount;
			FPrintF( stdout,
				"%{du:time} %s Got %sexpected result -- type: %s, hostname: %s, address: %##a, actual hostname: %s, "
				"from cache: %s\n",
				NULL, resultIsExpected ? "✓" : "x", resultIsExpected ? "" : "un",
				dnssd_getaddrinfo_result_type_to_string( resultType ), dnssd_getaddrinfo_result_get_hostname( result ),
				dnssd_getaddrinfo_result_get_address( result ), actualHostnameStr, YesNoStr( fromCache ) );
		}
		if( unexpectedResultCount > 0 ) _OptimisticDNSTestStop( me, kUnexpectedErr );
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		if( !gotExpiredResult || !gotAddResult )
		{
			if( !gotExpiredResult )
			{
				FPrintF( stdout, "%{du:time} Failed to get EXPIRED GAI result for '%s' after %u seconds.\n",
					NULL, me->hostname, timeLimitSecs );
			}
			if( !gotAddResult )
			{
				FPrintF( stdout, "%{du:time} Failed to get ADD GAI result for '%s' after %u seconds.\n",
					NULL, me->hostname, timeLimitSecs );
			}
			_OptimisticDNSTestStop( me, kTimeoutErr );
		}
		else
		{
			const OSStatus startErr = _OptimisticDNSTestStartSubtest6_1( me );
			if( startErr ) _OptimisticDNSTestStop( me, startErr );
		}
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestStartSubtest5_1( const OptimisticDNSTestRef me )
{
	OSStatus err;
	__block Boolean gotAddResult = false;
	FPrintF( stdout,
		"%{du:time} Subtest 5.1: First single-label GAI for hostname with CNAME record (appends search domain)\n",
		NULL );
	ForgetMem( &me->hostname );
	ASPrintF( &me->hostname, "%s", "alias-ttl-1" );
	require_action( me->hostname, exit, err = kNoMemoryErr );
	
	// Since this is a GAI for a single-label PQDN instead of a FQDN, and it depends on a search domain, to make sure
	// that the appropriate search domain was appended, check the actual hostname, which should be a child of the domain
	// that we used for the test DNS server. Specifically, the actual hostname should be <domain>.
	//
	// Note: We don't want results from another of the DNS server's search domains, such as dnssec.test. The domain
	// specified with --domain should come first in the list of search domains.
	
	err = DomainNameFromString( me->expectedHostname, mdns_domain_name_get_presentation( me->domain ), NULL );
	require_noerr( err, exit );
	
	// Set the expected IPv4 address.
	
	err = StringToSockAddr( "203.0.113.1", &me->expectedAddr, sizeof( me->expectedAddr ), NULL );
	require_noerr( err, exit );
	
	const unsigned int timeLimitSecs = 5;
	FPrintF( stdout,
		"%{du:time} * Starting GAI for single-label '%s' with %u second time limit\n", NULL, me->hostname, timeLimitSecs );
	const DNSServiceFlags flags = kDNSServiceFlagsReturnIntermediates | kDNSServiceFlagsAllowExpiredAnswers;
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, flags );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		size_t unexpectedResultCount = 0;
		for( size_t i = 0; i < inCount; ++i )
		{
			Boolean resultIsExpected = false;
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			const dnssd_getaddrinfo_result_type_t resultType = dnssd_getaddrinfo_result_get_type( result );
			const char * const actualHostnameStr = dnssd_getaddrinfo_result_get_actual_hostname( result );
			const bool fromCache = dnssd_getaddrinfo_result_is_from_cache( result );
			if( SockAddrCompareAddr( dnssd_getaddrinfo_result_get_address( result ), &me->expectedAddr.sa ) == 0 )
			{
				uint8_t actualHostname[ kDomainNameLengthMax ];
				OSStatus nameErr = DomainNameFromString( actualHostname, actualHostnameStr, NULL );
				if( !nameErr && DomainNameEqual( actualHostname, me->expectedHostname ) )
				{
					if( !gotAddResult )
					{
						if( ( resultType == dnssd_getaddrinfo_result_type_add ) && !fromCache )
						{
							gotAddResult     = true;
							resultIsExpected = true;
						}
					}
				}
			}
			if( !resultIsExpected ) ++unexpectedResultCount;
			FPrintF( stdout,
				"%{du:time} %s Got %sexpected result -- type: %s, hostname: %s, address: %##a, actual hostname: %s, "
				"from cache: %s\n",
				NULL, resultIsExpected ? "✓" : "x", resultIsExpected ? "" : "un",
				dnssd_getaddrinfo_result_type_to_string( resultType ), dnssd_getaddrinfo_result_get_hostname( result ),
				dnssd_getaddrinfo_result_get_address( result ), actualHostnameStr, YesNoStr( fromCache ) );
		}
		if( unexpectedResultCount > 0 ) _OptimisticDNSTestStop( me, kUnexpectedErr );
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		if( !gotAddResult )
		{
			FPrintF( stdout, "%{du:time} Failed to get ADD GAI result for '%s' after %u seconds.\n",
				NULL, me->hostname, timeLimitSecs );
			_OptimisticDNSTestStop( me, kTimeoutErr );
		}
		else
		{
			const unsigned int waitSecs = 15;
			FPrintF( stdout, "%{du:time} ⧖ Waiting %u seconds for CNAME record to expire\n", NULL, waitSecs );
			usleep( waitSecs * kMicrosecondsPerSecond );
			const OSStatus startErr = _OptimisticDNSTestStartSubtest5_2( me );
			if( startErr ) _OptimisticDNSTestStop( me, startErr );
		}
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestStartSubtest4_2( const OptimisticDNSTestRef me )
{
	OSStatus err;
	__block Boolean gotExpiredResult = false;
	__block Boolean gotAddResult = false;
	FPrintF( stdout, "%{du:time} Subtest 4.2: Second GAI for hostname with one CNAME record (A record expired)\n", NULL );
	check( me->hostname );
	const unsigned int timeLimitSecs = 5;
	FPrintF( stdout, "%{du:time} * Starting GAI for '%s' with %u second time limit\n", NULL, me->hostname, timeLimitSecs );
	const DNSServiceFlags flags = kDNSServiceFlagsReturnIntermediates | kDNSServiceFlagsAllowExpiredAnswers;
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, flags );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		size_t unexpectedResultCount = 0;
		for( size_t i = 0; i < inCount; ++i )
		{
			Boolean resultIsExpected = false;
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			const dnssd_getaddrinfo_result_type_t resultType = dnssd_getaddrinfo_result_get_type( result );
			const bool fromCache = dnssd_getaddrinfo_result_is_from_cache( result );
			if( SockAddrCompareAddr( dnssd_getaddrinfo_result_get_address( result ), &me->expectedAddr.sa ) == 0 )
			{
				if( !gotExpiredResult )
				{
					if( ( resultType == dnssd_getaddrinfo_result_type_expired ) && fromCache )
					{
						gotExpiredResult = true;
						resultIsExpected = true;
					}
				}
				else if ( !gotAddResult )
				{
					if( ( resultType == dnssd_getaddrinfo_result_type_add ) && !fromCache )
					{
						gotAddResult     = true;
						resultIsExpected = true;
					}
				}
			}
			if( !resultIsExpected ) ++unexpectedResultCount;
			FPrintF( stdout,
				"%{du:time} %s Got %sexpected result -- type: %s, hostname: %s, address: %##a, from cache: %s\n",
				NULL, resultIsExpected ? "✓" : "x", resultIsExpected ? "" : "un",
				dnssd_getaddrinfo_result_type_to_string( resultType ), dnssd_getaddrinfo_result_get_hostname( result ),
				dnssd_getaddrinfo_result_get_address( result ), YesNoStr( fromCache ) );
		}
		if( unexpectedResultCount > 0 ) _OptimisticDNSTestStop( me, kUnexpectedErr );
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		if( !gotExpiredResult || !gotAddResult )
		{
			if( !gotExpiredResult )
			{
				FPrintF( stdout, "%{du:time} Failed to get EXPIRED GAI result for '%s' after %u seconds.\n",
					NULL, me->hostname, timeLimitSecs );
			}
			if( !gotAddResult )
			{
				FPrintF( stdout, "%{du:time} Failed to get ADD GAI result for '%s' after %u seconds.\n",
					NULL, me->hostname, timeLimitSecs );
			}
			_OptimisticDNSTestStop( me, kTimeoutErr );
		}
		else
		{
			const OSStatus startErr = _OptimisticDNSTestStartSubtest5_1( me );
			if( startErr ) _OptimisticDNSTestStop( me, startErr );
		}
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestStartSubtest4_1( const OptimisticDNSTestRef me )
{
	OSStatus err;
	__block Boolean gotAddResult = false;
	FPrintF( stdout, "%{du:time} Subtest 4.1: First GAI for hostname with one CNAME record\n", NULL );
	
	// Create the hostname.
	
	const uint8_t offset = 40;
	ForgetMem( &me->hostname );
	ASPrintF( &me->hostname,
		"alias-ttl-600.tag-address-record-expires-first.ttl-1.offset-%u.%s",
		offset, mdns_domain_name_get_presentation( me->domain ) );
	require_action( me->hostname, exit, err = kNoMemoryErr );
	
	// Set the expected IPv4 address.
	
	char ipv4AddrStr[ kSockAddrStringMaxSize ];
	SNPrintF( ipv4AddrStr, sizeof( ipv4AddrStr ), "203.0.113.%u", offset + 1 );
	err = StringToSockAddr( ipv4AddrStr, &me->expectedAddr, sizeof( me->expectedAddr ), NULL );
	require_noerr( err, exit );
	
	const unsigned int timeLimitSecs = 5;
	FPrintF( stdout, "%{du:time} * Starting GAI for '%s' with %u second time limit\n", NULL, me->hostname, timeLimitSecs );
	const DNSServiceFlags flags = kDNSServiceFlagsReturnIntermediates | kDNSServiceFlagsAllowExpiredAnswers;
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, flags );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		size_t unexpectedResultCount = 0;
		for( size_t i = 0; i < inCount; ++i )
		{
			Boolean resultIsExpected = false;
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			const dnssd_getaddrinfo_result_type_t resultType = dnssd_getaddrinfo_result_get_type( result );
			const bool fromCache = dnssd_getaddrinfo_result_is_from_cache( result );
			if( SockAddrCompareAddr( dnssd_getaddrinfo_result_get_address( result ), &me->expectedAddr.sa ) == 0 )
			{
				if( !gotAddResult )
				{
					if( ( resultType == dnssd_getaddrinfo_result_type_add ) && !fromCache )
					{
						gotAddResult     = true;
						resultIsExpected = true;
					}
				}
			}
			if( !resultIsExpected ) ++unexpectedResultCount;
			FPrintF( stdout,
				"%{du:time} %s Got %sexpected result -- type: %s, hostname: %s, address: %##a, from cache: %s\n",
				NULL, resultIsExpected ? "✓" : "x", resultIsExpected ? "" : "un",
				dnssd_getaddrinfo_result_type_to_string( resultType ), dnssd_getaddrinfo_result_get_hostname( result ),
				dnssd_getaddrinfo_result_get_address( result ), YesNoStr( fromCache ) );
		}
		if( unexpectedResultCount > 0 ) _OptimisticDNSTestStop( me, kUnexpectedErr );
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		if( !gotAddResult )
		{
			FPrintF( stdout, "%{du:time} Failed to get ADD GAI result for '%s' after %u seconds.\n",
				NULL, me->hostname, timeLimitSecs );
			_OptimisticDNSTestStop( me, kTimeoutErr );
		}
		else
		{
			const unsigned int waitSecs = 15;
			FPrintF( stdout, "%{du:time} ⧖ Waiting %u seconds for A record to expire\n", NULL, waitSecs );
			usleep( waitSecs * kMicrosecondsPerSecond );
			const OSStatus startErr = _OptimisticDNSTestStartSubtest4_2( me );
			if( startErr ) _OptimisticDNSTestStop( me, startErr );
		}
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestStartSubtest3_2( const OptimisticDNSTestRef me )
{
	OSStatus err;
	__block Boolean gotExpiredResult = false;
	__block Boolean gotAddResult = false;
	FPrintF( stdout,
		"%{du:time} Subtest 3.2: Second GAI for hostname with two CNAME records (2nd CNAME record expired)\n", NULL );
	check( me->hostname );
	const unsigned int timeLimitSecs = 5;
	FPrintF( stdout, "%{du:time} * Starting GAI for '%s' with %u second time limit\n", NULL, me->hostname, timeLimitSecs );
	const DNSServiceFlags flags = kDNSServiceFlagsReturnIntermediates | kDNSServiceFlagsAllowExpiredAnswers;
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, flags );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		size_t unexpectedResultCount = 0;
		for( size_t i = 0; i < inCount; ++i )
		{
			Boolean resultIsExpected = false;
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			const dnssd_getaddrinfo_result_type_t resultType = dnssd_getaddrinfo_result_get_type( result );
			const bool fromCache = dnssd_getaddrinfo_result_is_from_cache( result );
			if( SockAddrCompareAddr( dnssd_getaddrinfo_result_get_address( result ), &me->expectedAddr.sa ) == 0 )
			{
				if( !gotExpiredResult )
				{
					if( ( resultType == dnssd_getaddrinfo_result_type_expired ) && fromCache )
					{
						gotExpiredResult = true;
						resultIsExpected = true;
					}
				}
				else if ( !gotAddResult )
				{
					if( ( resultType == dnssd_getaddrinfo_result_type_add ) && !fromCache )
					{
						gotAddResult     = true;
						resultIsExpected = true;
					}
				}
			}
			if( !resultIsExpected ) ++unexpectedResultCount;
			FPrintF( stdout,
				"%{du:time} %s Got %sexpected result -- type: %s, hostname: %s, address: %##a, from cache: %s\n",
				NULL, resultIsExpected ? "✓" : "x", resultIsExpected ? "" : "un",
				dnssd_getaddrinfo_result_type_to_string( resultType ), dnssd_getaddrinfo_result_get_hostname( result ),
				dnssd_getaddrinfo_result_get_address( result ), YesNoStr( fromCache ) );
		}
		if( unexpectedResultCount > 0 ) _OptimisticDNSTestStop( me, kUnexpectedErr );
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		if( !gotExpiredResult || !gotAddResult )
		{
			if( !gotExpiredResult )
			{
				FPrintF( stdout, "%{du:time} Failed to get EXPIRED GAI result for '%s' after %u seconds.\n",
					NULL, me->hostname, timeLimitSecs );
			}
			if( !gotAddResult )
			{
				FPrintF( stdout, "%{du:time} Failed to get ADD GAI result for '%s' after %u seconds.\n",
					NULL, me->hostname, timeLimitSecs );
			}
			_OptimisticDNSTestStop( me, kTimeoutErr );
		}
		else
		{
			const OSStatus startErr = _OptimisticDNSTestStartSubtest4_1( me );
			if( startErr ) _OptimisticDNSTestStop( me, startErr );
		}
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestStartSubtest3_1( const OptimisticDNSTestRef me )
{
	OSStatus err;
	__block Boolean gotAddResult = false;
	FPrintF( stdout, "%{du:time} Subtest 3.1: First GAI for hostname with two CNAME records\n", NULL );
	
	// Create the hostname.
	
	const uint8_t offset = 30;
	ForgetMem( &me->hostname );
	ASPrintF( &me->hostname,
		"alias-ttl-600-1.tag-second-cname-expires-first.ttl-600.offset-%u.%s",
		offset, mdns_domain_name_get_presentation( me->domain ) );
	require_action( me->hostname, exit, err = kNoMemoryErr );
	
	// Set the expected IPv4 address.
	
	char ipv4AddrStr[ kSockAddrStringMaxSize ];
	SNPrintF( ipv4AddrStr, sizeof( ipv4AddrStr ), "203.0.113.%u", offset + 1 );
	err = StringToSockAddr( ipv4AddrStr, &me->expectedAddr, sizeof( me->expectedAddr ), NULL );
	require_noerr( err, exit );
	
	const unsigned int timeLimitSecs = 5;
	FPrintF( stdout, "%{du:time} * Starting GAI for '%s' with %u second time limit\n", NULL, me->hostname, timeLimitSecs );
	const DNSServiceFlags flags = kDNSServiceFlagsReturnIntermediates | kDNSServiceFlagsAllowExpiredAnswers;
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, flags );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		size_t unexpectedResultCount = 0;
		for( size_t i = 0; i < inCount; ++i )
		{
			Boolean resultIsExpected = false;
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			const dnssd_getaddrinfo_result_type_t resultType = dnssd_getaddrinfo_result_get_type( result );
			const bool fromCache = dnssd_getaddrinfo_result_is_from_cache( result );
			if( SockAddrCompareAddr( dnssd_getaddrinfo_result_get_address( result ), &me->expectedAddr.sa ) == 0 )
			{
				if( !gotAddResult )
				{
					if( ( resultType == dnssd_getaddrinfo_result_type_add ) && !fromCache )
					{
						gotAddResult     = true;
						resultIsExpected = true;
					}
				}
			}
			if( !resultIsExpected ) ++unexpectedResultCount;
			FPrintF( stdout,
				"%{du:time} %s Got %sexpected result -- type: %s, hostname: %s, address: %##a, from cache: %s\n",
				NULL, resultIsExpected ? "✓" : "x", resultIsExpected ? "" : "un",
				dnssd_getaddrinfo_result_type_to_string( resultType ), dnssd_getaddrinfo_result_get_hostname( result ),
				dnssd_getaddrinfo_result_get_address( result ), YesNoStr( fromCache ) );
		}
		if( unexpectedResultCount > 0 ) _OptimisticDNSTestStop( me, kUnexpectedErr );
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		if( !gotAddResult )
		{
			FPrintF( stdout, "%{du:time} Failed to get ADD GAI result for '%s' after %u seconds.\n",
				NULL, me->hostname, timeLimitSecs );
			_OptimisticDNSTestStop( me, kTimeoutErr );
		}
		else
		{
			const unsigned int waitSecs = 15;
			FPrintF( stdout, "%{du:time} ⧖ Waiting %u seconds for CNAME record to expire\n", NULL, waitSecs );
			usleep( waitSecs * kMicrosecondsPerSecond );
			const OSStatus startErr = _OptimisticDNSTestStartSubtest3_2( me );
			if( startErr ) _OptimisticDNSTestStop( me, startErr );
		}
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestStartSubtest2_2( const OptimisticDNSTestRef me )
{
	OSStatus err;
	__block Boolean gotExpiredResult = false;
	__block Boolean gotAddResult = false;
	FPrintF( stdout,
		"%{du:time} Subtest 2.2: Second GAI for hostname with one CNAME record (CNAME record expired)\n", NULL );
	check( me->hostname );
	const unsigned int timeLimitSecs = 5;
	FPrintF( stdout, "%{du:time} * Starting GAI for '%s' with %u second time limit\n", NULL, me->hostname, timeLimitSecs );
	const DNSServiceFlags flags = kDNSServiceFlagsReturnIntermediates | kDNSServiceFlagsAllowExpiredAnswers;
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, flags );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		size_t unexpectedResultCount = 0;
		for( size_t i = 0; i < inCount; ++i )
		{
			Boolean resultIsExpected = false;
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			const dnssd_getaddrinfo_result_type_t resultType = dnssd_getaddrinfo_result_get_type( result );
			const bool fromCache = dnssd_getaddrinfo_result_is_from_cache( result );
			if( SockAddrCompareAddr( dnssd_getaddrinfo_result_get_address( result ), &me->expectedAddr.sa ) == 0 )
			{
				if( !gotExpiredResult )
				{
					if( ( resultType == dnssd_getaddrinfo_result_type_expired ) && fromCache )
					{
						gotExpiredResult = true;
						resultIsExpected = true;
					}
				}
				else if ( !gotAddResult )
				{
					if( ( resultType == dnssd_getaddrinfo_result_type_add ) && !fromCache )
					{
						gotAddResult     = true;
						resultIsExpected = true;
					}
				}
			}
			if( !resultIsExpected ) ++unexpectedResultCount;
			FPrintF( stdout,
				"%{du:time} %s Got %sexpected result -- type: %s, hostname: %s, address: %##a, from cache: %s\n",
				NULL, resultIsExpected ? "✓" : "x", resultIsExpected ? "" : "un",
				dnssd_getaddrinfo_result_type_to_string( resultType ), dnssd_getaddrinfo_result_get_hostname( result ),
				dnssd_getaddrinfo_result_get_address( result ), YesNoStr( fromCache ) );
		}
		if( unexpectedResultCount > 0 ) _OptimisticDNSTestStop( me, kUnexpectedErr );
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		if( !gotExpiredResult || !gotAddResult )
		{
			if( !gotExpiredResult )
			{
				FPrintF( stdout, "%{du:time} Failed to get EXPIRED GAI result for '%s' after %u seconds.\n",
					NULL, me->hostname, timeLimitSecs );
			}
			if( !gotAddResult )
			{
				FPrintF( stdout, "%{du:time} Failed to get ADD GAI result for '%s' after %u seconds.\n",
					NULL, me->hostname, timeLimitSecs );
			}
			_OptimisticDNSTestStop( me, kTimeoutErr );
		}
		else
		{
			const OSStatus startErr = _OptimisticDNSTestStartSubtest3_1( me );
			if( startErr ) _OptimisticDNSTestStop( me, startErr );
		}
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestStartSubtest2_1( const OptimisticDNSTestRef me )
{
	OSStatus err;
	__block Boolean gotAddResult = false;
	FPrintF( stdout, "%{du:time} Subtest 2.1: First GAI for hostname with one CNAME record\n", NULL );
	
	// Create the hostname.
	
	const uint8_t offset = 20;
	ForgetMem( &me->hostname );
	ASPrintF( &me->hostname,
		"alias-ttl-1.tag-cname-expires-first.ttl-600.offset-%u.%s",
		offset, mdns_domain_name_get_presentation( me->domain ) );
	require_action( me->hostname, exit, err = kNoMemoryErr );
	
	// Set the expected IPv4 address.
	
	char ipv4AddrStr[ kSockAddrStringMaxSize ];
	SNPrintF( ipv4AddrStr, sizeof( ipv4AddrStr ), "203.0.113.%u", offset + 1 );
	err = StringToSockAddr( ipv4AddrStr, &me->expectedAddr, sizeof( me->expectedAddr ), NULL );
	require_noerr( err, exit );
	
	const unsigned int timeLimitSecs = 5;
	FPrintF( stdout, "%{du:time} * Starting GAI for '%s' with %u second time limit\n", NULL, me->hostname, timeLimitSecs );
	const DNSServiceFlags flags = kDNSServiceFlagsReturnIntermediates | kDNSServiceFlagsAllowExpiredAnswers;
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, flags );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		size_t unexpectedResultCount = 0;
		for( size_t i = 0; i < inCount; ++i )
		{
			Boolean resultIsExpected = false;
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			const dnssd_getaddrinfo_result_type_t resultType = dnssd_getaddrinfo_result_get_type( result );
			const bool fromCache = dnssd_getaddrinfo_result_is_from_cache( result );
			if( SockAddrCompareAddr( dnssd_getaddrinfo_result_get_address( result ), &me->expectedAddr.sa ) == 0 )
			{
				if( !gotAddResult )
				{
					if( ( resultType == dnssd_getaddrinfo_result_type_add ) && !fromCache )
					{
						gotAddResult     = true;
						resultIsExpected = true;
					}
				}
			}
			if( !resultIsExpected ) ++unexpectedResultCount;
			FPrintF( stdout,
				"%{du:time} %s Got %sexpected result -- type: %s, hostname: %s, address: %##a, from cache: %s\n",
				NULL, resultIsExpected ? "✓" : "x", resultIsExpected ? "" : "un",
				dnssd_getaddrinfo_result_type_to_string( resultType ), dnssd_getaddrinfo_result_get_hostname( result ),
				dnssd_getaddrinfo_result_get_address( result ), YesNoStr( fromCache ) );
		}
		if( unexpectedResultCount > 0 ) _OptimisticDNSTestStop( me, kUnexpectedErr );
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		if( !gotAddResult )
		{
			FPrintF( stdout, "%{du:time} Failed to get ADD GAI result for '%s' after %u seconds.\n",
				NULL, me->hostname, timeLimitSecs );
			_OptimisticDNSTestStop( me, kTimeoutErr );
		}
		else
		{
			const unsigned int waitSecs = 15;
			FPrintF( stdout, "%{du:time} ⧖ Waiting %u seconds for CNAME record to expire\n", NULL, waitSecs );
			usleep( waitSecs * kMicrosecondsPerSecond );
			const OSStatus startErr = _OptimisticDNSTestStartSubtest2_2( me );
			if( startErr ) _OptimisticDNSTestStop( me, startErr );
		}
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestStartSubtest1_2( const OptimisticDNSTestRef me )
{
	OSStatus err;
	__block Boolean gotExpiredResult = false;
	__block Boolean gotAddResult = false;
	FPrintF( stdout, "%{du:time} Subtest 1.2: Second GAI for hostname without CNAME records (A record expired)\n", NULL );
	check( me->hostname );
	const unsigned int timeLimitSecs = 5;
	FPrintF( stdout,
		"%{du:time} * Starting GAI for '%s' with %u second time limit\n", NULL, me->hostname, timeLimitSecs );
	const DNSServiceFlags flags = kDNSServiceFlagsReturnIntermediates | kDNSServiceFlagsAllowExpiredAnswers;
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, flags );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		size_t unexpectedResultCount = 0;
		for( size_t i = 0; i < inCount; ++i )
		{
			Boolean resultIsExpected = false;
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			const dnssd_getaddrinfo_result_type_t resultType = dnssd_getaddrinfo_result_get_type( result );
			const bool fromCache = dnssd_getaddrinfo_result_is_from_cache( result );
			if( SockAddrCompareAddr( dnssd_getaddrinfo_result_get_address( result ), &me->expectedAddr.sa ) == 0 )
			{
				if( !gotExpiredResult )
				{
					if( ( resultType == dnssd_getaddrinfo_result_type_expired ) && fromCache )
					{
						gotExpiredResult = true;
						resultIsExpected = true;
					}
				}
				else if ( !gotAddResult )
				{
					if( ( resultType == dnssd_getaddrinfo_result_type_add ) && !fromCache )
					{
						gotAddResult     = true;
						resultIsExpected = true;
					}
				}
			}
			if( !resultIsExpected ) ++unexpectedResultCount;
			FPrintF( stdout,
				"%{du:time} %s Got %sexpected result -- type: %s, hostname: %s, address: %##a, from cache: %s\n",
				NULL, resultIsExpected ? "✓" : "x", resultIsExpected ? "" : "un",
				dnssd_getaddrinfo_result_type_to_string( resultType ), dnssd_getaddrinfo_result_get_hostname( result ),
				dnssd_getaddrinfo_result_get_address( result ), YesNoStr( fromCache ) );
		}
		if( unexpectedResultCount > 0 ) _OptimisticDNSTestStop( me, kUnexpectedErr );
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		if( !gotExpiredResult || !gotAddResult )
		{
			if( !gotExpiredResult )
			{
				FPrintF( stdout, "%{du:time} Failed to get EXPIRED GAI result for '%s' after %u seconds.\n",
					NULL, me->hostname, timeLimitSecs );
			}
			if( !gotAddResult )
			{
				FPrintF( stdout, "%{du:time} Failed to get ADD GAI result for '%s' after %u seconds.\n",
					NULL, me->hostname, timeLimitSecs );
			}
			_OptimisticDNSTestStop( me, kTimeoutErr );
		}
		else
		{
			const OSStatus startErr = _OptimisticDNSTestStartSubtest2_1( me );
			if( startErr ) _OptimisticDNSTestStop( me, startErr );
		}
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestStartSubtest1_1( const OptimisticDNSTestRef me )
{
	OSStatus err;
	__block Boolean gotAddResult = false;
	FPrintF( stdout, "%{du:time} Subtest 1.1: First GAI for hostname without CNAME records\n", NULL );
	
	// Create the hostname.
	
	const uint8_t offset = 10;
	ForgetMem( &me->hostname );
	ASPrintF( &me->hostname, "ttl-1.offset-%u.%s", offset, mdns_domain_name_get_presentation( me->domain ) );
	require_action( me->hostname, exit, err = kNoMemoryErr );
	
	// Set the expected IPv4 address.
	
	char ipv4AddrStr[ kSockAddrStringMaxSize ];
	SNPrintF( ipv4AddrStr, sizeof( ipv4AddrStr ), "203.0.113.%u", offset + 1 );
	err = StringToSockAddr( ipv4AddrStr, &me->expectedAddr, sizeof( me->expectedAddr ), NULL );
	require_noerr( err, exit );
	
	const unsigned int timeLimitSecs = 5;
	FPrintF( stdout, "%{du:time} * Starting GAI for '%s' with %u second time limit\n",
		NULL, me->hostname, timeLimitSecs );
	const DNSServiceFlags flags = kDNSServiceFlagsReturnIntermediates | kDNSServiceFlagsAllowExpiredAnswers;
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, flags );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		size_t unexpectedResultCount = 0;
		for( size_t i = 0; i < inCount; ++i )
		{
			Boolean resultIsExpected = false;
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			const dnssd_getaddrinfo_result_type_t resultType = dnssd_getaddrinfo_result_get_type( result );
			const bool fromCache = dnssd_getaddrinfo_result_is_from_cache( result );
			if( !gotAddResult )
			{
				if( ( resultType == dnssd_getaddrinfo_result_type_add ) && !fromCache )
				{
					if( SockAddrCompareAddr( dnssd_getaddrinfo_result_get_address( result ), &me->expectedAddr.sa ) == 0 )
					{
						gotAddResult     = true;
						resultIsExpected = true;
					}
				}
			}
			if( !resultIsExpected ) ++unexpectedResultCount;
			FPrintF( stdout,
				"%{du:time} %s Got %sexpected result -- type: %s, hostname: %s, address: %##a, from cache: %s\n",
				NULL, resultIsExpected ? "✓" : "x", resultIsExpected ? "" : "un",
				dnssd_getaddrinfo_result_type_to_string( resultType ), dnssd_getaddrinfo_result_get_hostname( result ),
				dnssd_getaddrinfo_result_get_address( result ), YesNoStr( fromCache ) );
		}
		if( unexpectedResultCount > 0 ) _OptimisticDNSTestStop( me, kUnexpectedErr );
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		dnssd_getaddrinfo_forget( &me->gai );
		if( !gotAddResult )
		{
			FPrintF( stdout, "%{du:time} Failed to get ADD GAI result for '%s' after %u seconds.\n",
				NULL, me->hostname, timeLimitSecs );
			_OptimisticDNSTestStop( me, kTimeoutErr );
		}
		else
		{
			const unsigned int waitSecs = 15;
			FPrintF( stdout, "%{du:time} ⧖ Waiting %u seconds for A record to expire\n", NULL, waitSecs );
			usleep( waitSecs * kMicrosecondsPerSecond );
			const OSStatus startErr = _OptimisticDNSTestStartSubtest1_2( me );
			if( startErr ) _OptimisticDNSTestStop( me, startErr );
		}
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static void	_OptimisticDNSTestStart( void * const inCtx )
{
	OSStatus err;
	char *domainStr = NULL;
	char *serverCmd = NULL;
	const OptimisticDNSTestRef me = (OptimisticDNSTestRef) inCtx;
	char tag[ 6 + 1 ];
	ASPrintF( &domainStr,
		"optimistic-dns-test-%s.test.",
		_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
	require_action( domainStr, exit, err = kNoMemoryErr );
	
	me->domain = mdns_domain_name_create( domainStr, mdns_domain_name_create_opts_none, &err );
	require_noerr( err, exit );
	
	// Use the --registerSC option because this test uses GAI operations that only specify a single label as the
	// hostname. The test depends on a search domain equal to a match domain for the test DNS server being appended to
    // PQDNs. SystemConfiguration will set up a search domain for each of the DNS service's match domains.
	//
	// Also use --default to make the DNS server act as a low-priority default DNS service. Currently, a negative
	// response from a server is required to iterate to the next search domain in the search domain list. If a test
	// device happens to not be connected to any network, then it won't have a DHCP-assigned DNS service to act as a
	// default DNS service that could provide a potentially negative response to move things along.
	
	ASPrintF( &serverCmd,
		DNSSDUTIL_TEST_DNS_SERVER_COMMAND_PREAMBLE " --loopback --registerSC --default --follow %lld --responseDelay 10"
		" --domain %s",
		(int64_t) getpid(), mdns_domain_name_get_presentation( me->domain ) );
	require_action_quiet( serverCmd, exit, err = kNoMemoryErr );
	
	FPrintF( stdout, "%{du:time} Starting DNS server with command: %s\n", NULL, serverCmd );
	err = _SpawnCommand( &me->serverPID, "/dev/null", "/dev/null", "%s", serverCmd );
	require_noerr( err, exit );
	
	ASPrintF( &me->hostname, "tag-probe.offset-250.%s", mdns_domain_name_get_presentation( me->domain ) );
	require_action( me->hostname, exit, err = kNoMemoryErr );
	
	me->gai = _OptimisticDNSTestCreateGAI( me, me->hostname, 0 );
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	const unsigned int timeoutSecs = 5;
	FPrintF( stdout,
		"%{du:time} Starting GAI to detect DNS server readiness with probe hostname '%s' and %u second timeout\n",
			NULL, me->hostname, timeoutSecs );
	const dnssd_getaddrinfo_t gai = me->gai;
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t * const inResults, const size_t inCount )
	{
		require_return( me->gai == gai );
		
		Boolean proceed = false;
		for( size_t i = 0; i < inCount; ++i )
		{
			const dnssd_getaddrinfo_result_t result = inResults[ i ];
			if( dnssd_getaddrinfo_result_get_type( result ) == dnssd_getaddrinfo_result_type_add )
			{
				proceed = true;
				break;
			}
		}
		if( proceed )
		{
			dispatch_source_forget( &me->timer );
			dnssd_getaddrinfo_forget( &me->gai );
			FPrintF( stdout, "%{du:time} Starting subtests...\n", NULL );
			const OSStatus startErr = _OptimisticDNSTestStartSubtest1_1( me );
			if( startErr ) _OptimisticDNSTestStop( me, startErr );
		}
	} );
	dnssd_getaddrinfo_activate( me->gai );
	
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeoutSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		FPrintF( stdout,
			"%{du:time} Probe GAI request for '%s' timed out after %u seconds.\n", NULL, me->hostname, timeoutSecs );
		_OptimisticDNSTestStop( me, kNotPreparedErr );
	} );
	dispatch_activate( me->timer );
	
exit:
	ForgetMem( &domainStr );
	ForgetMem( &serverCmd );
	if( err ) _OptimisticDNSTestStop( me, err );
}

//===========================================================================================================================

static OSStatus	_OptimisticDNSTestRun( const OptimisticDNSTestRef me )
{
	dispatch_async_f( me->queue, me, _OptimisticDNSTestStart );
	dispatch_semaphore_wait( me->doneSem, DISPATCH_TIME_FOREVER );
	return( me->error );
}

//===========================================================================================================================

static void	_OptimisticDNSTestRelease( const OptimisticDNSTestRef me )
{
	if( atomic_add_and_fetch_32( &me->refCount, -1 ) == 0 )
	{
		check( !me->gai );
		check( !me->timer );
		check( !me->domain );
		check( !me->hostname );
		check( me->serverPID < 0 );
		dispatch_forget( &me->queue );
		dispatch_forget( &me->doneSem );
		free( me );
	}
}

//===========================================================================================================================
//	RecordRegistrationTestCommand
//===========================================================================================================================

// The RRSet maximum member count is currently 32. This allows the use of 32-bit bitmaps for keeping track of RRSet
// membership.

#define kRecordRegistrationTestRRSetMemberCountMax		32

typedef struct RecordRegistrationTest *		RecordRegistrationTestRef;
struct RecordRegistrationTest
{
	dispatch_queue_t			queue;					// Serial queue for test events.
	dispatch_semaphore_t		doneSem;				// Semaphore to signal when the test is done.
	DNSServiceRef				query;					// Reference for DNSServiceQueryRecord() operation.
	DNSServiceRef				connection;				// Connection for record registrations.
	DNSRecordRef				recordRefs[ 32 ];		// Record registration references.
	dispatch_source_t			timer;					// Timer for enforcing time limits.
	char *						rrSetName;				// Name of current RRSet.
	MDNSColliderRef				collider;				// Creates an mDNS record conflict. [1]
	int32_t						refCount;				// Test's reference count.
	OSStatus					error;					// Test's error code.
	size_t						rrSetBitmapIndex;		// The current RRSet bitmap index.
	uint32_t					rrSetBitmapObserved;	// Keeps track of RRSet members seen by DNSServiceQueryRecord().
	size_t						subtestIndex;			// Current subtest index into kRegisterRecordSubtests array.
	size_t						interfaceSetIndex;		// Current subtest interface set index.
	uint32_t					registrationIfIndex;	// Current interface index to pass to DNSServiceRegisterRecord().
	DNSServiceFlags				registrationFlags;		// Current flags to pass to DNSServiceRegisterRecord().
	unsigned int				rrSetChangeIntervalMs;	// The time to wait in between RRSet changes.
	uint8_t						rdataBase[ 4 ];			// The randomly-generated A record RDATA base for the RRSet.
	Boolean						conflictOccurred;		// True if the subtest's expected record conflict has occurred. [1]
};

// Notes:
// 1. Exclusively For conflict subtests.

check_compile_time( countof_field( struct RecordRegistrationTest, recordRefs ) ==
	kRecordRegistrationTestRRSetMemberCountMax );

typedef enum
{
	kRecordRegistrationTestInterfaceSet_Any					= 1,	// "Any" interface, except AWDL and P2P interfaces. [1]
	kRecordRegistrationTestInterfaceSet_Loopback			= 2,	// The loopback interface.
	kRecordRegistrationTestInterfaceSet_LocalOnly			= 3,	// The LocalOnly pseudo-interface.
	kRecordRegistrationTestInterfaceSet_AnyPlusAWDL			= 4,	// "Any" interface plus AWDL interface. [2]
	kRecordRegistrationTestInterfaceSet_AnyPlusP2P			= 5,	// "Any" interface plus P2P interface. [3]
	kRecordRegistrationTestInterfaceSet_AnyPlusAWDLAndP2P	= 6,	// "Any" interface plus AWDL and P2P interfaces. [4]
	
}	RecordRegistrationTestInterfaceSet;

// Notes:
// 1. Uses kDNSServiceInterfaceIndexAny.
// 2. Uses kDNSServiceInterfaceIndexAny along with kDNSServiceFlagsIncludeAWDL.
// 3. Uses kDNSServiceInterfaceIndexAny along with kDNSServiceFlagsIncludeP2P.
// 4. Uses kDNSServiceInterfaceIndexAny along with kDNSServiceFlagsIncludeAWDL and kDNSServiceFlagsIncludeP2P.

typedef enum
{
	kRecordRegistrationTestRegistrationType_Shared			= 1,	// Shared record (use kDNSServiceFlagsShared).
	kRecordRegistrationTestRegistrationType_Unique			= 2,	// Unique record (use kDNSServiceFlagsUnique).
	kRecordRegistrationTestRegistrationType_KnownUnique		= 3,	// KnownUnique record (use kDNSServiceFlagsKnownUnique).
	
}	RecordRegistrationTestRegistrationType;

typedef enum
{
	kRecordRegistrationSubtestType_ChangingRRSet	= 1,	// Subtest that deals with an RRSet that changes over time.
	kRecordRegistrationSubtestType_Conflict			= 2,	// Subtest that deals with an RRSet that experiences a conflict.
	
}	RecordRegistrationSubtestType;

typedef struct
{
	RecordRegistrationTestRegistrationType			registrationType;	// Type of record registrations.
	const RecordRegistrationTestInterfaceSet *		interfaceSets;		// Interface sets to use for registrations.
	size_t											interfaceSetCount;	// Number of interface sets.
	const uint32_t *								rrSetBitmaps;		// The different instances of the RRSet to register.
	size_t											rrSetBitmapCount;	// Number of RRSet bitmaps.
	RecordRegistrationSubtestType					type;				// Type of subtest.
	
}	RegisterRecordSubtest;

static const RecordRegistrationTestInterfaceSet kRecordRegistrationTestAllInterfaceSets[] =
{
	kRecordRegistrationTestInterfaceSet_Any,
	kRecordRegistrationTestInterfaceSet_Loopback,
	kRecordRegistrationTestInterfaceSet_LocalOnly,
	kRecordRegistrationTestInterfaceSet_AnyPlusAWDL,
	kRecordRegistrationTestInterfaceSet_AnyPlusP2P,
	kRecordRegistrationTestInterfaceSet_AnyPlusAWDLAndP2P,
};

static const RecordRegistrationTestInterfaceSet kRecordRegistrationTestAllInterfaceSetsExceptLocalOnly[] =
{
	kRecordRegistrationTestInterfaceSet_Any,
	kRecordRegistrationTestInterfaceSet_Loopback,
	kRecordRegistrationTestInterfaceSet_AnyPlusAWDL,
	kRecordRegistrationTestInterfaceSet_AnyPlusP2P,
	kRecordRegistrationTestInterfaceSet_AnyPlusAWDLAndP2P,
};

#define _BitU32( N )		( UINT32_C( 1 ) << (N) )

// The following array defines the different instances of a test RRSet. Each bit represents the presence or absence of a
// member of the RRSet (1 means present, 0 means absent). The test RRSet consists of A records with the same name and
// class. If the RRSet's name is register-record-test-wfpx79.local, and the A records are offsets from a base IPv4
// address of 203.0.113.0, then a bitmap of 0x0000002A represents the RRSet with three members (1, 3, and 5):
//
//		register-record-test-wfpx79.local IN A 203.0.113.1
//		register-record-test-wfpx79.local IN A 203.0.113.3
//		register-record-test-wfpx79.local IN A 203.0.113.5
//
// The test actualizes each RRSet instance by making calls to DNSServiceRegisterRecord() and DNSServiceRemoveRecord() to
// transform the previous RRSet instance. The first RRSet instance is simply the product of DNSServiceRegisterRecord()
// calls.

static const uint32_t		kRecordRegistrationTestRRSetBitmaps_ChanginRRSet[] =
{
	_BitU32( 1 ),												// RRSet with members {1}
	_BitU32( 1 ) | _BitU32( 2 ),								// RRSet with members {1, 2}		(Add one member)
	_BitU32( 1 ) | _BitU32( 2 ) | _BitU32( 3 ),					// RRSet with members {1, 2, 3}		(Add one member)
	_BitU32( 2 ) | _BitU32( 3 ),								// RRSet with members {2, 3}		(Remove one member)
	_BitU32( 2 ) | _BitU32( 3 ) | _BitU32( 4 ) | _BitU32( 5 ),	// RRSet with members {2, 3, 4, 5}	(Add two members)
	_BitU32( 3 ) | _BitU32( 5 ),								// RRSet with members {3, 5}		(Remove two members)
	_BitU32( 3 ) | _BitU32( 6 ),								// RRSet with members {3, 6}		(Add one, Remove one)
	_BitU32( 7 ) | _BitU32( 8 ),								// RRSet with members {7, 8}		(Add two, Remove two)
	_BitU32( 7 ),												// RRSet with members {7}			(Remove one member)
	0,															// RRSet with members {}			(Remove one → empty set)
	_BitU32( 9 ) | _BitU32( 10 ),								// RRSet with members {9, 10}		(Add two to empty set)
	0,															// RRSet with members {}			(Remove two → empty set)
};

static const uint32_t		kRecordRegistrationTestConflictRRsetBitmaps_SingleRRSet[] =
{
	_BitU32( 1 ), // RRSet with one member.
};

static const RegisterRecordSubtest		kRegisterRecordSubtests[] =
{
	{
		.type				= kRecordRegistrationSubtestType_ChangingRRSet,
		.registrationType	= kRecordRegistrationTestRegistrationType_Unique,
		.interfaceSets		= kRecordRegistrationTestAllInterfaceSets,
		.interfaceSetCount	= countof( kRecordRegistrationTestAllInterfaceSets ),
		.rrSetBitmaps		= kRecordRegistrationTestRRSetBitmaps_ChanginRRSet,
		.rrSetBitmapCount	= countof( kRecordRegistrationTestRRSetBitmaps_ChanginRRSet ),
	},
	{
		.type				= kRecordRegistrationSubtestType_ChangingRRSet,
		.registrationType	= kRecordRegistrationTestRegistrationType_KnownUnique,
		.interfaceSets		= kRecordRegistrationTestAllInterfaceSets,
		.interfaceSetCount	= countof( kRecordRegistrationTestAllInterfaceSets ),
		.rrSetBitmaps		= kRecordRegistrationTestRRSetBitmaps_ChanginRRSet,
		.rrSetBitmapCount	= countof( kRecordRegistrationTestRRSetBitmaps_ChanginRRSet ),
	},
	{
		.type				= kRecordRegistrationSubtestType_ChangingRRSet,
		.registrationType	= kRecordRegistrationTestRegistrationType_Shared,
		.interfaceSets		= kRecordRegistrationTestAllInterfaceSets,
		.interfaceSetCount	= countof( kRecordRegistrationTestAllInterfaceSets ),
		.rrSetBitmaps		= kRecordRegistrationTestRRSetBitmaps_ChanginRRSet,
		.rrSetBitmapCount	= countof( kRecordRegistrationTestRRSetBitmaps_ChanginRRSet ),
	},
	{
		.type				= kRecordRegistrationSubtestType_Conflict,
		.registrationType	= kRecordRegistrationTestRegistrationType_Unique,
		.interfaceSets		= kRecordRegistrationTestAllInterfaceSetsExceptLocalOnly,
		.interfaceSetCount	= countof( kRecordRegistrationTestAllInterfaceSetsExceptLocalOnly ),
		.rrSetBitmaps		= kRecordRegistrationTestConflictRRsetBitmaps_SingleRRSet,
		.rrSetBitmapCount	= countof( kRecordRegistrationTestConflictRRsetBitmaps_SingleRRSet ),
	},
	{
		.type				= kRecordRegistrationSubtestType_Conflict,
		.registrationType	= kRecordRegistrationTestRegistrationType_KnownUnique,
		.interfaceSets		= kRecordRegistrationTestAllInterfaceSetsExceptLocalOnly,
		.interfaceSetCount	= countof( kRecordRegistrationTestAllInterfaceSetsExceptLocalOnly ),
		.rrSetBitmaps		= kRecordRegistrationTestConflictRRsetBitmaps_SingleRRSet,
		.rrSetBitmapCount	= countof( kRecordRegistrationTestConflictRRsetBitmaps_SingleRRSet ),
	},
};

//===========================================================================================================================

static void	_RecordRegistrationTestRelease( const RecordRegistrationTestRef me )
{
	if( atomic_add_and_fetch_32( &me->refCount, -1 ) == 0 )
	{
		dispatch_forget( &me->queue );
		dispatch_forget( &me->doneSem );
		check( !me->query );
		check( !me->connection );
		for( size_t i = 0; i < countof( me->recordRefs ); ++i )
		{
			check( !me->recordRefs[ i ] );
		}
		check( !me->timer );
		ForgetMem( &me->rrSetName );
		free( me );
	}
}

//===========================================================================================================================

static RecordRegistrationTestRef
	_RecordRegistrationTestCreate(
		const unsigned int	inRRSetChangeIntervalMs,
		OSStatus * const	outError )
{
	OSStatus err;
	RecordRegistrationTestRef test = NULL;
	RecordRegistrationTestRef obj = (RecordRegistrationTestRef) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->refCount				= 1;
	obj->error					= kInProgressErr;
	obj->rrSetChangeIntervalMs	= inRRSetChangeIntervalMs;
	obj->queue = dispatch_queue_create( "com.apple.dnssdutil.record-registration-test", DISPATCH_QUEUE_SERIAL );
	require_action( obj->queue, exit, err = kNoResourcesErr );
	
	obj->doneSem = dispatch_semaphore_create( 0 );
	require_action( obj->doneSem, exit, err = kNoResourcesErr );
	
	test = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( outError ) *outError = err;
	if( obj ) _RecordRegistrationTestRelease( obj );
	return( test );
}

//===========================================================================================================================

static void	_RecordRegistrationTestTearDownDNSServiceRefs( const RecordRegistrationTestRef me )
{
	// Forget the DNSServiceQueryRecord() operation.
	
	DNSServiceForget( &me->query );
	
	// Forget the connection for record registrations, then NULL out record registration references, which are now invalid.
	
	DNSServiceForget( &me->connection );
	for( size_t i = 0; i < countof( me->recordRefs ); ++i )
	{
		me->recordRefs[ i ] = NULL;
	}
}

//===========================================================================================================================

static void	_RecordRegistrationTestStop( const RecordRegistrationTestRef me, const OSStatus inError )
{
	me->error = inError;
	if( !me->error )
	{
		FPrintF( stdout, "%{du:time} Test PASSED\n", NULL );
	}
	else
	{
		FPrintF( stdout, "%{du:time} Test FAILED: %#m\n", NULL, me->error );
	}
	_RecordRegistrationTestTearDownDNSServiceRefs( me );
	MDNSColliderForget( &me->collider );
	dispatch_source_forget( &me->timer );
	ForgetMem( &me->rrSetName );
	dispatch_semaphore_signal( me->doneSem );
}

//===========================================================================================================================

static const RegisterRecordSubtest *	_RecordRegistrationTestGetCurrentSubtest( const RecordRegistrationTestRef me )
{
	require_fatal( me->subtestIndex < countof( kRegisterRecordSubtests ),
		"Invalid subtest index %zu >= %zu", me->subtestIndex, countof( kRegisterRecordSubtests ) );
	return &kRegisterRecordSubtests[ me->subtestIndex ];
}

//===========================================================================================================================

static RecordRegistrationSubtestType	_RecordRegistrationTestGetCurrentSubtestType( const RecordRegistrationTestRef me )
{
	const RegisterRecordSubtest * const subtest = _RecordRegistrationTestGetCurrentSubtest( me );
	return subtest->type;
}

//===========================================================================================================================

static void
	_RecordRegistrationTestRegisterRecordCallback(
		__unused const DNSServiceRef	inSDRef,
		const DNSRecordRef				inRecordRef,
		__unused const DNSServiceFlags	inFlags,
		const DNSServiceErrorType		inError,
		void * const					inCtx )
{
	OSStatus err;
	size_t i;
	Boolean foundRecordRef = false;
	const RecordRegistrationTestRef me = (RecordRegistrationTestRef) inCtx;
	for( i = 0; i < countof( me->recordRefs ); ++i )
	{
		if( me->recordRefs[ i ] == inRecordRef )
		{
			foundRecordRef = true;
			break;
		}
	}
	const RecordRegistrationSubtestType subtestType = _RecordRegistrationTestGetCurrentSubtestType( me );
	if( foundRecordRef )
	{
		me->rdataBase[ 3 ] = (uint8_t) i;
		if( inError )
		{
			err = inError;
			if( subtestType == kRecordRegistrationSubtestType_Conflict )
			{
				if( me->collider && ( inError == kDNSServiceErr_NameConflict ) )
				{
					FPrintF( stdout, "%{du:time} ✓ Got expected conflict for record registration: %s IN A %.4a\n",
						NULL, me->rrSetName, me->rdataBase );
					me->conflictOccurred = true;
					err = kNoErr;
				}
			}
			if( err )
			{
				FPrintF( stdout, "%{du:time} x Failed to register record: %s IN A %.4a\n",
					NULL, me->rrSetName, me->rdataBase );
			}
		}
		else
		{
			FPrintF( stdout, "%{du:time} + Registered record: %s IN A %.4a\n", NULL, me->rrSetName, me->rdataBase );
			err = kNoErr;
		}
	}
	else
	{
		FPrintF( stdout, "%{du:time} x Got register record callback for unrecognized record reference\n", NULL );
		err = kUnexpectedErr;
	}
	if( err ) _RecordRegistrationTestStop( me, err );
}

//===========================================================================================================================

static OSStatus
	_RecordRegistrationTestRegisterRecord(
		const RecordRegistrationTestRef	me,
		const uint8_t					inMemberID,
		DNSRecordRef * const			outRecordRef )
{
	me->rdataBase[ 3 ] = inMemberID;
	FPrintF( stdout, "%{du:time} ± Registering record: %s IN A %.4a\n", NULL, me->rrSetName, me->rdataBase );
	const OSStatus err = DNSServiceRegisterRecord( me->connection, outRecordRef, me->registrationFlags,
		me->registrationIfIndex, me->rrSetName, kDNSServiceType_A, kDNSServiceClass_IN, sizeof( me->rdataBase ),
		me->rdataBase, 1 * kSecondsPerHour, _RecordRegistrationTestRegisterRecordCallback, me );
	return( err );
}

//===========================================================================================================================

static void	_RecordRegistrationTestRetain( const RecordRegistrationTestRef me )
{
	atomic_add_32( &me->refCount, 1 );
}

//===========================================================================================================================

static RecordRegistrationTestInterfaceSet
	_RecordRegistrationTestGetCurrentInterfaceSet(
		const RecordRegistrationTestRef	me )
{
	const RegisterRecordSubtest * const subtest = _RecordRegistrationTestGetCurrentSubtest( me );
	require_fatal( me->interfaceSetIndex < subtest->interfaceSetCount,
		"Invalid interface set index %zu >= %zu", me->subtestIndex, subtest->interfaceSetCount );
	return subtest->interfaceSets[ me->interfaceSetIndex ];
}

//===========================================================================================================================

static RecordRegistrationTestRegistrationType
	_RecordRegistrationTestGetCurrentRegistrationType(
		const RecordRegistrationTestRef	me )
{
	const RegisterRecordSubtest * const subtest = _RecordRegistrationTestGetCurrentSubtest( me );
	return subtest->registrationType;
}

//===========================================================================================================================

static uint32_t	_RecordRegistrationTestGetCurrentRRSetBitmap( const RecordRegistrationTestRef me )
{
	const RegisterRecordSubtest * const subtest = _RecordRegistrationTestGetCurrentSubtest( me );
	require_fatal( me->rrSetBitmapIndex < subtest->rrSetBitmapCount,
		"Invalid RRSet bitmap index %zu >= %zu", me->rrSetBitmapIndex, subtest->rrSetBitmapCount );
	return subtest->rrSetBitmaps[ me->rrSetBitmapIndex ];
}

//===========================================================================================================================

static uint32_t	_RecordRegistrationTestGetLoopbackInterfaceIndex( void )
{
	static uint32_t sIfIndex = 0;
	if( sIfIndex == 0 )
	{
		const char * const loopbackIfName = "lo0";
		sIfIndex = if_nametoindex( loopbackIfName );
		require_fatal( sIfIndex != 0, "Failed to get loopback interface (%s) index", loopbackIfName );
	}
	return( sIfIndex );
}

//===========================================================================================================================

static OSStatus	_RecordRegistrationTestTriggerConflict( const RecordRegistrationTestRef me )
{
	OSStatus err;
	MDNSColliderRef collider = NULL;
	require_action_quiet( !me->collider, exit, err = kNoErr );
	
	// AAAA RDATA for IPv6 address 2001:db8::dead:beef.
	
	const uint8_t rdata[ 16 ] =
	{
		0x20, 0x01, 0x0D, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF
	};
	FPrintF( stdout, "%{du:time} ! Scheduling send of conflicting record: %s IN AAAA %.16a\n", NULL, me->rrSetName, rdata );
	err = MDNSColliderCreate( me->queue, &collider );
	require_noerr( err, exit );
	
	err = MDNSColliderSetProgram( collider, "probes 10r; send; wait 5000" );
	require_noerr( err, exit );
	
	uint8_t rrSetName[ kDomainNameLengthMax ];
	err = DomainNameFromString( rrSetName, me->rrSetName, NULL );
	require_noerr( err, exit );
	
	// The subtest's RRSet consists of A records, but a AAAA record with the same name and class is sufficient to cause
	// a conflict. See rdar://3483349 (Any rrtype is a conflict for unique records).
	
	err = MDNSColliderSetRecord( collider, rrSetName, kDNSServiceType_AAAA, rdata, sizeof( rdata ) );
	require_noerr( err, exit );
	
	MDNSColliderSetProtocols( collider, kMDNSColliderProtocol_IPv4 );
	MDNSColliderSetInterfaceIndex( collider, _RecordRegistrationTestGetLoopbackInterfaceIndex() );
	
	err = MDNSColliderStart( collider );
	require_noerr( err, exit );
	
	me->collider = collider;
	collider = NULL;
	
exit:
	CFForget( &collider );
	return( err );
}

//===========================================================================================================================

static void DNSSD_API
	_RecordRegistrationTestQueryRecordCallback(
		__unused const DNSServiceRef	inSDRef,
		const DNSServiceFlags			inFlags,
		__unused const uint32_t			inIfIndex,
		const DNSServiceErrorType		inError,
		const char * const				inFullName,
		const uint16_t					inType,
		const uint16_t					inClass,
		const uint16_t					inRDataLen,
		const void * const				inRDataPtr,
		__unused const uint32_t			inTTL,
		void * const					inCtx )
{
	OSStatus err;
	const RecordRegistrationTestRef me = (RecordRegistrationTestRef) inCtx;
	
	// Print result.
	
	char ifNameBuf[ kInterfaceNameBufLen ];
	const char * const ifName = InterfaceIndexToName( inIfIndex, ifNameBuf );
	char typeBuf[ 32 ];
	const char *typeStr = DNSRecordTypeValueToString( inType );
	if( !typeStr )
	{
		SNPrintF( typeBuf, sizeof( typeBuf ), "TYPE%u", inType );
		typeStr = typeBuf;
	}
	char classBuf[ 32 ];
	const char *classStr;
	if( inClass == kDNSServiceClass_IN )
	{
		classStr = "IN";
	}
	else
	{
		SNPrintF( classBuf, sizeof( classBuf ), "CLASS%u", inClass );
		classStr = classBuf;
	}
	char *rdataStr = NULL;
	DNSRecordDataToString( inRDataPtr, inRDataLen, inType, &rdataStr );
	if( !rdataStr )
	{
		ASPrintF( &rdataStr, "%#H", inRDataPtr, (int) inRDataLen, (int) inRDataLen );
		require_action( rdataStr, exit, err = kNoMemoryErr );
	}
	FPrintF( stdout, "%{du:time} → QueryRecord result -- flags: 0x%0X (%s), interface: %s/%d, error: %#m, name: %s,"
		" type: %s, class: %s, ttl: %u, rdata: %s\n",
		NULL, inFlags, (inFlags & kDNSServiceFlagsAdd) ? "Add" : "Rmv", ifName ? ifName : "", (int32_t) inIfIndex, inError,
		inFullName, typeStr, classStr, inTTL, rdataStr );
	
	// Check callback arguments.
	
	uint8_t fullName[ kDomainNameLengthMax ];
	err = DomainNameFromString( fullName, inFullName, NULL );
	require_noerr_quiet( err, exit );
	
	uint8_t rrSetName[ kDomainNameLengthMax ];
	err = DomainNameFromString( rrSetName, me->rrSetName, NULL );
	require_noerr_quiet( err, exit );
	
	const Boolean nameMatch = DomainNameEqual( fullName, rrSetName );
	require_action( nameMatch, exit, err = kNameErr );
	require_action( inType == kDNSServiceType_A, exit, err = kTypeErr );
	require_action( inClass == kDNSServiceClass_IN, exit, err = kTypeErr );
	require_action( inRDataLen == 4, exit, err = kSizeErr );
	
	const int cmp = memcmp( inRDataPtr, me->rdataBase, 3 );
	require_action( cmp == 0, exit, err = kValueErr );
	
	const uint8_t memberID = ( (const uint8_t *) inRDataPtr )[ 3 ];
	require_action( memberID < kRecordRegistrationTestRRSetMemberCountMax, exit, err = kValueErr );
	
	err = inError;
	require_noerr( err, exit );
	
	// Keep track of adds and removes making sure we don't get any unexpected adds or removes in the process.
	
	const uint32_t rrSetBitmap = _RecordRegistrationTestGetCurrentRRSetBitmap( me );
	const uint32_t bitmask = _BitU32( memberID );
	const RecordRegistrationSubtestType subtestType = _RecordRegistrationTestGetCurrentSubtestType( me );
	if( inFlags & kDNSServiceFlagsAdd )
	{
		if( ( subtestType == kRecordRegistrationSubtestType_Conflict ) && me->conflictOccurred )
		{
			FPrintF( stdout, "%{du:time} x Got an ADD query result after the expected conflict\n", NULL );
			err = kUnexpectedErr;
			goto exit;
		}
		require_action( rrSetBitmap & bitmask, exit, err = kUnexpectedErr );
		require_action( ( me->rrSetBitmapObserved & bitmask ) == 0, exit, err = kDuplicateErr );
		
		me->rrSetBitmapObserved |= bitmask;
		if( subtestType == kRecordRegistrationSubtestType_Conflict )
		{
			// If all members of the RRSet have been observed, proceed with the conflict.
			
			if( me->rrSetBitmapObserved == rrSetBitmap )
			{
				_RecordRegistrationTestTriggerConflict( me );
			}
		}
	}
	else
	{
		// During Changing RRSet subtests, getting a remove for members of the last registerered RRSet is considered
		// incorrect behavior because that's the very RRSet that we're expected to observe via the QueryRecord
		// operation.
		//
		// During Conflict RRSet subtests, this is also considered incorrect behavior, but only before the expected
		// conflict occurs. After the expected conflict, the removes are expected since the conflict is supposed to
		// flush the RRSet from the record cache.
		
		if( ( subtestType == kRecordRegistrationSubtestType_ChangingRRSet ) ||
			( ( subtestType == kRecordRegistrationSubtestType_Conflict ) && !me->conflictOccurred ) )
		{
			require_action( ( rrSetBitmap & bitmask ) == 0, exit, err = kUnexpectedErr );
		}
		require_action( ( me->rrSetBitmapObserved & bitmask ) != 0, exit, err = kUnexpectedErr );
		
		me->rrSetBitmapObserved &= ~bitmask;
	}
	
exit:
	ForgetMem( &rdataStr );
	if( err ) _RecordRegistrationTestStop( me, err );
}

//===========================================================================================================================

static const char *	_RecordRegistrationTestInterfaceSetToString( const RecordRegistrationTestInterfaceSet inInterfaceSet )
{
	switch( inInterfaceSet )
	{
		case kRecordRegistrationTestInterfaceSet_Any:				return( "Any Interface" );
		case kRecordRegistrationTestInterfaceSet_Loopback:			return( "Loopback Interface" );
		case kRecordRegistrationTestInterfaceSet_LocalOnly:			return( "LocalOnly Pseudo-Interface" );
		case kRecordRegistrationTestInterfaceSet_AnyPlusAWDL:		return( "Any+AWDL Interfaces" );
		case kRecordRegistrationTestInterfaceSet_AnyPlusP2P:		return( "Any+P2P Interfaces" );
		case kRecordRegistrationTestInterfaceSet_AnyPlusAWDLAndP2P:	return( "Any+AWDL+P2P Interfaces" );
	}
	return( "«INVALID INTERFACE SET»" );
}

//===========================================================================================================================

static const char *	_RecordRegistrationTestRegistrationTypeToString( const RecordRegistrationTestRegistrationType inRegType )
{
	switch( inRegType )
	{
		case kRecordRegistrationTestRegistrationType_Shared:		return( "Shared" );
		case kRecordRegistrationTestRegistrationType_Unique:		return( "Unique" );
		case kRecordRegistrationTestRegistrationType_KnownUnique:	return( "KnownUnique" );
	}
	return( "«INVALID REGISTRATION TYPE»" );
}

//===========================================================================================================================

static const char *	_RecordRegistrationSubtestTypeToString( const RecordRegistrationSubtestType inSubtestType )
{
	switch( inSubtestType )
	{
		case kRecordRegistrationSubtestType_ChangingRRSet:	return("Changing RRSet");
		case kRecordRegistrationSubtestType_Conflict:		return("Conflict");
	}
	return( "«INVALID SUBTEST TYPE»" );
}

//===========================================================================================================================

static OSStatus	_RecordRegistrationTestActualizeRRSet( RecordRegistrationTestRef inTest );

static OSStatus	_RecordRegistrationTestStartRegistrationsAndQuery( const RecordRegistrationTestRef me )
{
	// Clean up previous DNSServiceRefs and collider.
	
	_RecordRegistrationTestTearDownDNSServiceRefs( me );
	MDNSColliderForget( &me->collider );
	me->conflictOccurred = false;
	
	const RecordRegistrationSubtestType subtestType = _RecordRegistrationTestGetCurrentSubtestType( me );
	switch( subtestType )
	{
		case kRecordRegistrationSubtestType_ChangingRRSet:
		case kRecordRegistrationSubtestType_Conflict:
			break;
		
		default:
			FatalErrorF( "Unhandled RecordRegistrationSubtestType value %d", subtestType );
	}
	
	// Select interface indexes for DNSServiceRegisterRecord() and DNSServiceQueryRecord().
	
	uint32_t queryRecordIfIndex;
	const RecordRegistrationTestInterfaceSet interfaceSet = _RecordRegistrationTestGetCurrentInterfaceSet( me );
	switch( interfaceSet )
	{
		case kRecordRegistrationTestInterfaceSet_Any:
		case kRecordRegistrationTestInterfaceSet_AnyPlusAWDL:
		case kRecordRegistrationTestInterfaceSet_AnyPlusP2P:
		case kRecordRegistrationTestInterfaceSet_AnyPlusAWDLAndP2P:
			me->registrationIfIndex = kDNSServiceInterfaceIndexAny;
			queryRecordIfIndex = _RecordRegistrationTestGetLoopbackInterfaceIndex();
			break;
		
		case kRecordRegistrationTestInterfaceSet_Loopback:
			me->registrationIfIndex = _RecordRegistrationTestGetLoopbackInterfaceIndex();
			queryRecordIfIndex = _RecordRegistrationTestGetLoopbackInterfaceIndex();
			break;
		
		case kRecordRegistrationTestInterfaceSet_LocalOnly:
			me->registrationIfIndex = kDNSServiceInterfaceIndexLocalOnly;
			queryRecordIfIndex = kDNSServiceInterfaceIndexLocalOnly;
			break;
		
		default:
			FatalErrorF( "Unhandled RecordRegistrationTestInterfaceSet value %d", interfaceSet );
	}
	
	// Set up flags for DNSServiceRegisterRecord().
	
	me->registrationFlags = 0;
	switch( interfaceSet )
	{
		case kRecordRegistrationTestInterfaceSet_AnyPlusAWDL:
			me->registrationFlags |= kDNSServiceFlagsIncludeAWDL;
			break;
		
		case kRecordRegistrationTestInterfaceSet_AnyPlusP2P:
			me->registrationFlags |= kDNSServiceFlagsIncludeP2P;
			break;
		
		case kRecordRegistrationTestInterfaceSet_AnyPlusAWDLAndP2P:
			me->registrationFlags |= ( kDNSServiceFlagsIncludeAWDL | kDNSServiceFlagsIncludeP2P );
			break;
		
		case kRecordRegistrationTestInterfaceSet_Any:
		case kRecordRegistrationTestInterfaceSet_Loopback:
		case kRecordRegistrationTestInterfaceSet_LocalOnly:
			break;
	}
	const RecordRegistrationTestRegistrationType registrationType = _RecordRegistrationTestGetCurrentRegistrationType( me );
	switch( registrationType )
	{
		case kRecordRegistrationTestRegistrationType_Shared:
			me->registrationFlags |= kDNSServiceFlagsShared;
			break;
		
		case kRecordRegistrationTestRegistrationType_Unique:
			me->registrationFlags |= kDNSServiceFlagsUnique;
			break;
		
		case kRecordRegistrationTestRegistrationType_KnownUnique:
			me->registrationFlags |= kDNSServiceFlagsKnownUnique;
			break;
		
		default:
			FatalErrorF( "Unhandled RecordRegistrationTestRegistrationType value %d", registrationType );
	}
	FPrintF( stdout, "%{du:time} Subtest %zu.%zu: Registering %s Records on %s (%s)\n",
		NULL, me->subtestIndex + 1, me->interfaceSetIndex + 1,
		_RecordRegistrationTestRegistrationTypeToString( registrationType ),
		_RecordRegistrationTestInterfaceSetToString( interfaceSet ), _RecordRegistrationSubtestTypeToString( subtestType ) );
	char ifNameBuf[ kInterfaceNameBufLen ];
	const char * const ifName = InterfaceIndexToName( me->registrationIfIndex, ifNameBuf );
	FPrintF( stdout, "%{du:time} Record Registration interface and flags -- interface: %s/%d, flags: %#{flags}\n",
		NULL, ifName ? ifName : "", (int32_t) me->registrationIfIndex, me->registrationFlags, kDNSServiceFlagsDescriptors );
	
	// Set up connection for record registrations.
	
	FPrintF( stdout, "%{du:time} * Creating connection\n", NULL );
	OSStatus err = DNSServiceCreateConnection( &me->connection );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( me->connection, me->queue );
	require_noerr( err, exit );
	
	// Create a randomly-generated record name.
	
	char tag[ 6 + 1 ];
	_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag );
	ForgetMem( &me->rrSetName );
	ASPrintF( &me->rrSetName, "register-record-test-%s.local", tag );
	require_action( me->rrSetName, exit, err = kNoMemoryErr );
	
	// Randomly-generate RDATA base for A record RDATA.
	
	RandomBytes( me->rdataBase, sizeof( me->rdataBase ) );
	
	// Actualize the RRSet.
	
	me->rrSetBitmapIndex = 0;
	err = _RecordRegistrationTestActualizeRRSet( me );
	require_noerr( err, exit );
	
	// Start the query for the RRSet.
	
	me->rrSetBitmapObserved = 0;
	err = DNSServiceQueryRecord( &me->query, 0, queryRecordIfIndex, me->rrSetName, kDNSServiceType_A, kDNSServiceClass_IN,
		_RecordRegistrationTestQueryRecordCallback, me );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( me->query, me->queue );
	require_noerr( err, exit );
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_RecordRegistrationTestSubtestStart( const RecordRegistrationTestRef me )
{
	OSStatus err;
	if( me->subtestIndex >= countof( kRegisterRecordSubtests ) )
	{
		_RecordRegistrationTestRetain( me );
		dispatch_async( me->queue,
		^{
			_RecordRegistrationTestStop( me, kNoErr );
			_RecordRegistrationTestRelease( me );
		} );
		err = kNoErr;
		goto exit;
	}
	me->interfaceSetIndex = 0;
	err = _RecordRegistrationTestStartRegistrationsAndQuery( me );
	require_noerr( err, exit );
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_RecordRegistrationTestHandleTimeout( const RecordRegistrationTestRef me )
{
	uint32_t rrSetBitmapExpected;
	const RecordRegistrationSubtestType subtestType = _RecordRegistrationTestGetCurrentSubtestType( me );
	if( subtestType == kRecordRegistrationSubtestType_Conflict )
	{
		rrSetBitmapExpected = 0; // A conflict should cause all members of the RRSet to be flushed from the cache.
	}
	else
	{
		rrSetBitmapExpected = _RecordRegistrationTestGetCurrentRRSetBitmap( me );
	}
	FPrintF( stdout, "%{du:time} ⧖ Expected RRSet = {", NULL );
	const char *separator = "";
	for( unsigned int i = 0; i < kRecordRegistrationTestRRSetMemberCountMax; ++i )
	{
		if( rrSetBitmapExpected & _BitU32( i ) )
		{
			me->rdataBase[ 3 ] = (uint8_t) i;
			FPrintF( stdout, "%s\n\t%s IN A %.4a", separator, me->rrSetName, me->rdataBase );
			separator = ",";
		}
	}
	FPrintF( stdout, "\n}\n" );
	OSStatus err;
	if( me->rrSetBitmapObserved != rrSetBitmapExpected )
	{
		FPrintF( stdout, "%{du:time} x Actual RRSet = {", NULL );
		separator = "";
		for( unsigned int i = 0; i < kRecordRegistrationTestRRSetMemberCountMax; ++i )
		{
			if( me->rrSetBitmapObserved & _BitU32( i ) )
			{
				me->rdataBase[ 3 ] = (uint8_t) i;
				FPrintF( stdout, "%s\n\t%s IN A %.4a", separator, me->rrSetName, me->rdataBase );
				separator = ",";
			}
		}
		FPrintF( stdout, "\n}\n" );
		err = kMismatchErr;
	}
	else
	{
		FPrintF( stdout, "%{du:time} ✓ QueryRecord RRSet matches\n", NULL );
		++me->rrSetBitmapIndex;
		const RegisterRecordSubtest * const subtest = _RecordRegistrationTestGetCurrentSubtest( me );
		if( me->rrSetBitmapIndex < subtest->rrSetBitmapCount )
		{
			err = _RecordRegistrationTestActualizeRRSet( me );
		}
		else
		{
			FPrintF( stdout, "\n" );
			++me->interfaceSetIndex;
			if( me->interfaceSetIndex < subtest->interfaceSetCount )
			{
				err = _RecordRegistrationTestStartRegistrationsAndQuery( me );
			}
			else
			{
				++me->subtestIndex;
				err = _RecordRegistrationTestSubtestStart( me );
			}
		}
	}
	return( err );
}

//===========================================================================================================================

static OSStatus	_RecordRegistrationTestActualizeRRSet( const RecordRegistrationTestRef me )
{
	FPrintF( stdout, "%{du:time} ⚒ Making changes to RRSet\n", NULL );
	OSStatus err;
	const uint32_t rrSetBitmap = _RecordRegistrationTestGetCurrentRRSetBitmap( me );
	for( unsigned int i = 0; i < kRecordRegistrationTestRRSetMemberCountMax; ++i )
	{
		const uint32_t bitmask = _BitU32( i );
		const Boolean needRRSetMember = ( rrSetBitmap & bitmask ) != 0;
		if( needRRSetMember )
		{
			if( !me->recordRefs[ i ] )
			{
				err = _RecordRegistrationTestRegisterRecord( me, (uint8_t) i, &me->recordRefs[ i ] );
				require_noerr( err, exit );
			}
		}
		else
		{
			if( me->recordRefs[ i ] )
			{
				me->rdataBase[ 3 ] = (uint8_t) i;
				FPrintF( stdout, "%{du:time} - Deregistering record: %s IN A %.4a\n", NULL, me->rrSetName, me->rdataBase );
				err = DNSServiceRemoveRecord( me->connection, me->recordRefs[ i ], 0 );
				require_noerr( err, exit );
				
				me->recordRefs[ i ] = NULL;
			}
		}
	}
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	unsigned int timeLimitMs;
	const RecordRegistrationSubtestType subtestType = _RecordRegistrationTestGetCurrentSubtestType( me );
	switch( subtestType )
	{
		case kRecordRegistrationSubtestType_ChangingRRSet:
			timeLimitMs = me->rrSetChangeIntervalMs;
			break;
		
		case kRecordRegistrationSubtestType_Conflict:
			
			// Allow for six seconds to pass in case mDNSResponder is currently rate limiting conflicts to one every
			// five seconds. See rdar://3809484 (Rate limiting imposed too soon) for background on this behavior.
			
			timeLimitMs = 6 * kMillisecondsPerSecond;
			break;
	}
	FPrintF( stdout, "%{du:time} ⧗ Will check QueryRecord results after %u ms\n", NULL, timeLimitMs );
	dispatch_source_set_timer( me->timer, dispatch_time_milliseconds( timeLimitMs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		const OSStatus timeoutErr = _RecordRegistrationTestHandleTimeout( me );
		if( timeoutErr ) _RecordRegistrationTestStop( me, timeoutErr );
	} );
	dispatch_activate( me->timer );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================

static void	_RecordRegistrationTestStart( void * const inCtx )
{
	const RecordRegistrationTestRef me = (RecordRegistrationTestRef) inCtx;
	const OSStatus err = _RecordRegistrationTestSubtestStart( me );
	require_noerr( err, exit );
	
exit:
	if( err ) _RecordRegistrationTestStop( me, err );
}

//===========================================================================================================================

static OSStatus	_RecordRegistrationTestRun( const RecordRegistrationTestRef me )
{
	dispatch_async_f( me->queue, me, _RecordRegistrationTestStart );
	dispatch_semaphore_wait( me->doneSem, DISPATCH_TIME_FOREVER );
	return( me->error );
}

//===========================================================================================================================

static void	RecordRegistrationTestCommand( void )
{
	RecordRegistrationTestRef test = NULL;
	OSStatus err = CheckIntegerArgument( gRecordRegistrationTest_RRSetChangeIntervalMs, "interval", 0, INT_MAX );
	require_noerr_quiet( err, exit );
	
	unsigned int rrSetChangeIntervalMs = (unsigned int) gRecordRegistrationTest_RRSetChangeIntervalMs;
	if( rrSetChangeIntervalMs == 0 ) rrSetChangeIntervalMs = 1500;
	test = _RecordRegistrationTestCreate( rrSetChangeIntervalMs, &err );
	require_noerr( err, exit );
	
	err = _RecordRegistrationTestRun( test );
	require_noerr( err, exit );
	
exit:
	if( test ) _RecordRegistrationTestRelease( test );
    gExitCode = err ? 1 : 0;
}

//===========================================================================================================================
//	RecordCacheFlushTestCommand
//===========================================================================================================================

typedef struct RecordCacheFlushTest *		RecordCacheFlushTestRef;
struct RecordCacheFlushTest
{
	dispatch_queue_t			queue;				// Serial queue for test events.
	dispatch_semaphore_t		doneSem;			// Semaphore to signal when the test is done.
	DNSServiceRef				queryA;				// Reference for DNSServiceQueryRecord() operation for A record.
	DNSServiceRef				queryAAAA;			// Reference for DNSServiceQueryRecord() operation for AAAA record.
	dispatch_source_t			timer;				// Timer for enforcing time limits.
	char *						recordName;			// Name of records to put in record cache and then flush.
	MDNSColliderRef				colliderA;			// Collider for sending an A record to put in record cache.
	MDNSColliderRef				colliderAAAA;		// Collider for sending a AAAA record to put in record cache.
	mrc_record_cache_flush_t	flush;				// Record cache flush object to flush records.
	int32_t						refCount;			// Test's reference count.
	OSStatus					error;				// Test's error code.
	uint8_t						rdataAAAA[ 16 ];	// The randomly-generated AAAA record RDATA base for the RRSet.
	uint8_t						rdataA[ 4 ];		// The randomly-generated A record RDATA base for the RRSet.
	Boolean						haveRecordA;		// True if the A record has an outstanding Add result.
	Boolean						haveRecordAAAA;		// True if the AAAA record has an outstanding Add result.
};

//===========================================================================================================================

static void	_RecordCacheFlushTestRetain( const RecordCacheFlushTestRef me )
{
	atomic_add_32( &me->refCount, 1 );
}

//===========================================================================================================================

static void	_RecordCacheFlushTestRelease( const RecordCacheFlushTestRef me )
{
	if( atomic_add_and_fetch_32( &me->refCount, -1 ) == 0 )
	{
		dispatch_forget( &me->queue );
		dispatch_forget( &me->doneSem );
		check( !me->queryA );
		check( !me->queryAAAA );
		check( !me->timer );
		check( !me->recordName );
		check( !me->colliderA );
		check( !me->colliderAAAA );
		check( !me->flush );
		free( me );
	}
}

//===========================================================================================================================

static RecordCacheFlushTestRef	_RecordCacheFlushTestCreate( OSStatus * const outError )
{
	OSStatus err;
	RecordCacheFlushTestRef test = NULL;
	RecordCacheFlushTestRef obj = (RecordCacheFlushTestRef) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->refCount	= 1;
	obj->error		= kInProgressErr;
	obj->queue = dispatch_queue_create( "com.apple.dnssdutil.record-cache-flush-test", DISPATCH_QUEUE_SERIAL );
	require_action( obj->queue, exit, err = kNoResourcesErr );
	
	obj->doneSem = dispatch_semaphore_create( 0 );
	require_action( obj->doneSem, exit, err = kNoResourcesErr );
	
	test = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( outError ) *outError = err;
	if( obj ) _RecordCacheFlushTestRelease( obj );
	return( test );
}

//===========================================================================================================================

static void	_RecordCacheFlushTestStop( const RecordCacheFlushTestRef me, const OSStatus inError )
{
	me->error = inError;
	if( !me->error )
	{
		FPrintF( stdout, "%{du:time} Test PASSED\n", NULL );
	}
	else
	{
		FPrintF( stdout, "%{du:time} Test FAILED: %#m\n", NULL, me->error );
	}
	DNSServiceForget( &me->queryA );
	DNSServiceForget( &me->queryAAAA );
	dispatch_source_forget( &me->timer );
	ForgetMem( &me->recordName );
	MDNSColliderForget( &me->colliderA );
	MDNSColliderForget( &me->colliderAAAA );
	mrc_record_cache_flush_forget( &me->flush );
	dispatch_semaphore_signal( me->doneSem );
}

//===========================================================================================================================

static void DNSSD_API
	_RecordCacheFlushTestQueryRecordCallback(
		__unused const DNSServiceRef	inSDRef,
		const DNSServiceFlags			inFlags,
		__unused const uint32_t			inIfIndex,
		const DNSServiceErrorType		inError,
		const char * const				inFullName,
		const uint16_t					inType,
		const uint16_t					inClass,
		const uint16_t					inRDataLen,
		const void * const				inRDataPtr,
		__unused const uint32_t			inTTL,
		void * const					inCtx )
{
	OSStatus err;
	const RecordCacheFlushTestRef me = (RecordCacheFlushTestRef) inCtx;
	
	// Print result.
	
	char ifNameBuf[ kInterfaceNameBufLen ];
	const char * const ifName = InterfaceIndexToName( inIfIndex, ifNameBuf );
	char typeBuf[ 32 ];
	const char *typeStr = DNSRecordTypeValueToString( inType );
	if( !typeStr )
	{
		SNPrintF( typeBuf, sizeof( typeBuf ), "TYPE%u", inType );
		typeStr = typeBuf;
	}
	char classBuf[ 32 ];
	const char *classStr;
	if( inClass == kDNSServiceClass_IN )
	{
		classStr = "IN";
	}
	else
	{
		SNPrintF( classBuf, sizeof( classBuf ), "CLASS%u", inClass );
		classStr = classBuf;
	}
	char *rdataStr = NULL;
	DNSRecordDataToString( inRDataPtr, inRDataLen, inType, &rdataStr );
	if( !rdataStr )
	{
		ASPrintF( &rdataStr, "%#H", inRDataPtr, (int) inRDataLen, (int) inRDataLen );
		require_action( rdataStr, exit, err = kNoMemoryErr );
	}
	FPrintF( stdout, "%{du:time} → QueryRecord result -- flags: 0x%0X (%s), interface: %s/%d, error: %#m, name: %s,"
		" type: %s, class: %s, ttl: %u, rdata: %s\n",
		NULL, inFlags, (inFlags & kDNSServiceFlagsAdd) ? "Add" : "Rmv", ifName ? ifName : "", (int32_t) inIfIndex, inError,
		inFullName, typeStr, classStr, inTTL, rdataStr );
	
	// Check callback arguments.
	
	uint8_t fullName[ kDomainNameLengthMax ];
	err = DomainNameFromString( fullName, inFullName, NULL );
	require_noerr_quiet( err, exit );
	
	uint8_t recordName[ kDomainNameLengthMax ];
	err = DomainNameFromString( recordName, me->recordName, NULL );
	require_noerr_quiet( err, exit );
	
	const Boolean nameMatch = DomainNameEqual( fullName, recordName );
	require_action( nameMatch, exit, err = kNameErr );
	require_action( inClass == kDNSServiceClass_IN, exit, err = kTypeErr );
	
	switch( inType )
	{
		case kDNSServiceType_A:
		{
			require_action( inRDataLen == 4, exit, err = kSizeErr );
			
			const int cmp = memcmp( inRDataPtr, me->rdataA, sizeof( me->rdataA ) );
			require_action( cmp == 0, exit, err = kValueErr );
			break;
		}
		case kDNSServiceType_AAAA:
		{
			require_action( inRDataLen == 16, exit, err = kSizeErr );
			
			const int cmp = memcmp( inRDataPtr, me->rdataAAAA, sizeof( me->rdataAAAA ) );
			require_action( cmp == 0, exit, err = kValueErr );
			break;
		}
		default:
			err = kTypeErr;
			goto exit;
	}
	err = inError;
	require_noerr( err, exit );
	
	if( inFlags & kDNSServiceFlagsAdd )
	{
		// Add events are only expected before flushing the records from the record cache. After the flush, there should
		// be no Add events.
		
		require_action( !me->flush, exit, err = kUnexpectedErr );
		
		// Make sure we get exactly one Add for the A record and exactly one Add for the AAAA record.
		
		switch( inType )
		{
			case kDNSServiceType_A:
				require_action( !me->haveRecordA, exit, err = kDuplicateErr );
				
				me->haveRecordA = true;
				break;
			
			case kDNSServiceType_AAAA:
				require_action( !me->haveRecordAAAA, exit, err = kDuplicateErr );
				
				me->haveRecordAAAA = true;
				break;
		}
	}
	else
	{
		// Rmv events are only expected after flushing the records from the record cache. Before the flush, there should
		// be no Rmv events.
		
		require_action( me->flush, exit, err = kUnexpectedErr );
		
		// Make sure we get exactly one Rmv for the A record and exactly one Rmv for the AAAA record. Also, there should
		// be no Rmv event unless there was a corresponding Add event beforehand.
		
		switch( inType )
		{
			case kDNSServiceType_A:
				require_action( me->haveRecordA, exit, err = kUnexpectedErr );
				
				me->haveRecordA = false;
				break;
			
			case kDNSServiceType_AAAA:
				require_action( me->haveRecordAAAA, exit, err = kUnexpectedErr );
				
				me->haveRecordAAAA = false;
				break;
		}
	}
	
exit:
	ForgetMem( &rdataStr );
	if( err ) _RecordCacheFlushTestStop( me, err );
}

//===========================================================================================================================

static OSStatus	_RecordCacheFlushTestHandlePostFlushTimeout( const RecordCacheFlushTestRef me )
{
	OSStatus err;
	if( !me->haveRecordA && !me->haveRecordAAAA )
	{
		FPrintF( stdout, "%{du:time} ✓ Got expected QueryRecord Rmv results\n", NULL );
		err = kNoErr;
	}
	else
	{
		if( me->haveRecordA )
		{
			FPrintF( stdout, "%{du:time} x Didn't get Rmv for A record", NULL );
		}
		if( me->haveRecordAAAA )
		{
			FPrintF( stdout, "%{du:time} x Didn't get Rmv for AAAA record", NULL );
		}
		err = kUnexpectedErr;
	}
	return( err );
}

//===========================================================================================================================

static OSStatus	_RecordCacheFlushTestHandlePreFlushTimeout( const RecordCacheFlushTestRef me )
{
	OSStatus err;
	mdns_domain_name_t recordName = NULL;
	if( me->haveRecordA && me->haveRecordAAAA )
	{
		FPrintF( stdout, "%{du:time} ✓ Got expected QueryRecord Add results\n", NULL );
		
		// Start record cache flush.
		
		FPrintF( stdout, "%{du:time} ⚒ Flushing %s records from record cache\n", NULL, me->recordName );
		recordName = mdns_domain_name_create( me->recordName, 0, &err );
		require_noerr( err, exit );
		
		check( !me->flush );
		me->flush = mrc_record_cache_flush_create();
		require_action( me->flush, exit, err = kNoResourcesErr );
		
		mrc_record_cache_flush_set_queue( me->flush, me->queue );
		mrc_record_cache_flush_set_record_name( me->flush, recordName );
		_RecordCacheFlushTestRetain( me );
		mrc_record_cache_flush_set_result_handler( me->flush,
		^( const mrc_record_cache_flush_result_t inResult, const OSStatus inError )
		{
			if( me->flush )
			{
				switch( inResult )
				{
					case mrc_record_cache_flush_result_complete:
						FPrintF( stdout, "%{du:time} ✓ Record cache flush result: complete\n", NULL );
						break;
					
					case mrc_record_cache_flush_result_incomplete:
						FPrintF( stdout, "%{du:time} x Record cache flush result: incomplete", NULL );
						if( inError )
						{
							FPrintF( stdout, " (error: %#m)", inError );
						}
						FPrintF( stdout, "\n" );
						break;
				}
				if( inError ) _RecordCacheFlushTestStop( me, inError );
			}
			_RecordCacheFlushTestRelease( me );
		} );
		mrc_record_cache_flush_activate( me->flush );
		
		// Start timer.
		
		check( !me->timer );
		me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
		require_action_quiet( me->timer, exit, err = kNoResourcesErr );
		
		const unsigned int timeLimitMs = 3 * kMillisecondsPerSecond;
		FPrintF( stdout, "%{du:time} ⧗ Will check QueryRecord results after %u ms\n", NULL, timeLimitMs );
		dispatch_source_set_timer( me->timer, dispatch_time_milliseconds( timeLimitMs ), DISPATCH_TIME_FOREVER, 0 );
		dispatch_source_set_event_handler( me->timer,
		^{
			dispatch_source_forget( &me->timer );
			const OSStatus timeoutErr = _RecordCacheFlushTestHandlePostFlushTimeout( me );
			_RecordCacheFlushTestStop( me, timeoutErr );
		} );
		dispatch_activate( me->timer );
	}
	else
	{
		if( !me->haveRecordA )
		{
			FPrintF( stdout, "%{du:time} x Didn't get Add for A record", NULL );
		}
		if( !me->haveRecordAAAA )
		{
			FPrintF( stdout, "%{du:time} x Didn't get Add for AAAA record", NULL );
		}
		err = kUnexpectedErr;
	}
	
exit:
	mdns_forget( &recordName );
	return( err );
}

//===========================================================================================================================

static void	_RecordCacheFlushTestStart( const RecordCacheFlushTestRef me )
{
	// Create a randomly-generated record name.
	
	OSStatus err;
	char tag[ 6 + 1 ];
	_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag );
	check( !me->recordName );
	ASPrintF( &me->recordName, "record-cache-flush-test-%s.local", tag );
	require_action( me->recordName, exit, err = kNoMemoryErr );
	
	// Query for A record.
	
	FPrintF( stdout, "%{du:time} ☀ Starting QueryRecord for %s A record\n", NULL, me->recordName );
	check( !me->queryA );
	const uint32_t loopbackIndex = _RecordRegistrationTestGetLoopbackInterfaceIndex();
	err = DNSServiceQueryRecord( &me->queryA, 0, loopbackIndex, me->recordName, kDNSServiceType_A, kDNSServiceClass_IN,
		_RecordCacheFlushTestQueryRecordCallback, me );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( me->queryA, me->queue );
	require_noerr( err, exit );
	
	// Query for AAAA record.
	
	FPrintF( stdout, "%{du:time} ☀ Starting QueryRecord for %s AAAA record\n", NULL, me->recordName );
	check( !me->queryAAAA );
	err = DNSServiceQueryRecord( &me->queryAAAA, 0, loopbackIndex, me->recordName, kDNSServiceType_AAAA, kDNSServiceClass_IN,
		_RecordCacheFlushTestQueryRecordCallback, me );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( me->queryAAAA, me->queue );
	require_noerr( err, exit );
	
	// Generate random A and AAAA record RDATA.
	
	RandomBytes( me->rdataA, sizeof( me->rdataA ) );
	RandomBytes( me->rdataAAAA, sizeof( me->rdataAAAA ) );
	
	// Create collider to send A record.
	
	uint8_t recordName[ kDomainNameLengthMax ];
	err = DomainNameFromString( recordName, me->recordName, NULL );
	require_noerr( err, exit );
	
	MDNSColliderRef collider = NULL;
	FPrintF( stdout, "%{du:time} ✈ Sending mDNS message containing record: %s IN A %.4a\n",
		NULL, me->recordName, me->rdataA );
	err = MDNSColliderCreate( me->queue, &collider );
	require_noerr( err, exit );
	
	MDNSColliderSetProtocols( collider, kMDNSColliderProtocol_IPv4 );
	MDNSColliderSetInterfaceIndex( collider, loopbackIndex );
	err = MDNSColliderSetRecord( collider, recordName, kDNSServiceType_A, me->rdataA, sizeof( me->rdataA ) );
	require_noerr( err, exit );
	err = MDNSColliderSetProgram( collider, "send" );
	require_noerr( err, exit );
	
	err = MDNSColliderStart( collider );
	require_noerr( err, exit );
	
	check( !me->colliderA );
	me->colliderA = collider;
	collider = NULL;
	
	// Create collider to send AAAA record.
	
	FPrintF( stdout, "%{du:time} ✈ Sending mDNS message containing record: %s IN AAAA %.16a\n",
		NULL, me->recordName, me->rdataAAAA );
	err = MDNSColliderCreate( me->queue, &collider );
	require_noerr( err, exit );
	
	MDNSColliderSetProtocols( collider, kMDNSColliderProtocol_IPv4 );
	MDNSColliderSetInterfaceIndex( collider, loopbackIndex );
	err = MDNSColliderSetRecord( collider, recordName, kDNSServiceType_AAAA, me->rdataAAAA, sizeof( me->rdataAAAA ) );
	require_noerr( err, exit );
	
	err = MDNSColliderSetProgram( collider, "send" );
	require_noerr( err, exit );
	
	err = MDNSColliderStart( collider );
	require_noerr( err, exit );
	
	check( !me->colliderAAAA );
	me->colliderAAAA = collider;
	collider = NULL;
	
	// Start timer.
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	const unsigned int timeLimitMs = 3 * kMillisecondsPerSecond;
	FPrintF( stdout, "%{du:time} ⧗ Will check QueryRecord results after %u ms\n", NULL, timeLimitMs );
	dispatch_source_set_timer( me->timer, dispatch_time_milliseconds( timeLimitMs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		dispatch_source_forget( &me->timer );
		const OSStatus timeoutErr = _RecordCacheFlushTestHandlePreFlushTimeout( me );
		if( timeoutErr ) _RecordCacheFlushTestStop( me, timeoutErr );
	} );
	dispatch_activate( me->timer );
	
exit:
	if( err ) _RecordCacheFlushTestStop( me, err );
}

//===========================================================================================================================

static OSStatus	_RecordCacheFlushTestRun( const RecordCacheFlushTestRef me )
{
	dispatch_async( me->queue,
	^{
		_RecordCacheFlushTestStart( me );
	} );
	dispatch_semaphore_wait( me->doneSem, DISPATCH_TIME_FOREVER );
	return( me->error );
}

//===========================================================================================================================

static void	RecordCacheFlushTestCommand( void )
{
	OSStatus err;
	RecordCacheFlushTestRef test = _RecordCacheFlushTestCreate( &err );
	require_noerr( err, exit );
	
	err = _RecordCacheFlushTestRun( test );
	require_noerr( err, exit );
	
exit:
	if( test ) _RecordCacheFlushTestRelease( test );
    gExitCode = err ? 1 : 0;
}

//===========================================================================================================================
//	ResolverOverrideTestCommand
//===========================================================================================================================

// Each subtest uses DNSServiceQueryRecordWithAttribute() to query for an A record that can only be resolved via a DNS
// server that is launched by the test itself. The DNS server is configured to only provide responses for domain names
// in a randomized domain such as resolver-override-test-00zn9v.test, and is only capable of receiving DNS queries on
// the loopback interface at a randomly-selected port.
//
// A libnetwork resolver configuration that describes the test's DNS service is published, but it uses a bogus
// randomized domain such as resolver-override-test-cgm5xp.invalid, to preclude the possibility of mDNSResponder using
// domain matching to assign the test's DNS service to the test's DNS queries.
//
// The test uses DNSServiceAttributeSetResolverOverride() to set the libnetwork resolver configuration's UUID in the
// attribute passed to DNSServiceQueryRecordWithAttribute(). This should cause the associated DNS queries to be sent to
// the test's DNS service, which is the only way to get the correct RDATA. Some subtests don't use the resolver override
// attribute at all or use the resolver attribute with the incorrect resolver configuration UUID to verify that the
// correct RDATA cannot be obtained unless the correct resolver UUID is used.
//
// Subtests are performed with and without the kDNSServiceFlagsPathEvaluationDone flag to make sure that the expected
// outcome occurs regardless of whether or not path evaluation is performed. Path evaluation sometimes influences DNS
// service selection, but it should have no effect when a resolver override is used.

typedef enum
{
	kResolverOverrideAction_UseCorrectUUID		= 1,
	kResolverOverrideAction_UseIncorrectUUID	= 2,
	kResolverOverrideAction_DoNotUse			= 3,
	
}	ResolverOverrideAction;

typedef struct
{
	DNSServiceFlags			flags;
	ResolverOverrideAction	resolverOverrideAction;
	Boolean					queryRecordsWithCNAMEs;
	
}	ResolverOverrideSubtest;

static const ResolverOverrideSubtest		kResolverOverrideSubtests[] =
{
	{
		.resolverOverrideAction	= kResolverOverrideAction_DoNotUse,
		.flags					= 0,
		.queryRecordsWithCNAMEs	= false,
	},
	{
		.resolverOverrideAction	= kResolverOverrideAction_DoNotUse,
		.flags					= kDNSServiceFlagsPathEvaluationDone,
		.queryRecordsWithCNAMEs	= false,
	},
	{
		.resolverOverrideAction	= kResolverOverrideAction_UseCorrectUUID,
		.flags					= 0,
		.queryRecordsWithCNAMEs	= false,
	},
	{
		.resolverOverrideAction	= kResolverOverrideAction_UseCorrectUUID,
		.flags					= kDNSServiceFlagsPathEvaluationDone,
		.queryRecordsWithCNAMEs	= false,
	},
	{
		.resolverOverrideAction	= kResolverOverrideAction_UseCorrectUUID,
		.flags					= 0,
		.queryRecordsWithCNAMEs	= true,
	},
	{
		.resolverOverrideAction	= kResolverOverrideAction_UseCorrectUUID,
		.flags					= kDNSServiceFlagsPathEvaluationDone,
		.queryRecordsWithCNAMEs	= true,
	},
	{
		.resolverOverrideAction	= kResolverOverrideAction_UseIncorrectUUID,
		.flags					= 0,
		.queryRecordsWithCNAMEs	= false,
	},
	{
		.resolverOverrideAction	= kResolverOverrideAction_UseIncorrectUUID,
		.flags					= kDNSServiceFlagsPathEvaluationDone,
		.queryRecordsWithCNAMEs	= false,
	},
};

typedef struct ResolverOverrideTest *		ResolverOverrideTestRef;
struct ResolverOverrideTest
{
	dispatch_queue_t			queue;					// Serial queue for test events.
	dispatch_semaphore_t		doneSem;				// Semaphore to signal when the test is done.
	DNSServerRef				server;					// Reference to DNS server.
	DNSServiceAttributeRef		resolverOverrideAttr;	// Attribute for the resolver override.
	nw_resolver_config_t		resolverConfig;			// Resolver configuration for DNS service.
	DNSServiceRef				query;					// Reference for the QueryRecord operation to query for A records.
	dispatch_source_t			timer;					// Timer for enforcing time limits.
	size_t						subtestIndex;			// Index of current subtest.
	char *						recordName;				// Name of records to put in record cache and then flush.
	char *						domain;					// DNS service's domain.
	sockaddr_ip					serverAddr;				// DNS server address.
	int32_t						refCount;				// Test's reference count.
	OSStatus					error;					// Test's error code.
	uuid_t						resolverUUID;			// Resolver configuration's UUID.
	uint8_t						expectedRData[ 4 ];		// Expected A record RDATA in the result of the current QueryRecord.
	Boolean						gotRData;				// True if we got a QueryRecord Add result for the record name.
	Boolean						stopped;				// True if the test has been stopped.
};

//===========================================================================================================================

static void	_ResolverOverrideTestRelease( const ResolverOverrideTestRef me )
{
	if( atomic_add_and_fetch_32( &me->refCount, -1 ) == 0 )
	{
		dispatch_forget( &me->queue );
		dispatch_forget( &me->doneSem );
		check( !me->server );
		check( !me->resolverConfig );
		check( !me->resolverOverrideAttr );
		check( !me->query );
		check( !me->timer );
		check( !me->recordName );
		check( !me->domain );
		free( me );
	}
}

//===========================================================================================================================

static ResolverOverrideTestRef	_ResolverOverrideTestCreate( OSStatus * const outError )
{
	OSStatus err;
	ResolverOverrideTestRef test = NULL;
	ResolverOverrideTestRef obj = (ResolverOverrideTestRef) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->refCount	= 1;
	obj->error		= kInProgressErr;
	obj->queue = dispatch_queue_create( "com.apple.dnssdutil.resolver-override-test", DISPATCH_QUEUE_SERIAL );
	require_action( obj->queue, exit, err = kNoResourcesErr );
	
	obj->doneSem = dispatch_semaphore_create( 0 );
	require_action( obj->doneSem, exit, err = kNoResourcesErr );
	
	test = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( outError ) *outError = err;
	if( obj ) _ResolverOverrideTestRelease( obj );
	return( test );
}

//===========================================================================================================================

static void	_ResolverOverrideTestStop( const ResolverOverrideTestRef me, const OSStatus inError )
{
	require_return( !me->stopped );
	
	me->stopped = true;
	me->error = inError;
	if( me->resolverConfig )
	{
		uuid_t uuid;
		uuid_string_t uuidStr;
		nw_resolver_config_get_identifier( me->resolverConfig, uuid );
		uuid_unparse_upper( uuid, uuidStr );
		FPrintF( stdout, "\n%{du:time} ⚒ Unpublishing resolver configuration (%s): %@\n",
			NULL, uuidStr, me->resolverConfig );
		nw_resolver_config_unpublish( me->resolverConfig );
		nw_forget( &me->resolverConfig );
	}
	FPrintF( stdout, "\n" );
	if( !me->error )
	{
		FPrintF( stdout, "%{du:time} Test PASSED\n", NULL );
	}
	else
	{
		FPrintF( stdout, "%{du:time} Test FAILED: %#m\n", NULL, me->error );
	}
	if( me->server )
	{
		_DNSServerStop( me->server );
		CFForget( &me->server );
	}
	_DNSServiceAttrForget( &me->resolverOverrideAttr );
	DNSServiceForget( &me->query );
	dispatch_source_forget( &me->timer );
	ForgetMem( &me->recordName );
	ForgetMem( &me->domain );
	dispatch_semaphore_signal( me->doneSem );
}

//===========================================================================================================================

static const ResolverOverrideSubtest *	_ResolverOverrideTestGetCurrentSubtest( const ResolverOverrideTestRef me )
{
	require_fatal( me->subtestIndex < countof( kResolverOverrideSubtests ),
		"Invalid subtest index %zu >= %zu", me->subtestIndex, countof( kResolverOverrideSubtests ) );
	return( &kResolverOverrideSubtests[ me->subtestIndex ] );
}

//===========================================================================================================================

static void DNSSD_API
	_ResolverOverrideTestQueryRecordCallback(
		__unused const DNSServiceRef	inSDRef,
		const DNSServiceFlags			inFlags,
		__unused const uint32_t			inIfIndex,
		const DNSServiceErrorType		inError,
		const char * const				inFullName,
		const uint16_t					inType,
		const uint16_t					inClass,
		const uint16_t					inRDataLen,
		const void * const				inRDataPtr,
		__unused const uint32_t			inTTL,
		void * const					inCtx )
{
	OSStatus err;
	const ResolverOverrideTestRef me = (ResolverOverrideTestRef) inCtx;
	
	// Print result.
	
	char ifNameBuf[ kInterfaceNameBufLen ];
	const char * const ifName = InterfaceIndexToName( inIfIndex, ifNameBuf );
	char typeBuf[ 32 ];
	const char *typeStr = DNSRecordTypeValueToString( inType );
	if( !typeStr )
	{
		SNPrintF( typeBuf, sizeof( typeBuf ), "TYPE%u", inType );
		typeStr = typeBuf;
	}
	char classBuf[ 32 ];
	const char *classStr;
	if( inClass == kDNSServiceClass_IN )
	{
		classStr = "IN";
	}
	else
	{
		SNPrintF( classBuf, sizeof( classBuf ), "CLASS%u", inClass );
		classStr = classBuf;
	}
	char *rdataStr = NULL;
	DNSRecordDataToString( inRDataPtr, inRDataLen, inType, &rdataStr );
	if( !rdataStr )
	{
		ASPrintF( &rdataStr, "%#H", inRDataPtr, (int) inRDataLen, (int) inRDataLen );
		require_action( rdataStr, exit, err = kNoMemoryErr );
	}
	FPrintF( stdout, "%{du:time} → QueryRecord result -- flags: 0x%0X (%s), interface: %s/%d, error: %#m, name: %s,"
		" type: %s, class: %s, ttl: %u, rdata: %s\n",
		NULL, inFlags, (inFlags & kDNSServiceFlagsAdd) ? "Add" : "Rmv", ifName ? ifName : "", (int32_t) inIfIndex, inError,
		inFullName, typeStr, classStr, inTTL, rdataStr );
	
	// Check callback arguments.
	
	uint8_t fullName[ kDomainNameLengthMax ];
	err = DomainNameFromString( fullName, inFullName, NULL );
	require_noerr_quiet( err, exit );
	
	uint8_t queriedRecordName[ kDomainNameLengthMax ];
	err = DomainNameFromString( queriedRecordName, me->recordName, NULL );
	require_noerr_quiet( err, exit );
	
	const uint8_t *expectedRecordName;
	const ResolverOverrideSubtest * const subtest = _ResolverOverrideTestGetCurrentSubtest( me );
	if( subtest->queryRecordsWithCNAMEs )
	{
		expectedRecordName = DomainNameGetNextLabel( queriedRecordName ); // Skip alias label to get to the canonical name.
	}
	else
	{
		expectedRecordName = queriedRecordName;
	}
	const Boolean nameMatch = DomainNameEqual( fullName, expectedRecordName );
	require_action( nameMatch, exit, err = kNameErr );
	require_action( inClass == kDNSServiceClass_IN, exit, err = kTypeErr );
	
	switch( inType )
	{
		case kDNSServiceType_A:
		{
			require_action( inRDataLen == sizeof( me->expectedRData ), exit, err = kSizeErr );
			
			const int cmp = memcmp( inRDataPtr, me->expectedRData, sizeof( me->expectedRData ) );
			require_action( cmp == 0, exit, err = kValueErr );
			break;
		}
		default:
			err = kTypeErr;
			goto exit;
	}
	err = inError;
	require_noerr( err, exit );
	require_action( inFlags & kDNSServiceFlagsAdd, exit, err = kUnexpectedErr );
	require_action( !me->gotRData, exit, err = kDuplicateErr );
	
	me->gotRData = true;
	
exit:
	ForgetMem( &rdataStr );
	if( err ) _ResolverOverrideTestStop( me, err );
}

//===========================================================================================================================

static void	_ResolverOverrideTestRetain( const ResolverOverrideTestRef me )
{
	atomic_add_32( &me->refCount, 1 );
}

//===========================================================================================================================

static OSStatus	_ResolverOverrideTestHandleTimeout( ResolverOverrideTestRef inTest );

static OSStatus	_ResolverOverrideTestStartSubtest( const ResolverOverrideTestRef me )
{
	OSStatus err;
	if( me->subtestIndex >= countof( kResolverOverrideSubtests ) )
	{
		_ResolverOverrideTestRetain( me );
		dispatch_async( me->queue,
		^{
			_ResolverOverrideTestStop( me, kNoErr );
			_ResolverOverrideTestRelease( me );
		} );
		err = kNoErr;
		goto exit;
	}
	const char *resolverOverrideActionStr;
	const ResolverOverrideSubtest * const subtest = _ResolverOverrideTestGetCurrentSubtest( me );
	switch( subtest->resolverOverrideAction )
	{
		case kResolverOverrideAction_UseCorrectUUID:
			resolverOverrideActionStr = "correct UUID";
			break;
		
		case kResolverOverrideAction_UseIncorrectUUID:
			resolverOverrideActionStr = "incorrect UUID";
			break;
		
		case kResolverOverrideAction_DoNotUse:
			resolverOverrideActionStr = "not used";
			break;
		
		default:
			FatalErrorF( "Unhandled resolver overraide action value: %d", subtest->resolverOverrideAction );
	}
	FPrintF( stdout, "\n%{du:time} Subtest %zu: Querying for A record -- "
		"resolver override: %s, flags: %#{flags}, record has CNAMEs: %s\n",
		NULL, me->subtestIndex + 1, resolverOverrideActionStr, subtest->flags, kDNSServiceFlagsDescriptors,
		YesNoStr( subtest->queryRecordsWithCNAMEs ) );
	
	// Create record name.
	
	char tag[ 6 + 1 ];
	_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag );
	const uint8_t offset = (uint8_t)( ( me->subtestIndex + 1 ) * 10 );
	ForgetMem( &me->recordName );
	ASPrintF( &me->recordName,
		"%stag-%s.offset-%u.%s", subtest->queryRecordsWithCNAMEs ? "alias-3." : "", tag, offset, me->domain );
	require_action( me->recordName, exit, err = kNoMemoryErr );
	
	// Create an attribute for the resolver UUID.
	
	DNSServiceAttributeRef attr;
	switch( subtest->resolverOverrideAction )
	{
		case kResolverOverrideAction_UseCorrectUUID:
		case kResolverOverrideAction_UseIncorrectUUID:
			if( !me->resolverOverrideAttr )
			{
				me->resolverOverrideAttr = DNSServiceAttributeCreate();
				require_action( me->resolverOverrideAttr, exit, err = kNoResourcesErr );
			}
			uuid_t randomUUID;
			uuid_clear( randomUUID );
			const uuid_t *resolverUUID;
			if( subtest->resolverOverrideAction == kResolverOverrideAction_UseCorrectUUID )
			{
				resolverUUID = &me->resolverUUID;
			}
			else
			{
				uuid_generate_random( randomUUID );
				resolverUUID = &randomUUID;
			}
			if( __builtin_available( macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, * ) )
			{
				uuid_string_t uuidStr;
				uuid_unparse_upper( *resolverUUID, uuidStr );
				FPrintF( stdout, "%{du:time} ⚒ Setting resolver override: %s\n", NULL, uuidStr );
				err = DNSServiceAttributeSetResolverOverride( me->resolverOverrideAttr, *resolverUUID );
				require_noerr( err, exit );
			}
			else
			{
				FPrintF( stderr, "DNSServiceAttributeSetResolverOverride() is not available on this OS build." );
				err = kUnsupportedErr;
				goto exit;
			}
			attr = me->resolverOverrideAttr;
			break;
		
		case kResolverOverrideAction_DoNotUse:
			attr = NULL;
			break;
		
		default:
			FatalErrorF( "Unhandled resolver overraide action value: %d", subtest->resolverOverrideAction );
	}
	// Query for A record.
	
	FPrintF( stdout, "%{du:time} ☀ Starting QueryRecord for %s A record\n", NULL, me->recordName );
	const uint32_t expectedIPv4Addr = kDNSServerBaseAddrV4 + 1 + offset;
	WriteBig32Typed( me->expectedRData, expectedIPv4Addr );
	check( !me->query );
	err = DNSServiceQueryRecordWithAttribute( &me->query, subtest->flags, kDNSServiceInterfaceIndexAny, me->recordName,
		kDNSServiceType_A, kDNSServiceClass_IN, attr, _ResolverOverrideTestQueryRecordCallback, me );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( me->query, me->queue );
	require_noerr( err, exit );
	
	// Start timer.
	
	check( !me->timer );
	me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
	require_action_quiet( me->timer, exit, err = kNoResourcesErr );
	
	const unsigned int timeLimitSecs = 3;
	FPrintF( stdout, "%{du:time} ⧖ Will check QueryRecord results after %u seconds\n", NULL, timeLimitSecs );
	dispatch_source_set_timer( me->timer, dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER, 0 );
	dispatch_source_set_event_handler( me->timer,
	^{
		const OSStatus timeoutErr = _ResolverOverrideTestHandleTimeout( me );
		if( timeoutErr ) _ResolverOverrideTestStop( me, timeoutErr );
	} );
	dispatch_activate( me->timer );
	
exit:
	return( err );
}

//===========================================================================================================================

static OSStatus	_ResolverOverrideTestHandleTimeout( const ResolverOverrideTestRef me )
{
	OSStatus err;
	DNSServiceForget( &me->query );
	dispatch_source_forget( &me->timer );
	const ResolverOverrideSubtest * const subtest = _ResolverOverrideTestGetCurrentSubtest( me );
	switch( subtest->resolverOverrideAction )
	{
		case kResolverOverrideAction_UseCorrectUUID:
			FPrintF( stdout, "%{du:time} %s correct RDATA: %.4a\n",
				NULL, me->gotRData ? "✓ Got" : "x Didn't get", me->expectedRData );
			require_action( me->gotRData, exit, err = kValueErr );
			break;
		
		case kResolverOverrideAction_UseIncorrectUUID:
		case kResolverOverrideAction_DoNotUse:
			if( !me->gotRData )
			{
				FPrintF( stdout, "%{du:time} ✓ Didn't get RDATA, as expected, since correct resolver override wasn't used\n",
					NULL );
			}
			else
			{
				FPrintF( stdout, "%{du:time} x Got correct RDATA despite not using resolver override: %.4a\n",
					NULL, me->expectedRData );
			}
			require_action( !me->gotRData, exit, err = kUnexpectedErr );
			break;
		
		default:
			FatalErrorF( "Unhandled resolver overraide action value: %d", subtest->resolverOverrideAction );
	}
	++me->subtestIndex;
	me->gotRData = false;
	err = _ResolverOverrideTestStartSubtest( me );
	require_noerr( err, exit );
	
exit:
	return( err );
}

//===========================================================================================================================

static void	_ResolverOverrideTestDNSServerStartHandler( const uint16_t inDNSServerPort, void * const inCtx )
{
	OSStatus err;
	char *bogusDomain = NULL;
	const ResolverOverrideTestRef me = (ResolverOverrideTestRef) inCtx;
	SockAddrSetPort( &me->serverAddr, inDNSServerPort );
	nw_resolver_config_t resolverConfig = nw_resolver_config_create();
	require_action( resolverConfig, exit, err = kNoResourcesErr );
	
	nw_resolver_config_set_protocol( resolverConfig, nw_resolver_protocol_dns53 );
	nw_resolver_config_set_identifier( resolverConfig, me->resolverUUID );
	char serverAddrStr[ kSockAddrStringMaxSize ];
	err = SockAddrToString( &me->serverAddr.sa, kSockAddrStringFlagsNone, serverAddrStr );
	require_noerr( err, exit );
	
	nw_resolver_config_add_name_server( resolverConfig, serverAddrStr );
	char tag[ 6 + 1 ];
	_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag );
	ASPrintF( &bogusDomain, "resolver-override-test-%s.invalid", tag );
	require_action( bogusDomain, exit, err = kNoMemoryErr );
	
	nw_resolver_config_add_match_domain( resolverConfig, bogusDomain );
	uuid_t uuid;
	uuid_string_t uuidStr;
	nw_resolver_config_get_identifier( resolverConfig, uuid );
	uuid_unparse_upper( uuid, uuidStr );
	FPrintF( stdout, "%{du:time} ⚒ Publishing resolver configuration (%s): %@\n", NULL, uuidStr, resolverConfig );
	const bool ok = nw_resolver_config_publish( resolverConfig );
	require_action( ok, exit, err = kUnknownErr );
	
	check( !me->resolverConfig );
	me->resolverConfig = resolverConfig;
	resolverConfig = NULL;
	err = _ResolverOverrideTestStartSubtest( me );
	require_noerr( err, exit );
	
exit:
	ForgetMem( &bogusDomain );
	nw_forget( &resolverConfig );
	if( err ) _ResolverOverrideTestStop( me, err );
}

//===========================================================================================================================

static void	_ResolverOverrideTestDNSServerStopHandler( const OSStatus inError, void * const inCtx )
{
	if( inError )
	{
		FPrintF( stdout, "%{du:time} ! DNS server stopped with unexpected error: %#m\n", NULL, inError );
	}
	const ResolverOverrideTestRef me = (ResolverOverrideTestRef) inCtx;
	if( inError ) _ResolverOverrideTestStop( me, inError );
	_ResolverOverrideTestRelease( me );
}

//===========================================================================================================================

static void	_ResolverOverrideTestStart( const ResolverOverrideTestRef me )
{
	// Generate a resolver UUID.
	
	uuid_generate_random( me->resolverUUID );
	
	// Create a domain for DNS server.
	
	OSStatus err;
	char tag[ 6 + 1 ];
	_RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag );
	check( !me->domain );
	ASPrintF( &me->domain, "resolver-override-test-%s.test", tag );
	require_action( me->domain, exit, err = kNoMemoryErr );
	
	// Set up a loopback server address for DNS server.
	
	_SockAddrInitIPv4( &me->serverAddr.v4, INADDR_LOOPBACK, 0 );
	
	// Start up a DNS server.
	
	err = _DNSServerCreate( me->queue, _ResolverOverrideTestDNSServerStartHandler, _ResolverOverrideTestDNSServerStopHandler,
		me, 0, 60, &me->serverAddr, 1, me->domain, false, &me->server );
	require_noerr( err, exit );
	
	_ResolverOverrideTestRetain( me );
	_DNSServerStart( me->server );
	
exit:
	if( err ) _ResolverOverrideTestStop( me, err );
}

//===========================================================================================================================

static OSStatus	_ResolverOverrideTestRun( const ResolverOverrideTestRef me )
{
	dispatch_async( me->queue,
	^{
		_ResolverOverrideTestStart( me );
	} );
	dispatch_semaphore_wait( me->doneSem, DISPATCH_TIME_FOREVER );
	return( me->error );
}

//===========================================================================================================================

static void	ResolverOverrideTestCommand( void )
{
	OSStatus err;
	ResolverOverrideTestRef test = _ResolverOverrideTestCreate( &err );
	require_noerr( err, exit );
	
	err = _ResolverOverrideTestRun( test );
	require_noerr( err, exit );
	
exit:
	if( test ) _ResolverOverrideTestRelease( test );
    gExitCode = err ? 1 : 0;
}

//===========================================================================================================================
//	SSDPDiscoverCmd
//===========================================================================================================================

#define kSSDPPort		1900

typedef struct
{
	HTTPHeader				header;			// HTTP header object for sending and receiving.
	dispatch_source_t		readSourceV4;	// Read dispatch source for IPv4 socket.
	dispatch_source_t		readSourceV6;	// Read dispatch source for IPv6 socket.
	int						receiveSecs;	// After send, the amount of time to spend receiving.
	uint32_t				ifindex;		// Index of the interface over which to send the query.
	Boolean					useIPv4;		// True if the query should be sent via IPv4 multicast.
	Boolean					useIPv6;		// True if the query should be sent via IPv6 multicast.
	
}	SSDPDiscoverContext;

static void		SSDPDiscoverPrintPrologue( const SSDPDiscoverContext *inContext );
static void		SSDPDiscoverReadHandler( void *inContext );
static int		SocketToPortNumber( SocketRef inSock );
static OSStatus	WriteSSDPSearchRequest( HTTPHeader *inHeader, const void *inHostSA, int inMX, const char *inST );

static void	SSDPDiscoverCmd( void )
{
	OSStatus					err;
	struct timeval				now;
	SSDPDiscoverContext *		context;
	dispatch_source_t			signalSource	= NULL;
	SocketRef					sockV4			= kInvalidSocketRef;
	SocketRef					sockV6			= kInvalidSocketRef;
	ssize_t						n;
	int							sendCount;
	
	// Set up SIGINT handler.
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), Exit, kExitReason_SIGINT, &signalSource );
	require_noerr( err, exit );
	dispatch_resume( signalSource );
	
	// Check command parameters.
	
	if( gSSDPDiscover_ReceiveSecs < -1 )
	{
		FPrintF( stdout, "Invalid receive time: %d seconds.\n", gSSDPDiscover_ReceiveSecs );
		err = kParamErr;
		goto exit;
	}
	
	// Create context.
	
	context = (SSDPDiscoverContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	context->receiveSecs	= gSSDPDiscover_ReceiveSecs;
	context->useIPv4		= ( gSSDPDiscover_UseIPv4 || !gSSDPDiscover_UseIPv6 ) ? true : false;
	context->useIPv6		= ( gSSDPDiscover_UseIPv6 || !gSSDPDiscover_UseIPv4 ) ? true : false;
	
	err = InterfaceIndexFromArgString( gInterface, &context->ifindex );
	require_noerr_quiet( err, exit );
	
	// Set up IPv4 socket.
	
	if( context->useIPv4 )
	{
		int port;
		err = UDPClientSocketOpen( AF_INET, NULL, 0, -1, &port, &sockV4 );
		require_noerr( err, exit );
		
		err = SocketSetMulticastInterface( sockV4, NULL, context->ifindex );
		require_noerr( err, exit );
		
		err = setsockopt( sockV4, IPPROTO_IP, IP_MULTICAST_LOOP, (char *) &(uint8_t){ 1 }, (socklen_t) sizeof( uint8_t ) );
		err = map_socket_noerr_errno( sockV4, err );
		require_noerr( err, exit );
	}
	
	// Set up IPv6 socket.
	
	if( context->useIPv6 )
	{
		err = UDPClientSocketOpen( AF_INET6, NULL, 0, -1, NULL, &sockV6 );
		require_noerr( err, exit );
		
		err = SocketSetMulticastInterface( sockV6, NULL, context->ifindex );
		require_noerr( err, exit );
		
		err = setsockopt( sockV6, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, (char *) &(int){ 1 }, (socklen_t) sizeof( int ) );
		err = map_socket_noerr_errno( sockV6, err );
		require_noerr( err, exit );
	}
	
	// Print prologue.
	
	SSDPDiscoverPrintPrologue( context );
	
	// Send mDNS query message.
	
	sendCount = 0;
	if( IsValidSocket( sockV4 ) )
	{
		struct sockaddr_in		mcastAddr4;
		
		_SockAddrInitIPv4( &mcastAddr4, UINT32_C( 0xEFFFFFFA ), kSSDPPort );	// 239.255.255.250
		
		err = WriteSSDPSearchRequest( &context->header, &mcastAddr4, gSSDPDiscover_MX, gSSDPDiscover_ST );
		require_noerr( err, exit );
		
		n = sendto( sockV4, context->header.buf, context->header.len, 0, (const struct sockaddr *) &mcastAddr4,
			(socklen_t) sizeof( mcastAddr4 ) );
		err = map_socket_value_errno( sockV4, n == (ssize_t) context->header.len, n );
		if( err )
		{
			FPrintF( stderr, "*** Failed to send query on IPv4 socket with error %#m\n", err );
			ForgetSocket( &sockV4 );
		}
		else
		{
			if( gSSDPDiscover_Verbose )
			{
				gettimeofday( &now, NULL );
				FPrintF( stdout, "---\n" );
				FPrintF( stdout, "Send time:    %{du:time}\n",	&now );
				FPrintF( stdout, "Source Port:  %d\n",			SocketToPortNumber( sockV4 ) );
				FPrintF( stdout, "Destination:  %##a\n",		&mcastAddr4 );
				FPrintF( stdout, "Message size: %zu\n",			context->header.len );
				FPrintF( stdout, "HTTP header:\n%1{text}",		context->header.buf, context->header.len );
			}
			++sendCount;
		}
	}
	
	if( IsValidSocket( sockV6 ) )
	{
		struct sockaddr_in6		mcastAddr6;
		
		memset( &mcastAddr6, 0, sizeof( mcastAddr6 ) );
		SIN6_LEN_SET( &mcastAddr6 );
		mcastAddr6.sin6_family				= AF_INET6;
		mcastAddr6.sin6_port				= htons( kSSDPPort );
		mcastAddr6.sin6_addr.s6_addr[  0 ]	= 0xFF;	// SSDP IPv6 link-local multicast address FF02::C
		mcastAddr6.sin6_addr.s6_addr[  1 ]	= 0x02;
		mcastAddr6.sin6_addr.s6_addr[ 15 ]	= 0x0C;
		
		err = WriteSSDPSearchRequest( &context->header, &mcastAddr6, gSSDPDiscover_MX, gSSDPDiscover_ST );
		require_noerr( err, exit );
		
		n = sendto( sockV6, context->header.buf, context->header.len, 0, (const struct sockaddr *) &mcastAddr6,
			(socklen_t) sizeof( mcastAddr6 ) );
		err = map_socket_value_errno( sockV6, n == (ssize_t) context->header.len, n );
		if( err )
		{
			FPrintF( stderr, "*** Failed to send query on IPv6 socket with error %#m\n", err );
			ForgetSocket( &sockV6 );
		}
		else
		{
			if( gSSDPDiscover_Verbose )
			{
				gettimeofday( &now, NULL );
				FPrintF( stdout, "---\n" );
				FPrintF( stdout, "Send time:    %{du:time}\n",	&now );
				FPrintF( stdout, "Source Port:  %d\n",			SocketToPortNumber( sockV6 ) );
				FPrintF( stdout, "Destination:  %##a\n",		&mcastAddr6 );
				FPrintF( stdout, "Message size: %zu\n",			context->header.len );
				FPrintF( stdout, "HTTP header:\n%1{text}",		context->header.buf, context->header.len );
			}
			++sendCount;
		}
	}
	require_action_quiet( sendCount > 0, exit, err = kUnexpectedErr );
	
	// If there's no wait period after the send, then exit.
	
	if( context->receiveSecs == 0 ) goto exit;
	
	// Create dispatch read sources for socket(s).
	
	if( IsValidSocket( sockV4 ) )
	{
		SocketContext *		sockCtx;
		
		sockCtx = SocketContextCreate( sockV4, context, &err );
		require_noerr( err, exit );
		sockV4 = kInvalidSocketRef;
		
		err = DispatchReadSourceCreate( sockCtx->sock, NULL, SSDPDiscoverReadHandler, SocketContextCancelHandler, sockCtx,
			&context->readSourceV4 );
		if( err ) ForgetSocketContext( &sockCtx );
		require_noerr( err, exit );
		
		dispatch_resume( context->readSourceV4 );
	}
	
	if( IsValidSocket( sockV6 ) )
	{
		SocketContext *		sockCtx;
		
		sockCtx = SocketContextCreate( sockV6, context, &err );
		require_noerr( err, exit );
		sockV6 = kInvalidSocketRef;
		
		err = DispatchReadSourceCreate( sockCtx->sock, NULL, SSDPDiscoverReadHandler, SocketContextCancelHandler, sockCtx,
			&context->readSourceV6 );
		if( err ) ForgetSocketContext( &sockCtx );
		require_noerr( err, exit );
		
		dispatch_resume( context->readSourceV6 );
	}
	
	if( context->receiveSecs > 0 )
	{
		dispatch_after_f( dispatch_time_seconds( context->receiveSecs ), dispatch_get_main_queue(), kExitReason_Timeout,
			Exit );
	}
	dispatch_main();
	
exit:
	ForgetSocket( &sockV4 );
	ForgetSocket( &sockV6 );
	dispatch_source_forget( &signalSource );
	exit( err ? 1 : 0 );
}

static int	SocketToPortNumber( SocketRef inSock )
{
	OSStatus		err;
	sockaddr_ip		sip;
	socklen_t		len;
	
	len = (socklen_t) sizeof( sip );
	err = getsockname( inSock, &sip.sa, &len );
	err = map_socket_noerr_errno( inSock, err );
	check_noerr( err );
	return( err ? -1 : SockAddrGetPort( &sip ) );
}

static OSStatus	WriteSSDPSearchRequest( HTTPHeader *inHeader, const void *inHostSA, int inMX, const char *inST )
{
	OSStatus		err;
	
	err = HTTPHeader_InitRequest( inHeader, "M-SEARCH", "*", "HTTP/1.1" );
	require_noerr( err, exit );
	
	err = HTTPHeader_SetField( inHeader, "Host", "%##a", inHostSA );
	require_noerr( err, exit );
	
	err = HTTPHeader_SetField( inHeader, "ST", "%s", inST ? inST : "ssdp:all" );
	require_noerr( err, exit );
	
	err = HTTPHeader_SetField( inHeader, "Man", "\"ssdp:discover\"" );
	require_noerr( err, exit );
	
	err = HTTPHeader_SetField( inHeader, "MX", "%d", inMX );
	require_noerr( err, exit );
	
	err = HTTPHeader_Commit( inHeader );
	require_noerr( err, exit );
	
exit:
	return( err );
}

//===========================================================================================================================
//	SSDPDiscoverPrintPrologue
//===========================================================================================================================

static void	SSDPDiscoverPrintPrologue( const SSDPDiscoverContext *inContext )
{
	const int				receiveSecs = inContext->receiveSecs;
	const char *			ifName;
	char					ifNameBuf[ IF_NAMESIZE + 1 ];
	NetTransportType		ifType;
	
	ifName = if_indextoname( inContext->ifindex, ifNameBuf );
	
	ifType = kNetTransportType_Undefined;
	if( ifName ) SocketGetInterfaceInfo( kInvalidSocketRef, ifName, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &ifType );
	
	FPrintF( stdout, "Interface:        %s/%d/%s\n",
		ifName ? ifName : "?", inContext->ifindex, NetTransportTypeToString( ifType ) );
	FPrintF( stdout, "IP protocols:     %?s%?s%?s\n",
		inContext->useIPv4, "IPv4", ( inContext->useIPv4 && inContext->useIPv6 ), ", ", inContext->useIPv6, "IPv6" );
	FPrintF( stdout, "Receive duration: " );
	if( receiveSecs >= 0 )	FPrintF( stdout, "%d second%?c\n", receiveSecs, receiveSecs != 1, 's' );
	else					FPrintF( stdout, "∞\n" );
	FPrintF( stdout, "Start time:       %{du:time}\n", NULL );
}

//===========================================================================================================================
//	SSDPDiscoverReadHandler
//===========================================================================================================================

static Boolean	_HTTPHeader_Validate( HTTPHeader *inHeader );

static void	SSDPDiscoverReadHandler( void *inContext )
{
	OSStatus						err;
	struct timeval					now;
	SocketContext * const			sockCtx	= (SocketContext *) inContext;
	SSDPDiscoverContext * const		context	= (SSDPDiscoverContext *) sockCtx->userContext;
	HTTPHeader * const				header	= &context->header;
	sockaddr_ip						fromAddr;
	size_t							msgLen;
	
	gettimeofday( &now, NULL );
	
	err = SocketRecvFrom( sockCtx->sock, header->buf, sizeof( header->buf ), &msgLen, &fromAddr, sizeof( fromAddr ),
		NULL, NULL, NULL, NULL );
	require_noerr( err, exit );
	
	FPrintF( stdout, "---\n" );
	FPrintF( stdout, "Receive time: %{du:time}\n",	&now );
	FPrintF( stdout, "Source:       %##a\n", 		&fromAddr );
	FPrintF( stdout, "Message size: %zu\n",			msgLen );
	header->len = msgLen;
	if( _HTTPHeader_Validate( header ) )
	{
		FPrintF( stdout, "HTTP header:\n%1{text}", header->buf, header->len );
		if( header->extraDataLen > 0 )
		{
			FPrintF( stdout, "HTTP body: %1.1H", header->extraDataPtr, (int) header->extraDataLen, INT_MAX );
		}
	}
	else
	{
		FPrintF( stdout, "Invalid HTTP message:\n%1.1H", header->buf, (int) msgLen, INT_MAX );
		goto exit;
	}
	
exit:
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	_HTTPHeader_Validate
//
//	Parses for the end of an HTTP header and updates the HTTPHeader structure so it's ready to parse. Returns true if valid.
//	This assumes the "buf" and "len" fields are set. The other fields are set by this function.
//
//	Note: This was copied from CoreUtils because the HTTPHeader_Validate function is currently not exported in the framework.
//===========================================================================================================================

static Boolean	_HTTPHeader_Validate( HTTPHeader *inHeader )
{
	const char *		src;
	const char *		end;
	
	// Check for interleaved binary data (4 byte header that begins with $). See RFC 2326 section 10.12.
	
	require( inHeader->len < sizeof( inHeader->buf ), exit );
	src = inHeader->buf;
	end = src + inHeader->len;
	if( ( ( end - src ) >= 4 ) && ( src[ 0 ] == '$' ) )
	{
		src += 4;
	}
	else
	{
		// Search for an empty line (HTTP-style header/body separator). CRLFCRLF, LFCRLF, or LFLF accepted.
		// $$$ TO DO: Start from the last search location to avoid re-searching the same data over and over.
		
		for( ;; )
		{
			while( ( src < end ) && ( src[ 0 ] != '\n' ) ) ++src;
			if( src >= end ) goto exit;
			++src;
			if( ( ( end - src ) >= 2 ) && ( src[ 0 ] == '\r' ) && ( src[ 1 ] == '\n' ) ) // CFLFCRLF or LFCRLF
			{
				src += 2;
				break;
			}
			else if( ( ( end - src ) >= 1 ) && ( src[ 0 ] == '\n' ) ) // LFLF
			{
				src += 1;
				break;
			}
		}
	}
	inHeader->extraDataPtr	= src;
	inHeader->extraDataLen	= (size_t)( end - src );
	inHeader->len			= (size_t)( src - inHeader->buf );
	return( true );
	
exit:
	return( false );
}

#if( TARGET_OS_DARWIN )
//===========================================================================================================================
//	ResQueryCmd
//===========================================================================================================================

// res_query() from libresolv is actually called res_9_query (see /usr/include/resolv.h).

SOFT_LINK_LIBRARY_EX( "/usr/lib", resolv );
SOFT_LINK_FUNCTION_EX( resolv, res_9_query,
	int,
	( const char *dname, int class, int type, u_char *answer, int anslen ),
	( dname, class, type, answer, anslen ) );

// res_query() from libinfo

SOFT_LINK_LIBRARY_EX( "/usr/lib", info );
SOFT_LINK_FUNCTION_EX( info, res_query,
	int,
	( const char *dname, int class, int type, u_char *answer, int anslen ),
	( dname, class, type, answer, anslen ) );

typedef int ( *res_query_f )( const char *dname, int class, int type, u_char *answer, int anslen );

static void	ResQueryCmd( void )
{
	OSStatus		err;
	res_query_f		res_query_ptr;
	int				n;
	uint16_t		type, class;
	uint8_t			answer[ 1024 ];
	
	// Get pointer to one of the res_query() functions.
	
	if( gResQuery_UseLibInfo )
	{
		if( !SOFT_LINK_HAS_FUNCTION( info, res_query ) )
		{
			FPrintF( stderr, "Failed to soft link res_query from libinfo.\n" );
			err = kNotFoundErr;
			goto exit;
		}
		res_query_ptr = soft_res_query;
	}
	else
	{
		if( !SOFT_LINK_HAS_FUNCTION( resolv, res_9_query ) )
		{
			FPrintF( stderr, "Failed to soft link res_query from libresolv.\n" );
			err = kNotFoundErr;
			goto exit;
		}
		res_query_ptr = soft_res_9_query;
	}
	
	// Get record type.
	
	err = RecordTypeFromArgString( gResQuery_Type, &type );
	require_noerr( err, exit );
	
	// Get record class.
	
	if( gResQuery_Class )
	{
		err = RecordClassFromArgString( gResQuery_Class, &class );
		require_noerr( err, exit );
	}
	else
	{
		class = kDNSServiceClass_IN;
	}
	
	// Print prologue.
	
	FPrintF( stdout, "Name:       %s\n",			gResQuery_Name );
	FPrintF( stdout, "Type:       %s (%u)\n",		RecordTypeToString( type ), type );
	FPrintF( stdout, "Class:      %s (%u)\n",		( class == kDNSServiceClass_IN ) ? "IN" : "???", class );
	FPrintF( stdout, "Start time: %{du:time}\n",	NULL );
	FPrintF( stdout, "---\n" );
	
	// Call res_query().
	
	n = res_query_ptr( gResQuery_Name, class, type, (u_char *) answer, (int) sizeof( answer ) );
	if( n < 0 )
	{
		FPrintF( stderr, "res_query() failed with error: %d (%s).\n", h_errno, hstrerror( h_errno ) );
		err = kUnknownErr;
		goto exit;
	}
	
	// Print result.
	
	FPrintF( stdout, "Message size: %d\n\n%{du:dnsmsg}\n", n, answer, (size_t) n );
	
exit:
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	ResolvDNSQueryCmd
//===========================================================================================================================

// dns_handle_t is defined as a pointer to a privately-defined struct in /usr/include/dns.h. It's defined as a void * here to
// avoid including the header file.

typedef void *		dns_handle_t;

SOFT_LINK_FUNCTION_EX( resolv, dns_open, dns_handle_t, ( const char *path ), ( path ) );
SOFT_LINK_FUNCTION_VOID_RETURN_EX( resolv, dns_free, ( dns_handle_t *dns ), ( dns ) );
SOFT_LINK_FUNCTION_EX( resolv, dns_query,
	int32_t, (
		dns_handle_t		dns,
		const char *		name,
		uint32_t			dnsclass,
		uint32_t			dnstype,
		char *				buf,
		uint32_t			len,
		struct sockaddr *	from,
		uint32_t *			fromlen ),
	( dns, name, dnsclass, dnstype, buf, len, from, fromlen ) );

static void	ResolvDNSQueryCmd( void )
{
	OSStatus			err;
	int					n;
	dns_handle_t		dns = NULL;
	uint16_t			type, class;
	sockaddr_ip			from;
	uint32_t			fromLen;
	uint8_t				answer[ 1024 ];
	
	// Make sure that the required symbols are available.
	
	if( !SOFT_LINK_HAS_FUNCTION( resolv, dns_open ) )
	{
		FPrintF( stderr, "Failed to soft link dns_open from libresolv.\n" );
		err = kNotFoundErr;
		goto exit;
	}
	
	if( !SOFT_LINK_HAS_FUNCTION( resolv, dns_free ) )
	{
		FPrintF( stderr, "Failed to soft link dns_free from libresolv.\n" );
		err = kNotFoundErr;
		goto exit;
	}
	
	if( !SOFT_LINK_HAS_FUNCTION( resolv, dns_query ) )
	{
		FPrintF( stderr, "Failed to soft link dns_query from libresolv.\n" );
		err = kNotFoundErr;
		goto exit;
	}
	
	// Get record type.
	
	err = RecordTypeFromArgString( gResolvDNSQuery_Type, &type );
	require_noerr( err, exit );
	
	// Get record class.
	
	if( gResolvDNSQuery_Class )
	{
		err = RecordClassFromArgString( gResolvDNSQuery_Class, &class );
		require_noerr( err, exit );
	}
	else
	{
		class = kDNSServiceClass_IN;
	}
	
	// Get dns handle.
	
	dns = soft_dns_open( gResolvDNSQuery_Path );
	if( !dns )
	{
		FPrintF( stderr, "dns_open( %s ) failed.\n", gResolvDNSQuery_Path );
		err = kUnknownErr;
		goto exit;
	}
	
	// Print prologue.
	
	FPrintF( stdout, "Name:       %s\n",			gResolvDNSQuery_Name );
	FPrintF( stdout, "Type:       %s (%u)\n",		RecordTypeToString( type ), type );
	FPrintF( stdout, "Class:      %s (%u)\n",		( class == kDNSServiceClass_IN ) ? "IN" : "???", class );
	FPrintF( stdout, "Path:       %s\n",			gResolvDNSQuery_Path ? gResolvDNSQuery_Name : "<NULL>" );
	FPrintF( stdout, "Start time: %{du:time}\n",	NULL );
	FPrintF( stdout, "---\n" );
	
	// Call dns_query().
	
	memset( &from, 0, sizeof( from ) );
	fromLen = (uint32_t) sizeof( from );
	n = soft_dns_query( dns, gResolvDNSQuery_Name, class, type, (char *) answer, (uint32_t) sizeof( answer ), &from.sa,
		&fromLen );
	if( n < 0 )
	{
		FPrintF( stderr, "dns_query() failed with error: %d (%s).\n", h_errno, hstrerror( h_errno ) );
		err = kUnknownErr;
		goto exit;
	}
	
	// Print result.
	
	FPrintF( stdout, "From:         %##a\n", &from );
	FPrintF( stdout, "Message size: %d\n\n%{du:dnsmsg}\n", n, answer, (size_t) n );
	
exit:
	if( dns ) soft_dns_free( dns );
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	CFHostCmd
//===========================================================================================================================

static void
	_CFHostResolveCallback(
		CFHostRef				inHost,
		CFHostInfoType			inInfoType,
		const CFStreamError *	inError,
		void *					inInfo );

static void	CFHostCmd( void )
{
	OSStatus				err;
	CFStringRef				name;
	Boolean					success;
	CFHostRef				host = NULL;
	CFHostClientContext		context;
	CFStreamError			streamErr;
	
	name = CFStringCreateWithCString( kCFAllocatorDefault, gCFHost_Name, kCFStringEncodingUTF8 );
	require_action( name, exit, err = kUnknownErr );
	
	host = CFHostCreateWithName( kCFAllocatorDefault, name );
	ForgetCF( &name );
	require_action( host, exit, err = kUnknownErr );
	
	memset( &context, 0, sizeof( context ) );
	success = CFHostSetClient( host, _CFHostResolveCallback, &context );
	require_action( success, exit, err = kUnknownErr );
	
	CFHostScheduleWithRunLoop( host, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode );
	
	// Print prologue.
	
	FPrintF( stdout, "Hostname:   %s\n",			gCFHost_Name );
	FPrintF( stdout, "Start time: %{du:time}\n",	NULL );
	FPrintF( stdout, "---\n" );
	
	success = CFHostStartInfoResolution( host, kCFHostAddresses, &streamErr );
	require_action( success, exit, err = kUnknownErr );
	err = kNoErr;
	
	CFRunLoopRun();
	
exit:
	CFReleaseNullSafe( host );
	if( err ) exit( 1 );
}

static void	_CFHostResolveCallback( CFHostRef inHost, CFHostInfoType inInfoType, const CFStreamError *inError, void *inInfo )
{
	OSStatus			err;
	struct timeval		now;
	
	gettimeofday( &now, NULL );
	
	Unused( inInfoType );
	Unused( inInfo );
	
	if( inError && ( inError->domain != 0 ) && ( inError->error ) )
	{
		err = inError->error;
		if( inError->domain == kCFStreamErrorDomainNetDB )
		{
			FPrintF( stderr, "Error %d: %s.\n", err, gai_strerror( err ) );
		}
		else
		{
			FPrintF( stderr, "Error %#m\n", err );
		}
	}
	else
	{
		CFArrayRef					addresses;
		CFIndex						count, i;
		CFDataRef					addrData;
		const struct sockaddr *		sockAddr;
		Boolean						wasResolved = false;
		
		addresses = CFHostGetAddressing( inHost, &wasResolved );
		check( wasResolved );
		
		if( addresses )
		{
			count = CFArrayGetCount( addresses );
			for( i = 0; i < count; ++i )
			{
				addrData = CFArrayGetCFDataAtIndex( addresses, i, &err );
				require_noerr( err, exit );
				
				sockAddr = (const struct sockaddr *) CFDataGetBytePtr( addrData );
				FPrintF( stdout, "%##a\n", sockAddr );
			}
		}
		err = kNoErr;
	}
	
	FPrintF( stdout, "---\n" );
	FPrintF( stdout, "End time:   %{du:time}\n", &now );
	
	if( gCFHost_WaitSecs > 0 ) sleep( (unsigned int) gCFHost_WaitSecs );
	
exit:
	exit( err ? 1 : 0 );
}

//===========================================================================================================================
//	DNSConfigAddCmd
//
//	Note: Based on ajn's supplemental test tool.
//===========================================================================================================================

static void	DNSConfigAddCmd( void )
{
	OSStatus					err;
	mdns_dns_configurator_t		configurator;
	uint32_t					order;
	size_t						i;
	
	configurator = mdns_dns_configurator_create_with_cfstring_id( gDNSConfigAdd_ID, &err );
	require_noerr( err, exit );
	
	for( i = 0; i < gDNSConfigAdd_IPAddrCount; ++i )
	{
		err = mdns_dns_configurator_add_server_address_string( configurator, gDNSConfigAdd_IPAddrArray[ i ] );
		require_noerr( err, exit );
	}
	order = ( gDNSConfigAdd_SearchOrder > 0 ) ? ( (uint32_t) gDNSConfigAdd_SearchOrder ) : 0;
	if( gDNSConfigAdd_DomainCount > 0 )
	{
		for( i = 0; i < gDNSConfigAdd_DomainCount; ++i )
		{
			err = mdns_dns_configurator_add_domain( configurator, gDNSConfigAdd_DomainArray[ i ], order );
			require_noerr( err, exit );
		}
	}
	else
	{
		// There are no domains, but the domain array needs to be non-empty, so add a zero-length domain.
		
		err = mdns_dns_configurator_add_domain( configurator, "", order );
		require_noerr( err, exit );
	}
	
	if( gDNSConfigAdd_Interface )
	{
		err = mdns_dns_configurator_set_interface( configurator, gDNSConfigAdd_Interface );
		require_noerr( err, exit );
	}
	err = mdns_dns_configurator_register( configurator, CFSTR( kDNSSDUtilIdentifier ) );
	require_noerr( err, exit );
	
exit:
	mdns_forget( &configurator );
	gExitCode = err ? 1 : 0;
}

//===========================================================================================================================
//	DNSConfigRemoveCmd
//===========================================================================================================================

static void	DNSConfigRemoveCmd( void )
{
	OSStatus err = mdns_dns_configurator_deregister_configuration( gDNSConfigRemove_ID, CFSTR( kDNSSDUtilIdentifier ) );
	gExitCode = err ? 1 : 0;
}

//===========================================================================================================================
//	XPCSendCommand
//===========================================================================================================================

static OSStatus	_XPCDictionaryCreateFromString( const char *inString, xpc_object_t *outDict );

typedef struct
{
	dispatch_queue_t			queue;				// Dispatch queue.
	dispatch_semaphore_t		doneSem;			// Semaphore to signal when done.
	char *						serviceName;		// Mach service name.
	char *						msgStr;				// Message to send as a string.
	xpc_connection_t			connection;			// XPC connection.
	dispatch_source_t			cancelTimer;		// Timer cancelling the XPC connection.
	dispatch_source_t			sourceSigInt;		// Dispatch source for SIGINT.
	dispatch_source_t			sourceSigTerm;		// Dispatch source for SIGTERM.
	int32_t						refCount;			// Reference count.
	int							cancelDelaySecs;	// Number of seconds to wait before cancelling the connection.
	OSStatus					error;				// Command's final error.
	Boolean						noReply;			// True if there was a message to send and a reply is expected.
	Boolean						done;				// True if the command is done.
	
}	XPCSendCmd;

static OSStatus	_XPCSendCmdCreate( XPCSendCmd **outCmd );
static void		_XPCSendCmdRetain( XPCSendCmd *inCmd );
static void		_XPCSendCmdRelease( XPCSendCmd *inCmd );
static OSStatus	_XPCSendCmdRun( XPCSendCmd *inCmd );
static void		_XPCSendCmdStart( void *inCtx );
static void		_XPCSendCmdStop( XPCSendCmd *inCmd, OSStatus inError );
static OSStatus	_XPCSendCmdScheduleConnectionCancellation( XPCSendCmd *inCmd );

static void	XPCSendCommand( void )
{
	OSStatus			err;
	XPCSendCmd *		cmd = NULL;
	
	err = _XPCSendCmdCreate( &cmd );
	require_noerr( err, exit );
	
	cmd->serviceName = strdup( gXPCSend_ServiceName );
	require_action( cmd->serviceName, exit, err = kNoMemoryErr );
	
	if( gXPCSend_MessageStr )
	{
		cmd->msgStr = strdup( gXPCSend_MessageStr );
		require_action( cmd->msgStr, exit, err = kNoMemoryErr );
	}
	cmd->cancelDelaySecs	= gXPCSend_CancelDelaySecs;
	cmd->noReply			= gXPCSend_NoReply ? true : false;
	
	err = _XPCSendCmdRun( cmd );
	require_noerr_quiet( err, exit );
	
exit:
	if( err ) FPrintF( stderr, "error: %#m\n", err );
	if( cmd ) _XPCSendCmdRelease( cmd );
	gExitCode = err ? 1 : 0;
}

//===========================================================================================================================

static OSStatus	_XPCSendCmdCreate( XPCSendCmd ** const outCmd )
{
	OSStatus			err;
	XPCSendCmd *		cmd;
	
	cmd = (XPCSendCmd *) calloc( 1, sizeof( *cmd ) );
	require_action( cmd, exit, err = kNoResourcesErr );
	
	cmd->refCount	= 1;
	cmd->error		= kInProgressErr;
	
	cmd->queue = dispatch_queue_create( "com.apple.dnssdutil.xpc-send-command", DISPATCH_QUEUE_SERIAL );
	require_action( cmd->queue, exit, err = kNoResourcesErr );
	
	cmd->doneSem = dispatch_semaphore_create( 0 );
	require_action( cmd->doneSem, exit, err = kNoResourcesErr );
	
	*outCmd = cmd;
	cmd = NULL;
	err = kNoErr;
	
exit:
	if( cmd ) _XPCSendCmdRelease( cmd );
	return( err );
}

//===========================================================================================================================

static void	_XPCSendCmdRetain( XPCSendCmd * const me )
{
	atomic_add_32( &me->refCount, 1 );
}

//===========================================================================================================================

static void	_XPCSendCmdRelease( XPCSendCmd * const me )
{
	if( atomic_add_and_fetch_32( &me->refCount, -1 ) == 0 )
	{
		check( !me->connection );
		check( !me->cancelTimer );
		check( !me->sourceSigInt );
		check( !me->sourceSigTerm );
		dispatch_forget( &me->queue );
		dispatch_forget( &me->doneSem );
		ForgetMem( &me->serviceName );
		ForgetMem( &me->msgStr );
		free( me );
	}
}

//===========================================================================================================================

static OSStatus	_XPCSendCmdRun( XPCSendCmd * const me )
{
	dispatch_async_f( me->queue, me, _XPCSendCmdStart );
    dispatch_semaphore_wait( me->doneSem, DISPATCH_TIME_FOREVER );
	return( me->error );
}

//===========================================================================================================================

static void	_XPCSendCmdStart( void * const inCtx )
{
	OSStatus				err;
	XPCSendCmd * const		me				= (XPCSendCmd *) inCtx;
	xpc_object_t			msg				= NULL;
	dispatch_source_t		sourceSigInt	= NULL;
	dispatch_source_t		sourceSigTerm	= NULL;
	
	sourceSigInt = dispatch_source_create( DISPATCH_SOURCE_TYPE_SIGNAL, SIGINT, 0, me->queue );
	require_action( sourceSigInt, exit, err = kNoResourcesErr );
	
	sourceSigTerm = dispatch_source_create( DISPATCH_SOURCE_TYPE_SIGNAL, SIGTERM, 0, me->queue );
	require_action( sourceSigInt, exit, err = kNoResourcesErr );
	
	me->sourceSigInt	= sourceSigInt;
	sourceSigInt		= NULL;
	me->sourceSigTerm	= sourceSigTerm;
	sourceSigTerm		= NULL;
	
	signal( SIGINT, SIG_IGN );
	signal( SIGTERM, SIG_IGN );
	dispatch_source_set_event_handler( me->sourceSigInt,
	^{
		FPrintF( stdout, "*** Got SIGINT signal ***\n" );
		_XPCSendCmdStop( me, kNoErr );
	} );
	dispatch_activate( me->sourceSigInt );
	dispatch_source_set_event_handler( me->sourceSigTerm,
	^{
		FPrintF( stdout, "*** Got SIGTERM signal ***\n" );
		_XPCSendCmdStop( me, kNoErr );
	} );
	dispatch_activate( me->sourceSigTerm );
	
	if( me->msgStr )
	{
		err = _XPCDictionaryCreateFromString( me->msgStr, &msg );
		require_noerr_quiet( err, exit );
	}
	FPrintF( stdout, "Service:    %s\n",			me->serviceName );
	FPrintF( stdout, "Message:    '%s'\n",			me->msgStr );
	FPrintF( stdout, "Wait:       %d second%s\n",	me->cancelDelaySecs, ( me->cancelDelaySecs != 1 ) ? "s" : "" );
	FPrintF( stdout, "Start time: %{du:time}\n",	NULL );
	FPrintF( stdout, "---\n" );
	if( msg ) FPrintF( stdout, "XPC Message:\n%{xpc}\n", msg );
	
	me->connection = xpc_connection_create_mach_service( me->serviceName, me->queue, 0 );
	require_action( me->connection, exit, err = kNoResourcesErr );
	
	_XPCSendCmdRetain( me );
	xpc_connection_set_event_handler( me->connection,
	^( const xpc_object_t inEvent )
	{
		if( !me->done ) FPrintF( stdout, "[%{du:time}] Connection Event:\n%{xpc}\n", NULL, inEvent );
		if( inEvent == XPC_ERROR_CONNECTION_INVALID )
		{
			_XPCSendCmdRelease( me );
		}
	} );
	xpc_connection_activate( me->connection );
	
	if( msg )
	{
		if( me->noReply )
		{
			xpc_connection_send_message( me->connection, msg );
			_XPCSendCmdRetain( me );
			xpc_connection_send_barrier( me->connection,
			^{
				if( !me->done )
				{
					OSStatus		localErr;
					
					localErr = _XPCSendCmdScheduleConnectionCancellation( me );
					if( localErr ) _XPCSendCmdStop( me, localErr );
				}
				_XPCSendCmdRelease( me );
			} );
		}
		else
		{
			_XPCSendCmdRetain( me );
			xpc_connection_send_message_with_reply( me->connection, msg, me->queue,
			^( const xpc_object_t inReply )
			{
				if( !me->done )
				{
					OSStatus		localErr;
					
					FPrintF( stdout, "[%{du:time}] Reply:\n%{xpc}\n", NULL, inReply );
					localErr = _XPCSendCmdScheduleConnectionCancellation( me );
					if( localErr ) _XPCSendCmdStop( me, localErr );
				}
				_XPCSendCmdRelease( me );
			} );
		}
	}
	else
	{
		err = _XPCSendCmdScheduleConnectionCancellation( me );
		require_noerr( err, exit );
	}
	err = kNoErr;
	
exit:
	xpc_forget( &msg );
	dispatch_forget( &sourceSigInt );
	dispatch_forget( &sourceSigTerm );
	if( err ) _XPCSendCmdStop( me, err );
}

//===========================================================================================================================

static void	_XPCSendCmdStop( XPCSendCmd * const me, const OSStatus inError )
{
	if( !me->done )
	{
		me->done	= true;
		me->error	= inError;
		xpc_connection_forget( &me->connection );
		dispatch_source_forget( &me->cancelTimer );
		dispatch_source_forget( &me->sourceSigInt );
		dispatch_source_forget( &me->sourceSigTerm );
		FPrintF( stdout, "---\n" );
		FPrintF( stdout, "End time:   %{du:time}\n", NULL );
		dispatch_semaphore_signal( me->doneSem );
	}
}

//===========================================================================================================================

static OSStatus	_XPCSendCmdScheduleConnectionCancellation( XPCSendCmd * const me )
{
	OSStatus		err;
	
	if( me->cancelDelaySecs >= 0 )
	{
		me->cancelTimer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
		require_action( me->cancelTimer, exit, err = kNoResourcesErr );
		
		dispatch_source_set_timer( me->cancelTimer, dispatch_time_seconds( me->cancelDelaySecs ), DISPATCH_TIME_FOREVER, 0 );
		dispatch_source_set_event_handler( me->cancelTimer,
		^{
			_XPCSendCmdStop( me, kNoErr );
		} );
		dispatch_activate( me->cancelTimer );
	}
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_XPCDictionaryCreateFromString
//===========================================================================================================================

#define kXPCObjectPrefix_Bool			"bool:"
#define kXPCObjectPrefix_BoolAbbr		"b:"
#define kXPCObjectPrefix_Data			"data:"
#define kXPCObjectPrefix_DataAbbr		"d:"
#define kXPCObjectPrefix_Int64			"int:"
#define kXPCObjectPrefix_Int64Abbr		"i:"
#define kXPCObjectPrefix_String			"string:"
#define kXPCObjectPrefix_StringAbbr		"s:"
#define kXPCObjectPrefix_UInt64			"uint:"
#define kXPCObjectPrefix_UInt64Abbr		"u:"
#define kXPCObjectPrefix_UUID			"uuid:"

typedef struct XPCListItem		XPCListItem;
struct XPCListItem
{
	XPCListItem *		next;
	xpc_object_t		obj;
	char *				key;
};

static OSStatus	_XPCListItemCreate( xpc_object_t inObject, const char *inKey, XPCListItem **outItem );
static void		_XPCListItemFree( XPCListItem *inItem );
static void		_XPCListFree( XPCListItem *inList );

static OSStatus	_XPCObjectFromString( const char *inString, xpc_object_t *outObject );

static OSStatus	_XPCDictionaryCreateFromString( const char *inString, xpc_object_t *outDict )
{
	OSStatus				err;
	xpc_object_t			dict;
	xpc_object_t			container	= NULL;
	char *					keyMem		= NULL;
	const char *			ptr			= inString;
	const char * const		end			= inString + strlen( inString );
	XPCListItem *			stack		= NULL;
	
	dict = xpc_dictionary_create( NULL, NULL, 0 );
	require_action( dict, exit, err = kNoMemoryErr );
	
	container = xpc_retain( dict );
	while( *ptr )
	{
		xpc_type_t		containerType;
		int				c;
		
		// At this point, zero or more of the current container's elements have been parsed.
		// Skip the white space leading up to the container's next element, if any, or the container's end.
		
		while( isspace_safe( *ptr ) ) ++ptr;
		c = *ptr;
		if( c == '\0' ) break;
		
		// Check if we're done with the current container.
		
		containerType = xpc_get_type( container );
		if( ( ( containerType == XPC_TYPE_DICTIONARY ) && ( c == '}' ) ) ||
			( ( containerType == XPC_TYPE_ARRAY )      && ( c == ']' ) ) )
		{
			XPCListItem *		item;
			
			item = stack;
			require_action_quiet( item, exit, err = kMalformedErr );
			
			// Add the current container to its parent container.
			
			if( item->key )
			{
				xpc_dictionary_set_value( item->obj, item->key, container );
			}
			else
			{
				xpc_array_append_value( item->obj, container );
			}
			
			// Pop the parent container from the stack and continue with the parent container.
			
			xpc_forget( &container );
			container = xpc_retain( item->obj );
			stack = item->next;
			_XPCListItemFree( item );
			++ptr; // Increment past the end of the container.
		}
		else
		{
			const char *		keyStr;
			char				keyBuf[ 64 ];
			
			// If the current container is a dictionary, parse the key string.
			
			if( containerType == XPC_TYPE_DICTIONARY )
			{
				ForgetMem( &keyMem );
				err = _ParseEscapedStringWithCopy( ptr, end, "={}[]" kWhiteSpaceCharSet, keyBuf, sizeof( keyBuf ),
					&keyStr, &keyMem, &ptr );
				require_noerr_quiet( err, exit );
				require_action_quiet( *ptr == '=', exit, err = kMalformedErr );
				++ptr;
				check( keyStr );
			}
			else
			{
				keyStr = NULL;
			}
			
			// Parse the value string.
			
			c = *ptr;
			if( ( c == '{' ) || ( c == '[' ) )	// Check if the value is a container.
			{
				XPCListItem *		item;
				
				// Push the current container onto the container stack.
				
				err = _XPCListItemCreate( container, keyStr, &item );
				require_noerr( err, exit );
				
				item->next = stack;
				stack = item;
				item = NULL;
				
				// Create and continue with the child container.
				
				xpc_forget( &container );
				if( c == '{' )
				{
					container = xpc_dictionary_create( NULL, NULL, 0 );
					require_action( container, exit, err = kNoMemoryErr );
				}
				else
				{
					container = xpc_array_create( NULL, 0 );
					require_action( container, exit, err = kNoMemoryErr );
				}
				++ptr; // Increment past the start of the container.
			}
			else
			{
				const char *		valStr;
				char *				valMem;
				xpc_object_t		value;
				char				valBuf[ 64 ];
				
				err = _ParseEscapedStringWithCopy( ptr, end, "{}[]" kWhiteSpaceCharSet, valBuf, sizeof( valBuf ),
					&valStr, &valMem, &ptr );
				require_noerr_quiet( err, exit );
				
				err = _XPCObjectFromString( valStr, &value );
				valStr = NULL;
				ForgetMem( &valMem );
				require_noerr_quiet( err, exit );
				
				if( keyStr )
				{
					xpc_dictionary_set_value( container, keyStr, value );
				}
				else
				{
					xpc_array_append_value( container, value );
				}
				xpc_forget( &value );
			}
		}
	}
	
	// There should be no containers left on the stack.
	
	require_action_quiet( !stack, exit, err = kMalformedErr );
	check( container == dict );
	
	*outDict = dict;
	dict = NULL;
	err = kNoErr;
	
exit:
	xpc_forget( &dict );
	xpc_forget( &container );
	ForgetMem( &keyMem );
	if( stack ) _XPCListFree( stack );
	return( err );
}

typedef enum
{
	kXPCObjectType_Invalid	= 0,
	kXPCObjectType_Bool		= 1,
	kXPCObjectType_Data		= 2,
	kXPCObjectType_Int64	= 3,
	kXPCObjectType_String	= 4,
	kXPCObjectType_UInt64	= 5,
	kXPCObjectType_UUID		= 6
	
}	XPCObjectType;

static OSStatus	_XPCObjectFromString( const char *inString, xpc_object_t *outObject )
{
	OSStatus			err;
	xpc_object_t		object;
	const char *		valStr;
	size_t				valOffset;
	XPCObjectType		type;
	
	if( 0 ) {}
	
	// Bool
	
	else if( stricmp_prefix( inString, kXPCObjectPrefix_Bool ) == 0 )
	{
		valOffset = sizeof_string( kXPCObjectPrefix_Bool );
		type = kXPCObjectType_Bool;
	}
	else if( stricmp_prefix( inString, kXPCObjectPrefix_BoolAbbr ) == 0 )
	{
		valOffset = sizeof_string( kXPCObjectPrefix_BoolAbbr );
		type = kXPCObjectType_Bool;
	}
	
	// Data
	
	else if( stricmp_prefix( inString, kXPCObjectPrefix_Data ) == 0 )
	{
		valOffset = sizeof_string( kXPCObjectPrefix_Data );
		type = kXPCObjectType_Data;
	}
	else if( stricmp_prefix( inString, kXPCObjectPrefix_DataAbbr ) == 0 )
	{
		valOffset = sizeof_string( kXPCObjectPrefix_DataAbbr );
		type = kXPCObjectType_Data;
	}
	
	// Int64
	
	else if( stricmp_prefix( inString, kXPCObjectPrefix_Int64 ) == 0 )
	{
		valOffset = sizeof_string( kXPCObjectPrefix_Int64 );
		type = kXPCObjectType_Int64;
	}
	else if( stricmp_prefix( inString, kXPCObjectPrefix_Int64Abbr ) == 0 )
	{
		valOffset = sizeof_string( kXPCObjectPrefix_Int64Abbr );
		type = kXPCObjectType_Int64;
	}
	
	// String
	
	else if( stricmp_prefix( inString, kXPCObjectPrefix_String ) == 0 )
	{
		valOffset = sizeof_string( kXPCObjectPrefix_String );
		type = kXPCObjectType_String;
	}
	else if( stricmp_prefix( inString, kXPCObjectPrefix_StringAbbr ) == 0 )
	{
		valOffset = sizeof_string( kXPCObjectPrefix_StringAbbr );
		type = kXPCObjectType_String;
	}
	
	// UInt64
	
	else if( stricmp_prefix( inString, kXPCObjectPrefix_UInt64 ) == 0 )
	{
		valOffset = sizeof_string( kXPCObjectPrefix_UInt64 );
		type = kXPCObjectType_UInt64;
	}
	else if( stricmp_prefix( inString, kXPCObjectPrefix_UInt64Abbr ) == 0 )
	{
		valOffset = sizeof_string( kXPCObjectPrefix_UInt64Abbr );
		type = kXPCObjectType_UInt64;
	}
	
	// UUID
	
	else if( stricmp_prefix( inString, kXPCObjectPrefix_UUID ) == 0 )
	{
		valOffset = sizeof_string( kXPCObjectPrefix_UUID );
		type = kXPCObjectType_UUID;
	}

	// Unsupported prefix
	
	else
	{
		err = kValueErr;
		goto exit;
	}
	
	valStr = &inString[ valOffset ];
	switch( type )
	{
		case kXPCObjectType_Bool:
		{
			if( IsTrueString( valStr, kSizeCString ) )
			{
				object = xpc_bool_create( true );
			}
			else if( IsFalseString( valStr, kSizeCString ) )
			{
				object = xpc_bool_create( false );
			}
			else
			{
				err = kValueErr;
				goto exit;
			}
			break;
		}
		case kXPCObjectType_Data:
		{
			uint8_t *		dataPtr;
			size_t			dataLen;
			
			err = HexToDataCopy( valStr, kSizeCString, kHexToData_DefaultFlags, &dataPtr, &dataLen, NULL );
			require_noerr( err, exit );
			
			object = xpc_data_create( dataPtr, dataLen );
			free( dataPtr );
			require_action( object, exit, err = kNoMemoryErr );
			break;
		}
		case kXPCObjectType_Int64:
		{
			int64_t		i64;
			
			i64 = _StringToInt64( valStr, &err );
			require_noerr_quiet( err, exit );
			
			object = xpc_int64_create( i64 );
			require_action( object, exit, err = kNoMemoryErr );
			break;
		}
		case kXPCObjectType_String:
		{
			object = xpc_string_create( valStr );
			require_action( object, exit, err = kNoMemoryErr );
			break;
		}
		case kXPCObjectType_UInt64:
		{
			uint64_t		u64;
			
			u64 = _StringToUInt64( valStr, &err );
			require_noerr_quiet( err, exit );
			
			object = xpc_uint64_create( u64 );
			require_action( object, exit, err = kNoMemoryErr );
			break;
		}
		case kXPCObjectType_UUID:
		{
			uuid_t		uuid;
			
			err = uuid_parse( valStr, uuid );
			require_noerr_action_quiet( err, exit, err = kValueErr );
			
			object = xpc_uuid_create( uuid );
			require_action( object, exit, err = kNoMemoryErr );
			break;
		}
		default:
		{
			FatalErrorF( "Unhandled XPCObjectType %ld", (long) type );
		}
	}
	check( object );
	*outObject = object;
	err = kNoErr;
	
exit:
	return( err );
}

static OSStatus	_XPCListItemCreate( xpc_object_t inObject, const char *inKey, XPCListItem **outItem )
{
	OSStatus			err;
	XPCListItem *		item;
	
	item = (XPCListItem *) calloc( 1, sizeof( *item ) );
	require_action( item, exit, err = kNoMemoryErr );
	
	item->obj = xpc_retain( inObject );
	if( ( xpc_get_type( item->obj ) == XPC_TYPE_DICTIONARY ) && inKey )
	{
		item->key = strdup( inKey );
		require_action( item->key, exit, err = kNoMemoryErr );
	}
	
	*outItem = item;
	item = NULL;
	err = kNoErr;
	
exit:
	if( item ) _XPCListItemFree( item );
	return( err );
}

static void	_XPCListItemFree( XPCListItem *inItem )
{
	xpc_forget( &inItem->obj );
	ForgetMem( &inItem->key );
	free( inItem );
}

static void _XPCListFree( XPCListItem *inList )
{
	XPCListItem *		item;
	
	while( ( item = inList ) != NULL )
	{
		inList = item->next;
		_XPCListItemFree( item );
	}
}
#endif	// TARGET_OS_DARWIN

#if( MDNSRESPONDER_PROJECT )
//===========================================================================================================================
//	InterfaceMonitorCmd
//===========================================================================================================================

static void	_InterfaceMonitorPrint( mdns_interface_monitor_t inMonitor, mdns_interface_flags_t inUpdateFlags );
static void	_InterfaceMonitorSignalHandler( void *inContext );

static void	InterfaceMonitorCmd( void )
{
	OSStatus						err;
	mdns_interface_monitor_t		monitor;
	dispatch_source_t				signalSource = NULL;
	uint32_t						ifIndex;
	__block int						exitCode;
	
	err = InterfaceIndexFromArgString( gInterface, &ifIndex );
	require_noerr_quiet( err, exit );
	
	monitor = mdns_interface_monitor_create( ifIndex );
	require_action( monitor, exit, err = kNoResourcesErr );
	
	exitCode = 0;
	mdns_interface_monitor_set_queue( monitor, dispatch_get_main_queue() );
	mdns_interface_monitor_set_event_handler( monitor,
	^( mdns_event_t inEvent, OSStatus inError )
	{
		switch( inEvent )
		{
			case mdns_event_error:
				FPrintF( stderr, "error: Interface monitor failed: %#m\n", inError );
				mdns_interface_monitor_invalidate( monitor );
				exitCode = 1;
				break;
			
			case mdns_event_invalidated:
				FPrintF( stdout, "Interface monitor invalidated.\n" );
				mdns_release( monitor );
				exit( exitCode );
			
			default:
				FPrintF( stdout, "Unhandled event '%s' (%ld)\n", mdns_event_to_string( inEvent ), (long) inEvent );
				break;
		}
	} );
	mdns_interface_monitor_set_update_handler( monitor,
	^( __unused mdns_interface_flags_t inUpdateFlags )
	{
		_InterfaceMonitorPrint( monitor, inUpdateFlags );
	} );
	
	_InterfaceMonitorPrint( monitor, 0 );
	mdns_interface_monitor_activate( monitor );
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), _InterfaceMonitorSignalHandler, monitor,
		&signalSource );
	require_noerr( err, exit );
	dispatch_resume( signalSource );
	
	dispatch_main();
	
exit:
	if( err ) ErrQuit( 1, "error: %#m\n", err );
}

static void
	_InterfaceMonitorPrint(
		const mdns_interface_monitor_t	inMonitor,
		const mdns_interface_flags_t	inUpdateFlags )
{
	FPrintF( stdout, "%{du:time} %@%s\n",
		NULL, inMonitor, ( inUpdateFlags & mdns_interface_flag_network ) ? " (network changed)" : "" );
}

static void	_InterfaceMonitorSignalHandler( void *inContext )
{
	mdns_interface_monitor_invalidate( (mdns_interface_monitor_t) inContext );
}

//===========================================================================================================================
//	QuerierCommand
//===========================================================================================================================

typedef struct
{
	dispatch_queue_t				queue;					// Serial queue for command's events.
	dispatch_semaphore_t			doneSem;				// Semaphore to signal when command is done.
	uint8_t *						qname;					// Name of record to query for.
	mdns_querier_t					querier;				// Querier.
	dispatch_source_t				sourceSigInt;			// Dispatch source for SIGINT.
	dispatch_source_t				sourceSigTerm;			// Dispatch source for SIGTERM.
	int32_t							refCount;				// Reference count.
	OSStatus						error;					// Command's error.
	uint32_t						ifIndex;				// Interface index for scoping.
	int32_t							startTimeLeewayMs;		// Start time leeway in milliseconds.
	uint16_t						qtype;					// Type of record to query for.
	uint16_t						qclass;					// Class of record to query for.
	pid_t							delegatorPID;			// Delegator PID.
	uint8_t							delegatorUUID[ 16 ];	// Delegator UUID.
	Boolean							haveDelegatorPID;		// True if delegatorPID is set.
	Boolean							haveDelegatorUUID;		// True if delegatorUUID is set.
	Boolean							dnssecOK;				// True if queries need an OPT record with the DO bit set.
	Boolean							checkingDisabled;		// True if queries need the CD bit set.
	Boolean							sensitiveLogging;		// True if querier's sensitive logging should be enabled.
	Boolean							haveStartTimeLeeway;	// True if the start time leeway was set.
	Boolean							done;					// True if the command is done.
	
	// Variables for resolver.
	
	mdns_resolver_type_t			resolverType;			// Type of resolver to use.
	mdns_resolver_t					resolver;				// Resolver.
	CFMutableArrayRef				serverAddrs;			// Server addresses to use for resolver.
	char *							providerName;			// Provider name for resolver.
	char *							connectionHostname;		// Overrides hostname used for transport layer connection.
	char *							urlPath;				// URL path for resolver.
	void *							identityRefPtr;			// Keychain identity reference.
	size_t							identityRefLen;			// Keychain identity reference length.
	void *							odohCfgPtr;				// Oblivious DoH configuration.
	size_t							odohCfgLen;				// Oblivious DoH configuration length.
	Boolean							noConnectionReuse;		// True if connection reuse is to be disabled.
	Boolean							squashCNAMEs;			// True if CNAMEs should be squashed.
	
	// Variables for DNS service manager.
	
	mdns_dns_service_manager_t		manager;				// DNS service manager.
	mdns_dns_service_t				service;				// DNS service for query.
	mdns_dns_service_definition_t	definition;				// DNS service definition.
	mdns_dns_service_id_t			registered_service_id;	// ID of DNS service registered with DNS service definition.
	
}	QuerierCmd;

static OSStatus	_QuerierCmdCreate( QuerierCmd **outCmd );
static void		_QuerierCmdRetain( QuerierCmd *inCmd );
static void		_QuerierCmdRelease( QuerierCmd *inCmd );
static OSStatus	_QuerierCmdRun( QuerierCmd *inCmd );

static void	QuerierCommand( void )
{
	OSStatus			err;
	QuerierCmd *		cmd = NULL;
	size_t				i;
	uint8_t				qname[ kDomainNameLengthMax ];
	
	err = _QuerierCmdCreate( &cmd );
	require_noerr( err, exit );
	
	if( gInterface )
	{
		err = InterfaceIndexFromArgString( gInterface, &cmd->ifIndex );
		require_noerr_quiet( err, exit );
	}
	else
	{
		cmd->ifIndex = 0;
	}
	err = DomainNameFromString( qname, gQuerier_Name, NULL );
	if( err )
	{
		FPrintF( stderr, "error: Invalid domain name: '%s'\n", gDNSQuery_Name );
		goto exit;
	}
	err = DomainNameDup( qname, &cmd->qname, NULL );
	require_noerr( err, exit );
	
	err = RecordTypeFromArgString( gQuerier_Type, &cmd->qtype );
	require_noerr_quiet( err, exit );
	
	err = RecordClassFromArgString( gQuerier_Class, &cmd->qclass );
	require_noerr( err, exit );
	
	if( gQuerier_Delegator )
	{
		err = StringToUUID( gQuerier_Delegator, kSizeCString, false, cmd->delegatorUUID );
		if( !err )
		{
			cmd->haveDelegatorUUID = true;
		}
		else
		{
			cmd->delegatorPID = _StringToPID( gQuerier_Delegator, &err );
			if( err )
			{
				FPrintF( stderr, "error: Invalid delegator PID or UUID: %s\n", gQuerier_Delegator );
				err = kParamErr;
				goto exit;
			}
			cmd->haveDelegatorPID = true;
		}
	}
	if( gQuerier_ResolverType )
	{
		cmd->resolverType = (mdns_resolver_type_t) CLIArgToValue( "resolverType", gQuerier_ResolverType, &err,
			kMDNSResolverTypeStr_Normal,	(int) mdns_resolver_type_normal,
			kMDNSResolverTypeStr_TCPOnly,	(int) mdns_resolver_type_tcp,
			kMDNSResolverTypeStr_TLS,		(int) mdns_resolver_type_tls,
			kMDNSResolverTypeStr_HTTPS,		(int) mdns_resolver_type_https,
			NULL );
		require_noerr_quiet( err, exit );
		
		for( i = 0; i < gQuerier_ServerAddrCount; ++i )
		{
			const char * const		addrStr = gQuerier_ServerAddrs[ i ];
			mdns_address_t			serverAddr;
			
			serverAddr = mdns_address_create_from_ip_address_string( addrStr );
			if( !serverAddr )
			{
				FPrintF( stderr, "error: Failed to create address for '%s'\n", addrStr );
				err = kParamErr;
				goto exit;
			}
			CFArrayAppendValue( cmd->serverAddrs, serverAddr );
			mdns_release( serverAddr );
		}
		if( gQuerier_ProviderName )
		{
			cmd->providerName = strdup( gQuerier_ProviderName );
			require_action( cmd->providerName, exit, err = kNoMemoryErr );
		}
		if( gQuerier_ConnectionHostname )
		{
			cmd->connectionHostname = strdup( gQuerier_ConnectionHostname );
			require_action( cmd->connectionHostname, exit, err = kNoMemoryErr );
		}
		if( gQuerier_URLPath )
		{
			cmd->urlPath = strdup( gQuerier_URLPath );
			require_action( cmd->urlPath, exit, err = kNoMemoryErr );
		}
		if( gQuerier_IdentityReference )
		{
			err = HexToDataCopy( gQuerier_IdentityReference, kSizeCString, kHexToData_DefaultFlags, &cmd->identityRefPtr,
				&cmd->identityRefLen, NULL );
			require_noerr_action( err, exit, FPrintF( stderr,
				"error: Failed to parse identity reference hex string: '%s'\n", gQuerier_IdentityReference ) );
		}
		if( gQuerier_ODoHConfig )
		{
			err = HexToDataCopy( gQuerier_ODoHConfig, kSizeCString, kHexToData_DefaultFlags, &cmd->odohCfgPtr,
				&cmd->odohCfgLen, NULL );
			require_noerr_action( err, exit, FPrintF( stderr,
				"error: Failed to parse ODoH config hex string: '%s'\n", gQuerier_ODoHConfig ) );
		}
		cmd->noConnectionReuse	= gQuerier_NoConnectionReuse ? true : false;
		cmd->squashCNAMEs		= gQuerier_SquashCNAMEs		 ? true : false;
	}
	else if ( ( gQuerier_ServerAddrCount > 0 ) || ( gQuerier_DomainCount > 0 ) )
	{
		cmd->definition = mdns_dns_service_definition_create();
		require_action( cmd->definition, exit, err = kNoResourcesErr );
		
		if( cmd->ifIndex != 0 ) mdns_dns_service_definition_set_interface_index( cmd->definition, cmd->ifIndex, true );
		for( i = 0; i < gQuerier_ServerAddrCount; ++i )
		{
			const char * const		addrStr = gQuerier_ServerAddrs[ i ];
			mdns_address_t			serverAddr;
			
			serverAddr = mdns_address_create_from_ip_address_string( addrStr );
			if( !serverAddr )
			{
				FPrintF( stderr, "error: Failed to create address for '%s'\n", addrStr );
				err = kParamErr;
				goto exit;
			}
			err = mdns_dns_service_definition_append_server_address( cmd->definition, serverAddr );
			mdns_forget( &serverAddr );
			require_noerr( err, exit );
		}
		for( i = 0; i < gQuerier_DomainCount; ++i )
		{
			const char * const		domainStr = gQuerier_Domains[ i ];
			mdns_domain_name_t		domain;
			
			domain = mdns_domain_name_create( domainStr, mdns_domain_name_create_opts_none, &err );
			if( !domain )
			{
				FPrintF( stderr, "error: Failed to create domain name for '%s': %#m\n", domainStr, err );
				goto exit;
			}
			mdns_dns_service_definition_add_domain( cmd->definition, domain );
			mdns_forget( &domain );
		}
	}
	cmd->dnssecOK			= gQuerier_DNSSECOK			? true : false;
	cmd->checkingDisabled	= gQuerier_CheckingDisabled	? true : false;
	cmd->sensitiveLogging	= gQuerier_SensitiveLogging ? true : false;
	if( gQuerier_StartLeewayMs )
	{
		err = StringToInt32( gQuerier_StartLeewayMs, &cmd->startTimeLeewayMs );
		require_noerr_action_quiet( err, exit, FPrintF( stderr,
			"Invalid startLeeway value: '%s'\n", gQuerier_StartLeewayMs ) );

		cmd->haveStartTimeLeeway = true;
	}
	err = _QuerierCmdRun( cmd );
	require_noerr( err, exit );
	
exit:
	if( cmd ) _QuerierCmdRelease( cmd );
	gExitCode = err ? 1 : 0;
}

//===========================================================================================================================

static OSStatus	_QuerierCmdCreate( QuerierCmd **outCmd )
{
	OSStatus			err;
	QuerierCmd *		cmd;
	
	cmd = (QuerierCmd *) calloc( 1, sizeof( *cmd ) );
	require_action( cmd, exit, err = kNoResourcesErr );
	
	cmd->refCount		= 1;
	cmd->resolverType	= mdns_resolver_type_null;
	
	cmd->queue = dispatch_queue_create( "com.apple.dnssdutil.querier-command", DISPATCH_QUEUE_SERIAL );
	require_action( cmd->queue, exit, err = kNoResourcesErr );
	
	cmd->doneSem = dispatch_semaphore_create( 0 );
	require_action( cmd->doneSem, exit, err = kNoResourcesErr );
	
	cmd->serverAddrs = CFArrayCreateMutable( kCFAllocatorDefault, 0, &mdns_cfarray_callbacks );
	require_action( cmd->serverAddrs, exit, err = kNoResourcesErr );
	
	*outCmd = cmd;
	cmd = NULL;
	err = kNoErr;
	
exit:
	if( cmd ) _QuerierCmdRelease( cmd );
	return( err );
}

//===========================================================================================================================

static void	_QuerierCmdRetain( QuerierCmd *inCmd )
{
	atomic_add_32( &inCmd->refCount, 1 );
}

//===========================================================================================================================

static void	_QuerierCmdRelease( QuerierCmd *inCmd )
{
	if( atomic_add_and_fetch_32( &inCmd->refCount, -1 ) == 0 )
	{
		check( !inCmd->sourceSigInt );
		check( !inCmd->sourceSigTerm );
		check( !inCmd->resolver );
		check( !inCmd->manager );
		check( !inCmd->service );
		check( !inCmd->querier );
		dispatch_forget( &inCmd->queue );
		dispatch_forget( &inCmd->doneSem );
		ForgetMem( &inCmd->qname );
		ForgetCF( &inCmd->serverAddrs );
		ForgetMem( &inCmd->providerName );
		ForgetMem( &inCmd->connectionHostname );
		ForgetMem( &inCmd->urlPath );
		ForgetPtrLen( &inCmd->identityRefPtr, &inCmd->identityRefLen );
		ForgetPtrLen( &inCmd->odohCfgPtr, &inCmd->odohCfgLen );
		mdns_forget( &inCmd->definition );
		free( inCmd );
	}
}

//===========================================================================================================================

static void	_QuerierCmdStart( void *inCtx );
static void	_QuerierCmdStop( QuerierCmd *inCmd, OSStatus inError );
static void	_QuerierCmdSigIntHandler( void *inCtx );
static void	_QuerierCmdSigTermHandler( void *inCtx );

static OSStatus	_QuerierCmdRun( QuerierCmd *inCmd )
{
	dispatch_async_f( inCmd->queue, inCmd, _QuerierCmdStart );
    dispatch_semaphore_wait( inCmd->doneSem, DISPATCH_TIME_FOREVER );
	return( inCmd->error );
}

static void	_QuerierCmdStart( void *inCtx )
{
	OSStatus				err;
	QuerierCmd * const		cmd		= (QuerierCmd *) inCtx;
	dns_config_t *			config	= NULL;
	const char *			ifNamePtr;
	char					ifNameBuf[ IF_NAMESIZE + 1 ];
	
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, cmd->queue, _QuerierCmdSigIntHandler, cmd, &cmd->sourceSigInt );
	require_noerr( err, exit );
	dispatch_resume( cmd->sourceSigInt );
	
	signal( SIGTERM, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGTERM, cmd->queue, _QuerierCmdSigTermHandler, cmd, &cmd->sourceSigTerm );
	require_noerr( err, exit );
	dispatch_resume( cmd->sourceSigTerm );
	
	ifNamePtr = if_indextoname( cmd->ifIndex, ifNameBuf );
	FPrintF( stdout, "Interface:            %u (%s)\n",		cmd->ifIndex, ifNamePtr ? ifNamePtr : "?" );
	FPrintF( stdout, "Name:                 %{du:dname}\n",	cmd->qname );
	FPrintF( stdout, "Type:                 %s (%u)\n",		RecordTypeToString( cmd->qtype ), cmd->qtype );
	FPrintF( stdout, "Class:                %s (%u)\n",		RecordClassToString( cmd->qclass ), cmd->qclass );
	if( cmd->haveStartTimeLeeway )
	{
		FPrintF( stdout, "Start leeway:         " );
		if( cmd->startTimeLeewayMs >= 0 )	FPrintF( stdout, "%d ms\n", cmd->startTimeLeewayMs );
		else								FPrintF( stdout, "∞ ms\n" );
	}
	if( cmd->definition || ( cmd->resolverType == mdns_resolver_type_null ) )
	{
		FPrintF( stdout, "Start time:           %{du:time}\n", NULL );
		FPrintF( stdout, "---\n" );
		
		cmd->manager = mdns_dns_service_manager_create( cmd->queue, &err );
		require_noerr( err, exit );
		
		_QuerierCmdRetain( cmd );
		mdns_dns_service_manager_set_event_handler( cmd->manager,
		^( mdns_event_t inEvent, OSStatus inError )
		{
			switch( inEvent )
			{
				case mdns_event_error:
					if( !cmd->done )
					{
						FPrintF( stderr, "error: DNS service manager failed: %#m\n", inError );
						_QuerierCmdStop( cmd, inError );
					}
					break;
				
				case mdns_event_invalidated:
					_QuerierCmdRelease( cmd );
					break;
				
				default:
					break;
			}
		} );
		if( cmd->definition )
		{
			cmd->registered_service_id = mdns_dns_service_manager_register_native_service( cmd->manager, cmd->definition,
				&err );
			require_noerr( err, exit );
		}
		else
		{
			config = dns_configuration_copy();
			require_action( config, exit, err = kUnknownErr );
			
			mdns_dns_service_manager_apply_dns_config( cmd->manager, config );
		}
		
		if( cmd->definition )
		{
			if( cmd->ifIndex == 0 )
			{
				cmd->service = mdns_dns_service_manager_get_unscoped_native_service( cmd->manager, cmd->qname );
			}
			else
			{
				cmd->service = mdns_dns_service_manager_get_interface_scoped_native_service( cmd->manager, cmd->qname,
					cmd->ifIndex );
			}
		}
		else
		{
			if( cmd->ifIndex == 0 )
			{
				cmd->service = mdns_dns_service_manager_get_unscoped_system_service( cmd->manager, cmd->qname );
			}
			else
			{
				cmd->service = mdns_dns_service_manager_get_interface_scoped_system_service( cmd->manager, cmd->qname,
					cmd->ifIndex );
			}
		}
		if( !cmd->service )
		{
			FPrintF( stderr, "error: Failed to get DNS service for %{du:dname}\n", cmd->qname );
			err = kNotFoundErr;
			goto exit;
		}
		mdns_retain( cmd->service );
		FPrintF( stdout, "Using DNS service -- %@\n\n", cmd->service );
		
		cmd->querier = mdns_dns_service_create_querier( cmd->service, &err );
		require_noerr( err, exit );
	}
	else
	{
		CFIndex		n, i;
		
		FPrintF( stdout, "Resolver Type:        %s\n", mdns_resolver_type_to_string( cmd->resolverType ) );
		if( cmd->providerName )			FPrintF( stdout, "Provider Name:        %s\n", cmd->providerName );
		if( cmd->connectionHostname )	FPrintF( stdout, "Connection Hostname:  %s\n", cmd->connectionHostname );
		if( cmd->urlPath )				FPrintF( stdout, "URL path:             %s\n", cmd->urlPath );
		if( cmd->identityRefPtr )
		{
			FPrintF( stdout, "Identity Reference:   %H\n", cmd->identityRefPtr, (int) cmd->identityRefLen, INT_MAX );
		}
		if( cmd->odohCfgPtr )
		{
			FPrintF( stdout, "ODoH Config:          %H\n", cmd->odohCfgPtr, (int) cmd->odohCfgLen, INT_MAX );
		}
		FPrintF( stdout, "Server(s):            " );
		n = CFArrayGetCount( cmd->serverAddrs );
		for( i = 0; i < n; ++i )
		{
			FPrintF( stdout, "%s%@", ( i == 0 ) ? "" : ", ", CFArrayGetValueAtIndex( cmd->serverAddrs, i ) );
		}
		FPrintF( stdout, "\n" );
		FPrintF( stdout, "Start time:           %{du:time}\n", NULL );
		FPrintF( stdout, "---\n" );
		
		cmd->resolver = mdns_resolver_create( cmd->resolverType, cmd->ifIndex, &err );
		require_noerr( err, exit );
		
		if( cmd->providerName )
		{
			err = mdns_resolver_set_provider_name( cmd->resolver, cmd->providerName );
			require_noerr( err, exit );
		}
		if( cmd->connectionHostname )
		{
			err = mdns_resolver_set_connection_hostname( cmd->resolver, cmd->connectionHostname );
			require_noerr( err, exit );
		}
		if( cmd->urlPath )
		{
			err = mdns_resolver_set_url_path( cmd->resolver, cmd->urlPath );
			require_noerr( err, exit );
		}
		if( cmd->identityRefPtr )
		{
			err = mdns_resolver_set_identity_reference( cmd->resolver, cmd->identityRefPtr, cmd->identityRefLen );
			require_noerr( err, exit );
		}
		if( cmd->odohCfgPtr )
		{
			err = mdns_resolver_update_odoh_config( cmd->resolver, cmd->providerName, cmd->urlPath, cmd->odohCfgPtr,
				cmd->odohCfgLen, NULL );
			require_noerr( err, exit );
		}
		if( cmd->noConnectionReuse ) mdns_resolver_disable_connection_reuse( cmd->resolver, true );
		if( cmd->squashCNAMEs ) mdns_resolver_set_squash_cnames( cmd->resolver, true );
		for( i = 0; i < n; ++i )
		{
			const mdns_address_t		addr = (mdns_address_t) CFArrayGetValueAtIndex( cmd->serverAddrs, i );
			
			err = mdns_resolver_add_server_address( cmd->resolver, addr );
			require_noerr( err, exit );
		}
		mdns_resolver_activate( cmd->resolver );
		
		cmd->querier = mdns_resolver_create_querier( cmd->resolver, &err );
		require_noerr( err, exit );
	}
	err = mdns_querier_set_query( cmd->querier, cmd->qname, cmd->qtype, cmd->qclass );
	require_noerr( err, exit );
	
	if( cmd->haveDelegatorPID )			mdns_querier_set_delegator_pid( cmd->querier, cmd->delegatorPID );
	else if( cmd->haveDelegatorUUID )	mdns_querier_set_delegator_uuid( cmd->querier, cmd->delegatorUUID );
	if( cmd->dnssecOK )					mdns_querier_set_dnssec_ok( cmd->querier, true );
	if( cmd->checkingDisabled )			mdns_querier_set_checking_disabled( cmd->querier, true );
	if( cmd->haveStartTimeLeeway )		mdns_querier_set_start_time_leeway( cmd->querier, cmd->startTimeLeewayMs );
	if( cmd->sensitiveLogging )			mdns_querier_enable_sensitive_logging( cmd->querier, true );
	
	_QuerierCmdRetain( cmd );
	mdns_querier_set_queue( cmd->querier, cmd->queue );
	mdns_querier_set_result_handler( cmd->querier,
	^ {
		if( !cmd->done )
		{
			const mdns_querier_result_type_t		resultType = mdns_querier_get_result_type( cmd->querier );
			
			if( resultType == mdns_querier_result_type_response )
			{
				const uint8_t * const		msgPtr = mdns_querier_get_response_ptr( cmd->querier );
				const size_t				msgLen = mdns_querier_get_response_length( cmd->querier );
				
				FPrintF( stdout, "Message size:     %zu bytes\n", msgLen );
				FPrintF( stdout, "%{du:dnsmsg}\n", msgPtr, msgLen );
				_QuerierCmdStop( cmd, kNoErr );
			}
			else
			{
				OSStatus		querierErr;
				
				if( resultType == mdns_querier_result_type_error )
				{
					querierErr = mdns_querier_get_error( cmd->querier );
					if( !querierErr ) querierErr = kUnknownErr;
				}
				else
				{
					querierErr = kUnexpectedErr;
				}
				FPrintF( stderr, "error: Unexpected querier result: %s, error: %#m\n",
					mdns_querier_result_type_to_string( resultType ), querierErr );
				_QuerierCmdStop( cmd, querierErr );
			}
		}
		_QuerierCmdRelease( cmd );
	} );
	mdns_querier_activate( cmd->querier );
	
exit:
	if( config ) dns_configuration_free( config );
	if( err ) _QuerierCmdStop( cmd, err );
}

//===========================================================================================================================

#define mdns_dns_service_manager_forget( X )		ForgetCustomEx( X, mdns_dns_service_manager_invalidate, mdns_release )

static void	_QuerierCmdStop( QuerierCmd *inCmd, OSStatus inError )
{
	if( !inCmd->done )
	{
		inCmd->done		= true;
		inCmd->error	= inError;
		dispatch_source_forget( &inCmd->sourceSigInt );
		dispatch_source_forget( &inCmd->sourceSigTerm );
		mdns_querier_forget( &inCmd->querier );
		mdns_resolver_forget( &inCmd->resolver );
		if( inCmd->manager && ( inCmd->registered_service_id != 0 ) )
		{
			mdns_dns_service_manager_deregister_native_service( inCmd->manager, inCmd->registered_service_id );
		}
		mdns_dns_service_manager_forget( &inCmd->manager );
		mdns_forget( &inCmd->service );
		mdns_forget( &inCmd->definition );
		FPrintF( stdout, "---\n" );
		FPrintF( stdout, "End time:       %{du:time}\n", NULL );
		dispatch_semaphore_signal( inCmd->doneSem );
	}
}

//===========================================================================================================================

static void	_QuerierCmdSigIntHandler( void *inCtx )
{
	FPrintF( stdout, "*** Got SIGINIT signal ***\n" );
	_QuerierCmdStop( (QuerierCmd *) inCtx, kCanceledErr );
}

//===========================================================================================================================

static void	_QuerierCmdSigTermHandler( void *inCtx )
{
	FPrintF( stdout, "*** Got SIGTERM signal ***\n" );
	_QuerierCmdStop( (QuerierCmd *) inCtx, kCanceledErr );
}

//===========================================================================================================================
//	DNSProxyCmd
//===========================================================================================================================

static void	_DNSProxyCmdSignalHandler( void *inContext );

static void	DNSProxyCmd( void )
{
	OSStatus						err;
	size_t							i;
	mrc_dns_proxy_parameters_t		params			= NULL;
	mrc_dns_proxy_t					dnsProxy		= NULL;
	dispatch_source_t				sigIntSource	= NULL;
	dispatch_source_t				sigTermSource	= NULL;
	__block const char *			separator;
	uint32_t						outputIfIndex;
	char							ifName[ kInterfaceNameBufLen ];
	uint8_t							dns64Prefix[ 16 ];
	int								dns64PrefixBitLen;
	
	params = mrc_dns_proxy_parameters_create( &err );
	require_noerr( err, exit );
	
	for( i = 0; i < gDNSProxy_InputInterfaceCount; ++i )
	{
		uint32_t		ifIndex;
		
		err = InterfaceIndexFromArgString( gDNSProxy_InputInterfaces[ i ], &ifIndex );
		require_noerr_quiet( err, exit );
		
		mrc_dns_proxy_parameters_add_input_interface( params, ifIndex );
	}
	
	if( gDNSProxy_OutputInterface )
	{
		err = InterfaceIndexFromArgString( gDNSProxy_OutputInterface, &outputIfIndex );
		require_noerr_quiet( err, exit );
	}
	else
	{
		outputIfIndex = 0;
	}
	mrc_dns_proxy_parameters_set_output_interface( params, outputIfIndex );
	
	dns64PrefixBitLen = 0;
	if( gDNSProxy_DNS64IPv6Prefix )
	{
		const char *		end;
		
		err = _StringToIPv6Address( gDNSProxy_DNS64IPv6Prefix,
			kStringToIPAddressFlagsNoPort | kStringToIPAddressFlagsNoScope, dns64Prefix, NULL, NULL, &dns64PrefixBitLen,
			&end );
		if( !err && ( *end != '\0' ) ) err = kMalformedErr;
		require_noerr_quiet( err, exit );
		
		mrc_dns_proxy_parameters_set_nat64_prefix( params, dns64Prefix, (size_t) dns64PrefixBitLen );
	}
	if( gDNSProxy_ForceAAAASynthesis ) mrc_dns_proxy_parameters_set_force_aaaa_synthesis( params, true );
	FPrintF( stdout, "Input Interfaces:    " );
	separator = "";
	mrc_dns_proxy_parameters_enumerate_input_interfaces( params,
	^ bool ( const uint32_t inIfIndex )
	{
		char		inputIfName[ kInterfaceNameBufLen ];
		
		FPrintF( stdout, "%s %u (%s)", separator, inIfIndex, InterfaceIndexToName( inIfIndex, inputIfName ) );
		separator = ",";
		return( true );
	} );
	FPrintF( stdout, "\n" );
	FPrintF( stdout, "Output Interface:     %u (%s)\n",		outputIfIndex, InterfaceIndexToName( outputIfIndex, ifName ) );
	if( gDNSProxy_DNS64IPv6Prefix ) FPrintF( stdout, "DNS64 prefix:         %.16a/%d\n", dns64Prefix, dns64PrefixBitLen );
	FPrintF( stdout, "Force AAAA synthesis: %s\n", YesNoStr( mrc_dns_proxy_parameters_get_force_aaaa_synthesis( params ) ) );
	FPrintF( stdout, "Start time:           %{du:time}\n",	NULL );
	FPrintF( stdout, "---\n" );
	
	dnsProxy = mrc_dns_proxy_create( params, &err );
	mrc_forget( &params );
	require_noerr_quiet( err, exit );
	
	mrc_dns_proxy_set_queue( dnsProxy, dispatch_get_main_queue() );
	mrc_dns_proxy_set_event_handler( dnsProxy,
	^( const mrc_dns_proxy_event_t inEvent, const OSStatus inError )
	{
		switch( inEvent )
		{
			case mrc_dns_proxy_event_started:
			{
				FPrintF( stdout, "%{du:time}  DNS proxy was started\n", NULL );
				break;
			}
			case mrc_dns_proxy_event_interruption:
			{
				FPrintF( stdout, "%{du:time}  DNS proxy was interrupted\n", NULL );
				break;
			}
			case mrc_dns_proxy_event_invalidation:
			{
				struct timeval		now;
				
				gettimeofday( &now, NULL );
				
				mrc_release( dnsProxy );
				
				FPrintF( stdout, "---\n" );
				FPrintF( stdout, "Error:    %#m\n", inError );
				FPrintF( stdout, "End time: %{du:time}\n", &now );
				exit( 0 );
			}
			case mrc_dns_proxy_event_none:
				break;
		}
	} );
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), _DNSProxyCmdSignalHandler, dnsProxy,
		&sigIntSource );
	require_noerr( err, exit );
	dispatch_activate( sigIntSource );
	
	signal( SIGTERM, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGTERM, dispatch_get_main_queue(), _DNSProxyCmdSignalHandler, dnsProxy,
		&sigTermSource );
	require_noerr( err, exit );
	dispatch_activate( sigTermSource );
	
	mrc_dns_proxy_activate( dnsProxy );
	dispatch_main();
	
exit:
	if( err ) ErrQuit( 1, "error: %#m\n", err );
}

static void	_DNSProxyCmdSignalHandler( void *inContext )
{
	mrc_dns_proxy_invalidate( (mrc_dns_proxy_t) inContext );
}

//===========================================================================================================================
//	DNSProxyStateCmd
//===========================================================================================================================

static void	DNSProxyStateCmd( void )
{
	OSStatus							err;
	dispatch_queue_t					queue;
	dispatch_semaphore_t				doneSem	= NULL;
	mrc_dns_proxy_state_inquiry_t		inquiry	= NULL;
	
	queue = dispatch_queue_create( "com.apple.dnssdutil.dns-proxy-state-command", DISPATCH_QUEUE_SERIAL );
	require_action( queue, exit, err = kNoResourcesErr );
	
	doneSem = dispatch_semaphore_create( 0 );
	require_action( doneSem, exit, err = kNoResourcesErr );
	
	inquiry = mrc_dns_proxy_state_inquiry_create();
	require_action( inquiry, exit, err = kNoResourcesErr );
	
	mrc_dns_proxy_state_inquiry_set_queue( inquiry, queue );
	mrc_dns_proxy_state_inquiry_set_handler( inquiry,
	^( const char * const inState, const OSStatus inError )
	{
		if( inError ) FPrintF( stderr, "error: %#m\n", inError );
		if( inState ) FPrintF( stdout, "%s\n", inState );
		dispatch_semaphore_signal( doneSem );
	} );
	mrc_dns_proxy_state_inquiry_activate( inquiry );
	
	// Wait for the DNS proxy state inquiry to complete.
	// Note: We intentionally use a semaphore that will be signalled by the asynchronous handler. The analyzer considers
	// this an antipattern, which is OK for the purposes of this internal test tool. Actual well-written production code
	// would usually not have to emulate this type of synchronous behavior.
	
#if( !COMPILER_CLANG_ANALYZER )
    dispatch_semaphore_wait( doneSem, DISPATCH_TIME_FOREVER );
#endif
	err = kNoErr;
	
exit:
	dispatch_forget( &queue );
	dispatch_forget( &doneSem );
	mrc_forget( &inquiry );
	gExitCode = err ? 1 : 0;
}

//===========================================================================================================================
//	GetAddrInfoNewCommand
//===========================================================================================================================


typedef enum
{
	kDelegationType_None		= 0,	// No delegation.
	kDelegationType_PID			= 1,	// Delegation by PID.
	kDelegationType_UUID		= 2,	// Delegation by UUID.
	kDelegationType_AuditToken	= 3		// Delegation by audit token.
	
}	DelegationType;

typedef struct
{
	DelegationType			type;		// Type of delegation.
	union
	{
		pid_t				pid;		// Delegator's PID if type is kDelegationType_PID.
		uuid_t				uuid;		// Delegator's UUID if type is kDelegationType_UUID.
		audit_token_t		auditToken;	// Delegator's audit token if type is kDelegationType_AuditToken.
		
	}	ident;							// Delegator's identifier.
	
}	Delegation;

typedef struct
{
	dispatch_queue_t		queue;					// Serial queue for command's events.
	dispatch_group_t		group;					// GCD group to know when command is done.
	dnssd_getaddrinfo_t		gai;					// dnssd_getaddrinfo object.
	dispatch_source_t		timer;					// Timer to impose time limit on dnssd_getaddrinfo activity.
	dispatch_source_t		sigint;					// Dispatch source for SIGINT.
	dispatch_source_t		sigterm;				// Dispatch source for SIGTERM.
	const char *			hostname;				// dnssd_getaddrinfo's hostname argument.
	const char *			serviceScheme;			// dnssd_getaddrinfo's service scheme argument.
	const char *			accountID;				// dnssd_getaddrinfo's account ID argument.
	char *					stopReason;				// Reason for stopping the command.
	uuid_t *				resolverUUID;			// UUID of libnetwork DNS resolver configuration to use.
	Delegation				delegation;				// Specifies the type of delegation to use for dnssd_getaddrinfo, if any.
	int32_t					refCount;				// Reference count.
	DNSServiceFlags			flags;					// dnssd_getaddrinfo's flags argument.
	DNSServiceProtocol		protocols;				// dnssd_getaddrinfo's protocols argument.
	uint32_t				ifIndex;				// dnssd_getaddrinfo's interface index argument.
	unsigned int			timeLimitSecs;			// Time limit in seconds for dnssd_getaddrinfo activity.
	OSStatus				error;					// Command's error.
	Boolean					showTracker;			// True if tracker hostname, if any, should be displayed.
	Boolean					useFailover;			// True if DNS service failover should be used if necessary.
	Boolean					prohibitEncryptedDNS;	// True if use of encrypted DNS protocols is prohibited.
	Boolean					privateLogging;			// True if private logging is to be enabled.
	Boolean					stopped;				// True if the command has been stopped.
	Boolean					oneshot;				// True if the command should stop after first set of results.
	Boolean					printedHeader;			// True if the results header has been printed.
	
}	GetAddrInfoNewCmd;

static GetAddrInfoNewCmd *	_GetAddrInfoNewCmdCreateEx( qos_class_t inQoS, Boolean inUseQoS, OSStatus *outError );
#define _GetAddrInfoNewCmdCreate( OUT_ERROR )						_GetAddrInfoNewCmdCreateEx( 0, false, OUT_ERROR )
#define _GetAddrInfoNewCmdCreateWithQoS( IN_QOS, OUT_ERROR )		_GetAddrInfoNewCmdCreateEx( IN_QOS, true, OUT_ERROR )
static OSStatus				_GetAddrInfoNewCmdRun( GetAddrInfoNewCmd *inCmd );
static void					_GetAddrInfoNewCmdStopF( GetAddrInfoNewCmd *inCmd, const char *inFmt, ... );
static GetAddrInfoNewCmd *	_GetAddrInfoNewCmdRetain( GetAddrInfoNewCmd *inCmd );
static void					_GetAddrInfoNewCmdRelease( GetAddrInfoNewCmd *inCmd );

#define _GetAddrInfoNewCmdForget( X )		ForgetCustom( X, _GetAddrInfoNewCmdRelease )

static void	GetAddrInfoNewCommand( void )
{
	OSStatus				err;
	GetAddrInfoNewCmd *		cmd = NULL;
	
	if( gGAINew_QoS )
	{
		qos_class_t		qos;
		
		qos = (qos_class_t) CLIArgToValue( kQoSArgShortName, gGAINew_QoS, &err,
			kQoSTypeStr_Unspecified,		QOS_CLASS_UNSPECIFIED,
			kQoSTypeStr_Background,			QOS_CLASS_BACKGROUND,
			kQoSTypeStr_Utility,			QOS_CLASS_UTILITY,
			kQoSTypeStr_Default,			QOS_CLASS_DEFAULT,
			kQoSTypeStr_UserInitiated,		QOS_CLASS_USER_INITIATED,
			kQoSTypeStr_UserInteractive,	QOS_CLASS_USER_INTERACTIVE,
			NULL );
		require_noerr_quiet( err, exit );
		
		cmd = _GetAddrInfoNewCmdCreateWithQoS( qos, &err );
		require_noerr( err, exit );
	}
	else
	{
		cmd = _GetAddrInfoNewCmdCreate( &err );
		require_noerr( err, exit );
	}
	err = CheckIntegerArgument( gGAINew_TimeLimitSecs, "time limit", 0, INT_MAX );
	require_noerr_quiet( err, exit );
	
	cmd->timeLimitSecs = (unsigned int) gGAINew_TimeLimitSecs;
	
	cmd->hostname				= gGAINew_Hostname;
	cmd->flags					= GetDNSSDFlagsFromOpts();
	cmd->serviceScheme			= gGAINew_ServiceScheme;
	cmd->accountID				= gGAINew_AccountID;
	cmd->showTracker			= gGAINew_ShowTracker			? true : false;
	cmd->useFailover			= gGAINew_UseFailover			? true : false;
	cmd->prohibitEncryptedDNS	= gGAINew_ProhibitEncryptedDNS	? true : false;
	cmd->privateLogging			= gGAINew_PrivateLogging		? true : false;
	cmd->oneshot				= gGAINew_OneShot				? true : false;
	
	err = InterfaceIndexFromArgString( gInterface, &cmd->ifIndex );
	require_noerr_quiet( err, exit );
	
	cmd->protocols = 0;
	if( gGAINew_ProtocolIPv4 ) cmd->protocols |= kDNSServiceProtocol_IPv4;
	if( gGAINew_ProtocolIPv6 ) cmd->protocols |= kDNSServiceProtocol_IPv6;
	
	// Get delegate ID.
	
	if( gGAINew_DelegatorID )
	{
		pid_t		delegatorPID;
		
		delegatorPID = _StringToPID( gGAINew_DelegatorID, &err );
		if( !err )
		{
			if( delegatorPID >= 0 )
			{
				cmd->delegation.ident.pid	= delegatorPID;
				cmd->delegation.type		= kDelegationType_PID;
			}
			else
			{
				delegatorPID = -delegatorPID;
				if( audit_token_for_pid( delegatorPID, &cmd->delegation.ident.auditToken ) )
				{
					cmd->delegation.type = kDelegationType_AuditToken;
				}
				else
				{
					FPrintF( stderr, "Failed to get audit token for PID: %d\n", delegatorPID );
					err = kParamErr;
					goto exit;
				}
			}
		}
		else
		{
			err = uuid_parse( gGAINew_DelegatorID, cmd->delegation.ident.uuid );
			if( !err )
			{
				cmd->delegation.type = kDelegationType_UUID;
			}
			else
			{
				FPrintF( stderr, "Invalid delegate ID (PID or UUID): %s\n", gGAINew_DelegatorID );
				err = kParamErr;
				goto exit;
			}
		}
	}
	if( gGAINew_ResolverUUID )
	{
		uuid_t		uuid;
		
		err = uuid_parse( gGAINew_ResolverUUID, uuid );
		if( err )
		{
			FPrintF( stderr, "Invalid resolver UUID: %s\n", gGAINew_ResolverUUID );
			err = kParamErr;
			goto exit;
		}
		cmd->resolverUUID = (uuid_t *) _memdup( uuid, sizeof( uuid ) );
		require_action( cmd->resolverUUID, exit, err = kNoMemoryErr );
	}
	err = _GetAddrInfoNewCmdRun( cmd );
	require_noerr( err, exit );
	
exit:
	_GetAddrInfoNewCmdForget( &cmd );
	gExitCode = err ? 1 : 0;
}

//===========================================================================================================================

static GetAddrInfoNewCmd *	_GetAddrInfoNewCmdCreateEx( qos_class_t inQoS, Boolean inUseQoS, OSStatus *outError )
{
	OSStatus				err;
	GetAddrInfoNewCmd *		obj;
	GetAddrInfoNewCmd *		cmd = NULL;
	
	obj = (GetAddrInfoNewCmd *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->refCount = 1;
	if( inUseQoS )
	{
		dispatch_queue_attr_t		attr;
		
		attr = dispatch_queue_attr_make_with_qos_class( DISPATCH_QUEUE_SERIAL, inQoS, 0 );
		obj->queue = dispatch_queue_create( "com.apple.dnssdutil.getaddrinfo-new-command", attr );
		require_action( obj->queue, exit, err = kNoResourcesErr );
	}
	else
	{
		obj->queue = dispatch_queue_create( "com.apple.dnssdutil.getaddrinfo-new-command", DISPATCH_QUEUE_SERIAL );
		require_action( obj->queue, exit, err = kNoResourcesErr );
	}
	obj->group = dispatch_group_create();
	require_action( obj->group, exit, err = kNoResourcesErr );
	
	cmd = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( outError ) *outError = err;
	_GetAddrInfoNewCmdForget( &obj );
	return( cmd );
}

//===========================================================================================================================

static void	_GetAddrInfoNewCmdStart( void *inCtx );

static OSStatus	_GetAddrInfoNewCmdRun( GetAddrInfoNewCmd *me )
{
	dispatch_group_enter( me->group );
	dispatch_async_f( me->queue, me, _GetAddrInfoNewCmdStart );
    dispatch_group_wait( me->group, DISPATCH_TIME_FOREVER );
	FPrintF( stdout, "---\n" );
	FPrintF( stdout, "End time:   %{du:time}\n", NULL );
	if( me->stopReason ) FPrintF( stdout, "End reason: %s\n", me->stopReason );
	return( me->error );
}

static void	_GetAddrInfoNewCmdStart( void *inCtx )
{
	OSStatus						err;
	GetAddrInfoNewCmd * const		me = (GetAddrInfoNewCmd *) inCtx;
	char							ifName[ kInterfaceNameBufLen ];
	
	me->gai = dnssd_getaddrinfo_create();
	require_action( me->gai, exit, err = kNoResourcesErr );
	
	dnssd_getaddrinfo_set_hostname( me->gai, me->hostname );
	dnssd_getaddrinfo_set_flags( me->gai, me->flags );
	dnssd_getaddrinfo_set_interface_index( me->gai, me->ifIndex );
	dnssd_getaddrinfo_set_protocols( me->gai, me->protocols );
	dnssd_getaddrinfo_set_use_failover( me->gai, me->useFailover );
	dnssd_getaddrinfo_prohibit_encrypted_dns( me->gai, me->prohibitEncryptedDNS );
	if( me->privateLogging )
	{
		dnssd_getaddrinfo_set_log_privacy_level( me->gai, dnssd_log_privacy_level_private );
	}
	switch( me->delegation.type )
	{
		case kDelegationType_PID:
			dnssd_getaddrinfo_set_delegate_pid( me->gai, me->delegation.ident.pid );
			break;
		
		case kDelegationType_UUID:
			dnssd_getaddrinfo_set_delegate_uuid( me->gai, me->delegation.ident.uuid );
			break;
		
		case kDelegationType_AuditToken:
			dnssd_getaddrinfo_set_delegate_audit_token( me->gai, me->delegation.ident.auditToken );
			break;
		
		case kDelegationType_None:
		default:
			break;
	}
	if( me->serviceScheme )
	{
		dnssd_getaddrinfo_set_service_scheme( me->gai, me->serviceScheme );
	}
	if( me->accountID )
	{
		dnssd_getaddrinfo_set_account_id( me->gai, me->accountID );
	}
	if( me->resolverUUID )
	{
		dnssd_getaddrinfo_add_resolver_uuid( me->gai, *me->resolverUUID );
	}
	dnssd_getaddrinfo_set_queue( me->gai, me->queue );
	dnssd_getaddrinfo_set_result_handler( me->gai,
	^( dnssd_getaddrinfo_result_t *inResultArray, size_t inResultCount )
	{
		size_t		i;
		
		require_return( me->gai );
		require_return( inResultCount > 0 );
		
		for( i = 0; i < inResultCount; ++i )
		{
			const dnssd_getaddrinfo_result_t			result	= inResultArray[ i ];
			const dnssd_getaddrinfo_result_type_t		type	= dnssd_getaddrinfo_result_get_type( result );
			const char *								typeStr;
			const char *								cacheStr;
			
			if( !me->printedHeader )
			{
				FPrintF( stdout, "%-26s  Type C? IF %-30s Address\n", "Timestamp", "Hostname" );
				me->printedHeader = true;
			}
			switch( type )
			{
				case dnssd_getaddrinfo_result_type_add:				typeStr = "Add"; break;
				case dnssd_getaddrinfo_result_type_remove:			typeStr = "Rmv"; break;
				case dnssd_getaddrinfo_result_type_no_address:		typeStr = "NoA"; break;
				case dnssd_getaddrinfo_result_type_expired:			typeStr = "Exp"; break;
				case dnssd_getaddrinfo_result_type_service_binding:	typeStr = "SvB"; break;
				default:											typeStr = "???"; break;
			}
			if( type == dnssd_getaddrinfo_result_type_remove )
			{
				cacheStr = "-";
			}
			else
			{
				cacheStr = dnssd_getaddrinfo_result_is_from_cache( result ) ? "Y" : "N";
			}
			FPrintF( stdout, "%{du:time}  %-4s %-2s %2d %-30s %##a",
				NULL, typeStr, cacheStr, dnssd_getaddrinfo_result_get_interface_index( result ),
				dnssd_getaddrinfo_result_get_hostname( result ), dnssd_getaddrinfo_result_get_address( result ) );
			if( type == dnssd_getaddrinfo_result_type_no_address )
			{
				FPrintF( stdout, " (%s)",
					dnssd_negative_reason_to_string( dnssd_getaddrinfo_result_get_negative_reason( result ) ) );
			}
			FPrintF( stdout, "\n" );
			if( me->flags & kDNSServiceFlagsReturnIntermediates )
			{
				const dnssd_cname_array_t		cnames = dnssd_getaddrinfo_result_get_cnames( result );
				size_t							j, n;
				
				FPrintF( stdout, "    Canonical Names: [" );
				n = dnssd_cname_array_get_count( cnames );
				for( j = 0; j < n; ++j )
				{
					FPrintF( stdout, "%s%s", ( j == 0 ) ? "" : ", ", dnssd_cname_array_get_cname( cnames, j ) );
				}
				FPrintF( stdout, "]\n" );
			}
			if( me->showTracker && ( type != dnssd_getaddrinfo_result_type_remove ) )
			{
				const char * const		tracker = dnssd_getaddrinfo_result_get_tracker_hostname( result );
				
				if( tracker )
				{
					const char * const		owner = dnssd_getaddrinfo_result_get_tracker_owner( result );
					
					FPrintF( stdout, "    Tracker: %s", tracker );
					if( owner ) FPrintF( stdout, ", Owner: %s", owner );
					FPrintF( stdout, "\n" );
				}
				else
				{
					FPrintF( stdout, "    (Not a known tracker)\n" );
				}
			}
			if( dnssd_getaddrinfo_result_has_extended_dns_error( result ) )
			{
				FPrintF( stdout, "    Extended DNS Error: {code: %u, extra-text: '%s'}\n",
					dnssd_getaddrinfo_result_get_extended_dns_error_code( result ),
					dnssd_getaddrinfo_result_get_extended_dns_error_text( result ) );
			}
		}
		if( me->oneshot ) _GetAddrInfoNewCmdStopF( me, "one-shot done" );
	} );
	_GetAddrInfoNewCmdRetain( me );
	dispatch_group_enter( me->group );
	dnssd_getaddrinfo_set_event_handler( me->gai,
	^( dnssd_event_t inEvent, DNSServiceErrorType inError )
	{
		switch( inEvent )
		{
			case dnssd_event_invalidated:
				dispatch_group_leave( me->group );
				_GetAddrInfoNewCmdRelease( me );
				break;
			
			case dnssd_event_error:
				if( !me->error ) me->error = inError;
				_GetAddrInfoNewCmdStopF( me, "error %#m", inError );
				break;
			
			default:
				break;
		}
	} );
	
	// Set up signal handlers.
	
	signal( SIGINT, SIG_IGN );
	me->sigint = dispatch_source_create( DISPATCH_SOURCE_TYPE_SIGNAL, (uintptr_t) SIGINT, 0, me->queue );
	require_action( me->sigint, exit, err = kNoResourcesErr );
	
	dispatch_source_set_event_handler( me->sigint, ^{ _GetAddrInfoNewCmdStopF( me, "interrupt signal" ); } );
	dispatch_activate( me->sigint );
	
	signal( SIGTERM, SIG_IGN );
	me->sigterm = dispatch_source_create( DISPATCH_SOURCE_TYPE_SIGNAL, (uintptr_t) SIGTERM, 0, me->queue );
	require_action( me->sigterm, exit, err = kNoResourcesErr );
	
	dispatch_source_set_event_handler( me->sigterm, ^{ _GetAddrInfoNewCmdStopF( me, "termination signal" ); } );
	dispatch_activate( me->sigterm );
	
	// Start getaddrinfo operation.
	
	InterfaceIndexToName( me->ifIndex, ifName );
	FPrintF( stdout, "Service Scheme: %s\n", me->serviceScheme );
	FPrintF( stdout, "Flags:          %#{flags}\n",	me->flags, kDNSServiceFlagsDescriptors );
	FPrintF( stdout, "Interface:      %d (%s)\n",	(int32_t) me->ifIndex, ifName );
	FPrintF( stdout, "Protocols:      %#{flags}\n",	me->protocols, kDNSServiceProtocolDescriptors );
	FPrintF( stdout, "Name:           %s\n",		me->hostname );
	FPrintF( stdout, "Mode:           %s\n",		me->oneshot ? "one-shot" : "continuous" );
	if( me->serviceScheme )	FPrintF( stdout, "Service Scheme: %s\n", me->serviceScheme );
	if( me->accountID )		FPrintF( stdout, "Account ID:     %s\n", me->accountID );
	FPrintF( stdout, "Time limit:     " );
	if( me->timeLimitSecs > 0 )	FPrintF( stdout, "%d second%?c\n", me->timeLimitSecs, me->timeLimitSecs != 1, 's' );
	else						FPrintF( stdout, "∞\n" );
	FPrintF( stdout, "Start time: %{du:time}\n",	NULL );
	FPrintF( stdout, "---\n" );
	
	if( me->timeLimitSecs > 0 )
	{
		me->timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, me->queue );
		require_action( me->timer, exit, err = kNoResourcesErr );
		
		dispatch_source_set_timer( me->timer, dispatch_time_seconds( me->timeLimitSecs ), DISPATCH_TIME_FOREVER,
			me->timeLimitSecs * ( UINT64_C_safe( kNanosecondsPerSecond ) / 20 ) );
		dispatch_source_set_event_handler( me->timer, ^{ _GetAddrInfoNewCmdStopF( me, "time limit" ); } );
		dispatch_activate( me->timer );
	}
	dnssd_getaddrinfo_activate( me->gai );
	err = kNoErr;
	
exit:
	if( err ) _GetAddrInfoNewCmdStopF( me, "error %#m", err );
}

//===========================================================================================================================

static void	_GetAddrInfoNewCmdStopF( GetAddrInfoNewCmd *me, const char *inFmt, ... )
{
	if( !me->stopped )
	{
		me->stopped = true;
		dnssd_getaddrinfo_forget( &me->gai );
		dispatch_source_forget( &me->timer );
		dispatch_source_forget( &me->sigint );
		dispatch_source_forget( &me->sigterm );
		if( inFmt )
		{
			va_list		args;
			
			check( !me->stopReason );
			va_start( args, inFmt );
			VASPrintF( &me->stopReason, inFmt, args );
			va_end( args );
			check( me->stopReason );
		}
		dispatch_group_leave( me->group );
	}
}

//===========================================================================================================================

static GetAddrInfoNewCmd *	_GetAddrInfoNewCmdRetain( GetAddrInfoNewCmd *me )
{
	atomic_add_32( &me->refCount, 1 );
	return( me );
}

//===========================================================================================================================

static void	_GetAddrInfoNewCmdRelease( GetAddrInfoNewCmd *me )
{
	if( atomic_add_and_fetch_32( &me->refCount, -1 ) == 0 )
	{
		check( !me->gai );
		check( !me->timer );
		check( !me->sigint );
		check( !me->sigterm );
		dispatch_forget( &me->queue );
		dispatch_forget( &me->group );
		ForgetMem( &me->stopReason );
		ForgetMem( &me->resolverUUID );
		free( me );
	}
}

//===========================================================================================================================
//	TCPInfoCommand
//===========================================================================================================================

static void	TCPInfoCommand( void )
{
	OSStatus			err;
	sockaddr_ip			local, remote;
	struct tcp_info		ti;
	
	err = SockAddrFromArgString( gTCPInfo_LocalAddrStr, "local IP address", &local );
	require_noerr_return( err );
	
	err = SockAddrFromArgString( gTCPInfo_RemoteAddrStr, "remote IP address", &remote );
	require_noerr_return( err );
	cli_require_return( local.sa.sa_family == remote.sa.sa_family, "error: IP address version mismatch.\n" );
	
	FPrintF( stdout, "Local Address:  %##a\n", &local.sa );
	FPrintF( stdout, "Remote Address: %##a\n", &remote.sa );
	FPrintF( stdout, "Start time:     %{du:time}\n", NULL );
	FPrintF( stdout, "---\n" );
	
	if( local.sa.sa_family == AF_INET )
	{
		err = mdns_tcpinfo_get_ipv4( ntohl( local.v4.sin_addr.s_addr ), ntohs( local.v4.sin_port ),
			ntohl( remote.v4.sin_addr.s_addr ), ntohs( remote.v4.sin_port ), &ti );
		cli_require_noerr_return( err, "error: mdns_tcpinfo_get_ipv4: %#m\n", err );
	}
	else
	{
		err = mdns_tcpinfo_get_ipv6( local.v6.sin6_addr.s6_addr, ntohs( local.v6.sin6_port ),
			remote.v6.sin6_addr.s6_addr, ntohs( remote.v6.sin6_port ), &ti );
		cli_require_noerr_return( err, "error: mdns_tcpinfo_get_ipv6: %#m\n", err );
	}
#define _TCPInfoPrint( INFO, FIELD )		FPrintF( stdout, "%-28s%llu\n", # FIELD, (unsigned long long)( (INFO)->FIELD ) )
	_TCPInfoPrint( &ti, tcpi_state );
	_TCPInfoPrint( &ti, tcpi_options );
	_TCPInfoPrint( &ti, tcpi_snd_wscale );
	_TCPInfoPrint( &ti, tcpi_rcv_wscale );
	_TCPInfoPrint( &ti, tcpi_flags );
	_TCPInfoPrint( &ti, tcpi_rto );
	_TCPInfoPrint( &ti, tcpi_snd_mss );
	_TCPInfoPrint( &ti, tcpi_rcv_mss );
	_TCPInfoPrint( &ti, tcpi_rttcur );
	_TCPInfoPrint( &ti, tcpi_srtt );
	_TCPInfoPrint( &ti, tcpi_rttvar );
	_TCPInfoPrint( &ti, tcpi_rttbest );
	_TCPInfoPrint( &ti, tcpi_snd_ssthresh );
	_TCPInfoPrint( &ti, tcpi_snd_cwnd );
	_TCPInfoPrint( &ti, tcpi_rcv_space );
	_TCPInfoPrint( &ti, tcpi_snd_wnd );
	_TCPInfoPrint( &ti, tcpi_snd_nxt );
	_TCPInfoPrint( &ti, tcpi_rcv_nxt );
	_TCPInfoPrint( &ti, tcpi_last_outif );
	_TCPInfoPrint( &ti, tcpi_snd_sbbytes );
	_TCPInfoPrint( &ti, tcpi_txpackets );
	_TCPInfoPrint( &ti, tcpi_txbytes );
	_TCPInfoPrint( &ti, tcpi_txretransmitbytes );
	_TCPInfoPrint( &ti, tcpi_txunacked );
	_TCPInfoPrint( &ti, tcpi_rxpackets );
	_TCPInfoPrint( &ti, tcpi_rxbytes );
	_TCPInfoPrint( &ti, tcpi_rxduplicatebytes );
	_TCPInfoPrint( &ti, tcpi_rxoutoforderbytes );
	_TCPInfoPrint( &ti, tcpi_snd_bw );
	_TCPInfoPrint( &ti, tcpi_synrexmits );
	_TCPInfoPrint( &ti, tcpi_unused1 );
	_TCPInfoPrint( &ti, tcpi_unused2 );
	_TCPInfoPrint( &ti, tcpi_cell_rxpackets );
	_TCPInfoPrint( &ti, tcpi_cell_rxbytes );
	_TCPInfoPrint( &ti, tcpi_cell_txpackets );
	_TCPInfoPrint( &ti, tcpi_cell_txbytes );
	_TCPInfoPrint( &ti, tcpi_wifi_rxpackets );
	_TCPInfoPrint( &ti, tcpi_wifi_rxbytes );
	_TCPInfoPrint( &ti, tcpi_wifi_txpackets );
	_TCPInfoPrint( &ti, tcpi_wifi_txbytes );
	_TCPInfoPrint( &ti, tcpi_wired_rxpackets );
	_TCPInfoPrint( &ti, tcpi_wired_rxbytes );
	_TCPInfoPrint( &ti, tcpi_wired_txpackets );
	_TCPInfoPrint( &ti, tcpi_wired_txbytes );
	FPrintF( stdout, "tcpi_connstatus = {\n" );
	FPrintF( stdout, "    probe_activated         %u\n", ti.tcpi_connstatus.probe_activated );
	FPrintF( stdout, "    write_probe_failed      %u\n", ti.tcpi_connstatus.write_probe_failed );
	FPrintF( stdout, "    read_probe_failed       %u\n", ti.tcpi_connstatus.read_probe_failed );
	FPrintF( stdout, "    conn_probe_failed       %u\n", ti.tcpi_connstatus.conn_probe_failed );
	FPrintF( stdout, "}\n" );
	_TCPInfoPrint( &ti, tcpi_tfo_cookie_req );
	_TCPInfoPrint( &ti, tcpi_tfo_cookie_rcv );
	_TCPInfoPrint( &ti, tcpi_tfo_syn_loss );
	_TCPInfoPrint( &ti, tcpi_tfo_syn_data_sent );
	_TCPInfoPrint( &ti, tcpi_tfo_syn_data_acked );
	_TCPInfoPrint( &ti, tcpi_tfo_syn_data_rcv );
	_TCPInfoPrint( &ti, tcpi_tfo_cookie_req_rcv );
	_TCPInfoPrint( &ti, tcpi_tfo_cookie_sent );
	_TCPInfoPrint( &ti, tcpi_tfo_cookie_invalid );
	_TCPInfoPrint( &ti, tcpi_tfo_cookie_wrong );
	_TCPInfoPrint( &ti, tcpi_tfo_no_cookie_rcv );
	_TCPInfoPrint( &ti, tcpi_tfo_heuristics_disable );
	_TCPInfoPrint( &ti, tcpi_tfo_send_blackhole );
	_TCPInfoPrint( &ti, tcpi_tfo_recv_blackhole );
	_TCPInfoPrint( &ti, tcpi_tfo_onebyte_proxy );
	_TCPInfoPrint( &ti, tcpi_ecn_client_setup );
	_TCPInfoPrint( &ti, tcpi_ecn_server_setup );
	_TCPInfoPrint( &ti, tcpi_ecn_success );
	_TCPInfoPrint( &ti, tcpi_ecn_lost_syn );
	_TCPInfoPrint( &ti, tcpi_ecn_lost_synack );
	_TCPInfoPrint( &ti, tcpi_local_peer );
	_TCPInfoPrint( &ti, tcpi_if_cell );
	_TCPInfoPrint( &ti, tcpi_if_wifi );
	_TCPInfoPrint( &ti, tcpi_if_wired );
	_TCPInfoPrint( &ti, tcpi_if_wifi_infra );
	_TCPInfoPrint( &ti, tcpi_if_wifi_awdl );
	_TCPInfoPrint( &ti, tcpi_snd_background );
	_TCPInfoPrint( &ti, tcpi_rcv_background );
	_TCPInfoPrint( &ti, tcpi_ecn_recv_ce );
	_TCPInfoPrint( &ti, tcpi_ecn_recv_cwr );
	_TCPInfoPrint( &ti, tcpi_rcvoopack );
	_TCPInfoPrint( &ti, tcpi_pawsdrop );
	_TCPInfoPrint( &ti, tcpi_sack_recovery_episode );
	_TCPInfoPrint( &ti, tcpi_reordered_pkts );
	_TCPInfoPrint( &ti, tcpi_dsack_sent );
	_TCPInfoPrint( &ti, tcpi_dsack_recvd );
	_TCPInfoPrint( &ti, tcpi_flowhash );
	_TCPInfoPrint( &ti, tcpi_txretransmitpackets );
#undef _TCPInfoPrint
	FPrintF( stdout, "---\n" );
	FPrintF( stdout, "End time:       %{du:time}\n", NULL );
}

//===========================================================================================================================
//    XCTestCmd
//===========================================================================================================================

static void    XCTestCmd( void )
{
    int result = 0;
    setenv(DNSSDUTIL_XCTEST, DNSSDUTIL_XCTEST, 0);
    if(!run_xctest_named(gXCTest_Classname)) {
        result = 1;
    }
    unsetenv(DNSSDUTIL_XCTEST);
    exit( result );
}

//===========================================================================================================================
//    MultiConnectTestCmd
//===========================================================================================================================

static void    MultiConnectTestCmd( void )
{
    int result = 0;
	dispatch_group_t group = dispatch_group_create();
	int count = gMultiConnectTest_ConnectionCount;
	DNSServiceRef *	refs = calloc( (uint32_t)count, sizeof(DNSServiceRef) );

	// Create count connections on async queue
	// rdar://problem/59861422
	__block int goodcount = 0;
	for ( int i = 0; i < count; i++ ) {
		char label[256];
		sprintf( label, "multi-connect queue %d", i+1 );
		dispatch_group_async( group, dispatch_queue_create( label, DISPATCH_QUEUE_SERIAL ), ^{
			DNSServiceErrorType err = DNSServiceCreateConnection( &refs[i] );
			if ( err ) {
				fprintf( stderr, "%s Failed to create connection # %d err(%d)\n", __FUNCTION__, i, err );
            } else {
                goodcount++;
            }
		});
	}
	dispatch_group_wait( group, DISPATCH_TIME_FOREVER );
	dispatch_release( group );
	fprintf( stderr, "%s Created %d connections of %d\n", __FUNCTION__, goodcount, count );

	// Free them
	goodcount = 0;
	for ( int i = 0; i < count; i++ ) {
		if ( refs[i] ) {
			DNSServiceRefDeallocate( refs[i] );
			goodcount++;
		}
	}
	fprintf( stderr, "%s Stopped %d connections of %d\n", __FUNCTION__, goodcount, count );
	result = (goodcount == count) ? 0 : 1;
    exit( result );
}

#endif	// MDNSRESPONDER_PROJECT

//===========================================================================================================================
//	ThreadPFNAT64Command
//===========================================================================================================================

static void	ThreadPFNAT64Command( void )
{
	OSStatus					err;
	const char *				endPtr;
	StringToIPAddressFlags		flags;
	uint32_t					ipv4Addr;
	int							prefixLen;
	uint8_t						prefixAddr[ 16 ];
	
	FPrintF( stdout, "Setting Thread border router NAT64 PF rules with IPv6 prefix '%s' and IPv4 address '%s'\n",
		gThreadPFNAT64_IPv6Prefix, gThreadPFNAT64_IPv4Address );
	
	flags = kStringToIPAddressFlagsNoPort | kStringToIPAddressFlagsNoScope;
	err = _StringToIPv6Address( gThreadPFNAT64_IPv6Prefix, flags, prefixAddr, NULL, NULL, &prefixLen, NULL );
	cli_require_noerr_return( err, "Invalid IPv6 prefix: \"%s\".\n", gThreadPFNAT64_IPv6Prefix );
	
	flags = kStringToIPAddressFlagsNoPort | kStringToIPAddressFlagsNoPrefix;
	err = _StringToIPv4Address( gThreadPFNAT64_IPv4Address, flags, &ipv4Addr, NULL, NULL, NULL, &endPtr );
	cli_require_return( !err && ( *endPtr == '\0' ), "Invalid IPv4 address: \"%s\".\n", gThreadPFNAT64_IPv4Address );
	
	err = mdns_pf_set_thread_nat64_rules( prefixAddr, prefixLen, htonl( ipv4Addr ) );
	cli_require_noerr_return( err, "error: %#m\n", err );
}

//===========================================================================================================================
//	ThreadPFDeleteCommand
//===========================================================================================================================

static void	ThreadPFDeleteCommand( void )
{
	OSStatus		err;
	
	FPrintF( stdout, "Deleting Thread border router PF rules.\n" );
	err = mdns_pf_delete_thread_rules();
	cli_require_noerr_return( err, "error: %#m\n", err );
}

//===========================================================================================================================
//	IPv4FwdEnableCommand
//===========================================================================================================================

static void	IPv4FwdEnableCommand( void )
{
	OSStatus		err;
	
	FPrintF( stdout, "Enabling IPv4 forwarding.\n" );
	err = mdns_system_set_ipv4_forwarding( true );
	cli_require_noerr_return( err, "error: %#m\n", err );
}

//===========================================================================================================================
//	IPv4FwdDisableCommand
//===========================================================================================================================

static void	IPv4FwdDisableCommand( void )
{
	OSStatus		err;
	
	FPrintF( stdout, "Disabling IPv4 forwarding.\n" );
	err = mdns_system_set_ipv4_forwarding( false );
	cli_require_noerr_return( err, "error: %#m\n", err );
}

//===========================================================================================================================
//	IPv6FwdEnableCommand
//===========================================================================================================================

static void	IPv6FwdEnableCommand( void )
{
	OSStatus		err;
	
	FPrintF( stdout, "Enabling IPv6 forwarding.\n" );
	err = mdns_system_set_ipv6_forwarding( true );
	cli_require_noerr_return( err, "error: %#m\n", err );
}

//===========================================================================================================================
//	IPv6FwdDisableCommand
//===========================================================================================================================

static void	IPv6FwdDisableCommand( void )
{
	OSStatus		err;
	
	FPrintF( stdout, "Disabling IPv6 forwarding.\n" );
	err = mdns_system_set_ipv6_forwarding( false );
	cli_require_noerr_return( err, "error: %#m\n", err );
}

//===========================================================================================================================
//	PrintCommand
//===========================================================================================================================

static void	PrintCommand( void )
{
	OSStatus			err;
	const char *		inputPath;
	uint8_t *			msgPtr = NULL;
	size_t				msgLen;
	char *				msgStr = NULL;
	
	inputPath = CLINextArgOrNULL();
	if( !inputPath || ( strcmp( inputPath, "-" ) == 0 ) )
	{
		err = CopyFileDataByFile( stdin, (char **) &msgPtr, &msgLen );
		require_noerr_action_quiet( err, exit, FPrintF( stderr,
			"error: Failed to copy DNS message from stdin: %#m\n", err ) );
	}
	else
	{
		err = CopyFileDataByPath( inputPath, (char **) &msgPtr, &msgLen );
		require_noerr_action_quiet( err, exit, FPrintF( stderr,
			"error: Failed to copy DNS message from '%s': %#m\n", inputPath, err ) );
	}
	require_action_quiet( msgLen <= UINT16_MAX, exit, err = kSizeErr; FPrintF( stderr,
		"error: DNS message size is greater than the supported maximum (%zu > %d bytes)\n", msgLen, UINT16_MAX ) );
	
	err = DNSMessageToString( msgPtr, msgLen, kDNSMessageToStringFlag_Null, &msgStr );
	if( !err )
	{
		FPrintF( stdout, "%s\n", msgStr );
	}
	else
	{
		FPrintF( stderr, "error: Failed to pretty print DNS message: %#m\n", err );
		FPrintF( stdout, "%.1H\n", msgPtr, (int) msgLen, (int) msgLen );
		err = kNoErr;
	}
	
exit:
	ForgetMem( &msgPtr );
	ForgetMem( &msgStr );
	gExitCode = err ? 1 : 0;
}

//===========================================================================================================================
//	WiFiOnCommand
//===========================================================================================================================

static void	WiFiOnCommand( void )
{
	OSStatus		err;
	
#if( TARGET_OS_OSX )
	err = systemf( NULL, "/usr/sbin/networksetup -setairportpower Wi-Fi on" );
#else
	err = systemf( NULL, "/usr/local/bin/mobilewifitool -- manager power 1" );
#endif
	cli_require_noerr_return( err, "error: %#m\n", err );
}

//===========================================================================================================================
//	WiFiOffCommand
//===========================================================================================================================

static void	WiFiOffCommand( void )
{
	OSStatus		err;
	
#if( TARGET_OS_OSX )
	err = systemf( NULL, "/usr/sbin/networksetup -setairportpower Wi-Fi off" );
#else
	err = systemf( NULL, "/usr/local/bin/mobilewifitool -- manager power 0" );
#endif
	cli_require_noerr_return( err, "error: %#m\n", err );
}

//===========================================================================================================================
//	DiscoveryProxyCommand
//===========================================================================================================================

static void	_DiscoveryProxyCmdSignalHandler( void *inContext );

static void	DiscoveryProxyCommand( void )
{
	OSStatus							err;
	mrc_discovery_proxy_parameters_t	params			= NULL;
	mrc_discovery_proxy_t				proxy			= NULL;
	dispatch_source_t					sigIntSource	= NULL;
	dispatch_source_t					sigTermSource	= NULL;

	params = mrc_discovery_proxy_parameters_create();
	require_action( params, exit, err = kNoResourcesErr );

	uint32_t ifIndex;
	err = InterfaceIndexFromArgString( gInterface, &ifIndex );
	require_noerr_quiet( err, exit );

	mrc_discovery_proxy_parameters_set_interface( params, ifIndex );
	for( size_t i = 0; i < gDiscoveryProxy_ServerAddrCount; ++i )
	{
		sockaddr_ip sip;
		err = SockAddrFromArgString( gDiscoveryProxy_ServerAddrs[ i ], "server address", &sip );
		require_noerr_quiet( err, exit );
		
		switch( sip.sa.sa_family )
		{
			case AF_INET:
			{
				const struct sockaddr_in * const sin = &sip.v4;
				err = mrc_discovery_proxy_parameters_add_server_ipv4_address( params, ntohl( sin->sin_addr.s_addr ),
					ntohs( sin->sin_port ) );
				require_noerr_quiet( err, exit );
				break;
			}
			case AF_INET6:
			{
				const struct sockaddr_in6 * const sin6 = &sip.v6;
				err = mrc_discovery_proxy_parameters_add_server_ipv6_address( params, sin6->sin6_addr.s6_addr,
					ntohs( sin6->sin6_port ), sin6->sin6_scope_id );
				require_noerr_quiet( err, exit );
				break;
			}
		}
	}
	for( size_t i = 0; i < gDiscoveryProxy_MatchDomainCount; ++i )
	{
		err = mrc_discovery_proxy_parameters_add_match_domain( params, gDiscoveryProxy_MatchDomains[ i ] );
		require_noerr_quiet( err, exit );
	}
	for( size_t i = 0; i < gDiscoveryProxy_CertificateCount; ++i )
	{
		uint8_t *		certPtr;
		size_t			certLen;
		
		err = HexToDataCopy( gDiscoveryProxy_Certificates[ i ], kSizeCString, kHexToData_DefaultFlags, &certPtr,
			&certLen, NULL );
		require_noerr_action( err, exit, FPrintF( stderr,
			"error: Failed to parse identity reference hex string: '%s'\n", gDiscoveryProxy_Certificates[ i ] ) );
		
		mrc_discovery_proxy_parameters_add_server_certificate( params, certPtr, certLen );
		ForgetMem( &certPtr );
	}
	proxy = mrc_discovery_proxy_create( params );
	mrc_forget( &params );
	require_action_quiet( proxy, exit, err = kNoResourcesErr );
	
	mrc_discovery_proxy_set_queue( proxy, dispatch_get_main_queue() );
	mrc_discovery_proxy_set_event_handler( proxy,
	^( const mrc_discovery_proxy_event_t inEvent, const OSStatus inError )
	{
		switch( inEvent )
		{
			case mrc_discovery_proxy_event_started:
			{
				FPrintF( stdout, "%{du:time}  Discovery Proxy was started\n", NULL );
				break;
			}
			case mrc_discovery_proxy_event_interruption:
			{
				FPrintF( stdout, "%{du:time}  Discovery Proxy was interrupted\n", NULL );
				break;
			}
			case mrc_discovery_proxy_event_invalidation:
			{
				mrc_release( proxy );
				FPrintF( stdout, "---\n" );
				FPrintF( stdout, "Error:    %#m\n", inError );
				FPrintF( stdout, "End time: %{du:time}\n", NULL );
				exit( 0 );
			}
			case mrc_discovery_proxy_event_none:
				break;
		}
	} );
	signal( SIGINT, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGINT, dispatch_get_main_queue(), _DiscoveryProxyCmdSignalHandler, proxy,
		&sigIntSource );
	require_noerr( err, exit );
	dispatch_activate( sigIntSource );
	
	signal( SIGTERM, SIG_IGN );
	err = DispatchSignalSourceCreate( SIGTERM, dispatch_get_main_queue(), _DiscoveryProxyCmdSignalHandler, proxy,
		&sigTermSource );
	require_noerr( err, exit );
	dispatch_activate( sigTermSource );
	
	FPrintF( stdout, "Discovery Proxy:    %@\n", proxy );
	FPrintF( stdout, "Start time: %{du:time}\n",	NULL );
	FPrintF( stdout, "---\n" );
	mrc_discovery_proxy_activate( proxy );
	dispatch_main();
	
exit:
	if( err ) ErrQuit( 1, "error: %#m\n", err );
}

static void	_DiscoveryProxyCmdSignalHandler( void *inContext )
{
	mrc_discovery_proxy_invalidate( (mrc_discovery_proxy_t) inContext );
}

//===========================================================================================================================
//	CachedLocalRecordsCommand
//===========================================================================================================================

static void	CachedLocalRecordsCommand( void )
{
	OSStatus err;
	dispatch_semaphore_t doneSem = NULL;
	mrc_cached_local_records_inquiry_t inquiry = NULL;
	dispatch_queue_t queue = dispatch_queue_create( "com.apple.dnssdutil.cached-local-records-command",
		DISPATCH_QUEUE_SERIAL );
	require_action( queue, exit, err = kNoResourcesErr );
	
	doneSem = dispatch_semaphore_create( 0 );
	require_action( doneSem, exit, err = kNoResourcesErr );
	
	inquiry = mrc_cached_local_records_inquiry_create();
	require_action( inquiry, exit, err = kNoResourcesErr );
	
	mrc_cached_local_records_inquiry_set_queue( inquiry, queue );
	mrc_cached_local_records_inquiry_set_result_handler( inquiry,
	^( const xpc_object_t inRecordInfo, const OSStatus inError )
	{
		if( inError )
		{
			FPrintF( stderr, "error: %#m\n", inError );
		}
		const size_t recordInfoCount = inRecordInfo ? xpc_array_get_count( inRecordInfo ) : 0;
		FPrintF( stdout, "Record count: %zu\n", recordInfoCount );
		for( size_t i = 0; i < recordInfoCount; ++i )
		{
			const xpc_object_t dict = xpc_array_get_dictionary( inRecordInfo, i );
			if( !dict )
			{
				continue;
			}
			FPrintF( stdout, "Record %zu of %zu:\n", i + 1, recordInfoCount );
			const char * const name = xpc_dictionary_get_string( dict, mrc_cached_local_record_key_name );
			if( name )
			{
				FPrintF( stdout, "\tname:           %s\n", name );
			}
			const char * const first_label = xpc_dictionary_get_string( dict, mrc_cached_local_record_key_first_label );
			if( first_label )
			{
				FPrintF( stdout, "\tfirst label:    %s\n", first_label );
			}
			const char * const sourceAddr = xpc_dictionary_get_string( dict, mrc_cached_local_record_key_source_address );
			if( sourceAddr )
			{
				FPrintF( stdout, "\tsource address: %s\n", sourceAddr );
			}
			const char * const rdata = xpc_dictionary_get_string( dict, mrc_cached_local_record_key_rdata );
			if( rdata )
			{
				FPrintF( stdout, "\trdata:          %s\n", rdata );
			}
			FPrintF( stdout, "\n" );
		}
		dispatch_semaphore_signal( doneSem );
	} );
	mrc_cached_local_records_inquiry_activate( inquiry );
	
	// Wait for the inquiry to complete.
	// Note: We intentionally use a semaphore that will be signaled by the asynchronous handler. The analyzer considers
	// this an antipattern, which is OK for the purposes of this internal test tool. Actual well-written production code
	// would usually not have to emulate this type of synchronous behavior.
	
#if( !COMPILER_CLANG_ANALYZER )
	dispatch_semaphore_wait( doneSem, DISPATCH_TIME_FOREVER );
#endif
	err = kNoErr;
	
exit:
	dispatch_forget( &queue );
	dispatch_forget( &doneSem );
	mrc_forget( &inquiry );
	gExitCode = err ? 1 : 0;
}

//===========================================================================================================================
//	DaemonVersionCmd
//===========================================================================================================================

static void	DaemonVersionCmd( void )
{
	OSStatus		err;
	uint32_t		size, version;
	char			strBuf[ 16 ];
	
	size = (uint32_t) sizeof( version );
	err = DNSServiceGetProperty( kDNSServiceProperty_DaemonVersion, &version, &size );
	require_noerr( err, exit );
	
	FPrintF( stdout, "Daemon version: %s\n", _DNSSDSourceVersionToCString( version, strBuf, sizeof( strBuf ) ) );
	
exit:
	if( err ) exit( 1 );
}

//===========================================================================================================================
//	Exit
//===========================================================================================================================

static void	Exit( void *inContext )
{
	const char * const		reason = (const char *) inContext;
	
	FPrintF( stdout, "---\n" );
	FPrintF( stdout, "End time:   %{du:time}\n", NULL );
	if( reason ) FPrintF( stdout, "End reason: %s\n", reason );
	exit( gExitCode );
}

//===========================================================================================================================
//	_PrintFExtensionHandler_Timestamp
//===========================================================================================================================

static int
	_PrintFExtensionHandler_Timestamp(
		PrintFContext *	inContext,
		PrintFFormat *	inFormat,
		PrintFVAList *	inArgs,
		void *			inUserContext )
{
	struct timeval				now;
	const struct timeval *		tv;
	struct tm *					localTime;
	size_t						len;
	int							n;
	char						dateTimeStr[ 32 ];
	
	Unused( inUserContext );
	
	tv = va_arg( inArgs->args, const struct timeval * );
	require_action_quiet( !inFormat->suppress, exit, n = 0 );
	
	if( !tv )
	{
		gettimeofday( &now, NULL );
		tv = &now;
	}
	localTime = localtime( &tv->tv_sec );
	len = strftime( dateTimeStr, sizeof( dateTimeStr ), "%Y-%m-%d %H:%M:%S", localTime );
	if( len == 0 ) dateTimeStr[ 0 ] = '\0';
	
	n = PrintFCore( inContext, "%s.%06u", dateTimeStr, (unsigned int) tv->tv_usec );
	
exit:
	return( n );
}

//===========================================================================================================================
//	_PrintFExtensionHandler_DNSMessage
//===========================================================================================================================

static int
	_PrintFExtensionHandler_DNSMessageCommon(
		PrintFContext *	inContext,
		PrintFFormat *	inFormat,
		PrintFVAList *	inArgs,
		void *			inUserContext,
		Boolean			inRawRData );

static int
	_PrintFExtensionHandler_DNSMessage(
		PrintFContext *	inContext,
		PrintFFormat *	inFormat,
		PrintFVAList *	inArgs,
		void *			inUserContext )
{
	return( _PrintFExtensionHandler_DNSMessageCommon( inContext, inFormat, inArgs, inUserContext, false ) );
}

static int
	_PrintFExtensionHandler_DNSMessageCommon(
		PrintFContext *	inContext,
		PrintFFormat *	inFormat,
		PrintFVAList *	inArgs,
		void *			inUserContext,
		Boolean			inRawRData )
{
	OSStatus					err;
	const void *				msgPtr;
	size_t						msgLen;
	char *						msgStr;
	DNSMessageToStringFlags		flags;
	int							n;
	
	Unused( inUserContext );
	
	msgPtr = va_arg( inArgs->args, const void * );
	msgLen = va_arg( inArgs->args, size_t );
	require_action_quiet( !inFormat->suppress, exit, n = 0 );
	
	flags = kDNSMessageToStringFlags_None;
	if( inRawRData )				flags |= kDNSMessageToStringFlag_RawRData;
	if( inFormat->altForm   == 1 )	flags |= kDNSMessageToStringFlag_MDNS;
	if( inFormat->precision == 1 )	flags |= kDNSMessageToStringFlag_OneLine;
	err = DNSMessageToString( msgPtr, msgLen, flags, &msgStr );
	if( !err )
	{
		n = PrintFCore( inContext, "%*{text}", inFormat->fieldWidth, msgStr, kSizeCString );
		free( msgStr );
	}
	else
	{
		n = PrintFCore( inContext, "%*.1H", inFormat->fieldWidth, msgPtr, (int) msgLen, (int) msgLen );
	}
	
exit:
	return( n );
}

//===========================================================================================================================
//	_PrintFExtensionHandler_RawDNSMessage
//===========================================================================================================================

static int
	_PrintFExtensionHandler_RawDNSMessage(
		PrintFContext *	inContext,
		PrintFFormat *	inFormat,
		PrintFVAList *	inArgs,
		void *			inUserContext )
{
	return( _PrintFExtensionHandler_DNSMessageCommon( inContext, inFormat, inArgs, inUserContext, true ) );
}

//===========================================================================================================================
//	_PrintFExtensionHandler_CallbackFlags
//===========================================================================================================================

static int
	_PrintFExtensionHandler_CallbackFlags(
		PrintFContext *	inContext,
		PrintFFormat *	inFormat,
		PrintFVAList *	inArgs,
		void *			inUserContext )
{
	DNSServiceFlags		flags;
	int					n;
	char				dnssecResult;
	
	Unused( inUserContext );
	
	flags = va_arg( inArgs->args, DNSServiceFlags );
	require_action_quiet( !inFormat->suppress, exit, n = 0 );
	
	if(      ( flags & kDNSServiceFlagsValidate ) == 0 )						dnssecResult = ' ';
	else if( ( flags & kDNSServiceFlagsSecure )   == kDNSServiceFlagsSecure )	dnssecResult = 'S';
	else if( ( flags & kDNSServiceFlagsInsecure ) == kDNSServiceFlagsInsecure )	dnssecResult = 'I';
	else																		dnssecResult = 'E';
	n = PrintFCore( inContext, "%08X %s%c %c%c%c",
		flags, DNSServiceFlagsToAddRmvStr( flags ),
		( flags & kDNSServiceFlagsMoreComing )       ? '+' : ' ',
		( flags & kDNSServiceFlagAnsweredFromCache ) ? 'C' : ' ',
		( flags & kDNSServiceFlagsExpiredAnswer )    ? '*' : ' ',
		dnssecResult );
	
exit:
	return( n );
}

//===========================================================================================================================
//	_PrintFExtensionHandler_DNSRecordData
//===========================================================================================================================

static int
	_PrintFExtensionHandler_DNSRecordData(
		PrintFContext *	inContext,
		PrintFFormat *	inFormat,
		PrintFVAList *	inArgs,
		void *			inUserContext )
{
	const void *		rdataPtr;
	int					recordType, n, fieldWidth;
	unsigned int		rdataLen;
	
	Unused( inUserContext );
	
	recordType	= va_arg( inArgs->args, int );
	rdataPtr	= va_arg( inArgs->args, const void * );
	rdataLen	= va_arg( inArgs->args, unsigned int );
	require_action_quiet( !inFormat->suppress, exit, n = 0 );
	
	check( inFormat->fieldWidth < INT_MAX );
	fieldWidth = inFormat->leftJustify ? -( (int) inFormat->fieldWidth ) : ( (int) inFormat->fieldWidth );
	
	if( rdataLen > 0 )
	{
		char *		rdataStr = NULL;
		
		DNSRecordDataToString( rdataPtr, rdataLen, recordType, &rdataStr );
		if( rdataStr )
		{
			n = PrintFCore( inContext, "%*s", fieldWidth, rdataStr );
			free( rdataStr );
		}
		else
		{
			n = PrintFCore( inContext, "%*H", fieldWidth, rdataPtr, rdataLen, rdataLen );
		}
	}
	else
	{
		n = PrintFCore( inContext, "%*s", fieldWidth, "<< ZERO-LENGTH RDATA >>" );
	}
	
exit:
	return( n );
}

//===========================================================================================================================
//	_PrintFExtensionHandler_DomainName
//===========================================================================================================================

static int
	_PrintFExtensionHandler_DomainName(
		PrintFContext *	inContext,
		PrintFFormat *	inFormat,
		PrintFVAList *	inArgs,
		void *			inUserContext )
{
	OSStatus			err;
	const uint8_t *		namePtr;
	int					n, fieldWidth;
	char				nameStr[ kDNSServiceMaxDomainName ];
	
	Unused( inUserContext );
	
	namePtr = va_arg( inArgs->args, const uint8_t * );
	require_action_quiet( !inFormat->suppress, exit, n = 0 );
	
	check( inFormat->fieldWidth < INT_MAX );
	fieldWidth = inFormat->leftJustify ? -( (int) inFormat->fieldWidth ) : ( (int) inFormat->fieldWidth );
	
	err = DomainNameToString( namePtr, NULL, nameStr, NULL );
	check_noerr( err );
	if( !err )
	{
		n = PrintFCore( inContext, "%*s", fieldWidth, nameStr );
	}
	else
	{
		n = PrintFCore( inContext, "%*s", fieldWidth, "<< ERROR: domain name conversion failed >>" );
	}
	
exit:
	return( n );
}


//===========================================================================================================================
//	GetDNSSDFlagsFromOpts
//===========================================================================================================================

static DNSServiceFlags	GetDNSSDFlagsFromOpts( void )
{
	DNSServiceFlags		flags;
	
	flags = (DNSServiceFlags) gDNSSDFlags;
	if( flags & kDNSServiceFlagsShareConnection )
	{
		FPrintF( stderr, "*** Warning: kDNSServiceFlagsShareConnection (0x%X) is explicitly set in flag parameters.\n",
			kDNSServiceFlagsShareConnection );
	}
	
	if( gDNSSDFlag_AllowExpiredAnswers )	flags |= kDNSServiceFlagsAllowExpiredAnswers;
	if( gDNSSDFlag_BrowseDomains )			flags |= kDNSServiceFlagsBrowseDomains;
	if( gDNSSDFlag_DenyCellular )			flags |= kDNSServiceFlagsDenyCellular;
	if( gDNSSDFlag_DenyConstrained )		flags |= kDNSServiceFlagsDenyConstrained;
	if( gDNSSDFlag_DenyExpensive )			flags |= kDNSServiceFlagsDenyExpensive;
	if( gDNSSDFlag_ForceMulticast )			flags |= kDNSServiceFlagsForceMulticast;
	if( gDNSSDFlag_IncludeAWDL )			flags |= kDNSServiceFlagsIncludeAWDL;
	if( gDNSSDFlag_KnownUnique )			flags |= kDNSServiceFlagsKnownUnique;
	if( gDNSSDFlag_NoAutoRename )			flags |= kDNSServiceFlagsNoAutoRename;
	if( gDNSSDFlag_PathEvaluationDone )		flags |= kDNSServiceFlagsPathEvaluationDone;
	if( gDNSSDFlag_RegistrationDomains )	flags |= kDNSServiceFlagsRegistrationDomains;
	if( gDNSSDFlag_ReturnIntermediates )	flags |= kDNSServiceFlagsReturnIntermediates;
	if( gDNSSDFlag_Shared )					flags |= kDNSServiceFlagsShared;
	if( gDNSSDFlag_SuppressUnusable )		flags |= kDNSServiceFlagsSuppressUnusable;
	if( gDNSSDFlag_Timeout )				flags |= kDNSServiceFlagsTimeout;
	if( gDNSSDFlag_UnicastResponse )		flags |= kDNSServiceFlagsUnicastResponse;
	if( gDNSSDFlag_Unique )					flags |= kDNSServiceFlagsUnique;
	if( gDNSSDFlag_WakeOnResolve )			flags |= kDNSServiceFlagsWakeOnResolve;
	if( gDNSSDFlag_EnableDNSSEC )			flags |= kDNSServiceFlagsEnableDNSSEC;
	
	return( flags );
}

//===========================================================================================================================
//	CreateConnectionFromArgString
//===========================================================================================================================

static OSStatus
	CreateConnectionFromArgString(
		const char *			inString,
		dispatch_queue_t		inQueue,
		DNSServiceRef *			outSDRef,
		ConnectionDesc *		outDesc )
{
	OSStatus			err;
	DNSServiceRef		sdRef = NULL;
	ConnectionType		type;
	int32_t				pid = -1;	// Initializing because the analyzer claims pid may be used uninitialized.
	uint8_t				uuid[ 16 ];
	
	if( strcasecmp( inString, kConnectionArg_Normal ) == 0 )
	{
		err = DNSServiceCreateConnection( &sdRef );
		require_noerr( err, exit );
		type = kConnectionType_Normal;
	}
	else if( stricmp_prefix( inString, kConnectionArgPrefix_PID ) == 0 )
	{
		const char * const		pidStr = inString + sizeof_string( kConnectionArgPrefix_PID );
		
		err = StringToInt32( pidStr, &pid );
		if( err )
		{
			FPrintF( stderr, "Invalid delegate connection PID value: %s\n", pidStr );
			err = kParamErr;
			goto exit;
		}
		
		memset( uuid, 0, sizeof( uuid ) );
		err = DNSServiceCreateDelegateConnection( &sdRef, pid, uuid );
		if( err )
		{
			FPrintF( stderr, "DNSServiceCreateDelegateConnection() returned %#m for PID %d\n", err, pid );
			goto exit;
		}
		type = kConnectionType_DelegatePID;
	}
	else if( stricmp_prefix( inString, kConnectionArgPrefix_UUID ) == 0 )
	{
		const char * const		uuidStr = inString + sizeof_string( kConnectionArgPrefix_UUID );
		
		check_compile_time_code( sizeof( uuid ) == sizeof( uuid_t ) );
		
		err = StringToUUID( uuidStr, kSizeCString, false, uuid );
		if( err )
		{
			FPrintF( stderr, "Invalid delegate connection UUID value: %s\n", uuidStr );
			err = kParamErr;
			goto exit;
		}
		
		err = DNSServiceCreateDelegateConnection( &sdRef, 0, uuid );
		if( err )
		{
			FPrintF( stderr, "DNSServiceCreateDelegateConnection() returned %#m for UUID %#U\n", err, uuid );
			goto exit;
		}
		type = kConnectionType_DelegateUUID;
	}
	else
	{
		FPrintF( stderr, "Unrecognized connection string \"%s\".\n", inString );
		err = kParamErr;
		goto exit;
	}
	
	err = DNSServiceSetDispatchQueue( sdRef, inQueue );
	require_noerr( err, exit );
	
	*outSDRef = sdRef;
	if( outDesc )
	{
		outDesc->type = type;
		if(      type == kConnectionType_DelegatePID )	outDesc->delegate.pid = pid;
		else if( type == kConnectionType_DelegateUUID )	memcpy( outDesc->delegate.uuid, uuid, 16 );
	}
	sdRef = NULL;
	
exit:
	if( sdRef ) DNSServiceRefDeallocate( sdRef );
	return( err );
}

//===========================================================================================================================
//	InterfaceIndexFromArgString
//===========================================================================================================================

static OSStatus	InterfaceIndexFromArgString( const char *inString, uint32_t *outIndex )
{
	OSStatus		err;
	uint32_t		ifIndex;
	
	if( inString )
	{
		ifIndex = if_nametoindex( inString );
		if( ifIndex == 0 )
		{
			err = StringToUInt32( inString, &ifIndex );
			if( err )
			{
				FPrintF( stderr, "error: Invalid interface value: %s\n", inString );
				err = kParamErr;
				goto exit;
			}
		}
	}
	else
	{
		ifIndex	= 0;
	}
	
	*outIndex = ifIndex;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	RecordDataFromArgString
//===========================================================================================================================

static OSStatus	RecordDataFromArgString( const char *inString, uint8_t **outDataPtr, size_t *outDataLen )
{
	OSStatus		err;
	uint8_t *		dataPtr = NULL;
	size_t			dataLen;
	
	if( 0 ) {}
	
	// Domain name
	
	else if( stricmp_prefix( inString, kRDataArgPrefix_Domain ) == 0 )
	{
		const char * const		str = inString + sizeof_string( kRDataArgPrefix_Domain );
		
		err = StringToDomainName( str, &dataPtr, &dataLen );
		require_noerr_quiet( err, exit );
	}
	
	// File path
	
	else if( stricmp_prefix( inString, kRDataArgPrefix_File ) == 0 )
	{
		const char * const		path = inString + sizeof_string( kRDataArgPrefix_File );
		
		err = CopyFileDataByPath( path, (char **) &dataPtr, &dataLen );
		require_noerr( err, exit );
		require_action( dataLen <= kDNSRecordDataLengthMax, exit, err = kSizeErr );
	}
	
	// Hexadecimal string
	
	else if( stricmp_prefix( inString, kRDataArgPrefix_HexString ) == 0 )
	{
		const char * const		str = inString + sizeof_string( kRDataArgPrefix_HexString );
		
		err = HexToDataCopy( str, kSizeCString, kHexToData_DefaultFlags, &dataPtr, &dataLen, NULL );
		require_noerr( err, exit );
		require_action( dataLen <= kDNSRecordDataLengthMax, exit, err = kSizeErr );
	}
	
	// IPv4 address string
	
	else if( stricmp_prefix( inString, kRDataArgPrefix_IPv4 ) == 0 )
	{
		const char * const		str = inString + sizeof_string( kRDataArgPrefix_IPv4 );
		
		err = StringToARecordData( str, &dataPtr, &dataLen );
		require_noerr_quiet( err, exit );
	}
	
	// IPv6 address string
	
	else if( stricmp_prefix( inString, kRDataArgPrefix_IPv6 ) == 0 )
	{
		const char * const		str = inString + sizeof_string( kRDataArgPrefix_IPv6 );
		
		err = StringToAAAARecordData( str, &dataPtr, &dataLen );
		require_noerr_quiet( err, exit );
	}
	
	// SRV record
	
	else if( stricmp_prefix( inString, kRDataArgPrefix_SRV ) == 0 )
	{
		const char * const		str = inString + sizeof_string( kRDataArgPrefix_SRV );
		
		err = CreateSRVRecordDataFromString( str, &dataPtr, &dataLen );
		require_noerr( err, exit );
	}
	
	// String with escaped hex and octal bytes
	
	else if( stricmp_prefix( inString, kRDataArgPrefix_String ) == 0 )
	{
		const char * const		str = inString + sizeof_string( kRDataArgPrefix_String );
		const char * const		end = str + strlen( str );
		size_t					copiedLen;
		size_t					totalLen;
		Boolean					success;
		
		if( str < end )
		{
			success = _ParseQuotedEscapedString( str, end, "", NULL, 0, NULL, &totalLen, NULL );
			require_action( success, exit, err = kParamErr );
			require_action( totalLen <= kDNSRecordDataLengthMax, exit, err = kSizeErr );
			
			dataLen = totalLen;
			dataPtr = (uint8_t *) malloc( dataLen );
			require_action( dataPtr, exit, err = kNoMemoryErr );
			
			success = _ParseQuotedEscapedString( str, end, "", (char *) dataPtr, dataLen, &copiedLen, NULL, NULL );
			require_action( success, exit, err = kParamErr );
			check( copiedLen == dataLen );
		}
		else
		{
			dataPtr = NULL;
			dataLen = 0;
		}
	}
	
	// TXT record
	
	else if( stricmp_prefix( inString, kRDataArgPrefix_TXT ) == 0 )
	{
		const char * const		str = inString + sizeof_string( kRDataArgPrefix_TXT );
		
		err = CreateTXTRecordDataFromString( str, ',', &dataPtr, &dataLen );
		require_noerr( err, exit );
	}
	
	// Unrecognized format
	
	else
	{
		FPrintF( stderr, "Unrecognized record data string \"%s\".\n", inString );
		err = kParamErr;
		goto exit;
	}
	
	err = kNoErr;
	*outDataLen = dataLen;
	*outDataPtr = dataPtr;
	dataPtr = NULL;
	
exit:
	FreeNullSafe( dataPtr );
	return( err );
}

//===========================================================================================================================
//	RecordTypeFromArgString
//===========================================================================================================================

static OSStatus	RecordTypeFromArgString( const char *inString, uint16_t *outValue )
{
	OSStatus		err;
	uint16_t		value;
	
	value = DNSRecordTypeStringToValue( inString );
	if( value == 0 )
	{
		int32_t		i32;
		
		err = StringToInt32( inString, &i32 );
		require_noerr_quiet( err, exit );
		require_action_quiet( ( i32 >= 0 ) && ( i32 <= UINT16_MAX ), exit, err = kParamErr );
		value = (uint16_t) i32;
	}
	*outValue = value;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	RecordClassFromArgString
//===========================================================================================================================

static OSStatus	RecordClassFromArgString( const char *inString, uint16_t *outValue )
{
	OSStatus		err;
	int32_t			i32;
	
	if( strcasecmp( inString, "IN" ) == 0 )
	{
		*outValue = kDNSServiceClass_IN;
		err = kNoErr;
		goto exit;
	}
	
	err = StringToInt32( inString, &i32 );
	require_noerr_quiet( err, exit );
	require_action_quiet( ( i32 >= 0 ) && ( i32 <= UINT16_MAX ), exit, err = kParamErr );
	
	*outValue = (uint16_t) i32;
	
exit:
	return( err );
}

//===========================================================================================================================
//	SockAddrFromArgString
//===========================================================================================================================

static OSStatus	SockAddrFromArgString( const char *inArgStr, const char *inArgName, sockaddr_ip *outSA )
{
	OSStatus		err;
	sockaddr_ip		sip;
	
	err = StringToSockAddr( inArgStr, &sip, sizeof( sip ), NULL );
	if( !err && ( ( sip.sa.sa_family == AF_INET ) || ( sip.sa.sa_family == AF_INET6 ) ) )
	{
		if( outSA ) SockAddrCopy( &sip, outSA );
	}
	else
	{
		FPrintF( stderr, "error: Invalid %s: '%s'\n", inArgName, inArgStr );
		err = kParamErr;
	}
	return( err );
}

//===========================================================================================================================
//	InterfaceIndexToName
//===========================================================================================================================

static char * InterfaceIndexToName( uint32_t inIfIndex, char inNameBuf[ kInterfaceNameBufLen ] )
{
	switch( inIfIndex )
	{
		case kDNSServiceInterfaceIndexAny:
			strlcpy( inNameBuf, "Any", kInterfaceNameBufLen );
			break;
		
		case kDNSServiceInterfaceIndexLocalOnly:
			strlcpy( inNameBuf, "LocalOnly", kInterfaceNameBufLen );
			break;
		
		case kDNSServiceInterfaceIndexUnicast:
			strlcpy( inNameBuf, "Unicast", kInterfaceNameBufLen );
			break;
		
		case kDNSServiceInterfaceIndexP2P:
			strlcpy( inNameBuf, "P2P", kInterfaceNameBufLen );
			break;
		
	#if( defined( kDNSServiceInterfaceIndexBLE ) )
		case kDNSServiceInterfaceIndexBLE:
			strlcpy( inNameBuf, "BLE", kInterfaceNameBufLen );
			break;
	#endif
		
		default:
		{
			const char *		name;
			
			name = if_indextoname( inIfIndex, inNameBuf );
			if( !name ) strlcpy( inNameBuf, "NO NAME", kInterfaceNameBufLen );
			break;
		}
	}
	
	return( inNameBuf );
}

//===========================================================================================================================
//	RecordTypeToString
//===========================================================================================================================

static const char *	RecordTypeToString( int inValue )
{
	const char *		string;
	
	string = DNSRecordTypeValueToString( inValue );
	if( !string ) string = "???";
	return( string );
}

#if( MDNSRESPONDER_PROJECT )
//===========================================================================================================================
//	RecordClassToString
//===========================================================================================================================

static const char *	RecordClassToString( int inValue )
{
	return( ( inValue == kDNSServiceClass_IN ) ? "IN" : "???" );
}
#endif

//===========================================================================================================================
//	WriteDNSQueryMessage
//===========================================================================================================================

static OSStatus
	WriteDNSQueryMessage(
		uint8_t			inMsg[ kDNSQueryMessageMaxLen ],
		uint16_t		inMsgID,
		uint16_t		inFlags,
		const char *	inQName,
		uint16_t		inQType,
		uint16_t		inQClass,
		size_t *		outMsgLen )
{
	OSStatus		err;
	uint8_t			qname[ kDomainNameLengthMax ];
	
	err = DomainNameFromString( qname, inQName, NULL );
	require_noerr_quiet( err, exit );
	
	err = DNSMessageWriteQuery( inMsgID, inFlags, qname, inQType, inQClass, inMsg, outMsgLen );
	require_noerr_quiet( err, exit );
	
exit:
	return( err );
}

//===========================================================================================================================
//	DispatchSignalSourceCreate
//===========================================================================================================================

static OSStatus
	DispatchSignalSourceCreate(
		int					inSignal,
		dispatch_queue_t	inQueue,
		DispatchHandler		inEventHandler,
		void *				inContext,
		dispatch_source_t *	outSource )
{
	OSStatus				err;
	dispatch_source_t		source;
	
	source = dispatch_source_create( DISPATCH_SOURCE_TYPE_SIGNAL, (uintptr_t) inSignal, 0, inQueue );
	require_action( source, exit, err = kUnknownErr );
	
	dispatch_set_context( source, inContext );
	dispatch_source_set_event_handler_f( source, inEventHandler );
	
	*outSource = source;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	DispatchSocketSourceCreate
//===========================================================================================================================

static OSStatus
	DispatchSocketSourceCreate(
		SocketRef				inSock,
		dispatch_source_type_t	inType,
		dispatch_queue_t		inQueue,
		DispatchHandler			inEventHandler,
		DispatchHandler			inCancelHandler,
		void *					inContext,
		dispatch_source_t *		outSource )
{
	OSStatus				err;
	dispatch_source_t		source;
	
	source = dispatch_source_create( inType, (uintptr_t) inSock, 0, inQueue ? inQueue : dispatch_get_main_queue() );
	require_action( source, exit, err = kNoResourcesErr );
	
	dispatch_set_context( source, inContext );
	dispatch_source_set_event_handler_f( source, inEventHandler );
	dispatch_source_set_cancel_handler_f( source, inCancelHandler );
	
	*outSource = source;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	DispatchTimerCreate
//===========================================================================================================================

static OSStatus
	DispatchTimerCreate(
		dispatch_time_t		inStart,
		uint64_t			inIntervalNs,
		uint64_t			inLeewayNs,
		dispatch_queue_t	inQueue,
		DispatchHandler		inEventHandler,
		DispatchHandler		inCancelHandler,
		void *				inContext,
		dispatch_source_t *	outTimer )
{
	OSStatus				err;
	dispatch_source_t		timer;
	
	timer = dispatch_source_create( DISPATCH_SOURCE_TYPE_TIMER, 0, 0, inQueue ? inQueue : dispatch_get_main_queue() );
	require_action( timer, exit, err = kNoResourcesErr );
	
	dispatch_source_set_timer( timer, inStart, inIntervalNs, inLeewayNs );
	dispatch_set_context( timer, inContext );
	dispatch_source_set_event_handler_f( timer, inEventHandler );
	dispatch_source_set_cancel_handler_f( timer, inCancelHandler );
	
	*outTimer = timer;
	err = kNoErr;
	
exit:
	return( err );
}

#if( TARGET_OS_DARWIN )
//===========================================================================================================================
//	DispatchProcessMonitorCreate
//===========================================================================================================================

static OSStatus
	DispatchProcessMonitorCreate(
		pid_t				inPID,
		unsigned long		inFlags,
		dispatch_queue_t	inQueue,
		DispatchHandler		inEventHandler,
		DispatchHandler		inCancelHandler,
		void *				inContext,
		dispatch_source_t *	outMonitor )
{
	OSStatus				err;
	dispatch_source_t		monitor;
	
	monitor = dispatch_source_create( DISPATCH_SOURCE_TYPE_PROC, (uintptr_t) inPID, inFlags,
		inQueue ? inQueue : dispatch_get_main_queue() );
	require_action( monitor, exit, err = kUnknownErr );
	
	dispatch_set_context( monitor, inContext );
	dispatch_source_set_event_handler_f( monitor, inEventHandler );
	dispatch_source_set_cancel_handler_f( monitor, inCancelHandler );
	
	*outMonitor = monitor;
	err = kNoErr;
	
exit:
	return( err );
}
#endif

//===========================================================================================================================
//	ServiceTypeDescription
//===========================================================================================================================

typedef struct
{
	const char *		name;			// Name of the service type in two-label "_service._proto" format.
	const char *		description;	// Description of the service type.
	
}	ServiceType;

// A Non-comprehensive table of DNS-SD service types

static const ServiceType		kServiceTypes[] =
{
	{ "_acp-sync._tcp",			"AirPort Base Station Sync" },
	{ "_adisk._tcp",			"Automatic Disk Discovery" },
	{ "_afpovertcp._tcp",		"Apple File Sharing" },
	{ "_airdrop._tcp",			"AirDrop" },
	{ "_airplay._tcp",			"AirPlay" },
	{ "_airport._tcp",			"AirPort Base Station" },
	{ "_daap._tcp",				"Digital Audio Access Protocol (iTunes)" },
	{ "_eppc._tcp",				"Remote AppleEvents" },
	{ "_ftp._tcp",				"File Transfer Protocol" },
	{ "_home-sharing._tcp",		"Home Sharing" },
	{ "_homekit._tcp",			"HomeKit" },
	{ "_http._tcp",				"World Wide Web HTML-over-HTTP" },
	{ "_https._tcp",			"HTTP over SSL/TLS" },
	{ "_ipp._tcp",				"Internet Printing Protocol" },
	{ "_ldap._tcp",				"Lightweight Directory Access Protocol" },
	{ "_mediaremotetv._tcp",	"Media Remote" },
	{ "_net-assistant._tcp",	"Apple Remote Desktop" },
	{ "_od-master._tcp",		"OpenDirectory Master" },
	{ "_nfs._tcp",				"Network File System" },
	{ "_presence._tcp",			"Peer-to-peer messaging / Link-Local Messaging" },
	{ "_pdl-datastream._tcp",	"Printer Page Description Language Data Stream" },
	{ "_raop._tcp",				"Remote Audio Output Protocol" },
	{ "_rfb._tcp",				"Remote Frame Buffer" },
	{ "_scanner._tcp",			"Bonjour Scanning" },
	{ "_smb._tcp",				"Server Message Block over TCP/IP" },
	{ "_sftp-ssh._tcp",			"Secure File Transfer Protocol over SSH" },
	{ "_sleep-proxy._udp",		"Sleep Proxy Server" },
	{ "_ssh._tcp",				"SSH Remote Login Protocol" },
	{ "_teleport._tcp",			"teleport" },
	{ "_tftp._tcp",				"Trivial File Transfer Protocol" },
	{ "_workstation._tcp",		"Workgroup Manager" },
	{ "_webdav._tcp",			"World Wide Web Distributed Authoring and Versioning (WebDAV)" },
	{ "_webdavs._tcp",			"WebDAV over SSL/TLS" }
};

static const char *	ServiceTypeDescription( const char *inName )
{
	const ServiceType *				serviceType;
	const ServiceType * const		end = kServiceTypes + countof( kServiceTypes );
	
	for( serviceType = kServiceTypes; serviceType < end; ++serviceType )
	{
		if( ( stricmp_prefix( inName, serviceType->name ) == 0 ) )
		{
			const char * const		ptr = &inName[ strlen( serviceType->name ) ];
			
			if( ( ptr[ 0 ] == '\0' ) || ( ( ptr[ 0 ] == '.' ) && ( ptr[ 1 ] == '\0' ) ) )
			{
				return( serviceType->description );
			}
		}
	}
	return( NULL );
}

//===========================================================================================================================
//	SocketContextCreate
//===========================================================================================================================

static SocketContext *	SocketContextCreate( SocketRef inSock, void *inUserContext, OSStatus *outError )
{
	return( SocketContextCreateEx( inSock, inUserContext, NULL, outError ) );
}

//===========================================================================================================================
//	SocketContextCreateEx
//===========================================================================================================================

static SocketContext *
	SocketContextCreateEx(
		SocketRef					inSock,
		void *						inUserContext,
		SocketContextFinalizer_f	inUserFinalizer,
		OSStatus *					outError )
{
	OSStatus			err;
	SocketContext *		context;
	
	context = (SocketContext *) calloc( 1, sizeof( *context ) );
	require_action( context, exit, err = kNoMemoryErr );
	
	context->refCount		= 1;
	context->sock			= inSock;
	context->userContext	= inUserContext;
	context->userFinalizer	= inUserFinalizer;
	err = kNoErr;
	
exit:
	if( outError ) *outError = err;
	return( context );
}

//===========================================================================================================================
//	SocketContextRetain
//===========================================================================================================================

static SocketContext *	SocketContextRetain( SocketContext *inContext )
{
	atomic_add_32( &inContext->refCount, 1 );
	return( inContext );
}

//===========================================================================================================================
//	SocketContextRelease
//===========================================================================================================================

static void	SocketContextRelease( SocketContext *me )
{
	if( atomic_add_and_fetch_32( &me->refCount, -1 ) == 0 )
	{
		ForgetSocket( &me->sock );
		if( me->userFinalizer )
		{
			me->userFinalizer( me->userContext );
			me->userFinalizer = NULL;
		}
		me->userContext = NULL;
		free( me );
	}
}

//===========================================================================================================================
//	SocketContextCancelHandler
//===========================================================================================================================

static void	SocketContextCancelHandler( void *inContext )
{
	SocketContextRelease( (SocketContext *) inContext );
}

//===========================================================================================================================
//	SocketContextFinalizerCF
//===========================================================================================================================

static void	SocketContextFinalizerCF( void *inUserCtx )
{
	CFRelease( (CFTypeRef) inUserCtx );
}

//===========================================================================================================================
//	StringToInt32
//===========================================================================================================================

static OSStatus	StringToInt32( const char *inString, int32_t *outValue )
{
	OSStatus		err;
	long			value;
	char *			endPtr;
	
	value = strtol( inString, &endPtr, 0 );
	require_action_quiet( ( *endPtr == '\0' ) && ( endPtr != inString ), exit, err = kParamErr );
	require_action_quiet( ( value >= INT32_MIN ) && ( value <= INT32_MAX ), exit, err = kRangeErr );
	
	*outValue = (int32_t) value;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	StringToUInt32
//===========================================================================================================================

static OSStatus	StringToUInt32( const char *inString, uint32_t *outValue )
{
	OSStatus		err;
	uint32_t		value;
	char *			endPtr;
	
	value = (uint32_t) strtol( inString, &endPtr, 0 );
	require_action_quiet( ( *endPtr == '\0' ) && ( endPtr != inString ), exit, err = kParamErr );
	
	*outValue = value;
	err = kNoErr;
	
exit:
	return( err );
}

#if( TARGET_OS_DARWIN )
//===========================================================================================================================
//	_StringToInt64
//===========================================================================================================================

static int64_t	_StringToInt64( const char *inString, OSStatus *outError )
{
	OSStatus		err;
	long long		ll;
	char *			end;
	int64_t			i64 = 0;
	int				errnoVal;
	
	set_errno_compat( 0 );
	ll = strtoll( inString, &end, 0 );
	errnoVal = errno_compat();
	require_action_quiet( ( *end == '\0' ) && ( end != inString ), exit, err = kMalformedErr );
	require_action_quiet( ( ( ll != LLONG_MIN ) && ( ll != LLONG_MAX ) ) || ( errnoVal != ERANGE ), exit, err = kRangeErr );
	require_action_quiet( ( ll >= INT64_MIN ) && ( ll <= INT64_MAX ), exit, err = kRangeErr );
	i64 = (int64_t) ll;
	err = kNoErr;
	
exit:
	if( outError ) *outError = err;
	return( i64 );
}

//===========================================================================================================================
//	_StringToUInt64
//===========================================================================================================================

static uint64_t	_StringToUInt64( const char *inString, OSStatus *outError )
{
	OSStatus				err;
	unsigned long long		val;
	char *					end;
	int						errnoVal;
	
	set_errno_compat( 0 );
	val = strtoull( inString, &end, 0 );
	errnoVal = errno_compat();
	
	require_action_quiet( ( *end == '\0' ) && ( end != inString ), exit, err = kMalformedErr );
	require_action_quiet( ( val != ULLONG_MAX ) || ( errnoVal != ERANGE ), exit, err = kRangeErr );
	require_action_quiet( val <= UINT64_MAX, exit, err = kRangeErr );
	err = kNoErr;
	
exit:
	if( outError ) *outError = err;
	return( (uint64_t)val );
}

//===========================================================================================================================
//	_StringToPID
//===========================================================================================================================

static pid_t	_StringToPID( const char *inString, OSStatus *outError )
{
	OSStatus		err;
	int64_t			i64;
	pid_t			pid = 0;
	
	i64 = _StringToInt64( inString, &err );
	require_noerr_quiet( err, exit );
	require_action_quiet( i64 == (pid_t) i64, exit, err = kRangeErr );
	pid = (pid_t) i64;
	err = kNoErr;
	
exit:
	if( outError ) *outError = err;
	return( pid );
}

//===========================================================================================================================
//	_ParseEscapedString
//	
//	Note: Similar to ParseEscapedString() from CoreUtils except that _ParseEscapedString() takes an optional C string
//	containing delimiter characters instead of being limited to one delimiter character. Also, when the function returns
//	due to a delimiter, the output pointer is set to the delimiter character instead of the character after the delimiter.
//===========================================================================================================================

static OSStatus
	_ParseEscapedString(
		const char *	inSrc,
		const char *	inEnd,
		const char *	inDelimiters,
		char *			inBufPtr,
		size_t			inBufLen,
		size_t *		outCopiedLen,
		size_t *		outActualLen,
		const char **	outPtr )
{
	OSStatus				err;
	const char *			ptr;
	char *					dst = inBufPtr;
	const char * const		lim = ( inBufLen > 0 ) ? &inBufPtr[ inBufLen - 1 ] : inBufPtr;
	size_t					len;
	
	len = 0;
	ptr = inSrc;
	if( !inDelimiters ) inDelimiters = "";
	while( ptr < inEnd )
	{
		int					c;
		const char *		del;
		
		c = *ptr;
		for( del = inDelimiters; ( *del != '\0' ) && ( c != *del ); ++del ) {}
		if( *del != '\0' ) break;
		++ptr;
		if( c == '\\' )
		{
			require_action_quiet( ptr < inEnd, exit, err = kUnderrunErr );
			c = *ptr++;
		}
		++len;
		if( dst < lim ) *dst++ = (char) c;
	}
	if( inBufLen > 0 ) *dst = '\0';
	
	if( outCopiedLen )	*outCopiedLen	= (size_t)( dst - inBufPtr );
	if( outActualLen )	*outActualLen	= len;
	if( outPtr )		*outPtr			= ptr;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_ParseEscapedStringWithCopy
//===========================================================================================================================

static OSStatus
	_ParseEscapedStringWithCopy(
		const char *	inSrc,
		const char *	inEnd,
		const char *	inDelimiters,
		char *			inBufPtr,
		size_t			inBufLen,
		const char **	outString,
		char **			outMemory,
		const char **	outPtr )
{
	OSStatus		err;
	size_t			actualLen, bufLen;
	char *			bufPtr;
	char *			mem = NULL;
	
	err = _ParseEscapedString( inSrc, inEnd, inDelimiters, NULL, 0, NULL, &actualLen, NULL );
	require_noerr_quiet( err, exit );
	
	if( actualLen < inBufLen )
	{
		bufPtr = inBufPtr;
		bufLen = inBufLen;
	}
	else
	{
		bufLen = actualLen + 1;
		mem = (char *) malloc( bufLen );
		require_action( mem, exit, err = kNoMemoryErr );
		bufPtr = mem;
	}
	err = _ParseEscapedString( inSrc, inEnd, inDelimiters, bufPtr, bufLen, NULL, NULL, outPtr );
	require_noerr_quiet( err, exit );
	
	*outString = bufPtr;
	*outMemory = mem;
	mem = NULL;
	
exit:
	ForgetMem( &mem );
	return( err );
}

#endif

//===========================================================================================================================
//	StringToARecordData
//===========================================================================================================================

static OSStatus	StringToARecordData( const char *inString, uint8_t **outPtr, size_t *outLen )
{
	OSStatus			err;
	uint32_t *			addrPtr;
	const size_t		addrLen = sizeof( *addrPtr );
	const char *		end;
	
	addrPtr = (uint32_t *) malloc( addrLen );
	require_action( addrPtr, exit, err = kNoMemoryErr );
	
	err = _StringToIPv4Address( inString, kStringToIPAddressFlagsNoPort | kStringToIPAddressFlagsNoPrefix, addrPtr,
		NULL, NULL, NULL, &end );
	if( !err && ( *end != '\0' ) ) err = kMalformedErr;
	require_noerr_quiet( err, exit );
	
	*addrPtr = HostToBig32( *addrPtr );
	
	*outPtr = (uint8_t *) addrPtr;
	addrPtr = NULL;
	*outLen = addrLen;
	
exit:
	FreeNullSafe( addrPtr );
	return( err );
}

//===========================================================================================================================
//	StringToAAAARecordData
//===========================================================================================================================

static OSStatus	StringToAAAARecordData( const char *inString, uint8_t **outPtr, size_t *outLen )
{
	OSStatus			err;
	uint8_t *			addrPtr;
	const size_t		addrLen = 16;
	const char *		end;
	
	addrPtr = (uint8_t *) malloc( addrLen );
	require_action( addrPtr, exit, err = kNoMemoryErr );
	
	err = _StringToIPv6Address( inString,
		kStringToIPAddressFlagsNoPort | kStringToIPAddressFlagsNoPrefix | kStringToIPAddressFlagsNoScope,
		addrPtr, NULL, NULL, NULL, &end );
	if( !err && ( *end != '\0' ) ) err = kMalformedErr;
	require_noerr_quiet( err, exit );
	
	*outPtr = addrPtr;
	addrPtr = NULL;
	*outLen = addrLen;
	
exit:
	FreeNullSafe( addrPtr );
	return( err );
}

//===========================================================================================================================
//	StringToDomainName
//===========================================================================================================================

static OSStatus	StringToDomainName( const char *inString, uint8_t **outPtr, size_t *outLen )
{
	OSStatus		err;
	uint8_t *		namePtr;
	size_t			nameLen;
	uint8_t *		end;
	uint8_t			nameBuf[ kDomainNameLengthMax ];
	
	err = DomainNameFromString( nameBuf, inString, &end );
	require_noerr_quiet( err, exit );
	
	nameLen = (size_t)( end - nameBuf );
	namePtr = _memdup( nameBuf, nameLen );
	require_action( namePtr, exit, err = kNoMemoryErr );
	
	*outPtr = namePtr;
	namePtr = NULL;
	if( outLen ) *outLen = nameLen;
	
exit:
	return( err );
}

#if( TARGET_OS_DARWIN )
//===========================================================================================================================
//	GetDefaultDNSServer
//===========================================================================================================================

static OSStatus	GetDefaultDNSServer( sockaddr_ip *outAddr )
{
	OSStatus				err;
	dns_config_t *			config;
	struct sockaddr *		addr;
	int32_t					i;
	
	config = dns_configuration_copy();
	require_action( config, exit, err = kUnknownErr );
	
	addr = NULL;
	for( i = 0; i < config->n_resolver; ++i )
	{
		const dns_resolver_t * const		resolver = config->resolver[ i ];
		
		if( !resolver->domain && ( resolver->n_nameserver > 0 ) )
		{
			addr = resolver->nameserver[ 0 ];
			break;
		}
 	}
	require_action_quiet( addr, exit, err = kNotFoundErr );
	
	SockAddrCopy( addr, outAddr );
	err = kNoErr;
	
exit:
	if( config ) dns_configuration_free( config );
	return( err );
}
#endif

//===========================================================================================================================
//	GetMDNSMulticastAddrV4
//===========================================================================================================================

static void	_MDNSMulticastAddrV4Init( void *inContext );

static const struct sockaddr *	GetMDNSMulticastAddrV4( void )
{
	static struct sockaddr_in		sMDNSMulticastAddrV4;
	static dispatch_once_t			sMDNSMulticastAddrV4InitOnce = 0;
	
	dispatch_once_f( &sMDNSMulticastAddrV4InitOnce, &sMDNSMulticastAddrV4, _MDNSMulticastAddrV4Init );
	return( (const struct sockaddr *) &sMDNSMulticastAddrV4 );
}

static void	_MDNSMulticastAddrV4Init( void *inContext )
{
	struct sockaddr_in * const		addr = (struct sockaddr_in *) inContext;
	
	_SockAddrInitIPv4( addr, UINT32_C( 0xE00000FB ), kMDNSPort );	// The mDNS IPv4 multicast address is 224.0.0.251.
}

//===========================================================================================================================
//	GetMDNSMulticastAddrV6
//===========================================================================================================================

static void	_MDNSMulticastAddrV6Init( void *inContext );

static const struct sockaddr *	GetMDNSMulticastAddrV6( void )
{
	static struct sockaddr_in6		sMDNSMulticastAddrV6;
	static dispatch_once_t			sMDNSMulticastAddrV6InitOnce = 0;
	
	dispatch_once_f( &sMDNSMulticastAddrV6InitOnce, &sMDNSMulticastAddrV6, _MDNSMulticastAddrV6Init );
	return( (const struct sockaddr *) &sMDNSMulticastAddrV6 );
}

static void	_MDNSMulticastAddrV6Init( void *inContext )
{
	struct sockaddr_in6 * const		addr = (struct sockaddr_in6 *) inContext;
	
	memset( addr, 0, sizeof( *addr ) );
	SIN6_LEN_SET( addr );
	addr->sin6_family	= AF_INET6;
	addr->sin6_port		= htons( kMDNSPort );
	addr->sin6_addr.s6_addr[  0 ] = 0xFF;	// The mDNS IPv6 multicast address is FF02::FB.
	addr->sin6_addr.s6_addr[  1 ] = 0x02;
	addr->sin6_addr.s6_addr[ 15 ] = 0xFB;
}

//===========================================================================================================================
//	CreateMulticastSocket
//===========================================================================================================================

static OSStatus
	CreateMulticastSocket(
		const struct sockaddr *	inAddr,
		int						inPort,
		const char *			inIfName,
		uint32_t				inIfIndex,
		Boolean					inJoin,
		int *					outPort,
		SocketRef *				outSock )
{
	OSStatus		err;
	SocketRef		sock	= kInvalidSocketRef;
	const int		family	= inAddr->sa_family;
	int				port;
	
	require_action_quiet( ( family == AF_INET ) ||( family == AF_INET6 ), exit, err = kUnsupportedErr );
	
	err = ServerSocketOpen( family, SOCK_DGRAM, IPPROTO_UDP, inPort, &port, kSocketBufferSize_DontSet, &sock );
	require_noerr_quiet( err, exit );
	
	err = SocketSetMulticastInterface( sock, inIfName, inIfIndex );
	require_noerr_quiet( err, exit );
	
	if( family == AF_INET )
	{
		err = setsockopt( sock, IPPROTO_IP, IP_MULTICAST_LOOP, (char *) &(uint8_t){ 1 }, (socklen_t) sizeof( uint8_t ) );
		err = map_socket_noerr_errno( sock, err );
		require_noerr_quiet( err, exit );
	}
	else
	{
		err = setsockopt( sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, (char *) &(int){ 1 }, (socklen_t) sizeof( int ) );
		err = map_socket_noerr_errno( sock, err );
		require_noerr_quiet( err, exit );
	}
	
	if( inJoin )
	{
		err = SocketJoinMulticast( sock, inAddr, inIfName, inIfIndex );
		require_noerr_quiet( err, exit );
	}
	
	if( outPort ) *outPort = port;
	*outSock = sock;
	sock = kInvalidSocketRef;
	
exit:
	ForgetSocket( &sock );
	return( err );
}

//===========================================================================================================================
//	DecimalTextToUInt32
//===========================================================================================================================

static OSStatus	DecimalTextToUInt32( const char *inSrc, const char *inEnd, uint32_t *outValue, const char **outPtr )
{
	OSStatus			err;
	uint64_t			value;
	const char *		ptr = inSrc;
	
	require_action_quiet( ( ptr < inEnd ) && isdigit_safe( *ptr ), exit, err = kMalformedErr );
	
	value = (uint64_t)( *ptr++ - '0' );
	if( value == 0 )
	{
		if( ( ptr < inEnd ) && isdigit_safe( *ptr ) )
		{
			err = kMalformedErr;
			goto exit;
		}
	}
	else
	{
		while( ( ptr < inEnd ) && isdigit_safe( *ptr ) )
		{
			value = ( value * 10 ) + (uint64_t)( *ptr++ - '0' );
			require_action_quiet( value <= UINT32_MAX, exit, err = kRangeErr );
		}
	}
	
	*outValue = (uint32_t) value;
	if( outPtr ) *outPtr = ptr;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	CheckIntegerArgument
//===========================================================================================================================

static OSStatus	CheckIntegerArgument( int inArgValue, const char *inArgName, int inMin, int inMax )
{
	if( ( inArgValue >= inMin ) && ( inArgValue <= inMax ) ) return( kNoErr );
	
	FPrintF( stderr, "error: Invalid %s: %d. Valid range is [%d, %d].\n", inArgName, inArgValue, inMin, inMax );
	return( kRangeErr );
}

//===========================================================================================================================
//	CheckDoubleArgument
//===========================================================================================================================

static OSStatus	CheckDoubleArgument( double inArgValue, const char *inArgName, double inMin, double inMax )
{
	if( ( inArgValue >= inMin ) && ( inArgValue <= inMax ) ) return( kNoErr );
	
	FPrintF( stderr, "error: Invalid %s: %.1f. Valid range is [%.1f, %.1f].\n", inArgName, inArgValue, inMin, inMax );
	return( kRangeErr );
}

//===========================================================================================================================
//	CheckRootUser
//===========================================================================================================================

static OSStatus	CheckRootUser( void )
{
	if( geteuid() == 0 ) return( kNoErr );
	
	FPrintF( stderr, "error: This command must to be run as root.\n" );
	return( kPermissionErr );
}

//===========================================================================================================================
//	_SpawnCommand
//===========================================================================================================================

extern char **		environ;

static OSStatus
	_SpawnCommand(
		pid_t *			outPID,
		const char *	inStdOutRedirect,
		const char *	inStdErrRedirect,
		const char *	inFormat,
		... )
{
	OSStatus							err;
	va_list								args;
	char *								cmdStr		= NULL;
	size_t								cmdLen;
	const char *						cmdEnd;
	char *								bufPtr		= NULL;
	size_t								bufLen;
	const char *						bufLim;
	pid_t								pid;
	const char *						src;
	char *								dst;
	char **								argArray	= NULL;
	size_t								argCapacity, argCount;
	posix_spawn_file_actions_t			actions;
	posix_spawn_file_actions_t *		actionsPtr	= NULL;
	
	// Create command string from format string and its arguments.
	
	va_start( args, inFormat );
	VASPrintF( &cmdStr, inFormat, args );
	va_end( args );
	require_action( cmdStr, exit, err = kUnknownErr );
	
	cmdLen = strlen( cmdStr );
	cmdEnd = &cmdStr[ cmdLen ];
	
	// Allocate buffer for argument strings.
	// In the worst-case scenario in terms of memory requirements, the only non-escaped white space is one non-escaped
	// white space character between each pair of adjacent arguments. The amount of required memory is equal to the size
	// of cmdStr.
	
	bufLen = cmdLen + 1; // +1 for NUL terminator.
	bufPtr = (char *) malloc( bufLen );
	require_action( bufPtr, exit, err = kNoMemoryErr );
	bufLim = &bufPtr[ bufLen ];
	
	// Allocate initial argument array.
	
	argCount	= 0;
	argCapacity	= 8;
	argArray = (char **) malloc( ( argCapacity + 1 ) * sizeof( *argArray ) ); // +1 for NULL arg.
	require_action( argArray, exit, err = kNoMemoryErr );
	
	// Extract argument strings from command string.
	
	src = cmdStr;
	dst = bufPtr;
	for( ;; )
	{
		size_t		maxLen, copiedLen, totalLen;
		Boolean		more;
		
		maxLen = (size_t)( bufLim - dst );
		if( maxLen > 0 ) --maxLen; // -1 for NUL terminator.
		more = _ParseQuotedEscapedString( src, cmdEnd, kWhiteSpaceCharSet, dst, maxLen, &copiedLen, &totalLen, &src );
		if( !more ) break;
		require_fatal( copiedLen == totalLen, "Incorrect assumption about maximum required buffer space." );
		
		if( argCount >= argCapacity )
		{
			size_t		newCapactiy;
			char **		newArray;
			
			newCapactiy = 2 * argCapacity;
			newArray = (char **) realloc( argArray, ( newCapactiy + 1 ) * sizeof( *newArray ) ); // +1 for NULL arg.
			require_action( newArray, exit, err = kNoMemoryErr );
			
			argArray	= newArray;
			argCapacity	= newCapactiy;
		}
		argArray[ argCount++ ] = dst;
		dst += copiedLen;
		*dst++ = '\0';
		check( dst <= bufLim );
	}
	require_action_quiet( argCount > 0, exit, err = kCommandErr );
	
	argArray[ argCount ] = NULL;
	
	// Set up stdout and stderr redirections, if any.
	
	if( inStdOutRedirect || inStdErrRedirect )
	{
		err = posix_spawn_file_actions_init( &actions );
		require_noerr( err, exit );
		
		actionsPtr = &actions;
		if( inStdOutRedirect )
		{
			err = posix_spawn_file_actions_addopen( actionsPtr, STDOUT_FILENO, inStdOutRedirect, O_WRONLY, 0 );
			require_noerr( err, exit );
		}
		if( inStdErrRedirect )
		{
			err = posix_spawn_file_actions_addopen( actionsPtr, STDERR_FILENO, inStdErrRedirect, O_WRONLY, 0 );
			require_noerr( err, exit );
		}
	}
	// Spawn command.
	
	err = posix_spawnp( &pid, argArray[ 0 ], actionsPtr, NULL, argArray, environ );
	require_noerr_quiet( err, exit );
	
	if( outPID ) *outPID = pid;
	
exit:
	FreeNullSafe( cmdStr );
	FreeNullSafe( bufPtr );
	FreeNullSafe( argArray );
	if( actionsPtr ) posix_spawn_file_actions_destroy( actionsPtr );
	return( err );
}

//===========================================================================================================================
//	OutputFormatFromArgString
//===========================================================================================================================

static OSStatus	OutputFormatFromArgString( const char *inArgString, OutputFormatType *outFormat )
{
	OSStatus				err;
	OutputFormatType		format;
	
	format = (OutputFormatType) CLIArgToValue( "format", inArgString, &err,
		kOutputFormatStr_JSON,		kOutputFormatType_JSON,
		kOutputFormatStr_XML,		kOutputFormatType_XML,
		kOutputFormatStr_Binary,	kOutputFormatType_Binary,
		NULL );
	if( outFormat ) *outFormat = format;
	return( err );
}

//===========================================================================================================================
//	OutputPropertyList
//===========================================================================================================================

static OSStatus	OutputPropertyList( CFPropertyListRef inPList, OutputFormatType inType, const char *inOutputFilePath )
{
	OSStatus		err;
	CFDataRef		results = NULL;
	FILE *			file	= NULL;
	
	// Convert plist to a specific format.
	
	switch( inType )
	{
		case kOutputFormatType_JSON:
			results = CFCreateJSONData( inPList, kJSONFlags_None, NULL );
			require_action( results, exit, err = kUnknownErr );
			break;
		
		case kOutputFormatType_XML:
			results = CFPropertyListCreateData( NULL, inPList, kCFPropertyListXMLFormat_v1_0, 0, NULL );
			require_action( results, exit, err = kUnknownErr );
			break;
		
		case kOutputFormatType_Binary:
			results = CFPropertyListCreateData( NULL, inPList, kCFPropertyListBinaryFormat_v1_0, 0, NULL );
			require_action( results, exit, err = kUnknownErr );
			break;
		
		default:
			err = kTypeErr;
			goto exit;
	}
	
	// Write formatted results to file or stdout.
	
	if( inOutputFilePath )
	{
		file = fopen( inOutputFilePath, "wb" );
		err = map_global_value_errno( file, file );
		require_noerr( err, exit );
	}
	else
	{
		file = stdout;
	}
	
	err = WriteANSIFile( file, CFDataGetBytePtr( results ), (size_t) CFDataGetLength( results ) );
	require_noerr_quiet( err, exit );
	
	// Write a trailing newline for JSON-formatted results.
	
	if( inType == kOutputFormatType_JSON )
	{
		err = WriteANSIFile( file, "\n", 1 );
		require_noerr_quiet( err, exit );
	}
	
exit:
	if( file && ( file != stdout ) ) fclose( file );
	CFReleaseNullSafe( results );
	return( err );
}

//===========================================================================================================================
//	CreateSRVRecordDataFromString
//===========================================================================================================================

static OSStatus	CreateSRVRecordDataFromString( const char *inString, uint8_t **outPtr, size_t *outLen )
{
	OSStatus			err;
	DataBuffer			dataBuf;
	const char *		ptr;
	int					i;
	uint8_t *			end;
	uint8_t				target[ kDomainNameLengthMax ];
	
	DataBuffer_Init( &dataBuf, NULL, 0, ( 3 * 2 ) + kDomainNameLengthMax );
	
	// Parse and set the priority, weight, and port values (all three are unsigned 16-bit values).
	
	ptr = inString;
	for( i = 0; i < 3; ++i )
	{
		char *		next;
		long		value;
		uint8_t		buf[ 2 ];
		
		value = strtol( ptr, &next, 0 );
		require_action_quiet( ( next != ptr ) && ( *next == ',' ), exit, err = kMalformedErr );
		require_action_quiet( ( value >= 0 ) && ( value <= UINT16_MAX ), exit, err = kRangeErr );
		ptr = next + 1;
		
		WriteBig16Typed( buf, (uint16_t) value );
		
		err = DataBuffer_Append( &dataBuf, buf, sizeof( buf ) );
		require_noerr( err, exit );
	}
	
	// Set the target domain name.
	
	err = DomainNameFromString( target, ptr, &end );
    require_noerr_quiet( err, exit );
	
	err = DataBuffer_Append( &dataBuf, target, (size_t)( end - target ) );
	require_noerr( err, exit );
	
	err = DataBuffer_Detach( &dataBuf, outPtr, outLen );
	require_noerr( err, exit );
	
exit:
	DataBuffer_Free( &dataBuf );
	return( err );
}

//===========================================================================================================================
//	CreateTXTRecordDataFromString
//===========================================================================================================================

static OSStatus	CreateTXTRecordDataFromString(const char *inString, int inDelimiter, uint8_t **outPtr, size_t *outLen )
{
	OSStatus			err;
	DataBuffer			dataBuf;
	const char *		src;
	uint8_t				txtStr[ 256 ];	// Buffer for single TXT string: 1 length byte + up to 255 bytes of data.
	
	DataBuffer_Init( &dataBuf, NULL, 0, kDNSRecordDataLengthMax );
	
	src = inString;
	for( ;; )
	{
		uint8_t *					dst = &txtStr[ 1 ];
		const uint8_t * const		lim = &txtStr[ 256 ];
		int							c;
		
		while( *src && ( *src != inDelimiter ) )
		{
			if( ( c = *src++ ) == '\\' )
			{
				require_action_quiet( *src != '\0', exit, err = kUnderrunErr );
				c = *src++;
			}
			require_action_quiet( dst < lim, exit, err = kOverrunErr );
			*dst++ = (uint8_t) c;
		}
		txtStr[ 0 ] = (uint8_t)( dst - &txtStr[ 1 ] );
		err = DataBuffer_Append( &dataBuf, txtStr, 1 + txtStr[ 0 ] );
		require_noerr( err, exit );
		
		if( *src == '\0' ) break;
		++src;
	}
	
	err = DataBuffer_Detach( &dataBuf, outPtr, outLen );
	require_noerr( err, exit );
	
exit:
	DataBuffer_Free( &dataBuf );
	return( err );
}

//===========================================================================================================================
//	CreateNSECRecordData
//===========================================================================================================================

DECLARE_QSORT_NUMERIC_COMPARATOR( _QSortCmpUnsigned );
DEFINE_QSORT_NUMERIC_COMPARATOR( unsigned int, _QSortCmpUnsigned )

#define kNSECBitmapMaxLength		32	// 32 bytes (256 bits). See <https://tools.ietf.org/html/rfc4034#section-4.1.2>.

static OSStatus
	CreateNSECRecordData(
		const uint8_t *	inNextDomainName,
		uint8_t **		outPtr,
		size_t *		outLen,
		unsigned int	inTypeCount,
		... )
{
	OSStatus			err;
	va_list				args;
	DataBuffer			rdataDB;
	unsigned int *		array	= NULL;
	unsigned int		i, type, maxBit, currBlock, bitmapLen;
	uint8_t				fields[ 2 + kNSECBitmapMaxLength ];
	uint8_t * const		bitmap	= &fields[ 2 ];
	
	va_start( args, inTypeCount );
	DataBuffer_Init( &rdataDB, NULL, 0, kDNSRecordDataLengthMax );
	
	// Append Next Domain Name.
	
	err = DataBuffer_Append( &rdataDB, inNextDomainName, DomainNameLength( inNextDomainName ) );
	require_noerr( err, exit );
	
	// Append Type Bit Maps.
	
	maxBit = 0;
	memset( bitmap, 0, kNSECBitmapMaxLength );
	if( inTypeCount > 0 )
	{
		array = (unsigned int *) malloc( inTypeCount * sizeof_element( array ) );
		require_action( array, exit, err = kNoMemoryErr );
		
		for( i = 0; i < inTypeCount; ++i )
		{
			type = va_arg( args, unsigned int );
			require_action_quiet( type <= UINT16_MAX, exit, err = kRangeErr );
			array[ i ] = type;
		}
		qsort( array, inTypeCount, sizeof_element( array ), _QSortCmpUnsigned );
		
		currBlock = array[ 0 ] / 256;
		for( i = 0; i < inTypeCount; ++i )
		{
			const unsigned int		block	= array[ i ] / 256;
			const unsigned int		bit		= array[ i ] % 256;
			
			if( block != currBlock )
			{
				bitmapLen	= BitArray_MaxBytes( maxBit + 1 );
				fields[ 0 ] = (uint8_t) currBlock;
				fields[ 1 ] = (uint8_t) bitmapLen;
				
				err = DataBuffer_Append( &rdataDB, fields, 2 + bitmapLen );
				require_noerr( err, exit );
				
				maxBit		= 0;
				currBlock	= block;
				memset( bitmap, 0, bitmapLen );
			}
			BitArray_SetBit( bitmap, bit );
			if( bit > maxBit ) maxBit = bit;
		}
	}
	else
	{
		currBlock = 0;
	}
	
	bitmapLen	= BitArray_MaxBytes( maxBit + 1 );
	fields[ 0 ] = (uint8_t) currBlock;
	fields[ 1 ] = (uint8_t) bitmapLen;
	
	err = DataBuffer_Append( &rdataDB, fields, 2 + bitmapLen );
	require_noerr( err, exit );
	
	err = DataBuffer_Detach( &rdataDB, outPtr, outLen );
	require_noerr( err, exit );
	
exit:
	va_end( args );
	DataBuffer_Free( &rdataDB );
	FreeNullSafe( array );
	return( err );
}

//===========================================================================================================================
//	AppendSOARecord
//===========================================================================================================================

static OSStatus
	_AppendSOARecordData(
		DataBuffer *	inDB,
		const uint8_t *	inMName,
		const uint8_t *	inRName,
		uint32_t		inSerial,
		uint32_t		inRefresh,
		uint32_t		inRetry,
		uint32_t		inExpire,
		uint32_t		inMinimumTTL,
		size_t *		outLen );

static OSStatus
	AppendSOARecord(
		DataBuffer *	inDB,
		const uint8_t *	inNamePtr,
		size_t			inNameLen,
		uint16_t		inType,
		uint16_t		inClass,
		uint32_t		inTTL,
		const uint8_t *	inMName,
		const uint8_t *	inRName,
		uint32_t		inSerial,
		uint32_t		inRefresh,
		uint32_t		inRetry,
		uint32_t		inExpire,
		uint32_t		inMinimumTTL,
		size_t *		outLen )
{
	OSStatus		err;
	size_t			rdataLen;
	size_t			rdlengthOffset = 0;
	uint8_t *		rdlengthPtr;
	
	if( inDB )
	{
		err = _DataBuffer_AppendDNSRecord( inDB, inNamePtr, inNameLen, inType, inClass, inTTL, NULL, 0 );
		require_noerr( err, exit );
		
		rdlengthOffset = DataBuffer_GetLen( inDB ) - 2;
	}
	
	err = _AppendSOARecordData( inDB, inMName, inRName, inSerial, inRefresh, inRetry, inExpire, inMinimumTTL, &rdataLen );
	require_noerr( err, exit );
	
	if( inDB )
	{
		rdlengthPtr = DataBuffer_GetPtr( inDB ) + rdlengthOffset;
		WriteBig16Typed( rdlengthPtr, (uint16_t) rdataLen );
	}
	
	if( outLen ) *outLen = inNameLen + sizeof( dns_fixed_fields_record ) + rdataLen;
	err = kNoErr;
	
exit:
	return( err );
}

static OSStatus
	_AppendSOARecordData(
		DataBuffer *	inDB,
		const uint8_t *	inMName,
		const uint8_t *	inRName,
		uint32_t		inSerial,
		uint32_t		inRefresh,
		uint32_t		inRetry,
		uint32_t		inExpire,
		uint32_t		inMinimumTTL,
		size_t *		outLen )
{
	OSStatus					err;
	dns_fixed_fields_soa		fields;
	const size_t				mnameLen = DomainNameLength( inMName );
	const size_t				rnameLen = DomainNameLength( inRName );
	
	if( inDB )
	{
		err = DataBuffer_Append( inDB, inMName, mnameLen );
		require_noerr( err, exit );
		
		err = DataBuffer_Append( inDB, inRName, rnameLen );
		require_noerr( err, exit );
		
		dns_fixed_fields_soa_init( &fields, inSerial, inRefresh, inRetry, inExpire, inMinimumTTL );
		err = DataBuffer_Append( inDB, &fields, sizeof( fields ) );
		require_noerr( err, exit );
	}
	if( outLen ) *outLen = mnameLen + rnameLen + sizeof( fields );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	CreateSOARecordData
//===========================================================================================================================

static OSStatus
	CreateSOARecordData(
		const uint8_t *	inMName,
		const uint8_t *	inRName,
		uint32_t		inSerial,
		uint32_t		inRefresh,
		uint32_t		inRetry,
		uint32_t		inExpire,
		uint32_t		inMinimumTTL,
		uint8_t **		outPtr,
		size_t *		outLen )
{
	OSStatus		err;
	DataBuffer		rdataDB;
	
	DataBuffer_Init( &rdataDB, NULL, 0, kDNSRecordDataLengthMax );
	
	err = _AppendSOARecordData( &rdataDB, inMName, inRName, inSerial, inRefresh, inRetry, inExpire, inMinimumTTL, NULL );
	require_noerr( err, exit );
	
	err = DataBuffer_Detach( &rdataDB, outPtr, outLen );
	require_noerr( err, exit );
	
exit:
	DataBuffer_Free( &rdataDB );
	return( err );
}

//===========================================================================================================================
//	_DataBuffer_AppendDNSQuestion
//===========================================================================================================================

static OSStatus
	_DataBuffer_AppendDNSQuestion(
		DataBuffer *	inDB,
		const uint8_t *	inNamePtr,
		size_t			inNameLen,
		uint16_t		inType,
		uint16_t		inClass )
{
	OSStatus						err;
	dns_fixed_fields_question		fields;
	
	err = DataBuffer_Append( inDB, inNamePtr, inNameLen );
	require_noerr( err, exit );
	
	dns_fixed_fields_question_init( &fields, inType, inClass );
	err = DataBuffer_Append( inDB, &fields, sizeof( fields ) );
	require_noerr( err, exit );
	
exit:
	return( err );
}

//===========================================================================================================================
//	_DataBuffer_AppendDNSRecord
//===========================================================================================================================

static OSStatus
	_DataBuffer_AppendDNSRecord(
		DataBuffer *	inDB,
		const uint8_t *	inNamePtr,
		size_t			inNameLen,
		uint16_t		inType,
		uint16_t		inClass,
		uint32_t		inTTL,
		const uint8_t *	inRDataPtr,
		size_t			inRDataLen )
{
	OSStatus					err;
	dns_fixed_fields_record		fields;
	
	require_action_quiet( inRDataLen < kDNSRecordDataLengthMax, exit, err = kSizeErr );
	
	err = DataBuffer_Append( inDB, inNamePtr, inNameLen );
	require_noerr( err, exit );
	
	dns_fixed_fields_record_init( &fields, inType, inClass, inTTL, (uint16_t) inRDataLen );
	err = DataBuffer_Append( inDB, &fields, sizeof( fields ) );
	require_noerr( err, exit );
	
	if( inRDataPtr )
	{
		err = DataBuffer_Append( inDB, inRDataPtr, inRDataLen );
		require_noerr( err, exit );
	}
	
exit:
	return( err );
}

//===========================================================================================================================
//	_NanoTime64ToTimestamp
//===========================================================================================================================

static char *	_NanoTime64ToTimestamp( NanoTime64 inTime, char *inBuf, size_t inMaxLen )
{
	struct  timeval		tv;
	
	NanoTimeToTimeVal( inTime, &tv );
	return( MakeFractionalDateString( &tv, inBuf, inMaxLen ) );
}

//===========================================================================================================================
//	_MDNSInterfaceListCreate
//===========================================================================================================================

static Boolean	_MDNSInterfaceIsBlacklisted( SocketRef inInfoSock, const char *inIfName );

static OSStatus	_MDNSInterfaceListCreate( MDNSInterfaceSubset inSubset, size_t inItemSize, MDNSInterfaceItem **outList )
{
	OSStatus					err;
	struct ifaddrs *			ifaList;
	const struct ifaddrs *		ifa;
	MDNSInterfaceItem *			interfaceList;
	MDNSInterfaceItem **		ptr;
	SocketRef					infoSock;
	
	ifaList			= NULL;
	interfaceList	= NULL;
	infoSock		= kInvalidSocketRef;
	if( inItemSize == 0 ) inItemSize = sizeof( MDNSInterfaceItem );
	require_action_quiet( inItemSize >= sizeof( MDNSInterfaceItem ), exit, err = kSizeErr );
	
	infoSock = socket( AF_INET, SOCK_DGRAM, 0 );
	err = map_socket_creation_errno( infoSock );
	require_noerr( err, exit );
	
	err = getifaddrs( &ifaList );
	err = map_global_noerr_errno( err );
	require_noerr( err, exit );
	
	ptr = &interfaceList;
	for( ifa = ifaList; ifa; ifa = ifa->ifa_next )
	{
		MDNSInterfaceItem *		item;
		int						family;
		const unsigned int		flagsMask	= IFF_UP | IFF_MULTICAST | IFF_POINTOPOINT;
		const unsigned int		flagsNeeded	= IFF_UP | IFF_MULTICAST;
		
		if( ( ifa->ifa_flags & flagsMask ) != flagsNeeded )		continue;
		if( !ifa->ifa_addr || !ifa->ifa_name )					continue;
		family = ifa->ifa_addr->sa_family;
		if( ( family != AF_INET ) && ( family != AF_INET6 ) )	continue;
		
		for( item = interfaceList; item && ( strcmp( item->ifName, ifa->ifa_name ) != 0 ); item = item->next ) {}
		if( !item )
		{
			NetTransportType		type;
			uint32_t				ifIndex;
			const char * const		ifName = ifa->ifa_name;
			
			if( _MDNSInterfaceIsBlacklisted( infoSock, ifName ) ) continue;
			err = SocketGetInterfaceInfo( infoSock, ifName, NULL, &ifIndex, NULL, NULL, NULL, NULL, NULL, &type );
			require_noerr( err, exit );
			
			if( ifIndex == 0 ) continue;
			if( type == kNetTransportType_AWDL )
			{
				if( inSubset == kMDNSInterfaceSubset_NonAWDL ) continue;
			}
			else
			{
				if( inSubset == kMDNSInterfaceSubset_AWDL ) continue;
			}
			item = (MDNSInterfaceItem *) calloc( 1, inItemSize );
			require_action( item, exit, err = kNoMemoryErr );
			
			*ptr =  item;
			 ptr = &item->next;
			
			item->ifName = strdup( ifName );
			require_action( item->ifName, exit, err = kNoMemoryErr );
			
			item->ifIndex = ifIndex;
			if(      type == kNetTransportType_AWDL ) item->isAWDL = true;
			else if( type == kNetTransportType_WiFi ) item->isWiFi = true;
		}
		if( family == AF_INET )	item->hasIPv4 = true;
		else					item->hasIPv6 = true;
	}
	require_action_quiet( interfaceList, exit, err = kNotFoundErr );
	
	if( outList )
	{
		*outList = interfaceList;
		interfaceList = NULL;
	}
	
exit:
	if( ifaList ) freeifaddrs( ifaList );
	_MDNSInterfaceListFree( interfaceList );
	ForgetSocket( &infoSock );
	return( err );
}

static Boolean	_MDNSInterfaceIsBlacklisted( SocketRef inInfoSock, const char *inIfName )
{
	OSStatus						err;
	int								i;
	static const char * const		kMDNSInterfacePrefixBlacklist[] = { "llw", "nan" };
	struct ifreq					ifr;
	
	// Check if the interface name's prefix matches the prefix blacklist.
	
	for( i = 0; i < (int) countof( kMDNSInterfacePrefixBlacklist ); ++i )
	{
		const char * const		prefix	= kMDNSInterfacePrefixBlacklist[ i ];
		
        if( strcmp_prefix( inIfName, prefix ) == 0 )
		{
			const char *		ptr = &inIfName[ strlen( prefix ) ];
			
			while( isdigit_safe( *ptr ) ) ++ptr;
			if( *ptr == '\0' ) return( true );
		}
	}
	
	// Check if the interface is used for inter-(co)processor networking.
	
	memset( &ifr, 0, sizeof( ifr ) );
	strlcpy( ifr.ifr_name, inIfName, sizeof( ifr.ifr_name ) );
	err = ioctl( inInfoSock, SIOCGIFFUNCTIONALTYPE, &ifr );
	err = map_global_value_errno( err != -1, err );
	if( !err && ( ifr.ifr_functional_type == IFRTYPE_FUNCTIONAL_INTCOPROC ) ) return( true );
	
	return( false );
}

//===========================================================================================================================
//	_MDNSInterfaceListFree
//===========================================================================================================================

static void	_MDNSInterfaceListFree( MDNSInterfaceItem *inList )
{
	MDNSInterfaceItem *		item;
	
	while( ( item = inList ) != NULL )
	{
		inList = item->next;
		FreeNullSafe( item->ifName );
		free( item );
	}
}

//===========================================================================================================================
//	_MDNSInterfaceGetAny
//===========================================================================================================================

static OSStatus	_MDNSInterfaceGetAny( MDNSInterfaceSubset inSubset, char inNameBuf[ IF_NAMESIZE + 1 ], uint32_t *outIndex )
{
	OSStatus						err;
	MDNSInterfaceItem *				list;
	const MDNSInterfaceItem *		item;
	
	list = NULL;
	err = _MDNSInterfaceListCreate( inSubset, 0, &list );
	require_noerr_quiet( err, exit );
	require_action_quiet( list, exit, err = kNotFoundErr );
	
	for( item = list; item; item = item->next )
	{
		if( item->hasIPv4 && item->hasIPv6 ) break;
	}
	if( !item ) item = list;
	if( inNameBuf )	strlcpy( inNameBuf, item->ifName, IF_NAMESIZE + 1 );
	if( outIndex ) *outIndex = item->ifIndex;
	
exit:
	_MDNSInterfaceListFree( list );
	return( err );
}

//===========================================================================================================================
//	_SetComputerName
//===========================================================================================================================

static OSStatus	_SetComputerName( CFStringRef inComputerName, CFStringEncoding inEncoding )
{
	OSStatus				err;
	SCPreferencesRef		prefs;
	Boolean					ok;
	
	prefs = SCPreferencesCreateWithAuthorization( NULL, CFSTR( kDNSSDUtilIdentifier ), NULL, NULL );
	err = map_scerror( prefs );
	require_noerr_quiet( err, exit );
	
	ok = SCPreferencesSetComputerName( prefs, inComputerName, inEncoding );
	err = map_scerror( ok );
	require_noerr_quiet( err, exit );
	
	ok = SCPreferencesCommitChanges( prefs );
	err = map_scerror( ok );
	require_noerr_quiet( err, exit );
	
	ok = SCPreferencesApplyChanges( prefs );
	err = map_scerror( ok );
	require_noerr_quiet( err, exit );
	
exit:
	CFReleaseNullSafe( prefs );
	return( err );
}

//===========================================================================================================================
//	_SetComputerNameWithUTF8CString
//===========================================================================================================================

static OSStatus	_SetComputerNameWithUTF8CString( const char *inComputerName )
{
	OSStatus		err;
	CFStringRef		computerName;
	
	computerName = CFStringCreateWithCString( NULL, inComputerName, kCFStringEncodingUTF8 );
	require_action( computerName, exit, err = kNoMemoryErr );
	
	err = _SetComputerName( computerName, kCFStringEncodingUTF8 );
	require_noerr_quiet( err, exit );
	
exit:
	CFReleaseNullSafe( computerName );
	return( err );
}

//===========================================================================================================================
//	_SetLocalHostName
//===========================================================================================================================

static OSStatus	_SetLocalHostName( CFStringRef inLocalHostName )
{
	OSStatus				err;
	SCPreferencesRef		prefs;
	Boolean					ok;
	
	prefs = SCPreferencesCreateWithAuthorization( NULL, CFSTR( kDNSSDUtilIdentifier ), NULL, NULL );
	err = map_scerror( prefs );
	require_noerr_quiet( err, exit );
	
	ok = SCPreferencesSetLocalHostName( prefs, inLocalHostName );
	err = map_scerror( ok );
	require_noerr_quiet( err, exit );
	
	ok = SCPreferencesCommitChanges( prefs );
	err = map_scerror( ok );
	require_noerr_quiet( err, exit );
	
	ok = SCPreferencesApplyChanges( prefs );
	err = map_scerror( ok );
	require_noerr_quiet( err, exit );
	
exit:
	CFReleaseNullSafe( prefs );
	return( err );
}

//===========================================================================================================================
//	_SetLocalHostNameWithUTF8CString
//===========================================================================================================================

static OSStatus	_SetLocalHostNameWithUTF8CString( const char *inLocalHostName )
{
	OSStatus		err;
	CFStringRef		localHostName;
	
	localHostName = CFStringCreateWithCString( NULL, inLocalHostName, kCFStringEncodingUTF8 );
	require_action( localHostName, exit, err = kNoMemoryErr );
	
	err = _SetLocalHostName( localHostName );
	require_noerr_quiet( err, exit );
	
exit:
	CFReleaseNullSafe( localHostName );
	return( err );
}

#if( TARGET_OS_DARWIN )
//===========================================================================================================================
//	_InterfaceIPv6AddressAdd
//===========================================================================================================================

static OSStatus	_InterfaceIPv6AddressAdd( const char *inIfName, uint8_t inAddr[ STATIC_PARAM 16 ], int inMaskBitLen )
{
	OSStatus					err;
	SocketRef					infoSock = kInvalidSocketRef;
	struct in6_aliasreq			ifra;
	size_t						len;
	struct sockaddr_in6 *		sin6;
	int							wholeBytes, remainingBits;
	
	require_action_quiet( ( inMaskBitLen >= 0 ) &&  ( inMaskBitLen <= 128 ), exit, err = kSizeErr );
	
	infoSock = socket( AF_INET6, SOCK_DGRAM, 0 );
	err = map_socket_creation_errno( infoSock );
	require_noerr( err, exit );
	
	// Set interface name.
	
	memset( &ifra, 0, sizeof( ifra ) );
	len = strlcpy( ifra.ifra_name, inIfName, sizeof( ifra.ifra_name ) );
	require_action_quiet( len < sizeof( ifra.ifra_name ), exit, err = kSizeErr );
	
	// Set IPv6 address.
	
	sin6 = &ifra.ifra_addr;
	SIN6_LEN_SET( sin6 );
	sin6->sin6_family = AF_INET6;
	memcpy( sin6->sin6_addr.s6_addr, inAddr, 16 );
	
	// Set prefix mask.
	
	sin6 = &ifra.ifra_prefixmask;
	SIN6_LEN_SET( sin6 );
	sin6->sin6_family = AF_INET6;
	wholeBytes = inMaskBitLen / 8;
	if( wholeBytes > 0 ) memset( sin6->sin6_addr.s6_addr, 0xFF, (size_t) wholeBytes );
	remainingBits = inMaskBitLen % 8;
	if( remainingBits > 0 ) sin6->sin6_addr.s6_addr[ wholeBytes ] = ( 0xFFU << ( 8 - remainingBits ) ) & 0xFFU;
	
	ifra.ifra_lifetime.ia6t_vltime = ND6_INFINITE_LIFETIME;
	ifra.ifra_lifetime.ia6t_pltime = ND6_INFINITE_LIFETIME;
	
	err = ioctl( infoSock, SIOCAIFADDR_IN6, &ifra );
	err = map_global_value_errno( err != -1, err );
	require_noerr_quiet( err, exit );
	
exit:
	ForgetSocket( &infoSock );
	return( err );
}

//===========================================================================================================================
//	_InterfaceIPv6AddressRemove
//===========================================================================================================================

static OSStatus	_InterfaceIPv6AddressRemove( const char *inIfName, const uint8_t inAddr[ STATIC_PARAM 16 ] )
{
	OSStatus					err;
	SocketRef					infoSock = kInvalidSocketRef;
	struct in6_ifreq			ifr;
	size_t						len;
	struct sockaddr_in6 *		sin6;
	
	infoSock = socket( AF_INET6, SOCK_DGRAM, 0 );
	err = map_socket_creation_errno( infoSock );
	require_noerr( err, exit );
	
	memset( &ifr, 0, sizeof( ifr ) );
	len = strlcpy( ifr.ifr_name, inIfName, sizeof( ifr.ifr_name ) );
	require_action_quiet( len < sizeof( ifr.ifr_name ), exit, err = kSizeErr );
	
	sin6 = &ifr.ifr_ifru.ifru_addr;
	SIN6_LEN_SET( sin6 );
	sin6->sin6_family = AF_INET6;
	memcpy( sin6->sin6_addr.s6_addr, inAddr, 16 );
	
	err = ioctl( infoSock, SIOCDIFADDR_IN6, &ifr );
	err = map_global_value_errno( err != -1, err );
	require_noerr_quiet( err, exit );
	
exit:
	ForgetSocket( &infoSock );
	return( err );
}
#endif	// TARGET_OS_DARWIN

//===========================================================================================================================
//	_TicksDiff
//===========================================================================================================================

static int64_t	_TicksDiff( uint64_t inT1, uint64_t inT2 )
{
	return( (int64_t)( inT1 - inT2 ) );
}

//===========================================================================================================================
//	_SockAddrInitIPv4
//===========================================================================================================================

static void	_SockAddrInitIPv4( struct sockaddr_in *inSA, uint32_t inIPv4, uint16_t inPort )
{
	memset( inSA, 0, sizeof( *inSA ) );
	SIN_LEN_SET( inSA );
	inSA->sin_family		= AF_INET;
	inSA->sin_port			= htons( inPort );
	inSA->sin_addr.s_addr	= htonl( inIPv4 );
}

//===========================================================================================================================
//	_SockAddrInitIPv6
//===========================================================================================================================

static void
	_SockAddrInitIPv6(
		struct sockaddr_in6 *	inSA,
		const uint8_t			inIPv6[ STATIC_PARAM 16 ],
		uint32_t				inScope,
		uint16_t				inPort )
{
	check_compile_time_code( sizeof( inSA->sin6_addr.s6_addr ) == 16 );
	
	memset( inSA, 0, sizeof( *inSA ) );
	SIN6_LEN_SET( inSA );
	inSA->sin6_family	= AF_INET6;
	inSA->sin6_port		= htons( inPort );
	memcpy( inSA->sin6_addr.s6_addr, inIPv6, 16 );
	inSA->sin6_scope_id	= inScope;
}

//===========================================================================================================================
//	_WriteReverseIPv6DomainNameString
//===========================================================================================================================

static void
	_WriteReverseIPv6DomainNameString(
		const uint8_t	inIPv6Addr[ STATIC_PARAM 16 ],
		char			outBuffer[ STATIC_PARAM kReverseIPv6DomainNameBufLen ] )
{
	char *		dst;
	int			i;
	
	dst = outBuffer;
	for( i = 0; i < 16; ++i )
	{
		const unsigned int		octet = inIPv6Addr[ 15 - i ];
		
		*dst++ = kHexDigitsLowercase[ octet & 0x0F ];
		*dst++ = '.';
		*dst++ = kHexDigitsLowercase[ octet >> 4 ];
		*dst++ = '.';
	}
	memcpy( dst, kIP6ArpaDomainStr, sizeof( kIP6ArpaDomainStr ) );
	dst += sizeof( kIP6ArpaDomainStr );
	check( ( dst - outBuffer ) == kReverseIPv6DomainNameBufLen );
}

//===========================================================================================================================
//	_WriteReverseIPv4DomainNameString
//===========================================================================================================================

static void
	_WriteReverseIPv4DomainNameString(
		uint32_t	inIPv4Addr,
		char		outBuffer[ STATIC_PARAM kReverseIPv4DomainNameBufLen ] )
{
	SNPrintF( outBuffer, kReverseIPv4DomainNameBufLen, "%u.%u.%u.%u.%s",
		  inIPv4Addr         & 0xFF,
		( inIPv4Addr >>  8 ) & 0xFF,
		( inIPv4Addr >> 16 ) & 0xFF,
		( inIPv4Addr >> 24 ) & 0xFF, kInAddrArpaDomainStr );
}

#if( MDNSRESPONDER_PROJECT )
//===========================================================================================================================
//	_SetDefaultFallbackDNSService
//===========================================================================================================================

static OSStatus	_SetDefaultFallbackDNSService( const char *inFallbackDNSServiceStr )
{
	OSStatus					err;
	nw_resolver_config_t		resolverConfig;
	CFDataRef					plistData;
	
	if( stricmp_prefix( inFallbackDNSServiceStr, kFallbackDNSServiceArgPrefix_DoH ) == 0 )
	{
		nw_endpoint_t			endpoint;
		const char * const		url = inFallbackDNSServiceStr + sizeof_string( kFallbackDNSServiceArgPrefix_DoH );
		
		endpoint = nw_endpoint_create_url( url );
		require_action( endpoint, exit, err = kUnknownErr );
		
		resolverConfig = nw_resolver_config_create_https( endpoint );
		nw_forget( &endpoint );
		require_action( resolverConfig, exit, err = kUnknownErr );
	}
	else if( stricmp_prefix( inFallbackDNSServiceStr, kFallbackDNSServiceArgPrefix_DoT ) == 0 )
	{
		nw_endpoint_t			endpoint;
		const char * const		hostname = inFallbackDNSServiceStr + sizeof_string( kFallbackDNSServiceArgPrefix_DoT );
		
		endpoint = nw_endpoint_create_host( hostname, "0" );
		require_action( endpoint, exit, err = kUnknownErr );
		
		resolverConfig = nw_resolver_config_create_tls( endpoint );
		nw_forget( &endpoint );
		require_action( resolverConfig, exit, err = kUnknownErr );
	}
	else
	{
		FPrintF( stderr, "error: Unrecognized fallback DNS service string: \"%s\"\n", inFallbackDNSServiceStr );
		err = kParamErr;
		goto exit;
	}
	plistData = nw_resolver_config_copy_plist_data_ref( resolverConfig );
	require_action( plistData, exit, err = kUnknownErr );
	
	err = DNSServiceSetResolverDefaults( CFDataGetBytePtr( plistData ), (size_t) CFDataGetLength( plistData ), true );
	ForgetCF( &plistData );
	require_noerr( err, exit );
	
exit:
	return( err );
}
#endif

//===========================================================================================================================
//	MDNSColliderCreate
//===========================================================================================================================

typedef enum
{
	kMDNSColliderOpCode_Invalid			= 0,
	kMDNSColliderOpCode_Send			= 1,
	kMDNSColliderOpCode_Wait			= 2,
	kMDNSColliderOpCode_SetProbeActions	= 3,
	kMDNSColliderOpCode_LoopPush		= 4,
	kMDNSColliderOpCode_LoopPop			= 5,
	kMDNSColliderOpCode_Exit			= 6
	
}	MDNSColliderOpCode;

typedef struct
{
	MDNSColliderOpCode		opcode;
	uint32_t				operand;
	
}	MDNSCInstruction;

#define kMaxLoopDepth		16

struct MDNSColliderPrivate
{
	CFRuntimeBase					base;							// CF object base.
	dispatch_queue_t				queue;							// Queue for collider's events.
	dispatch_source_t				readSourceV4;					// Read dispatch source for IPv4 socket.
	dispatch_source_t				readSourceV6;					// Read dispatch source for IPv6 socket.
	SocketRef						sockV4;							// IPv4 UDP socket for mDNS.
	SocketRef						sockV6;							// IPv6 UDP socket for mDNS.
	uint8_t *						target;							// Record name being targeted. (malloced)
	uint8_t *						responsePtr;					// Response message pointer. (malloced)
	size_t							responseLen;					// Response message length.
	uint8_t *						probePtr;						// Probe query message pointer. (malloced)
	size_t							probeLen;						// Probe query message length.
	unsigned int					probeCount;						// Count of probe queries received for collider's record.
	uint32_t						probeActionMap;					// Bitmap of actions to take for 
	MDNSCInstruction *				program;						// Program to execute.
	uint32_t						pc;								// Program's program counter.
	uint32_t						loopCounts[ kMaxLoopDepth ];	// Stack of loop counters.
	uint32_t						loopDepth;						// Current loop depth.
	dispatch_source_t				waitTimer;						// Timer for program's wait commands.
	uint32_t						interfaceIndex;					// Interface over which to send and receive mDNS msgs.
	MDNSColliderStopHandler_f		stopHandler;					// User's stop handler.
	void *							stopContext;					// User's stop handler context.
	MDNSColliderProtocols			protocols;						// Protocols to use, i.e., IPv4, IPv6.
	Boolean							stopped;						// True if the collider has been stopped.
	uint8_t							msgBuf[ kMDNSMessageSizeMax ];	// mDNS message buffer.
};

static void		_MDNSColliderStop( MDNSColliderRef inCollider, OSStatus inError );
static void		_MDNSColliderReadHandler( void *inContext );
static void		_MDNSColliderExecuteProgram( void *inContext );
static OSStatus	_MDNSColliderSendResponse( MDNSColliderRef inCollider, SocketRef inSock, const struct sockaddr *inDest );
static OSStatus	_MDNSColliderSendProbe( MDNSColliderRef inCollider, SocketRef inSock, const struct sockaddr *inDest );

CF_CLASS_DEFINE( MDNSCollider );

ulog_define_ex( kDNSSDUtilIdentifier, MDNSCollider, kLogLevelInfo, kLogFlags_None, "MDNSCollider", NULL );
#define mc_ulog( LEVEL, ... )		ulog( &log_category_from_name( MDNSCollider ), (LEVEL), __VA_ARGS__ )

static OSStatus	MDNSColliderCreate( dispatch_queue_t inQueue, MDNSColliderRef *outCollider )
{
	OSStatus			err;
	MDNSColliderRef		obj = NULL;
	
	CF_OBJECT_CREATE( MDNSCollider, obj, err, exit );
	
	ReplaceDispatchQueue( &obj->queue, inQueue );
	obj->sockV4 = kInvalidSocketRef;
	obj->sockV6 = kInvalidSocketRef;
	
	*outCollider = obj;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_MDNSColliderFinalize
//===========================================================================================================================

static void	_MDNSColliderFinalize( CFTypeRef inObj )
{
	MDNSColliderRef const		me = (MDNSColliderRef) inObj;
	
	check( !me->waitTimer );
	check( !me->readSourceV4 );
	check( !me->readSourceV6 );
	check( !IsValidSocket( me->sockV4 ) );
	check( !IsValidSocket( me->sockV6 ) );
	ForgetMem( &me->target );
	ForgetMem( &me->responsePtr );
	ForgetMem( &me->probePtr );
	ForgetMem( &me->program );
	dispatch_forget( &me->queue );
}

//===========================================================================================================================
//	MDNSColliderStart
//===========================================================================================================================

static void	_MDNSColliderStart( void *inContext );

static OSStatus	MDNSColliderStart( MDNSColliderRef me )
{
	OSStatus		err;
	
	require_action_quiet( me->target,         exit, err = kNotPreparedErr );
	require_action_quiet( me->responsePtr,    exit, err = kNotPreparedErr );
	require_action_quiet( me->probePtr,       exit, err = kNotPreparedErr );
	require_action_quiet( me->program,        exit, err = kNotPreparedErr );
	require_action_quiet( me->interfaceIndex, exit, err = kNotPreparedErr );
	require_action_quiet( me->protocols,      exit, err = kNotPreparedErr );
	
	CFRetain( me );
	dispatch_async_f( me->queue, me, _MDNSColliderStart );
	err = kNoErr;
	
exit:
	return( err );
}

static void	_MDNSColliderStart( void *inContext )
{
	OSStatus					err;
	MDNSColliderRef const		me		= (MDNSColliderRef) inContext;
	SocketRef					sock	= kInvalidSocketRef;
	SocketContext *				sockCtx	= NULL;
	
	if( me->protocols & kMDNSColliderProtocol_IPv4 )
	{
		err = CreateMulticastSocket( GetMDNSMulticastAddrV4(), kMDNSPort, NULL, me->interfaceIndex, true, NULL, &sock );
		require_noerr( err, exit );
		
		sockCtx = SocketContextCreate( sock, me, &err );
		require_noerr( err, exit );
		sock = kInvalidSocketRef;
		
		err = DispatchReadSourceCreate( sockCtx->sock, me->queue, _MDNSColliderReadHandler, SocketContextCancelHandler,
			sockCtx, &me->readSourceV4 );
		require_noerr( err, exit );
		me->sockV4 = sockCtx->sock;
		sockCtx = NULL;
		
		dispatch_resume( me->readSourceV4 );
	}
	
	if( me->protocols & kMDNSColliderProtocol_IPv6 )
	{
		err = CreateMulticastSocket( GetMDNSMulticastAddrV6(), kMDNSPort, NULL, me->interfaceIndex, true, NULL, &sock );
		require_noerr( err, exit );
		
		sockCtx = SocketContextCreate( sock, me, &err );
		require_noerr( err, exit );
		sock = kInvalidSocketRef;
		
		err = DispatchReadSourceCreate( sockCtx->sock, me->queue, _MDNSColliderReadHandler, SocketContextCancelHandler,
			sockCtx, &me->readSourceV6 );
		require_noerr( err, exit );
		me->sockV6 = sockCtx->sock;
		sockCtx = NULL;
		
		dispatch_resume( me->readSourceV6 );
	}
	
	_MDNSColliderExecuteProgram( me );
	err = kNoErr;
	
exit:
	ForgetSocket( &sock );
	ForgetSocketContext( &sockCtx );
	if( err ) _MDNSColliderStop( me, err );
}

//===========================================================================================================================
//	MDNSColliderStop
//===========================================================================================================================

static void	_MDNSColliderUserStop( void *inContext );

static void	MDNSColliderStop( MDNSColliderRef me )
{
	CFRetain( me );
	dispatch_async_f( me->queue, me, _MDNSColliderUserStop );
}

static void	_MDNSColliderUserStop( void *inContext )
{
	MDNSColliderRef const		me = (MDNSColliderRef) inContext;
	
	_MDNSColliderStop( me, kCanceledErr );
	CFRelease( me );
}

//===========================================================================================================================
//	MDNSColliderSetProtocols
//===========================================================================================================================

static void	MDNSColliderSetProtocols( MDNSColliderRef me, MDNSColliderProtocols inProtocols )
{
	me->protocols = inProtocols;
}

//===========================================================================================================================
//	MDNSColliderSetInterfaceIndex
//===========================================================================================================================

static void	MDNSColliderSetInterfaceIndex( MDNSColliderRef me, uint32_t inInterfaceIndex )
{
	me->interfaceIndex = inInterfaceIndex;
}

//===========================================================================================================================
//	MDNSColliderSetProgram
//===========================================================================================================================

#define kMDNSColliderProgCmd_Done		"done"
#define kMDNSColliderProgCmd_Loop		"loop"
#define kMDNSColliderProgCmd_Send		"send"
#define kMDNSColliderProgCmd_Probes		"probes"
#define kMDNSColliderProgCmd_Wait		"wait"

typedef uint32_t		MDNSColliderProbeAction;

#define kMDNSColliderProbeAction_None					0
#define kMDNSColliderProbeAction_Respond				1
#define kMDNSColliderProbeAction_RespondUnicast			2
#define kMDNSColliderProbeAction_RespondMulticast		3
#define kMDNSColliderProbeAction_Probe					4
#define kMDNSColliderProbeAction_MaxValue				kMDNSColliderProbeAction_Probe

#define kMDNSColliderProbeActionBits_Count			3
#define kMDNSColliderProbeActionBits_Mask			( ( 1U << kMDNSColliderProbeActionBits_Count ) - 1 )
#define kMDNSColliderProbeActionMaxProbeCount		( 32 / kMDNSColliderProbeActionBits_Count )

check_compile_time( kMDNSColliderProbeAction_MaxValue <= kMDNSColliderProbeActionBits_Mask );

static OSStatus	_MDNSColliderParseProbeActionString( const char *inString, size_t inLen, uint32_t *outBitmap );

static OSStatus	MDNSColliderSetProgram( MDNSColliderRef me, const char *inProgramStr )
{
	OSStatus				err;
	uint32_t				insCount;
	unsigned int			loopDepth;
	const char *			cmd;
	const char *			end;
	const char *			next;
	MDNSCInstruction *		program = NULL;
	uint32_t				loopStart[ kMaxLoopDepth ];
	
	insCount = 0;
	for( cmd = inProgramStr; *cmd; cmd = next )
	{
		for( end = cmd; *end && ( *end != ';' ); ++end ) {}
		require_action_quiet( end != cmd, exit, err = kMalformedErr );
		next = ( *end == ';' ) ? ( end + 1 ) : end;
		++insCount;
	}
	
	program = (MDNSCInstruction *) calloc( insCount + 1, sizeof( *program ) );
	require_action( program, exit, err = kNoMemoryErr );
	
	insCount	= 0;
	loopDepth	= 0;
	for( cmd = inProgramStr; *cmd; cmd = next )
	{
		size_t							cmdLen;
		const char *					ptr;
		const char *					arg;
		size_t							argLen;
		uint32_t						value;
		MDNSCInstruction * const		ins = &program[ insCount ];
		
		while( isspace_safe( *cmd ) ) ++cmd;
		for( end = cmd; *end && ( *end != ';' ); ++end ) {}
		next = ( *end == ';' ) ? ( end + 1 ) : end;
		
		for( ptr = cmd; ( ptr < end ) && !isspace_safe( *ptr ); ++ptr ) {}
		cmdLen = (size_t)( ptr - cmd );
		
		// Done statement
		
		if( strnicmpx( cmd, cmdLen, kMDNSColliderProgCmd_Done ) == 0 )
		{
			while( ( ptr < end ) && isspace_safe( *ptr ) ) ++ptr;
			require_action_quiet( ptr == end, exit, err = kMalformedErr );
			
			require_action_quiet( loopDepth > 0, exit, err = kMalformedErr );
			
			ins->opcode		= kMDNSColliderOpCode_LoopPop;
			ins->operand	= loopStart[ --loopDepth ];
		}
		
		// Loop command
		
		else if( strnicmpx( cmd, cmdLen, kMDNSColliderProgCmd_Loop ) == 0 )
		{
			for( arg = ptr; ( arg < end ) && isspace_safe( *arg ); ++arg ) {}
			err = DecimalTextToUInt32( arg, end, &value, &ptr );
			require_noerr_quiet( err, exit );
			require_action_quiet( value > 0, exit, err = kValueErr );
			
			while( ( ptr < end ) && isspace_safe( *ptr ) ) ++ptr;
			require_action_quiet( ptr == end, exit, err = kMalformedErr );
			
			ins->opcode 	= kMDNSColliderOpCode_LoopPush;
			ins->operand	= value;
			
			require_action_quiet( loopDepth < kMaxLoopDepth, exit, err = kNoSpaceErr );
			loopStart[ loopDepth++ ] = insCount + 1;
		}
		
		// Probes command
		
		else if( strnicmpx( cmd, cmdLen, kMDNSColliderProgCmd_Probes ) == 0 )
		{
			for( arg = ptr; ( arg < end ) &&  isspace_safe( *arg ); ++arg ) {}
			for( ptr = arg; ( ptr < end ) && !isspace_safe( *ptr ); ++ptr ) {}
			argLen = (size_t)( ptr - arg );
			if( argLen > 0 )
			{
				err = _MDNSColliderParseProbeActionString( arg, argLen, &value );
				require_noerr_quiet( err, exit );
			}
			else
			{
				value = 0;
			}
			
			while( ( ptr < end ) && isspace_safe( *ptr ) ) ++ptr;
			require_action_quiet( ptr == end, exit, err = kMalformedErr );
			
			ins->opcode 	= kMDNSColliderOpCode_SetProbeActions;
			ins->operand	= value;
		}
		
		// Send command
		
		else if( strnicmpx( cmd, cmdLen, kMDNSColliderProgCmd_Send ) == 0 )
		{
			while( ( ptr < end ) && isspace_safe( *ptr ) ) ++ptr;
			require_action_quiet( ptr == end, exit, err = kMalformedErr );
			
			ins->opcode = kMDNSColliderOpCode_Send;
		}
		
		// Wait command
		
		else if( strnicmpx( cmd, cmdLen, kMDNSColliderProgCmd_Wait ) == 0 )
		{
			for( arg = ptr; ( arg < end ) && isspace_safe( *arg ); ++arg ) {}
			err = DecimalTextToUInt32( arg, end, &value, &ptr );
			require_noerr_quiet( err, exit );
			
			while( ( ptr < end ) && isspace_safe( *ptr ) ) ++ptr;
			require_action_quiet( ptr == end, exit, err = kMalformedErr );
			
			ins->opcode		= kMDNSColliderOpCode_Wait;
			ins->operand	= value;
		}
		
		// Unrecognized command
		
		else
		{
			err = kCommandErr;
			goto exit;
		}
		++insCount;
	}
	require_action_quiet( loopDepth == 0, exit, err = kMalformedErr );
	
	program[ insCount ].opcode = kMDNSColliderOpCode_Exit;
	
	FreeNullSafe( me->program );
	me->program = program;
	program = NULL;
	err = kNoErr;
	
exit:
	FreeNullSafe( program );
	return( err );
}

static OSStatus	_MDNSColliderParseProbeActionString( const char *inString, size_t inLen, uint32_t *outBitmap )
{
	OSStatus				err;
	const char *			ptr;
	const char * const		end = &inString[ inLen ];
	uint32_t				bitmap;
	int						index;
	
	bitmap	= 0;
	index	= 0;
	ptr		= inString;
	while( ptr < end )
	{
		int							c, count;
		MDNSColliderProbeAction		action;
		
		c = *ptr++;
		if( isdigit_safe( c ) )
		{
			count = 0;
			do
			{
				count = ( count * 10 ) + ( c - '0' );
				require_action_quiet( count <= ( kMDNSColliderProbeActionMaxProbeCount - index ), exit, err = kCountErr );
				require_action_quiet( ptr < end, exit, err = kUnderrunErr );
				c = *ptr++;
				
			}	while( isdigit_safe( c ) );
			require_action_quiet( count > 0, exit, err = kCountErr );
		}
		else
		{
			require_action_quiet( index < kMDNSColliderProbeActionMaxProbeCount, exit, err = kMalformedErr );
			count = 1;
		}
		
		switch( c )
		{
			case 'n':	action = kMDNSColliderProbeAction_None;				break;
			case 'r':	action = kMDNSColliderProbeAction_Respond;			break;
			case 'u':	action = kMDNSColliderProbeAction_RespondUnicast;	break;
			case 'm':	action = kMDNSColliderProbeAction_RespondMulticast;	break;
			case 'p':	action = kMDNSColliderProbeAction_Probe;			break;
			default:	err = kMalformedErr;								goto exit;
		}
		if( ptr < end )
		{
			c = *ptr++;
			require_action_quiet( ( c == '-' ) && ( ptr < end ), exit, err = kMalformedErr );
		}
		while( count-- > 0 )
		{
			bitmap |= ( action << ( index * kMDNSColliderProbeActionBits_Count ) );
			++index;
		}
	}
	
	*outBitmap = bitmap;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	MDNSColliderSetStopHandler
//===========================================================================================================================

static void	MDNSColliderSetStopHandler( MDNSColliderRef me, MDNSColliderStopHandler_f inStopHandler, void *inStopContext )
{
	me->stopHandler = inStopHandler;
	me->stopContext = inStopContext;
}

//===========================================================================================================================
//	MDNSColliderSetRecord
//===========================================================================================================================

#define kMDNSColliderDummyStr			"\x16" "mdnscollider-sent-this" "\x05" "local"
#define kMDNSColliderDummyName			( (const uint8_t *) kMDNSColliderDummyStr )
#define kMDNSColliderDummyNameLen		sizeof( kMDNSColliderDummyStr )

static OSStatus
	MDNSColliderSetRecord(
		MDNSColliderRef	me,
		const uint8_t *	inName,
		uint16_t		inType,
		const void *	inRDataPtr,
		size_t			inRDataLen )
{
	OSStatus		err;
	DataBuffer		msgDB;
	DNSHeader		header;
	uint8_t *		targetPtr	= NULL;
	size_t			targetLen;
	uint8_t *		responsePtr	= NULL;
	size_t			responseLen;
	uint8_t *		probePtr	= NULL;
	size_t			probeLen;
	
	DataBuffer_Init( &msgDB, NULL, 0, kMDNSMessageSizeMax );
	
	err = DomainNameDup( inName, &targetPtr, &targetLen );
	require_noerr_quiet( err, exit );
	
	// Create response message.
	
	memset( &header, 0, sizeof( header ) );
	DNSHeaderSetFlags( &header, kDNSHeaderFlag_Response | kDNSHeaderFlag_AuthAnswer );
	DNSHeaderSetAnswerCount( &header, 1 );
	
	err = DataBuffer_Append( &msgDB, &header, sizeof( header ) );
	require_noerr( err, exit );
	
	err = _DataBuffer_AppendDNSRecord( &msgDB, targetPtr, targetLen, inType, kDNSServiceClass_IN | kMDNSClassCacheFlushBit,
		1976, inRDataPtr, inRDataLen );
	require_noerr( err, exit );
	
	err = DataBuffer_Detach( &msgDB, &responsePtr, &responseLen );
	require_noerr( err, exit );
	
	// Create probe message.
	
	memset( &header, 0, sizeof( header ) );
	DNSHeaderSetQuestionCount( &header, 2 );
	DNSHeaderSetAuthorityCount( &header, 1 );
	
	err = DataBuffer_Append( &msgDB, &header, sizeof( header ) );
	require_noerr( err, exit );
	
	err = _DataBuffer_AppendDNSQuestion( &msgDB, targetPtr, targetLen, kDNSServiceType_ANY, kDNSServiceClass_IN );
	require_noerr( err, exit );
	
	err = _DataBuffer_AppendDNSQuestion( &msgDB, kMDNSColliderDummyName, kMDNSColliderDummyNameLen,
		kDNSServiceType_NULL, kDNSServiceClass_IN );
	require_noerr( err, exit );
	
	err = _DataBuffer_AppendDNSRecord( &msgDB, targetPtr, targetLen, inType, kDNSServiceClass_IN,
		1976, inRDataPtr, inRDataLen );
	require_noerr( err, exit );
	
	err = DataBuffer_Detach( &msgDB, &probePtr, &probeLen );
	require_noerr( err, exit );
	
	FreeNullSafe( me->target );
	me->target = targetPtr;
	targetPtr = NULL;
	
	FreeNullSafe( me->responsePtr );
	me->responsePtr = responsePtr;
	me->responseLen = responseLen;
	responsePtr = NULL;
	
	FreeNullSafe( me->probePtr );
	me->probePtr = probePtr;
	me->probeLen = probeLen;
	probePtr = NULL;
	
exit:
	DataBuffer_Free( &msgDB );
	FreeNullSafe( targetPtr );
	FreeNullSafe( responsePtr );
	FreeNullSafe( probePtr );
	return( err );
}

//===========================================================================================================================
//	_MDNSColliderStop
//===========================================================================================================================

static void	_MDNSColliderStop( MDNSColliderRef me, OSStatus inError )
{
	dispatch_source_forget( &me->waitTimer );
	dispatch_source_forget( &me->readSourceV4 );
	dispatch_source_forget( &me->readSourceV6 );
	me->sockV4 = kInvalidSocketRef;
	me->sockV6 = kInvalidSocketRef;
	
	if( !me->stopped )
	{
		me->stopped = true;
		if( me->stopHandler ) me->stopHandler( me->stopContext, inError );
		CFRelease( me );
	}
}

//===========================================================================================================================
//	_MDNSColliderReadHandler
//===========================================================================================================================

static MDNSColliderProbeAction	_MDNSColliderGetProbeAction( uint32_t inBitmap, unsigned int inProbeNumber );
static const char *				_MDNSColliderProbeActionToString( MDNSColliderProbeAction inAction );

static void	_MDNSColliderReadHandler( void *inContext )
{
	OSStatus					err;
	struct timeval				now;
	SocketContext * const		sockCtx	= (SocketContext *) inContext;
	MDNSColliderRef const		me		= (MDNSColliderRef) sockCtx->userContext;
	size_t						msgLen;
	sockaddr_ip					sender;
	const DNSHeader *			hdr;
	const uint8_t *				ptr;
	const struct sockaddr *		dest;
	int							probeFound, probeIsQU;
	unsigned int				qCount, i;
	MDNSColliderProbeAction		action;
	
	gettimeofday( &now, NULL );
	
	err = SocketRecvFrom( sockCtx->sock, me->msgBuf, sizeof( me->msgBuf ), &msgLen, &sender, sizeof( sender ),
		NULL, NULL, NULL, NULL );
	require_noerr( err, exit );
	
	require_quiet( msgLen >= kDNSHeaderLength, exit );
	hdr = (const DNSHeader *) me->msgBuf;
	
	probeFound	= false;
	probeIsQU	= false;
	qCount = DNSHeaderGetQuestionCount( hdr );
	ptr = (const uint8_t *) &hdr[ 1 ];
	for( i = 0; i < qCount; ++i )
	{
		uint16_t		qtype, qclass;
		uint8_t			qname[ kDomainNameLengthMax ];
		
		err = DNSMessageExtractQuestion( me->msgBuf, msgLen, ptr, qname, &qtype, &qclass, &ptr );
		require_noerr_quiet( err, exit );
		
		if( ( qtype == kDNSServiceType_NULL ) && ( qclass == kDNSServiceClass_IN ) &&
			DomainNameEqual( qname, kMDNSColliderDummyName ) )
		{
			probeFound = false;
			break;
		}
		
		if( qtype != kDNSServiceType_ANY ) continue;
		if( ( qclass & ~kMDNSClassUnicastResponseBit ) != kDNSServiceClass_IN ) continue;
		if( !DomainNameEqual( qname, me->target ) ) continue;
		
		if( !probeFound )
		{
			probeFound	= true;
			probeIsQU	= ( qclass & kMDNSClassUnicastResponseBit ) ? true : false;
		}
	}
	require_quiet( probeFound, exit );
	
	++me->probeCount;
	action = _MDNSColliderGetProbeAction( me->probeActionMap, me->probeCount );
	
	mc_ulog( kLogLevelInfo, "Received probe from %##a at %{du:time} (action: %s) -- %#.1{du:dnsmsg}\n",
		&sender, &now, _MDNSColliderProbeActionToString( action ), me->msgBuf, msgLen );
	
	if( ( action == kMDNSColliderProbeAction_Respond )			||
		( action == kMDNSColliderProbeAction_RespondUnicast )	||
		( action == kMDNSColliderProbeAction_RespondMulticast ) )
	{
		if( ( ( action == kMDNSColliderProbeAction_Respond ) && probeIsQU ) ||
			(   action == kMDNSColliderProbeAction_RespondUnicast ) )
		{
			dest = &sender.sa;
		}
		else if( ( ( action == kMDNSColliderProbeAction_Respond ) && !probeIsQU ) ||
				 (   action == kMDNSColliderProbeAction_RespondMulticast ) )
		{
			dest = ( sender.sa.sa_family == AF_INET ) ? GetMDNSMulticastAddrV4() : GetMDNSMulticastAddrV6();
		}
		
		err = _MDNSColliderSendResponse( me, sockCtx->sock, dest );
		require_noerr( err, exit );
	}
	else if( action == kMDNSColliderProbeAction_Probe )
	{
		dest = ( sender.sa.sa_family == AF_INET ) ? GetMDNSMulticastAddrV4() : GetMDNSMulticastAddrV6();
		
		err = _MDNSColliderSendProbe( me, sockCtx->sock, dest );
		require_noerr( err, exit );
	}
	
exit:
	return;
}

static MDNSColliderProbeAction	_MDNSColliderGetProbeAction( uint32_t inBitmap, unsigned int inProbeNumber )
{
	MDNSColliderProbeAction		action;
	
	if( ( inProbeNumber >= 1 ) && ( inProbeNumber <= kMDNSColliderProbeActionMaxProbeCount ) )
	{
		action = ( inBitmap >> ( ( inProbeNumber - 1 ) * kMDNSColliderProbeActionBits_Count ) ) &
			kMDNSColliderProbeActionBits_Mask;
	}
	else
	{
		action = kMDNSColliderProbeAction_None;
	}
	return( action );
}

static const char *	_MDNSColliderProbeActionToString( MDNSColliderProbeAction inAction )
{
	switch( inAction )
	{
		case kMDNSColliderProbeAction_None:				return( "None" );
		case kMDNSColliderProbeAction_Respond:			return( "Respond" );
		case kMDNSColliderProbeAction_RespondUnicast:	return( "Respond (unicast)" );
		case kMDNSColliderProbeAction_RespondMulticast:	return( "Respond (multicast)" );
		case kMDNSColliderProbeAction_Probe:			return( "Probe" );
		default:										return( "???" );
	}
}

//===========================================================================================================================
//	_MDNSColliderExecuteProgram
//===========================================================================================================================

static void	_MDNSColliderExecuteProgram( void *inContext )
{
	OSStatus					err;
	MDNSColliderRef const		me = (MDNSColliderRef) inContext;
	int							stop;
	
	dispatch_forget( &me->waitTimer );
	
	stop = false;
	for( ;; )
	{
		const MDNSCInstruction * const		ins = &me->program[ me->pc++ ];
		uint32_t							waitMs;
		
		switch( ins->opcode )
		{
			case kMDNSColliderOpCode_Send:
				if( IsValidSocket( me->sockV4 ) )
				{
					err = _MDNSColliderSendResponse( me, me->sockV4, GetMDNSMulticastAddrV4() );
					require_noerr( err, exit );
				}
				if( IsValidSocket( me->sockV6 ) )
				{
					err = _MDNSColliderSendResponse( me, me->sockV6, GetMDNSMulticastAddrV6() );
					require_noerr( err, exit );
				}
				break;
			
			case kMDNSColliderOpCode_Wait:
				waitMs = ins->operand;
				if( waitMs > 0 )
				{
					err = DispatchTimerOneShotCreate( dispatch_time_milliseconds( waitMs ), 1, me->queue,
						_MDNSColliderExecuteProgram, me, &me->waitTimer );
					require_noerr( err, exit );
					dispatch_resume( me->waitTimer );
					goto exit;
				}
				break;
			
			case kMDNSColliderOpCode_SetProbeActions:
				me->probeCount		= 0;
				me->probeActionMap	= ins->operand;
				break;
			
			case kMDNSColliderOpCode_LoopPush:
				check( me->loopDepth < kMaxLoopDepth );
				me->loopCounts[ me->loopDepth++ ] = ins->operand;
				break;
			
			case kMDNSColliderOpCode_LoopPop:
				check( me->loopDepth > 0 );
				if( --me->loopCounts[ me->loopDepth - 1 ] > 0 )
				{
					me->pc = ins->operand;
				}
				else
				{
					--me->loopDepth;
				}
				break;
			
			case kMDNSColliderOpCode_Exit:
				stop = true;
				err	= kNoErr;
				goto exit;
			
			default:
				dlogassert( "Unhandled opcode %u\n", ins->opcode );
				err = kCommandErr;
				goto exit;
		}
	}
	
exit:
	if( err || stop ) _MDNSColliderStop( me, err );
}

//===========================================================================================================================
//	_MDNSColliderSendResponse
//===========================================================================================================================

static OSStatus	_MDNSColliderSendResponse( MDNSColliderRef me, SocketRef inSock, const struct sockaddr *inDest )
{
	OSStatus		err;
	ssize_t			n;
	
	n = sendto( inSock, (char *) me->responsePtr, me->responseLen, 0, inDest, SockAddrGetSize( inDest ) );
	err = map_socket_value_errno( inSock, n == (ssize_t) me->responseLen, n );
	return( err );
}

//===========================================================================================================================
//	_MDNSColliderSendProbe
//===========================================================================================================================

static OSStatus	_MDNSColliderSendProbe( MDNSColliderRef me, SocketRef inSock, const struct sockaddr *inDest )
{
	OSStatus		err;
	ssize_t			n;
	
	n = sendto( inSock, (char *) me->probePtr, me->probeLen, 0, inDest, SockAddrGetSize( inDest ) );
	err = map_socket_value_errno( inSock, n == (ssize_t) me->probeLen, n );
	return( err );
}

//===========================================================================================================================
//	ServiceBrowserCreate
//===========================================================================================================================

typedef struct SBDomain					SBDomain;
typedef struct SBServiceType			SBServiceType;
typedef struct SBServiceBrowse			SBServiceBrowse;
typedef struct SBServiceInstance		SBServiceInstance;
typedef struct SBIPAddress				SBIPAddress;

struct ServiceBrowserPrivate
{
	CFRuntimeBase					base;				// CF object base.
	dispatch_queue_t				queue;				// Queue for service browser's events.
	DNSServiceRef					connection;			// Shared connection for DNS-SD ops.
	DNSServiceRef					domainsQuery;		// Query for recommended browsing domains.
	char *							domain;				// If non-null, then browsing is limited to this domain.
	StringListItem *				serviceTypeList;	// If non-null, then browsing is limited to these service types.
	ServiceBrowserCallback_f		userCallback;		// User's callback. Called when browsing stops.
	void *							userContext;		// User's callback context.
	SBDomain *						domainList;			// List of domains and their browse results.
	dispatch_source_t				stopTimer;			// Timer to stop browsing after browseTimeSecs.
	uint32_t						ifIndex;			// If non-zero, then browsing is limited to this interface.
	unsigned int					browseTimeSecs;		// Amount of time to spend browsing in seconds.
#if( MDNSRESPONDER_PROJECT )
	Boolean							useNewGAI;			// Use dnssd_getaddrinfo_* instead of DNSServiceGetAddrInfo().
#endif
	Boolean							includeAWDL;		// True if the IncludeAWDL flag should be used for DNS-SD ops that
														// use the "any" interface.
	Boolean							validateResults;	// Validate results.
};

struct SBDomain
{
	SBDomain *				next;			// Next domain object in list.
	ServiceBrowserRef		browser;		// Pointer to parent service browser.
	char *					name;			// Name of the domain.
	DNSServiceRef			servicesQuery;	// Query for services (_services._dns-sd._udp.<domain> PTR record) in domain.
	SBServiceType *			typeList;		// List of service types to browse for in this domain.
};

struct SBServiceType
{
	SBServiceType *			next;		// Next service type object in list.
	char *					name;		// Name of the service type.
	SBServiceBrowse *		browseList;	// List of browses for this service type.
};

struct SBServiceBrowse
{
	SBServiceBrowse *		next;			// Next browse object in list.
	ServiceBrowserRef		browser;		// Pointer to parent service browser.
	DNSServiceRef			browse;			// Reference to DNSServiceBrowse op.
	SBServiceInstance *		instanceList;	// List of service instances that were discovered by this browse.
	uint64_t				startTicks;		// Value of UpTicks() when the browse op began.
	uint32_t				ifIndex;		// If non-zero, then the browse is limited to this interface.
};

struct SBServiceInstance
{
	SBServiceInstance *		next;				// Next service instance object in list.
	ServiceBrowserRef		browser;			// Pointer to parent service browser.
	char *					name;				// Name of the service instance.
	char *					fqdn;				// Fully qualified domain name of service instance (for logging/debugging).
	uint32_t				ifIndex;			// Index of interface over which this service instance was discovered.
	uint64_t				discoverTimeUs;		// Time it took to discover this service instance in microseconds.
	DNSServiceRef			resolve;			// Reference to DNSServiceResolve op for this service instance.
	uint64_t				resolveStartTicks;	// Value of UpTicks() when the DNSServiceResolve op began.
	uint64_t				resolveTimeUs;		// Time it took to resolve this service instance.
	char *					hostname;			// Service instance's hostname. Result of DNSServiceResolve.
	uint16_t				port;				// Service instance's port number. Result of DNSServiceResolve.
	uint8_t *				txtPtr;				// Service instance's TXT record data. Result of DNSServiceResolve.
	size_t					txtLen;				// Length of service instance's TXT record data.
	DNSServiceRef			gai;				// Reference to DNSServiceGetAddrInfo op for service instance's hostname.
#if( MDNSRESPONDER_PROJECT )
	dnssd_getaddrinfo_t		newGAI;				// Reference to dnssd_getaddrinfo object for service instance's hostname.
#endif
	uint64_t				gaiStartTicks;		// Value of UpTicks() when the DNSServiceGetAddrInfo op began.
	SBIPAddress *			ipaddrList;			// List of IP addresses that the hostname resolved to.
	int32_t					refCount;			// This object's reference count.
};

struct SBIPAddress
{
	SBIPAddress *		next;			// Next IP address object in list.
	sockaddr_ip			sip;			// IPv4 or IPv6 address.
	uint64_t			resolveTimeUs;	// Time it took to resolve this IP address in microseconds.
	Boolean				validated;		// True if IP address is validated.
};

typedef struct
{
	SBRDomain *		domainList;	// List of domains in which services were found.
	int32_t			refCount;	// This object's reference count.
	
}	ServiceBrowserResultsPrivate;

static void		_ServiceBrowserStop( ServiceBrowserRef me, OSStatus inError );
static OSStatus	_ServiceBrowserAddDomain( ServiceBrowserRef inBrowser, const char *inDomain );
static OSStatus	_ServiceBrowserRemoveDomain( ServiceBrowserRef inBrowser, const char *inName );
static void		_ServiceBrowserTimerHandler( void *inContext );
static void DNSSD_API
	_ServiceBrowserDomainsQueryCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext );
static void DNSSD_API
	_ServiceBrowserServicesQueryCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext );
static void DNSSD_API
	_ServiceBrowserBrowseCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		uint32_t			inInterfaceIndex,
		DNSServiceErrorType	inError,
		const char *		inName,
		const char *		inRegType,
		const char *		inDomain,
		void *				inContext );
static void DNSSD_API
	_ServiceBrowserResolveCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		const char *			inHostname,
		uint16_t				inPort,
		uint16_t				inTXTLen,
		const unsigned char *	inTXTPtr,
		void *					inContext );
#if( MDNSRESPONDER_PROJECT )
static void
	_ServiceBrowserGAIResultHandler(
		ServiceBrowserRef				inBrowser,
		SBServiceInstance *				inInstance,
		dnssd_getaddrinfo_result_t *	inResultArray,
		size_t							inResultCount );
#endif
static void DNSSD_API
	_ServiceBrowserGAICallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext );
static OSStatus
	_ServiceBrowserAddServiceType(
		ServiceBrowserRef	inBrowser,
		SBDomain *			inDomain,
		const char *		inName,
		uint32_t			inIfIndex );
static OSStatus
	_ServiceBrowserRemoveServiceType(
		ServiceBrowserRef	inBrowser,
		SBDomain *			inDomain,
		const char *		inName,
		uint32_t			inIfIndex );
static OSStatus
	_ServiceBrowserAddServiceInstance(
		ServiceBrowserRef			inBrowser,
		SBServiceBrowse *			inBrowse,
		uint32_t					inIfIndex,
		const char *				inName,
		const char *				inRegType,
		const char *				inDomain,
		uint64_t					inDiscoverTimeUs,
		const DNSServiceAttribute *	inAttr );
static OSStatus
	_ServiceBrowserRemoveServiceInstance(
		ServiceBrowserRef	inBrowser,
		SBServiceBrowse *	inBrowse,
		const char *		inName,
		uint32_t			inIfIndex );
static OSStatus
	_ServiceBrowserAddIPAddress(
		ServiceBrowserRef		inBrowser,
		SBServiceInstance *		inInstance,
		const struct sockaddr *	inSockAddr,
		uint64_t				inResolveTimeUs,
		Boolean					inValidated );
static OSStatus
	_ServiceBrowserRemoveIPAddress(
		ServiceBrowserRef		inBrowser,
		SBServiceInstance *		inInstance,
		const struct sockaddr *	inSockAddr );
static OSStatus	_ServiceBrowserCreateResults( ServiceBrowserRef me, ServiceBrowserResults **outResults );
static OSStatus	_SBDomainCreate( const char *inName, ServiceBrowserRef inBrowser, SBDomain **outDomain );
static void		_SBDomainFree( SBDomain *inDomain );
static OSStatus	_SBServiceTypeCreate( const char *inName, SBServiceType **outType );
static void		_SBServiceTypeFree( SBServiceType *inType );
static OSStatus	_SBServiceBrowseCreate( uint32_t inIfIndex, ServiceBrowserRef inBrowser, SBServiceBrowse **outBrowse );
static void		_SBServiceBrowseFree( SBServiceBrowse *inBrowse );
static OSStatus
	_SBServiceInstanceCreate(
		const char *			inName,
		const char *			inType,
		const char *			inDomain,
		uint32_t				inIfIndex,
		uint64_t				inDiscoverTimeUs,
		ServiceBrowserRef		inBrowser,
		SBServiceInstance **	outInstance );
#if( MDNSRESPONDER_PROJECT )
static void		_SBServiceInstanceRetain( SBServiceInstance *inInstance );
#endif
static void		_SBServiceInstanceStop( SBServiceInstance *inInstance );
static void		_SBServiceInstanceRelease( SBServiceInstance *inInstance );
#define _SBServiceInstanceForget( X )		ForgetCustomEx( X, _SBServiceInstanceStop, _SBServiceInstanceRelease )
static OSStatus
	_SBIPAddressCreate(
		const struct sockaddr *	inSockAddr,
		uint64_t				inResolveTimeUs,
		Boolean 				inValidated,
		SBIPAddress **			outIPAddress );
static void		_SBIPAddressFree( SBIPAddress *inIPAddress );
static void		_SBIPAddressFreeList( SBIPAddress *inList );
static OSStatus	_SBRDomainCreate( const char *inName, SBRDomain **outDomain );
static void		_SBRDomainFree( SBRDomain *inDomain );
static OSStatus	_SBRServiceTypeCreate( const char *inName, SBRServiceType **outType );
static void		_SBRServiceTypeFree( SBRServiceType *inType );
static OSStatus
	_SBRServiceInstanceCreate(
		const char *			inName,
		uint32_t				inInterfaceIndex,
		const char *			inHostname,
		uint16_t				inPort,
		const uint8_t *			inTXTPtr,
		size_t					inTXTLen,
		uint64_t				inDiscoverTimeUs,
		uint64_t				inResolveTimeUs,
		SBRServiceInstance **	outInstance );
static void		_SBRServiceInstanceFree( SBRServiceInstance *inInstance );
static OSStatus
	_SBRIPAddressCreate(
		const struct sockaddr *	inSockAddr,
		uint64_t				inResolveTimeUs,
		Boolean					inValidated,
		SBRIPAddress **			outIPAddress );
static void		_SBRIPAddressFree( SBRIPAddress *inIPAddress );

#define ForgetSBIPAddressList( X )		ForgetCustom( X, _SBIPAddressFreeList )

CF_CLASS_DEFINE( ServiceBrowser );

ulog_define_ex( kDNSSDUtilIdentifier, ServiceBrowser, kLogLevelTrace, kLogFlags_None, "ServiceBrowser", NULL );
#define sb_ulog( LEVEL, ... )		ulog( &log_category_from_name( ServiceBrowser ), (LEVEL), __VA_ARGS__ )

static OSStatus
	ServiceBrowserCreate(
		dispatch_queue_t	inQueue,
		uint32_t			inInterfaceIndex,
		const char *		inDomain,
		unsigned int		inBrowseTimeSecs,
		Boolean				inIncludeAWDL,
		ServiceBrowserRef *	outBrowser )
{
	OSStatus				err;
	ServiceBrowserRef		obj;
	
	CF_OBJECT_CREATE( ServiceBrowser, obj, err, exit );
	
	ReplaceDispatchQueue( &obj->queue, inQueue );
	obj->ifIndex		= inInterfaceIndex;
	if( inDomain )
	{
		obj->domain = strdup( inDomain );
		require_action( obj->domain, exit, err = kNoMemoryErr );
	}
	obj->browseTimeSecs	= inBrowseTimeSecs;
	obj->includeAWDL	= inIncludeAWDL;
	
	*outBrowser = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	CFReleaseNullSafe( obj );
	return( err );
}

//===========================================================================================================================
//	_ServiceBrowserFinalize
//===========================================================================================================================

static void	_ServiceBrowserFinalize( CFTypeRef inObj )
{
	ServiceBrowserRef const		me = (ServiceBrowserRef) inObj;
	StringListItem *			serviceType;
	
	dispatch_forget( &me->queue );
	check( !me->connection );
	check( !me->domainsQuery );
	ForgetMem( &me->domain );
	while( ( serviceType = me->serviceTypeList ) != NULL )
	{
		me->serviceTypeList = serviceType->next;
		ForgetMem( &serviceType->str );
		free( serviceType );
	}
	check( !me->domainList );
	check( !me->stopTimer );
}

//===========================================================================================================================
//	ServiceBrowserSetUseNewGAI
//===========================================================================================================================

static void	ServiceBrowserSetUseNewGAI( ServiceBrowserRef me, Boolean inUseNewGAI )
{
	me->useNewGAI = inUseNewGAI;
}

//===========================================================================================================================
//	ServiceBrowserSetValidateResults
//===========================================================================================================================

static void	ServiceBrowserSetValidateResults( const ServiceBrowserRef me, const Boolean inValidateResults )
{
	me->validateResults = inValidateResults;
}

//===========================================================================================================================
//	ServiceBrowserStart
//===========================================================================================================================

static void	_ServiceBrowserStart( void *inContext );

static void	ServiceBrowserStart( ServiceBrowserRef me )
{
	CFRetain( me );
	dispatch_async_f( me->queue, me, _ServiceBrowserStart );
}

static void	_ServiceBrowserStart( void *inContext )
{
	OSStatus					err;
	ServiceBrowserRef const		me = (ServiceBrowserRef) inContext;
	
	err = DNSServiceCreateConnection( &me->connection );
	require_noerr( err, exit );
	
	err = DNSServiceSetDispatchQueue( me->connection, me->queue );
	require_noerr( err, exit );
	
	if( me->domain )
	{
		err = _ServiceBrowserAddDomain( me, me->domain );
		require_noerr( err, exit );
	}
	else
	{
		DNSServiceRef			sdRef;
		const char * const		recordName	= "b._dns-sd._udp.local.";
		const uint32_t			ifIndex		= kDNSServiceInterfaceIndexLocalOnly;
		
		// Perform PTR meta-query for "b._dns-sd._udp.local." to enumerate recommended browsing domains.
		// See <https://tools.ietf.org/html/rfc6763#section-11>.
		
		sb_ulog( kLogLevelTrace, "Starting PTR QueryRecord on interface %d for %s", (int32_t) ifIndex, recordName );
		
		sdRef = me->connection;
		err = DNSServiceQueryRecord( &sdRef, kDNSServiceFlagsShareConnection, ifIndex, recordName,
			kDNSServiceType_PTR, kDNSServiceClass_IN, _ServiceBrowserDomainsQueryCallback, me );
		require_noerr( err, exit );
		
		me->domainsQuery = sdRef;
	}
	
	err = DispatchTimerCreate( dispatch_time_seconds( me->browseTimeSecs ), DISPATCH_TIME_FOREVER,
		100 * kNanosecondsPerMillisecond, me->queue, _ServiceBrowserTimerHandler, NULL, me, &me->stopTimer );
	require_noerr( err, exit );
	dispatch_resume( me->stopTimer );
	
exit:
	if( err ) _ServiceBrowserStop( me, err );
}

//===========================================================================================================================
//	ServiceBrowserAddServiceType
//===========================================================================================================================

static OSStatus	ServiceBrowserAddServiceType( ServiceBrowserRef me, const char *inServiceType )
{
	OSStatus				err;
	StringListItem *		item;
	StringListItem **		itemPtr;
	StringListItem *		newItem = NULL;
	
	for( itemPtr = &me->serviceTypeList; ( item = *itemPtr ) != NULL; itemPtr = &item->next )
	{
		if( strcmp( item->str, inServiceType ) == 0 ) break;
	}
	if( !item )
	{
		newItem = (StringListItem *) calloc( 1, sizeof( *newItem ) );
		require_action( newItem, exit, err = kNoMemoryErr );
		
		newItem->str = strdup( inServiceType );
		require_action( newItem->str, exit, err = kNoMemoryErr );
		
		*itemPtr = newItem;
		newItem = NULL;
	}
	err = kNoErr;
	
exit:
	FreeNullSafe( newItem );
	return( err );
}

//===========================================================================================================================
//	ServiceBrowserSetCallback
//===========================================================================================================================

static void	ServiceBrowserSetCallback( ServiceBrowserRef me, ServiceBrowserCallback_f inCallback, void *inContext )
{
	me->userCallback	= inCallback;
	me->userContext		= inContext;
}

//===========================================================================================================================
//	ServiceBrowserResultsRetain
//===========================================================================================================================

static void	ServiceBrowserResultsRetain( ServiceBrowserResults *inResults )
{
	ServiceBrowserResultsPrivate * const		results = (ServiceBrowserResultsPrivate *) inResults;
	
	atomic_add_32( &results->refCount, 1 );
}

//===========================================================================================================================
//	ServiceBrowserResultsRelease
//===========================================================================================================================

static void	ServiceBrowserResultsRelease( ServiceBrowserResults *inResults )
{
	ServiceBrowserResultsPrivate * const		results = (ServiceBrowserResultsPrivate *) inResults;
	SBRDomain *									domain;
	
	if( atomic_add_and_fetch_32( &results->refCount, -1 ) == 0 )
	{
		while( ( domain = inResults->domainList ) != NULL )
		{
			inResults->domainList = domain->next;
			_SBRDomainFree( domain );
		}
		free( inResults );
	}
}

//===========================================================================================================================
//	_ServiceBrowserStop
//===========================================================================================================================

static void	_ServiceBrowserStop( ServiceBrowserRef me, OSStatus inError )
{
	OSStatus		err;
	SBDomain *		d;
	
	if( me->userCallback )
	{
		ServiceBrowserResults *		results = NULL;
		
		err = _ServiceBrowserCreateResults( me, &results );
		if( !err ) err = inError;
		
		me->userCallback( results, err, me->userContext );
		me->userCallback	= NULL;
		me->userContext		= NULL;
		if( results ) ServiceBrowserResultsRelease( results );
	}
	dispatch_source_forget( &me->stopTimer );
	DNSServiceForget( &me->domainsQuery );
	for( d = me->domainList; d; d = d->next )
	{
		SBServiceType *		t;
		
		DNSServiceForget( &d->servicesQuery );
		for( t = d->typeList; t; t = t->next )
		{
			SBServiceBrowse *		b;
			
			for( b = t->browseList; b; b = b->next )
			{
				SBServiceInstance *		i;
				
				DNSServiceForget( &b->browse );
				for( i = b->instanceList; i; i = i->next )
				{
					_SBServiceInstanceStop( i );
				}
			}
		}
	}
	DNSServiceForget( &me->connection );
	while( ( d = me->domainList ) != NULL )
	{
		me->domainList = d->next;
		_SBDomainFree( d );
	}
	CFRelease( me );
}

//===========================================================================================================================
//	_ServiceBrowserAddDomain
//===========================================================================================================================

static OSStatus	_ServiceBrowserAddDomain( ServiceBrowserRef me, const char *inDomain )
{
	OSStatus		err;
	SBDomain *		domain;
	SBDomain **		domainPtr;
	SBDomain *		newDomain = NULL;
	
	for( domainPtr = &me->domainList; ( domain = *domainPtr ) != NULL; domainPtr = &domain->next )
	{
		if( strcasecmp( domain->name, inDomain ) == 0 ) break;
	}
	require_action_quiet( !domain, exit, err = kDuplicateErr );
	
	err = _SBDomainCreate( inDomain, me, &newDomain );
	require_noerr_quiet( err, exit );
	
	if( me->serviceTypeList )
	{
		const StringListItem *		item;
		
		for( item = me->serviceTypeList; item; item = item->next )
		{
			err = _ServiceBrowserAddServiceType( me, newDomain, item->str, me->ifIndex );
			if( err == kDuplicateErr ) err = kNoErr;
			require_noerr( err, exit );
		}
	}
	else
	{
		char *				recordName;
		DNSServiceRef		sdRef;
		DNSServiceFlags		flags;
		
		// Perform PTR meta-query for _services._dns-sd._udp.<domain> to enumerate service types in domain.
		// See <https://tools.ietf.org/html/rfc6763#section-9>.
		
		ASPrintF( &recordName, "_services._dns-sd._udp.%s", newDomain->name );
		require_action( recordName, exit, err = kNoMemoryErr );
		
		flags = kDNSServiceFlagsShareConnection;
		if( ( me->ifIndex == kDNSServiceInterfaceIndexAny ) && me->includeAWDL ) flags |= kDNSServiceFlagsIncludeAWDL;
		
		sb_ulog( kLogLevelTrace, "Starting PTR QueryRecord on interface %d for %s", (int32_t) me->ifIndex, recordName );
		
		sdRef = newDomain->browser->connection;
		err = DNSServiceQueryRecord( &sdRef, flags, me->ifIndex, recordName, kDNSServiceType_PTR, kDNSServiceClass_IN,
			_ServiceBrowserServicesQueryCallback, newDomain );
		free( recordName );
		require_noerr( err, exit );
		
		newDomain->servicesQuery = sdRef;
	}
	
	*domainPtr	= newDomain;
	newDomain	= NULL;
	err = kNoErr;
	
exit:
	if( newDomain ) _SBDomainFree( newDomain );
	return( err );
}

//===========================================================================================================================
//	_ServiceBrowserRemoveDomain
//===========================================================================================================================

static OSStatus	_ServiceBrowserRemoveDomain( ServiceBrowserRef me, const char *inName )
{
	OSStatus		err;
	SBDomain *		domain;
	SBDomain **		domainPtr;
	
	for( domainPtr = &me->domainList; ( domain = *domainPtr ) != NULL; domainPtr = &domain->next )
	{
		if( strcasecmp( domain->name, inName ) == 0 ) break;
	}
	
	if( domain )
	{
		*domainPtr = domain->next;
		_SBDomainFree( domain );
		err = kNoErr;
	}
	else
	{
		err = kNotFoundErr;
	}
	
	return( err );
}

//===========================================================================================================================
//	_ServiceBrowserTimerHandler
//===========================================================================================================================

static void	_ServiceBrowserTimerHandler( void *inContext )
{
	ServiceBrowserRef const		me = (ServiceBrowserRef) inContext;
	
	_ServiceBrowserStop( me, kNoErr );
}

//===========================================================================================================================
//	_ServiceBrowserDomainsQueryCallback
//===========================================================================================================================

static void DNSSD_API
	_ServiceBrowserDomainsQueryCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext )
{
	ServiceBrowserRef const		me = (ServiceBrowserRef) inContext;
	OSStatus					err;
	char						domainStr[ kDNSServiceMaxDomainName ];
	
	Unused( inSDRef );
	Unused( inClass );
	Unused( inTTL );
	
	sb_ulog( kLogLevelTrace, "QueryRecord result: %s on interface %d for %s -> %{du:rdata}%?{end} (error: %#m)",
		DNSServiceFlagsToAddRmvStr( inFlags ), (int32_t) inInterfaceIndex, inFullName, inType, inRDataPtr, inRDataLen,
		!inError, inError );
	
	require_noerr( inError, exit );
	
	err = DomainNameToString( inRDataPtr, ( (const uint8_t *) inRDataPtr ) + inRDataLen, domainStr, NULL );
	require_noerr( err, exit );
	
	if( inFlags & kDNSServiceFlagsAdd )
	{
		err = _ServiceBrowserAddDomain( me, domainStr );
		if( err == kDuplicateErr ) err = kNoErr;
		require_noerr( err, exit );
	}
	else
	{
		err = _ServiceBrowserRemoveDomain( me, domainStr );
		if( err == kNotFoundErr ) err = kNoErr;
		require_noerr( err, exit );
	}
	
exit:
	return;
}

//===========================================================================================================================
//	_ServiceBrowserServicesQueryCallback
//===========================================================================================================================

static void DNSSD_API
	_ServiceBrowserServicesQueryCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		uint16_t				inType,
		uint16_t				inClass,
		uint16_t				inRDataLen,
		const void *			inRDataPtr,
		uint32_t				inTTL,
		void *					inContext )
{
	OSStatus					err;
	SBDomain * const			domain	= (SBDomain *) inContext;
	ServiceBrowserRef const		me		= domain->browser;
	const uint8_t *				src;
	const uint8_t *				end;
	uint8_t *					dst;
	int							i;
	uint8_t						serviceType[ 2 * ( 1 + kDomainLabelLengthMax ) + 1 ];
	char						serviceTypeStr[ kDNSServiceMaxDomainName ];
	
	Unused( inSDRef );
	Unused( inTTL );
	Unused( inClass );
	
	sb_ulog( kLogLevelTrace, "QueryRecord result: %s on interface %d for %s -> %{du:rdata}%?{end} (error: %#m)",
		DNSServiceFlagsToAddRmvStr( inFlags ), (int32_t) inInterfaceIndex, inFullName, inType, inRDataPtr, inRDataLen,
		!inError, inError );
	
	require_noerr( inError, exit );
	
	check( inType  == kDNSServiceType_PTR );
	check( inClass == kDNSServiceClass_IN );
	
	// The first two labels of the domain name in the RDATA describe a service type.
	// See <https://tools.ietf.org/html/rfc6763#section-9>.
	
	src = (const uint8_t *) inRDataPtr;
	end = src + inRDataLen;
	dst = serviceType;
	for( i = 0; i < 2; ++i )
	{
		size_t		labelLen;
		
		require_action_quiet( ( end - src ) > 0, exit, err = kUnderrunErr );
		
		labelLen = *src;
		require_action_quiet( ( labelLen > 0 ) && ( labelLen <= kDomainLabelLengthMax ), exit, err = kMalformedErr );
		require_action_quiet( ( (size_t)( end - src ) ) >= ( 1 + labelLen ), exit, err = kUnderrunErr );
		
		memcpy( dst, src, 1 + labelLen );
		src += 1 + labelLen;
		dst += 1 + labelLen;
	}
	*dst = 0;
	
	err = DomainNameToString( serviceType, NULL, serviceTypeStr, NULL );
	require_noerr( err, exit );
	
	if( inFlags & kDNSServiceFlagsAdd )
	{
		err = _ServiceBrowserAddServiceType( me, domain, serviceTypeStr, inInterfaceIndex );
		if( err == kDuplicateErr ) err = kNoErr;
		require_noerr( err, exit );
	}
	else
	{
		err = _ServiceBrowserRemoveServiceType( me, domain, serviceTypeStr, inInterfaceIndex );
		if( err == kNotFoundErr ) err = kNoErr;
		require_noerr( err, exit );
	}
	
exit:
	return;
}

//===========================================================================================================================
//	_ServiceBrowserBrowseCallback
//===========================================================================================================================

static void DNSSD_API
	_ServiceBrowserBrowseCallback(
		DNSServiceRef		inSDRef,
		DNSServiceFlags		inFlags,
		uint32_t			inInterfaceIndex,
		DNSServiceErrorType	inError,
		const char *		inName,
		const char *		inRegType,
		const char *		inDomain,
		void *				inContext )
{
	OSStatus					err;
	const uint64_t				nowTicks	= UpTicks();
	SBServiceBrowse * const		browse		= (SBServiceBrowse *) inContext;
	ServiceBrowserRef const		me			= (ServiceBrowserRef) browse->browser;
	DNSServiceAttributeRef		attr		= NULL;
	
	Unused( inSDRef );
	
	sb_ulog( kLogLevelTrace, "Browse result: %s on interface %d for %s.%s%s%?{end} (error: %#m)",
		DNSServiceFlagsToAddRmvStr( inFlags ), (int32_t) inInterfaceIndex, inName, inRegType, inDomain, !inError, inError );
	
	require_noerr( inError, exit );
	
	if( inFlags & kDNSServiceFlagsAdd )
	{
		if( me->validateResults )
		{
			const uint8_t *		dataPtr;
			size_t				dataLen;
			
			dataPtr = DNSServiceGetValidationData( inSDRef, &dataLen );
			sb_ulog( kLogLevelTrace, "Got %zu bytes of validation data for browse result", dataLen );
			if( dataPtr )
			{
				attr = DNSServiceAttributeCreate();
				require( attr, exit );
				
				err = DNSServiceAttrSetValidationData( attr, dataPtr, dataLen );
				require_noerr( err, exit );
			}
		}
		err = _ServiceBrowserAddServiceInstance( me, browse, inInterfaceIndex, inName, inRegType, inDomain,
			UpTicksToMicroseconds( nowTicks - browse->startTicks ), attr );
		if( err == kDuplicateErr ) err = kNoErr;
		require_noerr( err, exit );
	}
	else
	{
		err = _ServiceBrowserRemoveServiceInstance( me, browse, inName, inInterfaceIndex );
		if( err == kNotFoundErr ) err = kNoErr;
		require_noerr( err, exit );
	}
	
exit:
	_DNSServiceAttrForget( &attr );
}

//===========================================================================================================================
//	_ServiceBrowserResolveCallback
//===========================================================================================================================

static void DNSSD_API
	_ServiceBrowserResolveCallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inFullName,
		const char *			inHostname,
		uint16_t				inPort,
		uint16_t				inTXTLen,
		const unsigned char *	inTXTPtr,
		void *					inContext )
{
	OSStatus						err;
	const uint64_t					nowTicks			= UpTicks();
	SBServiceInstance * const		instance			= (SBServiceInstance *) inContext;
	ServiceBrowserRef const			me					= (ServiceBrowserRef) instance->browser;
	DNSServiceAttributeRef			attr				= NULL;
	const uint8_t *					validationDataPtr	= NULL;
	size_t							validationDataLen	= 0;
	
	Unused( inSDRef );
	Unused( inFlags );
	
	sb_ulog( kLogLevelTrace, "Resolve result on interface %d for %s -> %s:%u %#{txt}%?{end} (error %#m)",
		(int32_t) inInterfaceIndex, inFullName, inHostname, inPort, inTXTPtr, (size_t) inTXTLen, !inError, inError );
	
	require_noerr( inError, exit );
	
	if( !MemEqual( instance->txtPtr, instance->txtLen, inTXTPtr, inTXTLen ) )
	{
		FreeNullSafe( instance->txtPtr );
		instance->txtPtr = _memdup( inTXTPtr, inTXTLen );
		require_action( instance->txtPtr, exit, err = kNoMemoryErr );
		
		instance->txtLen = inTXTLen;
	}
	if( me->validateResults )
	{
		const uint8_t *		dataPtr;
		size_t				dataLen;
		
		dataPtr = DNSServiceGetValidationData( inSDRef, &dataLen );
		sb_ulog( kLogLevelTrace, "Got %zu bytes of validation data for resolve result\n", dataLen );
		if( dataPtr )
		{
			mdns_signed_resolve_result_t		signedResult;
			
			signedResult = mdns_signed_resolve_result_create_from_data( dataPtr, dataLen, &err );
			bc_ulog( kLogLevelTrace, "Signed resolve result -- %@", signedResult );
			if( signedResult )
			{
				if( mdns_signed_resolve_result_covers_txt_rdata( signedResult, instance->txtPtr, instance->txtLen ) )
				{
					sb_ulog( kLogLevelTrace, "Signed resolve result covers TXT record data\n" );
					validationDataPtr = dataPtr;
					validationDataLen = dataLen;
				}
				else
				{
					sb_ulog( kLogLevelError, "Signed resolve result doesn't cover TXT record data\n" );
				}
				mdns_forget( &signedResult );
			}
			else
			{
				sb_ulog( kLogLevelError, "mdns_signed_resolve_result_create_from_data() failed: %#m\n", err );
			}
		}
	}
	instance->port = ntohs( inPort );
	
	if( !instance->hostname || ( strcasecmp( instance->hostname, inHostname ) != 0 ) )
	{
		DNSServiceRef		sdRef;
		
		if( !instance->hostname ) instance->resolveTimeUs = UpTicksToMicroseconds( nowTicks - instance->resolveStartTicks );
		
		err = ReplaceString( &instance->hostname, NULL, inHostname, kSizeCString );
		require_noerr( err, exit );
		
		sb_ulog( kLogLevelTrace,
			"Starting GetAddrInfo on interface %d for %s", (int32_t) instance->ifIndex, instance->hostname );
		
		ForgetSBIPAddressList( &instance->ipaddrList );
		
		if( me->useNewGAI )
		{
			dnssd_getaddrinfo_t		gai;
			
			dnssd_getaddrinfo_forget( &instance->newGAI );
			
			gai = dnssd_getaddrinfo_create();
			require_action( gai, exit, err = kNoResourcesErr );
			
			dnssd_getaddrinfo_set_hostname( gai, instance->hostname );
			dnssd_getaddrinfo_set_flags( gai, 0 );
			dnssd_getaddrinfo_set_interface_index( gai, instance->ifIndex );
			dnssd_getaddrinfo_set_protocols( gai, kDNSServiceProtocol_IPv4 | kDNSServiceProtocol_IPv6 );
			if( validationDataPtr )
			{
				dnssd_getaddrinfo_set_validation_data( gai, validationDataPtr, validationDataLen );
			}
			dnssd_getaddrinfo_set_queue( gai, me->queue );
			_SBServiceInstanceRetain( instance );
			CFRetain( me );
			dnssd_getaddrinfo_set_result_handler( gai,
			^( dnssd_getaddrinfo_result_t * const inResultArray, const size_t inResultCount )
			{
				if( instance->newGAI == gai )
				{
					_ServiceBrowserGAIResultHandler( me, instance, inResultArray, inResultCount );
				}
			} );
			dnssd_getaddrinfo_set_event_handler( gai,
			^( dnssd_event_t inEvent, DNSServiceErrorType inGAIError )
			{
				switch( inEvent )
				{
					case dnssd_event_invalidated:
						dnssd_release( gai );
						_SBServiceInstanceRelease( instance );
						CFRelease( me );
						break;
					
					case dnssd_event_error:
						if( instance->newGAI == gai )
						{
							sb_ulog( kLogLevelError, "dnssd_getaddrinfo error %#m\n", inGAIError );
							dnssd_getaddrinfo_forget( &instance->newGAI );
						}
						break;
					
					default:
						break;
				}
			} );
			instance->newGAI = gai;
			dnssd_retain( instance->newGAI );
			dnssd_getaddrinfo_activate( instance->newGAI );
		}
		else
		{
			DNSServiceForget( &instance->gai );
			
			sdRef = me->connection;
			instance->gaiStartTicks = UpTicks();
			if( validationDataPtr )
			{
				attr = DNSServiceAttributeCreate();
				require( attr, exit );
				
				err = DNSServiceAttrSetValidationData( attr, validationDataPtr, validationDataLen );
				require_noerr( err, exit );
				
				err = DNSServiceGetAddrInfoEx( &sdRef, kDNSServiceFlagsShareConnection, instance->ifIndex,
					kDNSServiceProtocol_IPv4 | kDNSServiceProtocol_IPv6, instance->hostname, attr,
					_ServiceBrowserGAICallback, instance );
				require_noerr( err, exit );
			}
			else
			{
				err = DNSServiceGetAddrInfo( &sdRef, kDNSServiceFlagsShareConnection, instance->ifIndex,
					kDNSServiceProtocol_IPv4 | kDNSServiceProtocol_IPv6, instance->hostname, _ServiceBrowserGAICallback,
					instance );
				require_noerr( err, exit );
			}
			instance->gai = sdRef;
		}
	}
	
exit:
	_DNSServiceAttrForget( &attr );
}

#if( MDNSRESPONDER_PROJECT )
//===========================================================================================================================
//	_ServiceBrowserGAIResultHandler
//===========================================================================================================================

static void
	_ServiceBrowserGAIResultHandler(
		ServiceBrowserRef				me,
		SBServiceInstance *				inInstance,
		dnssd_getaddrinfo_result_t *	inResultArray,
		size_t							inResultCount )
{
	OSStatus			err;
	size_t				i;
	const uint64_t		nowTicks = UpTicks();
	
	for( i = 0; i < inResultCount; ++i )
	{
		const dnssd_getaddrinfo_result_t			result	= inResultArray[ i ];
		const dnssd_getaddrinfo_result_type_t		type	= dnssd_getaddrinfo_result_get_type( result );
		
		sb_ulog( kLogLevelTrace, "dnssd_getaddrinfo result: %@\n", result );
		
		if( type == dnssd_getaddrinfo_result_type_add )
		{
			Boolean		validated = false;
			
			if( me->validateResults )
			{
				const uint8_t *		dataPtr;
				size_t				dataLen;
				
				dataPtr = dnssd_getaddrinfo_result_get_validation_data( result, &dataLen );
				sb_ulog( kLogLevelTrace, "Got %zu bytes of validation data for dnssd_getaddrinfo result", dataLen );
				if( dataPtr )
				{
					OSStatus							createErr;
					mdns_signed_hostname_result_t		signedResult;
					
					signedResult = mdns_signed_hostname_result_create_from_data( dataPtr, dataLen, &createErr );
					sb_ulog( kLogLevelTrace, "Signed hostname result -- %@", signedResult );
					if( signedResult )
					{
						validated = true;
						mdns_forget( &signedResult );
					}
				}
			}
			err = _ServiceBrowserAddIPAddress( me, inInstance, dnssd_getaddrinfo_result_get_address( result ),
				UpTicksToMicroseconds( nowTicks - inInstance->gaiStartTicks ), validated );
			if( err == kDuplicateErr ) err = kNoErr;
			require_noerr( err, exit );
		}
		else if( type == dnssd_getaddrinfo_result_type_remove )
		{
			err = _ServiceBrowserRemoveIPAddress( me, inInstance, dnssd_getaddrinfo_result_get_address( result ) );
			if( err == kNotFoundErr ) err = kNoErr;
			require_noerr( err, exit );
		}
	}
	
exit:
	return;
}
#endif

//===========================================================================================================================
//	_ServiceBrowserGAICallback
//===========================================================================================================================

static void DNSSD_API
	_ServiceBrowserGAICallback(
		DNSServiceRef			inSDRef,
		DNSServiceFlags			inFlags,
		uint32_t				inInterfaceIndex,
		DNSServiceErrorType		inError,
		const char *			inHostname,
		const struct sockaddr *	inSockAddr,
		uint32_t				inTTL,
		void *					inContext )
{
	OSStatus						err;
	const uint64_t					nowTicks	= UpTicks();
	SBServiceInstance * const		instance	= (SBServiceInstance *) inContext;
	ServiceBrowserRef const			me			= (ServiceBrowserRef) instance->browser;
	
	Unused( inSDRef );
	Unused( inTTL );
	
	sb_ulog( kLogLevelTrace, "GetAddrInfo result: %s on interface %d for (%s ->) %s -> %##a%?{end} (error: %#m)",
		DNSServiceFlagsToAddRmvStr( inFlags ), (int32_t) inInterfaceIndex, instance->fqdn, inHostname, inSockAddr,
		!inError, inError );
	
	require_noerr( inError, exit );
	
	if( ( inSockAddr->sa_family != AF_INET ) && ( inSockAddr->sa_family != AF_INET6 ) )
	{
		dlogassert( "Unexpected address family: %d", inSockAddr->sa_family );
		goto exit;
	}
	
	if( inFlags & kDNSServiceFlagsAdd )
	{
		Boolean		validated = false;
		
		if( me->validateResults )
		{
			const uint8_t *		dataPtr;
			size_t				dataLen;
			
			dataPtr = DNSServiceGetValidationData( inSDRef, &dataLen );
			sb_ulog( kLogLevelTrace, "Got %zu bytes of validation data for GetAddrInfo result", dataLen );
			if( dataPtr )
			{
				OSStatus							createErr;
				mdns_signed_hostname_result_t		signedResult;
				
				signedResult = mdns_signed_hostname_result_create_from_data( dataPtr, dataLen, &createErr );
				sb_ulog( kLogLevelTrace, "Signed hostname result -- %@", signedResult );
				if( signedResult )
				{
					validated = true;
					mdns_forget( &signedResult );
				}
			}
		}
		err = _ServiceBrowserAddIPAddress( me, instance, inSockAddr,
			UpTicksToMicroseconds( nowTicks - instance->gaiStartTicks ), validated );
		if( err == kDuplicateErr ) err = kNoErr;
		require_noerr( err, exit );
	}
	else
	{
		err = _ServiceBrowserRemoveIPAddress( me, instance, inSockAddr );
		if( err == kNotFoundErr ) err = kNoErr;
		require_noerr( err, exit );
	}
	
exit:
	return;
}

//===========================================================================================================================
//	_ServiceBrowserAddServiceType
//===========================================================================================================================

static OSStatus
	_ServiceBrowserAddServiceType(
		ServiceBrowserRef	me,
		SBDomain *			inDomain,
		const char *		inName,
		uint32_t			inIfIndex )
{
	OSStatus				err;
	SBServiceType *			type;
	SBServiceType **		typePtr;
	SBServiceType *			newType		= NULL;
	SBServiceBrowse *		browse;
	SBServiceBrowse **		browsePtr;
	SBServiceBrowse *		newBrowse	= NULL;
	DNSServiceRef			sdRef;
	DNSServiceFlags			flags;
	
	for( typePtr = &inDomain->typeList; ( type = *typePtr ) != NULL; typePtr = &type->next )
	{
		if( strcasecmp( type->name, inName ) == 0 ) break;
	}
	if( !type )
	{
		err = _SBServiceTypeCreate( inName, &newType );
		require_noerr_quiet( err, exit );
		
		type = newType;
	}
	
	for( browsePtr = &type->browseList; ( browse = *browsePtr ) != NULL; browsePtr = &browse->next )
	{
		if( browse->ifIndex == inIfIndex ) break;
	}
	require_action_quiet( !browse, exit, err = kDuplicateErr );
	
	err = _SBServiceBrowseCreate( inIfIndex, me, &newBrowse );
	require_noerr_quiet( err, exit );
	
	flags = kDNSServiceFlagsShareConnection;
	if( ( newBrowse->ifIndex == kDNSServiceInterfaceIndexAny ) && me->includeAWDL ) flags |= kDNSServiceFlagsIncludeAWDL;
	
	sb_ulog( kLogLevelTrace, "Starting Browse on interface %d for %s%s",
		(int32_t) newBrowse->ifIndex, type->name, inDomain->name );
	
	sdRef = me->connection;
	newBrowse->startTicks = UpTicks();
	if( me->validateResults )
	{
		err = DNSServiceBrowseEx( &sdRef, flags, newBrowse->ifIndex, type->name, inDomain->name,
			&kDNSServiceAttrValidationRequired, _ServiceBrowserBrowseCallback, newBrowse );
		require_noerr( err, exit );
	}
	else
	{
		err = DNSServiceBrowse( &sdRef, flags, newBrowse->ifIndex, type->name, inDomain->name, _ServiceBrowserBrowseCallback,
			newBrowse );
		require_noerr( err, exit );
	}
	newBrowse->browse = sdRef;
	*browsePtr	= newBrowse;
	newBrowse	= NULL;
	
	if( newType )
	{
		*typePtr	= newType;
		newType		= NULL;
	}
	
exit:
	if( newBrowse )	_SBServiceBrowseFree( newBrowse );
	if( newType )	_SBServiceTypeFree( newType );
	return( err );
}

//===========================================================================================================================
//	_ServiceBrowserRemoveServiceType
//===========================================================================================================================

static OSStatus
	_ServiceBrowserRemoveServiceType(
		ServiceBrowserRef	me,
		SBDomain *			inDomain,
		const char *		inName,
		uint32_t			inIfIndex )
{
	OSStatus				err;
	SBServiceType *			type;
	SBServiceType **		typePtr;
	SBServiceBrowse *		browse;
	SBServiceBrowse **		browsePtr;
	
	Unused( me );
	
	for( typePtr = &inDomain->typeList; ( type = *typePtr ) != NULL; typePtr = &type->next )
	{
		if( strcasecmp( type->name, inName ) == 0 ) break;
	}
	require_action_quiet( type, exit, err = kNotFoundErr );
	
	for( browsePtr = &type->browseList; ( browse = *browsePtr ) != NULL; browsePtr = &browse->next )
	{
		if( browse->ifIndex == inIfIndex ) break;
	}
	require_action_quiet( browse, exit, err = kNotFoundErr );
	
	*browsePtr = browse->next;
	_SBServiceBrowseFree( browse );
	if( !type->browseList )
	{
		*typePtr = type->next;
		_SBServiceTypeFree( type );
	}
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_ServiceBrowserAddServiceInstance
//===========================================================================================================================

static OSStatus
	_ServiceBrowserAddServiceInstance(
		ServiceBrowserRef			me,
		SBServiceBrowse *			inBrowse,
		uint32_t					inIfIndex,
		const char *				inName,
		const char *				inRegType,
		const char *				inDomain,
		uint64_t					inDiscoverTimeUs,
		const DNSServiceAttribute *	inAttr )
{
	OSStatus					err;
	DNSServiceRef				sdRef;
	SBServiceInstance *			instance;
	SBServiceInstance **		instancePtr;
	SBServiceInstance *			newInstance	= NULL;
	
	for( instancePtr = &inBrowse->instanceList; ( instance = *instancePtr ) != NULL; instancePtr = &instance->next )
	{
		if( ( instance->ifIndex == inIfIndex ) && ( strcasecmp( instance->name, inName ) == 0 ) ) break;
	}
	require_action_quiet( !instance, exit, err = kDuplicateErr );
	
	err = _SBServiceInstanceCreate( inName, inRegType, inDomain, inIfIndex, inDiscoverTimeUs, me, &newInstance );
	require_noerr_quiet( err, exit );
	
	sb_ulog( kLogLevelTrace, "Starting Resolve on interface %d for %s.%s%s",
		(int32_t) newInstance->ifIndex, inName, inRegType, inDomain );
	
	sdRef = me->connection;
	newInstance->resolveStartTicks = UpTicks();
	if( inAttr )
	{
		err = DNSServiceResolveEx( &sdRef, kDNSServiceFlagsShareConnection, newInstance->ifIndex, inName, inRegType,
			inDomain, inAttr, _ServiceBrowserResolveCallback, newInstance );
		require_noerr( err, exit );
	}
	else
	{
		err = DNSServiceResolve( &sdRef, kDNSServiceFlagsShareConnection, newInstance->ifIndex, inName, inRegType,
			inDomain, _ServiceBrowserResolveCallback, newInstance );
		require_noerr( err, exit );
	}
	newInstance->resolve = sdRef;
	*instancePtr	= newInstance;
	newInstance		= NULL;
	
exit:
	if( newInstance ) _SBServiceInstanceRelease( newInstance );
	return( err );
}

//===========================================================================================================================
//	_ServiceBrowserRemoveServiceInstance
//===========================================================================================================================

static OSStatus
	_ServiceBrowserRemoveServiceInstance(
		ServiceBrowserRef	me,
		SBServiceBrowse *	inBrowse,
		const char *		inName,
		uint32_t			inIfIndex )
{
	OSStatus					err;
	SBServiceInstance *			instance;
	SBServiceInstance **		ptr;
	
	Unused( me );
	
	for( ptr = &inBrowse->instanceList; ( instance = *ptr ) != NULL; ptr = &instance->next )
	{
		if( ( instance->ifIndex == inIfIndex ) && ( strcasecmp( instance->name, inName ) == 0 ) ) break;
	}
	require_action_quiet( instance, exit, err = kNotFoundErr );
	
	*ptr = instance->next;
	_SBServiceInstanceForget( &instance );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_ServiceBrowserAddIPAddress
//===========================================================================================================================

static OSStatus
	_ServiceBrowserAddIPAddress(
		ServiceBrowserRef		me,
		SBServiceInstance *		inInstance,
		const struct sockaddr *	inSockAddr,
		uint64_t				inResolveTimeUs,
		Boolean					inValidated )
{
	OSStatus			err;
	SBIPAddress *		ipaddr;
	SBIPAddress **		ipaddrPtr;
	SBIPAddress *		newIPAddr = NULL;
	
	Unused( me );
	
	if( ( inSockAddr->sa_family != AF_INET ) && ( inSockAddr->sa_family != AF_INET6 ) )
	{
		dlogassert( "Unexpected address family: %d", inSockAddr->sa_family );
		err = kTypeErr;
		goto exit;
	}
	
	for( ipaddrPtr = &inInstance->ipaddrList; ( ipaddr = *ipaddrPtr ) != NULL; ipaddrPtr = &ipaddr->next )
	{
		if( SockAddrCompareAddr( &ipaddr->sip, inSockAddr ) == 0 ) break;
	}
	require_action_quiet( !ipaddr, exit, err = kDuplicateErr );
	
	err = _SBIPAddressCreate( inSockAddr, inResolveTimeUs, inValidated, &newIPAddr );
	require_noerr_quiet( err, exit );
	
	*ipaddrPtr = newIPAddr;
	newIPAddr = NULL;
	err = kNoErr;
	
exit:
	if( newIPAddr ) _SBIPAddressFree( newIPAddr );
	return( err );
}

//===========================================================================================================================
//	_ServiceBrowserRemoveIPAddress
//===========================================================================================================================

static OSStatus
	_ServiceBrowserRemoveIPAddress(
		ServiceBrowserRef		me,
		SBServiceInstance *		inInstance,
		const struct sockaddr *	inSockAddr )
{
	OSStatus			err;
	SBIPAddress *		ipaddr;
	SBIPAddress **		ipaddrPtr;
	
	Unused( me );
	
	for( ipaddrPtr = &inInstance->ipaddrList; ( ipaddr = *ipaddrPtr ) != NULL; ipaddrPtr = &ipaddr->next )
	{
		if( SockAddrCompareAddr( &ipaddr->sip.sa, inSockAddr ) == 0 ) break;
	}
	require_action_quiet( ipaddr, exit, err = kNotFoundErr );
	
	*ipaddrPtr = ipaddr->next;
	_SBIPAddressFree( ipaddr );
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_ServiceBrowserCreateResults
//===========================================================================================================================

static OSStatus	_ServiceBrowserCreateResults( ServiceBrowserRef me, ServiceBrowserResults **outResults )
{
	OSStatus							err;
	SBDomain *							d;
	SBServiceType *						t;
	SBServiceBrowse *					b;
	SBServiceInstance *					i;
	SBIPAddress *						a;
	ServiceBrowserResultsPrivate *		results;
	SBRDomain **						domainPtr;
	
	results = (ServiceBrowserResultsPrivate *) calloc( 1, sizeof( *results ) );
	require_action( results, exit, err = kNoMemoryErr );
	
	results->refCount = 1;
	
	domainPtr = &results->domainList;
	for( d = me->domainList; d; d = d->next )
	{
		SBRDomain *				domain;
		SBRServiceType **		typePtr;
		
		err = _SBRDomainCreate( d->name, &domain );
		require_noerr_quiet( err, exit );
		*domainPtr = domain;
		 domainPtr = &domain->next;
		
		typePtr = &domain->typeList;
		for( t = d->typeList; t; t = t->next )
		{
			SBRServiceType *			type;
			SBRServiceInstance **		instancePtr;
			
			err = _SBRServiceTypeCreate( t->name, &type );
			require_noerr_quiet( err, exit );
			*typePtr = type;
			 typePtr = &type->next;
			
			instancePtr = &type->instanceList;
			for( b = t->browseList; b; b = b->next )
			{
				for( i = b->instanceList; i; i = i->next )
				{
					SBRServiceInstance *		instance;
					SBRIPAddress **				ipaddrPtr;
					
					err = _SBRServiceInstanceCreate( i->name, i->ifIndex, i->hostname, i->port, i->txtPtr, i->txtLen,
						i->discoverTimeUs, i->resolveTimeUs, &instance );
					require_noerr_quiet( err, exit );
					*instancePtr = instance;
					 instancePtr = &instance->next;
					
					ipaddrPtr = &instance->ipaddrList;
					for( a = i->ipaddrList; a; a = a->next )
					{
						SBRIPAddress *		ipaddr;
						
						err = _SBRIPAddressCreate( &a->sip.sa, a->resolveTimeUs, a->validated, &ipaddr );
						require_noerr_quiet( err, exit );
						
						*ipaddrPtr = ipaddr;
						 ipaddrPtr = &ipaddr->next;
					}
				}
			}
		}
	}
	
	*outResults = (ServiceBrowserResults *) results;
	results = NULL;
	err = kNoErr;
	
exit:
	if( results ) ServiceBrowserResultsRelease( (ServiceBrowserResults *) results );
	return( err );
}

//===========================================================================================================================
//	_SBDomainCreate
//===========================================================================================================================

static OSStatus	_SBDomainCreate( const char *inName, ServiceBrowserRef inBrowser, SBDomain **outDomain )
{
	OSStatus		err;
	SBDomain *		obj;
	
	obj = (SBDomain *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->name = strdup( inName );
	require_action( obj->name, exit, err = kNoMemoryErr );
	
	obj->browser = inBrowser;
	
	*outDomain = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) _SBDomainFree( obj );
	return( err );
}

//===========================================================================================================================
//	_SBDomainFree
//===========================================================================================================================

static void	_SBDomainFree( SBDomain *inDomain )
{
	SBServiceType *		type;
	
	ForgetMem( &inDomain->name );
	DNSServiceForget( &inDomain->servicesQuery );
	while( ( type = inDomain->typeList ) != NULL )
	{
		inDomain->typeList = type->next;
		_SBServiceTypeFree( type );
	}
	free( inDomain );
}

//===========================================================================================================================
//	_SBServiceTypeCreate
//===========================================================================================================================

static OSStatus	_SBServiceTypeCreate( const char *inName, SBServiceType **outType )
{
	OSStatus			err;
	SBServiceType *		obj;
	
	obj = (SBServiceType *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->name = strdup( inName );
	require_action( obj->name, exit, err = kNoMemoryErr );
	
	*outType = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) _SBServiceTypeFree( obj );
	return( err );
}

//===========================================================================================================================
//	_SBServiceTypeFree
//===========================================================================================================================

static void	_SBServiceTypeFree( SBServiceType *inType )
{
	SBServiceBrowse *		browse;
	
	ForgetMem( &inType->name );
	while( ( browse = inType->browseList ) != NULL )
	{
		inType->browseList = browse->next;
		_SBServiceBrowseFree( browse );
	}
	free( inType );
}

//===========================================================================================================================
//	_SBServiceBrowseCreate
//===========================================================================================================================

static OSStatus	_SBServiceBrowseCreate( uint32_t inIfIndex, ServiceBrowserRef inBrowser, SBServiceBrowse **outBrowse )
{
	OSStatus				err;
	SBServiceBrowse *		obj;
	
	obj = (SBServiceBrowse *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->ifIndex = inIfIndex;
	obj->browser = inBrowser;
	*outBrowse = obj;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_SBServiceBrowseFree
//===========================================================================================================================

static void	_SBServiceBrowseFree( SBServiceBrowse *inBrowse )
{
	SBServiceInstance *		instance;
	
	DNSServiceForget( &inBrowse->browse );
	while( ( instance = inBrowse->instanceList ) != NULL )
	{
		inBrowse->instanceList = instance->next;
		_SBServiceInstanceForget( &instance );
	}
	free( inBrowse );
}

//===========================================================================================================================
//	_SBServiceInstanceCreate
//===========================================================================================================================

static OSStatus
	_SBServiceInstanceCreate(
		const char *			inName,
		const char *			inType,
		const char *			inDomain,
		uint32_t				inIfIndex,
		uint64_t				inDiscoverTimeUs,
		ServiceBrowserRef		inBrowser,
		SBServiceInstance **	outInstance )
{
	OSStatus				err;
	SBServiceInstance *		obj;
	
	obj = (SBServiceInstance *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->refCount = 1;
	
	obj->name = strdup( inName );
	require_action( obj->name, exit, err = kNoMemoryErr );
	
	ASPrintF( &obj->fqdn, "%s.%s%s", obj->name, inType, inDomain );
	require_action( obj->fqdn, exit, err = kNoMemoryErr );
	
	obj->ifIndex		= inIfIndex;
	obj->discoverTimeUs	= inDiscoverTimeUs;
	obj->browser		= inBrowser;
	
	*outInstance = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) _SBServiceInstanceRelease( obj );
	return( err );
}

#if( MDNSRESPONDER_PROJECT )
//===========================================================================================================================
//	_SBServiceInstanceRetain
//===========================================================================================================================

static void	_SBServiceInstanceRetain( SBServiceInstance *inInstance )
{
	atomic_add_32( &inInstance->refCount, 1 );
}
#endif

//===========================================================================================================================
//	_SBServiceInstanceStop
//===========================================================================================================================

static void	_SBServiceInstanceStop( SBServiceInstance *inInstance )
{
	DNSServiceForget( &inInstance->resolve );
	DNSServiceForget( &inInstance->gai );
#if( MDNSRESPONDER_PROJECT )
	dnssd_getaddrinfo_forget( &inInstance->newGAI );
#endif
}

//===========================================================================================================================
//	_SBServiceInstanceRelease
//===========================================================================================================================

static void	_SBServiceInstanceRelease( SBServiceInstance *inInstance )
{
	if( atomic_add_and_fetch_32( &inInstance->refCount, -1 ) == 0 )
	{
		check( !inInstance->resolve );
		check( !inInstance->gai );
	#if( MDNSRESPONDER_PROJECT )
		check( !inInstance->newGAI );
	#endif
		ForgetMem( &inInstance->name );
		ForgetMem( &inInstance->fqdn );
		ForgetMem( &inInstance->hostname );
		ForgetMem( &inInstance->txtPtr );
		ForgetSBIPAddressList( &inInstance->ipaddrList );
		free( inInstance );
	}
}

//===========================================================================================================================
//	_SBIPAddressCreate
//===========================================================================================================================

static OSStatus	_SBIPAddressCreate( const struct sockaddr *inSockAddr, uint64_t inResolveTimeUs, Boolean inValidated,
	SBIPAddress **outIPAddress )
{
	OSStatus			err;
	SBIPAddress *		obj;
	
	obj = (SBIPAddress *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	SockAddrCopy( inSockAddr, &obj->sip );
	obj->resolveTimeUs	= inResolveTimeUs;
	obj->validated		= inValidated;
	
	*outIPAddress = obj;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_SBIPAddressFree
//===========================================================================================================================

static void _SBIPAddressFree( SBIPAddress *inIPAddress )
{
	free( inIPAddress );
}

//===========================================================================================================================
//	_SBIPAddressFreeList
//===========================================================================================================================

static void	_SBIPAddressFreeList( SBIPAddress *inList )
{
	SBIPAddress *		ipaddr;
	
	while( ( ipaddr = inList ) != NULL )
	{
		inList = ipaddr->next;
		_SBIPAddressFree( ipaddr );
	}
}

//===========================================================================================================================
//	_SBRDomainCreate
//===========================================================================================================================

static OSStatus	_SBRDomainCreate( const char *inName, SBRDomain **outDomain )
{
	OSStatus		err;
	SBRDomain *		obj;
	
	obj = (SBRDomain *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->name = strdup( inName );
	require_action( obj->name, exit, err = kNoMemoryErr );
	
	*outDomain = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) _SBRDomainFree( obj );
	return( err );
}

//===========================================================================================================================
//	_SBRDomainFree
//===========================================================================================================================

static void	_SBRDomainFree( SBRDomain *inDomain )
{
	SBRServiceType *		type;
	
	ForgetMem( &inDomain->name );
	while( ( type = inDomain->typeList ) != NULL )
	{
		inDomain->typeList = type->next;
		_SBRServiceTypeFree( type );
	}
	free( inDomain );
}

//===========================================================================================================================
//	_SBRServiceTypeCreate
//===========================================================================================================================

static OSStatus	_SBRServiceTypeCreate( const char *inName, SBRServiceType **outType )
{
	OSStatus				err;
	SBRServiceType *		obj;
	
	obj = (SBRServiceType *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->name = strdup( inName );
	require_action( obj->name, exit, err = kNoMemoryErr );
	
	*outType = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) _SBRServiceTypeFree( obj );
	return( err );
}

//===========================================================================================================================
//	_SBRServiceTypeFree
//===========================================================================================================================

static void	_SBRServiceTypeFree( SBRServiceType *inType )
{
	SBRServiceInstance *		instance;
	
	ForgetMem( &inType->name );
	while( ( instance = inType->instanceList ) != NULL )
	{
		inType->instanceList = instance->next;
		_SBRServiceInstanceFree( instance );
	}
	free( inType );
}

//===========================================================================================================================
//	_SBRServiceInstanceCreate
//===========================================================================================================================

static OSStatus
	_SBRServiceInstanceCreate(
		const char *			inName,
		uint32_t				inInterfaceIndex,
		const char *			inHostname,
		uint16_t				inPort,
		const uint8_t *			inTXTPtr,
		size_t					inTXTLen,
		uint64_t				inDiscoverTimeUs,
		uint64_t				inResolveTimeUs,
		SBRServiceInstance **	outInstance )
{
	OSStatus					err;
	SBRServiceInstance *		obj;
	
	obj = (SBRServiceInstance *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	obj->name = strdup( inName );
	require_action( obj->name, exit, err = kNoMemoryErr );
	
	if( inHostname )
	{
		obj->hostname = strdup( inHostname );
		require_action( obj->hostname, exit, err = kNoMemoryErr );
	}
	if( inTXTLen > 0 )
	{
		obj->txtPtr = (uint8_t *) _memdup( inTXTPtr, inTXTLen );
		require_action( obj->txtPtr, exit, err = kNoMemoryErr );
		obj->txtLen = inTXTLen;
	}
	obj->discoverTimeUs	= inDiscoverTimeUs;
	obj->resolveTimeUs	= inResolveTimeUs;
	obj->ifIndex		= inInterfaceIndex;
	obj->port			= inPort;
	
	*outInstance = obj;
	obj = NULL;
	err = kNoErr;
	
exit:
	if( obj ) _SBRServiceInstanceFree( obj );
	return( err );
}

//===========================================================================================================================
//	_SBRServiceInstanceFree
//===========================================================================================================================

static void	_SBRServiceInstanceFree( SBRServiceInstance *inInstance )
{
	SBRIPAddress *		ipaddr;
	
	ForgetMem( &inInstance->name );
	ForgetMem( &inInstance->hostname );
	ForgetMem( &inInstance->txtPtr );
	while( ( ipaddr = inInstance->ipaddrList ) != NULL )
	{
		inInstance->ipaddrList = ipaddr->next;
		_SBRIPAddressFree( ipaddr );
	}
	free( inInstance );
}

//===========================================================================================================================
//	_SBRIPAddressCreate
//===========================================================================================================================

static OSStatus
	_SBRIPAddressCreate(
		const struct sockaddr *	inSockAddr,
		uint64_t				inResolveTimeUs,
		Boolean 				inValidated,
		SBRIPAddress **			outIPAddress )
{
	OSStatus			err;
	SBRIPAddress *		obj;
	
	obj = (SBRIPAddress *) calloc( 1, sizeof( *obj ) );
	require_action( obj, exit, err = kNoMemoryErr );
	
	SockAddrCopy( inSockAddr, &obj->sip );
	obj->resolveTimeUs 	= inResolveTimeUs;
	obj->validated 		= inValidated;
	
	*outIPAddress = obj;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_SBRIPAddressFree
//===========================================================================================================================

static void	_SBRIPAddressFree( SBRIPAddress *inIPAddress )
{
	free( inIPAddress );
}

//===========================================================================================================================
//	_ParseIPv4Address
//
//	Warning: "inBuffer" may be modified even in error cases.
//
//	Note: This was copied from CoreUtils because the StringToIPv4Address function is currently not exported in the framework.
//===========================================================================================================================

static OSStatus	_ParseIPv4Address( const char *inStr, uint8_t inBuffer[ 4 ], const char **outStr )
{
	OSStatus		err;
	uint8_t *		dst;
	int				segments;
	int				sawDigit;
	int				c;
	int				v;
	
	check( inBuffer );
	check( outStr );
	
	dst		 = inBuffer;
	*dst	 = 0;
	sawDigit = 0;
	segments = 0;
	for( ; ( c = *inStr ) != '\0'; ++inStr )
	{
		if( isdigit_safe( c ) )
		{
			v = ( *dst * 10 ) + ( c - '0' );
			require_action_quiet( v <= 255, exit, err = kRangeErr );
			*dst = (uint8_t) v;
			if( !sawDigit )
			{
				++segments;
				require_action_quiet( segments <= 4, exit, err = kOverrunErr );
				sawDigit = 1;
			}
		}
		else if( ( c == '.' ) && sawDigit )
		{
			require_action_quiet( segments < 4, exit, err = kMalformedErr );
			*++dst = 0;
			sawDigit = 0;
		}
		else
		{
			break;
		}
	}
	require_action_quiet( segments == 4, exit, err = kUnderrunErr );
	
	*outStr = inStr;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_StringToIPv4Address
//
//	Note: This was copied from CoreUtils because the StringToIPv4Address function is currently not exported in the framework.
//===========================================================================================================================

static OSStatus
	_StringToIPv4Address( 
		const char *			inStr, 
		StringToIPAddressFlags	inFlags, 
		uint32_t *				outIP, 
		int *					outPort, 
		uint32_t *				outSubnet, 
		uint32_t *				outRouter, 
		const char **			outStr )
{
	OSStatus			err;
	uint8_t				buf[ 4 ];
	int					c;
	uint32_t			ip;
	int					hasPort;
	int					port;
	int					hasPrefix;
	int					prefix;
	uint32_t			subnetMask;
	uint32_t			router;
	
	require_action( inStr, exit, err = kParamErr );
	
	// Parse the address-only part of the address (e.g. "1.2.3.4").
	
	err = _ParseIPv4Address( inStr, buf, &inStr );
	require_noerr_quiet( err, exit );
	ip = (uint32_t)( ( buf[ 0 ] << 24 ) | ( buf[ 1 ] << 16 ) | ( buf[ 2 ] << 8 ) | buf[ 3 ] );
	c = *inStr;
	
	// Parse the port (if any).
	
	hasPort = 0;
	port    = 0;
	if( c == ':' )
	{
		require_action_quiet( !( inFlags & kStringToIPAddressFlagsNoPort ), exit, err = kUnexpectedErr );
		while( ( ( c = *( ++inStr ) ) != '\0' ) && ( ( c >= '0' ) && ( c <= '9' ) ) ) port = ( port * 10 ) + ( c - '0' );
		require_action_quiet( port <= 65535, exit, err = kRangeErr );
		hasPort = 1;
	}
	
	// Parse the prefix length (if any).
	
	hasPrefix  = 0;
	prefix     = 0;
	subnetMask = 0;
	router     = 0;
	if( c == '/' )
	{
		require_action_quiet( !( inFlags & kStringToIPAddressFlagsNoPrefix ), exit, err = kUnexpectedErr );
		while( ( ( c = *( ++inStr ) ) != '\0' ) && ( ( c >= '0' ) && ( c <= '9' ) ) ) prefix = ( prefix * 10 ) + ( c - '0' );
		require_action_quiet( ( prefix >= 0 ) && ( prefix <= 32 ), exit, err = kRangeErr );
		hasPrefix = 1;
		
		subnetMask = ( prefix > 0 ) ? ( UINT32_C( 0xFFFFFFFF ) << ( 32 - prefix ) ) : 0;
		router	   = ( ip & subnetMask ) | 1;
	}
	
	// Return the results. Only fill in port/prefix/router results if the info was found to allow for defaults.
	
	if( outIP )					 *outIP		= ip;
	if( outPort   && hasPort )	 *outPort	= port;
	if( outSubnet && hasPrefix ) *outSubnet	= subnetMask;
	if( outRouter && hasPrefix ) *outRouter	= router;
	if( outStr )				 *outStr	= inStr;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_ParseIPv6Address
//
//	Note: Parsed according to the rules specified in RFC 3513.
//	Warning: "inBuffer" may be modified even in error cases.
//
//	Note: This was copied from CoreUtils because the StringToIPv6Address function is currently not exported in the framework.
//===========================================================================================================================

static OSStatus	_ParseIPv6Address( const char *inStr, int inAllowV4Mapped, uint8_t inBuffer[ 16 ], const char **outStr )
{
													// Table to map uppercase hex characters - '0' to their numeric values.
													// 0  1  2  3  4  5  6  7  8  9  :  ;  <  =  >  ?  @  A   B   C   D   E   F
	static const uint8_t		kASCIItoHexTable[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13, 14, 15 };
	OSStatus					err;
	const char *				ptr;
	uint8_t *					dst;
	uint8_t *					lim;
	uint8_t *					colonPtr;
	int							c;
	int							sawDigit;
	unsigned int				v;
	int							i;
	int							n;
	
	// Pre-zero the address to simplify handling of compressed addresses (e.g. "::1").
	
	for( i = 0; i < 16; ++i ) inBuffer[ i ] = 0;
	
	// Special case leading :: (e.g. "::1") to simplify processing later.
	
	if( *inStr == ':' )
	{
		++inStr;
		require_action_quiet( *inStr == ':', exit, err = kMalformedErr );
	}
	
	// Parse the address.
	
	ptr		 = inStr;
	dst		 = inBuffer;
	lim		 = dst + 16;
	colonPtr = NULL;
	sawDigit = 0;
	v		 = 0;
	while( ( ( c = *inStr++ ) != '\0' ) && ( c != '%' ) && ( c != '/' ) && ( c != ']' ) )
	{
		if(   ( c >= 'a' ) && ( c <= 'f' ) ) c -= ( 'a' - 'A' );
		if( ( ( c >= '0' ) && ( c <= '9' ) ) || ( ( c >= 'A' ) && ( c <= 'F' ) ) )
		{
			c -= '0';
			check( c < (int) countof( kASCIItoHexTable ) );
			v = ( v << 4 ) | kASCIItoHexTable[ c ];
			require_action_quiet( v <= 0xFFFF, exit, err = kRangeErr );
			sawDigit = 1;
			continue;
		}
		if( c == ':' )
		{
			ptr = inStr;
			if( !sawDigit )
			{
				require_action_quiet( !colonPtr, exit, err = kMalformedErr );
				colonPtr = dst;
				continue;
			}
			require_action_quiet( *inStr != '\0', exit, err = kUnderrunErr );
			require_action_quiet( ( dst + 2 ) <= lim, exit, err = kOverrunErr );
			*dst++ = (uint8_t)( ( v >> 8 ) & 0xFF );
			*dst++ = (uint8_t)(   v        & 0xFF );
			sawDigit = 0;
			v = 0;
			continue;
		}
		
		// Handle IPv4-mapped/compatible addresses (e.g. ::FFFF:1.2.3.4).
		
		if( inAllowV4Mapped && ( c == '.' ) && ( ( dst + 4 ) <= lim ) )
		{
			err = _ParseIPv4Address( ptr, dst, &inStr );
			require_noerr_quiet( err, exit );
			dst += 4;
			sawDigit = 0;
			++inStr; // Increment because the code below expects the end to be at "inStr - 1".
		}
		break;
	}
	if( sawDigit )
	{
		require_action_quiet( ( dst + 2 ) <= lim, exit, err = kOverrunErr );
		*dst++ = (uint8_t)( ( v >> 8 ) & 0xFF );
		*dst++ = (uint8_t)(   v        & 0xFF );
	}
	check( dst <= lim );
	if( colonPtr )
	{
		require_action_quiet( dst < lim, exit, err = kOverrunErr );
		n = (int)( dst - colonPtr );
		for( i = 1; i <= n; ++i )
		{
			lim[ -i ] = colonPtr[ n - i ];
			colonPtr[ n - i ] = 0;
		}
		dst = lim;
	}
	require_action_quiet( dst == lim, exit, err = kUnderrunErr );
	
	*outStr = inStr - 1;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_ParseIPv6Scope
//
//	Note: This was copied from CoreUtils because the StringToIPv6Address function is currently not exported in the framework.
//===========================================================================================================================

static OSStatus	_ParseIPv6Scope( const char *inStr, uint32_t *outScope, const char **outStr )
{
#if( TARGET_OS_POSIX )
	OSStatus			err;
	char				scopeStr[ 64 ];
	char *				dst;
	char *				lim;
	int					c;
	uint32_t			scope;
	const char *		ptr;
	
	// Copy into a local NULL-terminated string since that is what if_nametoindex expects.
	
	dst = scopeStr;
	lim = dst + ( countof( scopeStr ) - 1 );
	while( ( ( c = *inStr ) != '\0' ) && ( c != ':' ) && ( c != '/' ) && ( c != ']' ) && ( dst < lim ) )
	{
		*dst++ = *inStr++;
	}
	*dst = '\0';
	check( dst <= lim );
	
	// First try to map as a name and if that fails, treat it as a numeric scope.
	
	scope = if_nametoindex( scopeStr );
	if( scope == 0 )
	{
		for( ptr = scopeStr; ( ( c = *ptr ) >= '0' ) && ( c <= '9' ); ++ptr )
		{
			scope = ( scope * 10 ) + ( ( (uint8_t) c ) - '0' );
		}
		require_action_quiet( c == '\0', exit, err = kMalformedErr );
		require_action_quiet( ( ptr != scopeStr ) && ( ( (int)( ptr - scopeStr ) ) <= 10 ), exit, err = kMalformedErr );
	}
	
	*outScope = scope;
	*outStr   = inStr;
	err = kNoErr;
	
exit:
	return( err );
#else
	OSStatus			err;
	uint32_t			scope;
	const char *		start;
	int					c;
	
	scope = 0;
	for( start = inStr; ( ( c = *inStr ) >= '0' ) && ( c <= '9' ); ++inStr )
	{
		scope = ( scope * 10 ) + ( c - '0' );
	}
	require_action_quiet( ( inStr != start ) && ( ( (int)( inStr - start ) ) <= 10 ), exit, err = kMalformedErr );
	
	*outScope = scope;
	*outStr   = inStr;
	err = kNoErr;
	
exit:
	return( err );
#endif
}

//===========================================================================================================================
//	_StringToIPv6Address
//
//	Note: This was copied from CoreUtils because the StringToIPv6Address function is currently not exported in the framework.
//===========================================================================================================================

static OSStatus
	_StringToIPv6Address( 
		const char *			inStr, 
		StringToIPAddressFlags	inFlags, 
		uint8_t					outIPv6[ 16 ], 
		uint32_t *				outScope, 
		int *					outPort, 
		int *					outPrefix, 
		const char **			outStr )
{
	OSStatus		err;
	uint8_t			ipv6[ 16 ];
	int				c;
	int				hasScope;
	uint32_t		scope;
	int				hasPort;
	int				port;
	int				hasPrefix;
	int				prefix;
	int				hasBracket;
	int				i;
	
	require_action( inStr, exit, err = kParamErr );
	
	if( *inStr == '[' ) ++inStr; // Skip a leading bracket for []-wrapped addresses (e.g. "[::1]:80").
	
	// Parse the address-only part of the address (e.g. "1::1").
	
	err = _ParseIPv6Address( inStr, !( inFlags & kStringToIPAddressFlagsNoIPv4Mapped ), ipv6, &inStr );
	require_noerr_quiet( err, exit );
	c = *inStr;
	
	// Parse the scope, port, or prefix length.
	
	hasScope	= 0;
	scope		= 0;
	hasPort		= 0;
	port		= 0;
	hasPrefix	= 0;
	prefix		= 0;
	hasBracket	= 0;
	for( ;; )
	{
		if( c == '%' )		// Scope (e.g. "%en0" or "%5")
		{
			require_action_quiet( !hasScope, exit, err = kMalformedErr );
			require_action_quiet( !( inFlags & kStringToIPAddressFlagsNoScope ), exit, err = kUnexpectedErr );
			++inStr;
			err = _ParseIPv6Scope( inStr, &scope, &inStr );
			require_noerr_quiet( err, exit );
			hasScope = 1;
			c = *inStr;
		}
		else if( c == ':' )	// Port (e.g. ":80")
		{
			require_action_quiet( !hasPort, exit, err = kMalformedErr );
			require_action_quiet( !( inFlags & kStringToIPAddressFlagsNoPort ), exit, err = kUnexpectedErr );
			while( ( ( c = *( ++inStr ) ) != '\0' ) && ( ( c >= '0' ) && ( c <= '9' ) ) ) port = ( port * 10 ) + ( c - '0' );
			require_action_quiet( port <= 65535, exit, err = kRangeErr );
			hasPort = 1;
		}
		else if( c == '/' )	// Prefix Length (e.g. "/64")
		{
			require_action_quiet( !hasPrefix, exit, err = kMalformedErr );
			require_action_quiet( !( inFlags & kStringToIPAddressFlagsNoPrefix ), exit, err = kUnexpectedErr );
			while( ( ( c = *( ++inStr ) ) != '\0' ) && ( ( c >= '0' ) && ( c <= '9' ) ) ) prefix = ( prefix * 10 ) + ( c - '0' );
			require_action_quiet( ( prefix >= 0 ) && ( prefix <= 128 ), exit, err = kRangeErr );
			hasPrefix = 1;
		}
		else if( c == ']' )
		{
			require_action_quiet( !hasBracket, exit, err = kMalformedErr );
			hasBracket = 1;
			c = *( ++inStr );
		}
		else
		{
			break;
		}
	}
	
	// Return the results. Only fill in scope/port/prefix results if the info was found to allow for defaults.
	
	if( outIPv6 )				 for( i = 0; i < 16; ++i ) outIPv6[ i ] = ipv6[ i ];
	if( outScope  && hasScope )  *outScope	= scope;
	if( outPort   && hasPort )   *outPort	= port;
	if( outPrefix && hasPrefix ) *outPrefix	= prefix;
	if( outStr )				 *outStr	= inStr;
	err = kNoErr;
	
exit:
	return( err );
}

//===========================================================================================================================
//	_ParseQuotedEscapedString
//
//	Note: This was copied from CoreUtils because it's currently not exported in the framework.
//===========================================================================================================================

static Boolean
	_ParseQuotedEscapedString( 
		const char *	inSrc, 
		const char *	inEnd, 
		const char *	inDelimiters, 
		char *			inBuf, 
		size_t			inMaxLen, 
		size_t *		outCopiedLen, 
		size_t *		outTotalLen, 
		const char **	outSrc )
{
	const unsigned char *		src;
	const unsigned char *		end;
	unsigned char *				dst;
	unsigned char *				lim;
	unsigned char				c;
	unsigned char				c2;
	size_t						totalLen;
	Boolean						singleQuote;
	Boolean						doubleQuote;
	
	if( inEnd == NULL ) inEnd = inSrc + strlen( inSrc );
	src = (const unsigned char *) inSrc;
	end = (const unsigned char *) inEnd;
	dst = (unsigned char *) inBuf;
	lim = dst + inMaxLen;
	while( ( src < end ) && isspace_safe( *src ) ) ++src; // Skip leading spaces.
	if( src >= end ) return( false );
	
	// Parse each argument from the string.
	//
	// See <http://resources.mpi-inf.mpg.de/departments/rg1/teaching/unixffb-ss98/quoting-guide.html> for details.
	
	totalLen = 0;
	singleQuote = false;
	doubleQuote = false;
	while( src < end )
	{
		c = *src++;
		if( singleQuote )
		{
			// Single quotes protect everything (even backslashes, newlines, etc.) except single quotes.
			
			if( c == '\'' )
			{
				singleQuote = false;
				continue;
			}
		}
		else if( doubleQuote )
		{
			// Double quotes protect everything except double quotes and backslashes. A backslash can be 
			// used to protect " or \ within double quotes. A backslash-newline pair disappears completely.
			// A backslash followed by x or X and 2 hex digits (e.g. "\x1f") is stored as that hex byte.
			// A backslash followed by 3 octal digits (e.g. "\377") is stored as that octal byte.
			// A backslash that does not precede ", \, x, X, or a newline is taken literally.
			
			if( c == '"' )
			{
				doubleQuote = false;
				continue;
			}
			else if( c == '\\' )
			{
				if( src < end )
				{
					c2 = *src;
					if( ( c2 == '"' ) || ( c2 == '\\' ) )
					{
						++src;
						c = c2;
					}
					else if( c2 == '\n' )
					{
						++src;
						continue;
					}
					else if( ( c2 == 'x' ) || ( c2 == 'X' ) )
					{
						++src;
						c = c2;
						if( ( ( end - src ) >= 2 ) && IsHexPair( src ) )
						{
							c = HexPairToByte( src );
							src += 2;
						}
					}
					else if( isoctal_safe( c2 ) )
					{
						if( ( ( end - src ) >= 3 ) && IsOctalTriple( src ) )
						{
							c = OctalTripleToByte( src );
							src += 3;
						}
					}
				}
			}
		}
		else if( strchr( inDelimiters, c ) )
		{
			break;
		}
		else if( c == '\\' )
		{
			// A backslash protects the next character, except a newline, x, X and 2 hex bytes or 3 octal bytes. 
			// A backslash followed by a newline disappears completely.
			// A backslash followed by x or X and 2 hex digits (e.g. "\x1f") is stored as that hex byte.
			// A backslash followed by 3 octal digits (e.g. "\377") is stored as that octal byte.
			
			if( src < end )
			{
				c = *src;
				if( c == '\n' )
				{
					++src;
					continue;
				}
				else if( ( c == 'x' ) || ( c == 'X' ) )
				{
					++src;
					if( ( ( end - src ) >= 2 ) && IsHexPair( src ) )
					{
						c = HexPairToByte( src );
						src += 2;
					}
				}
				else if( isoctal_safe( c ) )
				{
					if( ( ( end - src ) >= 3 ) && IsOctalTriple( src ) )
					{
						c = OctalTripleToByte( src );
						src += 3;
					}
					else
					{
						++src;
					}
				}
				else
				{
					++src;
				}
			}
		}
		else if( c == '\'' )
		{
			singleQuote = true;
			continue;
		}
		else if( c == '"' )
		{
			doubleQuote = true;
			continue;
		}
		
		if( dst < lim )
		{
			if( inBuf ) *dst = c;
			++dst;
		}
		++totalLen;
	}
	
	if( outCopiedLen )	*outCopiedLen	= (size_t)( dst - ( (unsigned char *) inBuf ) );
	if( outTotalLen )	*outTotalLen	= totalLen;
	if( outSrc )		*outSrc			= (const char *) src;
	return( true );
}

//===========================================================================================================================
//	_ServerSocketOpenEx2
//
//	Note: Based on ServerSocketOpenEx() from CoreUtils. Added parameter to not use SO_REUSEPORT.
//===========================================================================================================================

static OSStatus
	_ServerSocketOpenEx2( 
		int				inFamily, 
		int				inType, 
		int				inProtocol, 
		const void *	inAddr, 
		int				inPort, 
		int *			outPort, 
		int				inRcvBufSize, 
		Boolean			inNoPortReuse,
		SocketRef *		outSock )
{
	OSStatus		err;
	int				port;
	SocketRef		sock;
	int				name;
	int				option;
	sockaddr_ip		sip;
	socklen_t		len;
	
	port = ( inPort < 0 ) ? -inPort : inPort; // Negated port number means "try this port, but allow dynamic".
	
	sock = socket( inFamily, inType, inProtocol );
	err = map_socket_creation_errno( sock );
	require_noerr_quiet( err, exit );
	
#if( defined( SO_NOSIGPIPE ) )
	setsockopt( sock, SOL_SOCKET, SO_NOSIGPIPE, &(int){ 1 }, (socklen_t) sizeof( int ) );
#endif
	
	err = SocketMakeNonBlocking( sock );
	require_noerr( err, exit );
	
	// Set receive buffer size. This has to be done on the listening socket *before* listen is called because
	// accept does not return until after the window scale option is exchanged during the 3-way handshake. 
	// Since accept returns a new socket, the only way to use a larger window scale option is to set the buffer
	// size on the listening socket since SO_RCVBUF is inherited by the accepted socket. See UNPv1e3 Section 7.5.
	
	err = SocketSetBufferSize( sock, SO_RCVBUF, inRcvBufSize );
	check_noerr( err );
	
	// Allow port or address reuse because we may bind separate IPv4 and IPv6 sockets to the same port.
	
	if( ( inType != SOCK_DGRAM ) || !inNoPortReuse )
	{
		option = 1;
		name = ( inType == SOCK_DGRAM ) ? SO_REUSEPORT : SO_REUSEADDR;
		err = setsockopt( sock, SOL_SOCKET, name, (char *) &option, (socklen_t) sizeof( option ) );
		err = map_socket_noerr_errno( sock, err );
		require_noerr( err, exit );
	}
	
	if( inFamily == AF_INET )
	{
		// Bind to the port. If it fails, retry with a dynamic port.
		
		memset( &sip.v4, 0, sizeof( sip.v4 ) );
		SIN_LEN_SET( &sip.v4 );
		sip.v4.sin_family		= AF_INET;
		sip.v4.sin_port			= htons( (uint16_t) port );
		sip.v4.sin_addr.s_addr	= inAddr ? *( (const uint32_t *) inAddr ) : htonl( INADDR_ANY );
		err = bind( sock, &sip.sa, (socklen_t) sizeof( sip.v4 ) );
		err = map_socket_noerr_errno( sock, err );
		if( err && ( inPort < 0 ) )
		{
			sip.v4.sin_port = 0;
			err = bind( sock, &sip.sa, (socklen_t) sizeof( sip.v4 ) );
			err = map_socket_noerr_errno( sock, err );
		}
		require_noerr( err, exit );
	}
#if( defined( AF_INET6 ) )
	else if( inFamily == AF_INET6 )
	{
		// Restrict this socket to IPv6 only because we're going to use a separate socket for IPv4.
		
		option = 1;
		err = setsockopt( sock, IPPROTO_IPV6, IPV6_V6ONLY, (char *) &option, (socklen_t) sizeof( option ) );
		err = map_socket_noerr_errno( sock, err );
		require_noerr( err, exit );
		
		// Bind to the port. If it fails, retry with a dynamic port.
		
		memset( &sip.v6, 0, sizeof( sip.v6 ) );
		SIN6_LEN_SET( &sip.v6 );
		sip.v6.sin6_family	= AF_INET6;
		sip.v6.sin6_port	= htons( (uint16_t) port );
		sip.v6.sin6_addr	= inAddr ? *( (const struct in6_addr *) inAddr ) : in6addr_any;	
		err = bind( sock, &sip.sa, (socklen_t) sizeof( sip.v6 ) );
		err = map_socket_noerr_errno( sock, err );
		if( err && ( inPort < 0 ) )
		{
			sip.v6.sin6_port = 0;
			err = bind( sock, &sip.sa, (socklen_t) sizeof( sip.v6 ) );
			err = map_socket_noerr_errno( sock, err );
		}
		require_noerr( err, exit );
	}
#endif
	else
	{
		dlogassert( "Unsupported family: %d", inFamily );
		err = kUnsupportedErr;
		goto exit;
	}
	
	if( inType == SOCK_STREAM )
	{
		err = listen( sock, SOMAXCONN );
		err = map_socket_noerr_errno( sock, err );
		if( err )
		{
			err = listen( sock, 5 );
			err = map_socket_noerr_errno( sock, err );
			require_noerr( err, exit );
		}
	}
	
	if( outPort )
	{
		len = (socklen_t) sizeof( sip );
		err = getsockname( sock, &sip.sa, &len );
		err = map_socket_noerr_errno( sock, err );
		require_noerr( err, exit );
		
		*outPort = SockAddrGetPort( &sip );
	}
	*outSock = sock;
	sock = kInvalidSocketRef;
	
exit:
	ForgetSocket( &sock );
	return( err );
}

//===========================================================================================================================
//	_memdup
//
//	Note: This was copied from CoreUtils because it's currently not exported in the framework.
//===========================================================================================================================

static void *	_memdup( const void *inPtr, size_t inLen )
{
	void *		mem;
	
	mem = malloc( ( inLen > 0 ) ? inLen : 1 ); // If inLen is 0, use 1 since malloc( 0 ) is not well defined.
	require( mem, exit );
	if( inLen > 0 ) memcpy( mem, inPtr, inLen );
	
exit:
	return( mem );
}

//===========================================================================================================================
//	_memicmp
//
//	Note: This was copied from CoreUtils because it's currently not exported in the framework.
//===========================================================================================================================

static int	_memicmp( const void *inP1, const void *inP2, size_t inLen )
{
	const unsigned char *		p1;
	const unsigned char *		e1;
	const unsigned char *		p2;
	int							c1;
	int							c2;
	
	p1 = (const unsigned char *) inP1;
	e1 = p1 + inLen;
	p2 = (const unsigned char *) inP2;
	while( p1 < e1 )
	{
		c1 = *p1++;
		c2 = *p2++;
		c1 = tolower( c1 );
		c2 = tolower( c2 );
		if( c1 < c2 ) return( -1 );
		if( c1 > c2 ) return(  1 );
	}
	return( 0 );
}

//===========================================================================================================================
//	_FNV1
//
//	Note: This was copied from CoreUtils because it's currently not exported in the framework.
//===========================================================================================================================

static uint32_t	_FNV1( const void *inData, size_t inSize )
{
	const uint8_t *				src = (const uint8_t *) inData;
	const uint8_t * const		end = src + inSize;
	uint32_t					hash;
	
	hash = 0x811c9dc5U;
	while( src != end )
	{
		hash *= 0x01000193;
		hash ^= *src++;
	}
	return( hash );
}

//===========================================================================================================================
//	_UIint32FromArgString
//===========================================================================================================================

static OSStatus	_UInt32FromArgString( const char * const inArgStr, const char * const inArgName, uint32_t * const outValue )
{
	OSStatus		err;
	int64_t			i64;
	
	i64 = _StringToInt64( inArgStr, &err );
	require_noerr_quiet( err, exit );
	require_action_quiet( ( i64 >= 0 ) && ( i64 <= UINT32_MAX ), exit, err = kRangeErr );
	
	*outValue = (uint32_t) i64;
	
exit:
	if( err ) FPrintF( stderr, "error: Invalid %s '%s'. Valid range is [0, %u].\n", inArgName, inArgStr, UINT32_MAX );
	return( err );
}

//===========================================================================================================================
//	_UnixTimeToDateAndTimeString
//===========================================================================================================================

static char *	_UnixTimeToDateAndTimeString( const int64_t inTimeSecs, char * const inBufPtr, const size_t inBufLen )
{
	OSStatus		err;
	
	// If the number of seconds is representable as a time_t value, then use the standard library functions to create a
	// local date and time string. If not (e.g., time_t may be a 32-bit signed integer type), then try to create a UTC
	// date and time string with CoreUtils as a fallback.
	
	if( ( (time_t) inTimeSecs ) == inTimeSecs )
	{
		struct tm			tm;
		struct tm *			tmPtr;
		size_t				len;
		const time_t		timeSecs = (time_t) inTimeSecs;
		
		tmPtr = localtime_r( &timeSecs, &tm );
		require_action( tmPtr, exit, err = kUnknownErr );
		
		// Local date and time in ISO 8601 date, hours, minutes, seconds, and time zone format.
		
		len = strftime( inBufPtr, inBufLen, "%Y-%m-%dT%H:%M:%S%z", tmPtr );
		require_action_quiet( len > 0, exit, err = kUnknownErr );
	}
	else
	{
		const int64_t		epochSecs	= INT64_C_safe( kDaysToUnixEpoch ) * kSecondsPerDay;
		const int64_t		year10KSecs	= ( YearToDays( 10000 ) - INT64_C_safe( kDaysToUnixEpoch ) ) * kSecondsPerDay;
		int					year, month, day, hour, minute, second;
		
		// Ensure that the time's year is representable by four digits, i.e., before 10,000 A.D.
		
		require_action_quiet( inTimeSecs >= -epochSecs, exit, err = kNotHandledErr );
		require_action_quiet( inTimeSecs < year10KSecs, exit, err = kNotHandledErr );
		
		// UTC date and time in ISO 8601 date, hours, minutes, and seconds format.
		
		SecondsToYMD_HMS( epochSecs + inTimeSecs, &year, &month, &day, &hour, &minute, &second );
		SNPrintF( inBufPtr, inBufLen, "%04d-%02d-%02dT%02d:%02d:%02dZ", year, month, day, hour, minute, second );
	}
	err = kNoErr;
	
exit:
	if( err && ( inBufLen > 0 ) ) inBufPtr[ 0 ] = '\0';
	return( err ? NULL : inBufPtr );
}

//===========================================================================================================================
//	_DNSSDSourceVersionToCString
//===========================================================================================================================

static char *	_DNSSDSourceVersionToCString( const uint32_t inVersion, char * const inBufPtr, const size_t inBufLen )
{
	uint32_t		x, y, z;
	
	// Version strings are of the form x[.y[.z]].
	// Newer version strings are encoded as (x * 1000000) + (y * 1000) + z, where 0 ≤ y,z ≤ 999.
	// Older version strings were encoded as (x * 10000) + (y * 100) + z, where 0 ≤ y,z ≤ 99.
	
	if( inVersion > DNS_SD_ORIGINAL_ENCODING_VERSION_NUMBER_MAX )
	{
		x =   inVersion / 1000000;
		y = ( inVersion /    1000 ) % 1000;
		z =   inVersion             % 1000;
	}
	else
	{
		x =   inVersion / 10000;
		y = ( inVersion /   100 ) % 100;
		z =   inVersion           % 100;
	}
	if( z > 0 )			SNPrintF( inBufPtr, inBufLen, "%u.%u.%u", x, y, z );
	else if( y > 0 )	SNPrintF( inBufPtr, inBufLen, "%u.%u", x, y );
	else				SNPrintF( inBufPtr, inBufLen, "%u", x );
	return( inBufPtr );
}

//===========================================================================================================================
//	_StdOutIsTTY
//===========================================================================================================================

static Boolean	_StdOutIsTTY( void )
{
	static dispatch_once_t		sOnce	= 0;
	static Boolean				sIsTTY	= false;
	
	dispatch_once( &sOnce,
	^{
		sIsTTY = isatty( STDOUT_FILENO ) ? true : false;
	});
	return( sIsTTY );
}

#if( TARGET_OS_IOS )
//===========================================================================================================================
//	_StdErrIsTTY
//===========================================================================================================================

static Boolean	_StdErrIsTTY( void )
{
	static dispatch_once_t		sOnce	= 0;
	static Boolean				sIsTTY	= false;
	
	dispatch_once( &sOnce,
	^{
		sIsTTY = isatty( STDERR_FILENO ) ? true : false;
	});
	return( sIsTTY );
}
#endif

//===========================================================================================================================
//	_PrintValidatedToStdOut
//===========================================================================================================================

static void	_PrintValidatedToStdOut( const char * const inPrefix, const Boolean inValidated, const char * const inSuffix )
{
	FPrintF_safe( stdout, "%s%s%svalidated%s%s",
		inPrefix,
		_StdOutIsTTY() ? ( inValidated ? kANSIGreen : kANSIRed ) : "",
		inValidated ? "" : "not ",
		_StdOutIsTTY() ? kANSINormal : "",
		inSuffix );
}

//===========================================================================================================================
//	_DNSProtocolIsSecure
//===========================================================================================================================

static Boolean	_DNSProtocolIsSecure( const DNSProtocol inProtocol )
{
	switch( inProtocol )
	{
		case kDNSProtocol_Do53:	return( false );
		case kDNSProtocol_DoT:	return( true );
		case kDNSProtocol_DoH:	return( true );
	}
	return( false );
}

//===========================================================================================================================
//	_DNSProtocolToString
//===========================================================================================================================

static const char *	_DNSProtocolToString( const DNSProtocol inProtocol )
{
	switch( inProtocol )
	{
		case kDNSProtocol_Do53:	return( kDNSProtocolStr_Do53 );
		case kDNSProtocol_DoT:	return( kDNSProtocolStr_DoT );
		case kDNSProtocol_DoH:	return( kDNSProtocolStr_DoH );
	}
	return( NULL );
}
