+}
+
+#if( TARGET_OS_DARWIN )
+//===========================================================================================================================
+// _DNSServerCmdRegisterResolver
+//===========================================================================================================================
+
+static OSStatus _DNSServerCmdRegisterResolver( void )
+{
+ OSStatus err;
+ SCDynamicStoreRef store;
+ CFPropertyListRef plist = NULL;
+ CFStringRef key = NULL;
+ const uint32_t loopbackV4 = htonl( INADDR_LOOPBACK );
+ Boolean success;
+
+ store = SCDynamicStoreCreate( NULL, CFSTR( "com.apple.dnssdutil" ), NULL, NULL );
+ err = map_scerror( store );
+ require_noerr( err, exit );
+
+ err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &plist,
+ "{"
+ "%kO="
+ "["
+ "%s"
+ "]"
+ "%kO="
+ "["
+ "%.4a"
+ "%.16a"
+ "]"
+ "}",
+ kSCPropNetDNSSupplementalMatchDomains, "d.test.",
+ kSCPropNetDNSServerAddresses, &loopbackV4, in6addr_loopback.s6_addr );
+ require_noerr( err, exit );
+
+ key = SCDynamicStoreKeyCreateNetworkServiceEntity( NULL, kSCDynamicStoreDomainState,
+ CFSTR( "com.apple.dnssdutil.server" ), kSCEntNetDNS );
+ require_action( key, exit, err = kUnknownErr );
+
+ success = SCDynamicStoreSetValue( store, key, plist );
+ require_action( success, exit, err = kUnknownErr );
+
+exit:
+ CFReleaseNullSafe( store );
+ CFReleaseNullSafe( plist );
+ CFReleaseNullSafe( key );
+ return( err );
+}
+
+//===========================================================================================================================
+// _DNSServerCmdUnregisterResolver
+//===========================================================================================================================
+
+static OSStatus _DNSServerCmdUnregisterResolver( void )
+{
+ OSStatus err;
+ SCDynamicStoreRef store;
+ CFStringRef key = NULL;
+ Boolean success;
+
+ store = SCDynamicStoreCreate( NULL, CFSTR( "com.apple.dnssdutil" ), NULL, NULL );
+ err = map_scerror( store );
+ require_noerr( err, exit );
+
+ key = SCDynamicStoreKeyCreateNetworkServiceEntity( NULL, kSCDynamicStoreDomainState,
+ CFSTR( "com.apple.dnssdutil.server" ), kSCEntNetDNS );
+ require_action( key, exit, err = kUnknownErr );
+
+ success = SCDynamicStoreRemoveValue( store, key );
+ require_action( success, exit, err = kUnknownErr );
+
+exit:
+ CFReleaseNullSafe( store );
+ CFReleaseNullSafe( key );
+ return( err );
+}
+#endif
+
+//===========================================================================================================================
+// DNSServerCmdSigIntHandler
+//===========================================================================================================================
+
+static void _DNSServerCmdExternalExit( DNSServerCmdContext *inContext, int inSignal );
+
+static void DNSServerCmdSigIntHandler( void *inContext )
+{
+ _DNSServerCmdExternalExit( (DNSServerCmdContext *) inContext, SIGINT );
+}
+
+//===========================================================================================================================
+// DNSServerCmdSigTermHandler
+//===========================================================================================================================
+
+static void DNSServerCmdSigTermHandler( void *inContext )
+{
+ _DNSServerCmdExternalExit( (DNSServerCmdContext *) inContext, SIGTERM );
+}
+
+#if( TARGET_OS_DARWIN )
+//===========================================================================================================================
+// DNSServerCmdFollowedProcessHandler
+//===========================================================================================================================
+
+static void DNSServerCmdFollowedProcessHandler( void *inContext )
+{
+ DNSServerCmdContext * const context = (DNSServerCmdContext *) inContext;
+
+ if( dispatch_source_get_data( context->processMonitor ) & DISPATCH_PROC_EXIT )
+ {
+ _DNSServerCmdExternalExit( context, 0 );
+ }
+}
+#endif
+
+//===========================================================================================================================
+// _DNSServerCmdExternalExit
+//===========================================================================================================================
+
+#define SignalNumberToString( X ) ( \
+ ( (X) == SIGINT ) ? "SIGINT" : \
+ ( (X) == SIGTERM ) ? "SIGTERM" : \
+ "???" )
+
+static void _DNSServerCmdExternalExit( DNSServerCmdContext *inContext, int inSignal )
+{
+ OSStatus err;
+
+#if( TARGET_OS_DARWIN )
+ if( inSignal == 0 )
+ {
+ ds_ulog( kLogLevelNotice, "Exiting: followed process (%lld) exited\n", (int64_t) inContext->followPID );
+ }
+ else
+#endif
+ {
+ ds_ulog( kLogLevelNotice, "Exiting: received signal %d (%s)\n", inSignal, SignalNumberToString( inSignal ) );
+ }
+
+#if( TARGET_OS_DARWIN )
+ if( inContext->resolverRegistered )
+ {
+ err = _DNSServerCmdUnregisterResolver();
+ if( err )
+ {
+ ds_ulog( kLogLevelError, "Failed to remove resolver from DNS configuration: %#m\n", err );
+ goto exit;
+ }
+ inContext->resolverRegistered = false;
+ }
+#endif
+ if( inContext->serverStarted )
+ {
+ DNSServerStop( inContext->server );
+ inContext->calledStop = true;
+ }
+ err = kNoErr;
+
+exit:
+ exit( err ? 1 : 0 );
+}
+
+//===========================================================================================================================
+// DNSServerCreate
+//===========================================================================================================================
+
+typedef struct DNSDelayedResponse DNSDelayedResponse;
+struct DNSDelayedResponse
+{
+ DNSDelayedResponse * next;
+ sockaddr_ip clientAddr;
+ uint64_t targetTicks;
+ uint8_t * msgPtr;
+ size_t msgLen;
+};
+
+#define DNSScheduledResponseFree( X ) do { ForgetMem( &(X)->msgPtr ) ; free( X ); } while( 0 )
+
+struct DNSServerPrivate
+{
+ CFRuntimeBase base; // CF object base.
+ dispatch_queue_t queue; // Queue for DNS server's events.
+ dispatch_source_t readSourceUDPv4; // Read source for IPv4 UDP socket.
+ dispatch_source_t readSourceUDPv6; // Read source for IPv6 UDP socket.
+ dispatch_source_t readSourceTCPv4; // Read source for IPv4 TCP socket.
+ dispatch_source_t readSourceTCPv6; // Read source for IPv6 TCP socket.
+ DNSServerEventHandler_f eventHandler;
+ void * eventContext;
+ DNSDelayedResponse * responseList;
+ int responseDelayMs;
+ dispatch_source_t responseTimer;
+ Boolean loopbackOnly;
+ Boolean stopped;
+};
+
+CF_CLASS_DEFINE( DNSServer );
+
+static OSStatus
+ DNSServerCreate(
+ dispatch_queue_t inQueue,
+ DNSServerEventHandler_f inEventHandler,
+ void * inEventContext,
+ int inResponseDelayMs,
+ Boolean inLoopbackOnly,
+ DNSServerRef * outServer )
+{
+ OSStatus err;
+ DNSServerRef obj = NULL;
+
+ CF_OBJECT_CREATE( DNSServer, obj, err, exit );
+
+ ReplaceDispatchQueue( &obj->queue, inQueue );
+ obj->eventHandler = inEventHandler;
+ obj->eventContext = inEventContext;
+ obj->responseDelayMs = inResponseDelayMs;
+ if( inLoopbackOnly ) obj->loopbackOnly = true;
+
+ *outServer = obj;
+ obj = NULL;
+ err = kNoErr;
+
+exit:
+ CFReleaseNullSafe( obj );
+ return( err );
+}
+
+//===========================================================================================================================
+// _DNSServerFinalize
+//===========================================================================================================================
+
+static void _DNSServerFinalize( CFTypeRef inObj )
+{
+ DNSServerRef const me = (DNSServerRef) inObj;
+
+ check( !me->readSourceUDPv4 );
+ check( !me->readSourceUDPv6 );
+ check( !me->readSourceTCPv4 );
+ check( !me->readSourceTCPv6 );
+ check( !me->responseTimer );
+ dispatch_forget( &me->queue );
+}
+
+//===========================================================================================================================
+// DNSServerStart
+//===========================================================================================================================
+
+static void _DNSServerStart( void *inContext );
+static void _DNSServerUDPReadHandler( void *inContext );
+static void _DNSServerTCPReadHandler( void *inContext );
+
+static void DNSServerStart( DNSServerRef me )
+{
+ CFRetain( me );
+ dispatch_async_f( me->queue, me, _DNSServerStart );
+}
+
+static void _DNSServerStart( void *inContext )
+{
+ OSStatus err;
+ DNSServerRef const me = (DNSServerRef) inContext;
+ SocketRef sock = kInvalidSocketRef;
+ SocketContext * sockCtx = NULL;
+ const uint32_t loopbackV4 = htonl( INADDR_LOOPBACK );
+
+ // Create IPv4 UDP socket.
+
+ err = _ServerSocketOpenEx2( AF_INET, SOCK_DGRAM, IPPROTO_UDP, me->loopbackOnly ? &loopbackV4 : NULL,
+ kDNSPort, NULL, kSocketBufferSize_DontSet, me->loopbackOnly ? true : false, &sock );
+ require_noerr( err, exit );
+
+ // Create read source for IPv4 UDP socket.
+
+ err = SocketContextCreate( sock, me, &sockCtx );
+ require_noerr( err, exit );
+ sock = kInvalidSocketRef;
+
+ err = DispatchReadSourceCreate( sockCtx->sock, me->queue, _DNSServerUDPReadHandler, SocketContextCancelHandler, sockCtx,
+ &me->readSourceUDPv4 );
+ require_noerr( err, exit );
+ dispatch_resume( me->readSourceUDPv4 );
+ sockCtx = NULL;
+
+ // Create IPv6 UDP socket.
+
+ err = _ServerSocketOpenEx2( AF_INET6, SOCK_DGRAM, IPPROTO_UDP, me->loopbackOnly ? &in6addr_loopback : NULL,
+ kDNSPort, NULL, kSocketBufferSize_DontSet, me->loopbackOnly ? true : false, &sock );
+ require_noerr( err, exit );
+
+ // Create read source for IPv6 UDP socket.
+
+ err = SocketContextCreate( sock, me, &sockCtx );
+ require_noerr( err, exit );
+ sock = kInvalidSocketRef;
+
+ err = DispatchReadSourceCreate( sockCtx->sock, me->queue, _DNSServerUDPReadHandler, SocketContextCancelHandler, sockCtx,
+ &me->readSourceUDPv6 );
+ require_noerr( err, exit );
+ dispatch_resume( me->readSourceUDPv6 );
+ sockCtx = NULL;
+
+ // Create IPv4 TCP socket.
+
+ err = _ServerSocketOpenEx2( AF_INET, SOCK_STREAM, IPPROTO_TCP, me->loopbackOnly ? &loopbackV4 : NULL,
+ kDNSPort, NULL, kSocketBufferSize_DontSet, false, &sock );
+ require_noerr( err, exit );
+
+ // Create read source for IPv4 TCP socket.
+
+ err = SocketContextCreate( sock, me, &sockCtx );
+ require_noerr( err, exit );
+ sock = kInvalidSocketRef;
+
+ err = DispatchReadSourceCreate( sockCtx->sock, me->queue, _DNSServerTCPReadHandler, SocketContextCancelHandler, sockCtx,
+ &me->readSourceTCPv4 );
+ require_noerr( err, exit );
+ dispatch_resume( me->readSourceTCPv4 );
+ sockCtx = NULL;
+
+ // Create IPv6 TCP socket.
+
+ err = _ServerSocketOpenEx2( AF_INET6, SOCK_STREAM, IPPROTO_TCP, me->loopbackOnly ? &in6addr_loopback : NULL,
+ kDNSPort, NULL, kSocketBufferSize_DontSet, false, &sock );
+ require_noerr( err, exit );
+
+ // Create read source for IPv6 TCP socket.
+
+ err = SocketContextCreate( sock, me, &sockCtx );
+ require_noerr( err, exit );
+ sock = kInvalidSocketRef;
+
+ err = DispatchReadSourceCreate( sockCtx->sock, me->queue, _DNSServerTCPReadHandler, SocketContextCancelHandler, sockCtx,
+ &me->readSourceTCPv6 );
+ require_noerr( err, exit );
+ dispatch_resume( me->readSourceTCPv6 );
+ sockCtx = NULL;
+
+ CFRetain( me );
+ if( me->eventHandler ) me->eventHandler( kDNSServerEvent_Started, me->eventContext );
+
+exit:
+ ForgetSocket( &sock );
+ if( sockCtx ) SocketContextRelease( sockCtx );
+ if( err ) DNSServerStop( me );
+ CFRelease( me );
+}
+
+//===========================================================================================================================
+// DNSServerStop
+//===========================================================================================================================
+
+static void _DNSServerStop( void *inContext );
+static void _DNSServerStop2( void *inContext );
+
+static void DNSServerStop( DNSServerRef me )
+{
+ CFRetain( me );
+ dispatch_async_f( me->queue, me, _DNSServerStop );
+}
+
+static void _DNSServerStop( void *inContext )
+{
+ DNSServerRef const me = (DNSServerRef) inContext;
+ DNSDelayedResponse * resp;
+
+ dispatch_source_forget( &me->readSourceUDPv4 );
+ dispatch_source_forget( &me->readSourceUDPv6 );
+ dispatch_source_forget( &me->readSourceTCPv4 );
+ dispatch_source_forget( &me->readSourceTCPv6 );
+ dispatch_source_forget( &me->responseTimer );
+
+ while( ( resp = me->responseList ) != NULL )
+ {
+ me->responseList = resp->next;
+ DNSScheduledResponseFree( resp );
+ }
+
+ dispatch_async_f( me->queue, me, _DNSServerStop2 );
+}
+
+static void _DNSServerStop2( void *inContext )
+{
+ DNSServerRef const me = (DNSServerRef) inContext;
+
+ if( !me->stopped )
+ {
+ me->stopped = true;
+ if( me->eventHandler ) me->eventHandler( kDNSServerEvent_Stopped, me->eventContext );
+ CFRelease( me );
+ }
+ CFRelease( me );
+}
+
+//===========================================================================================================================
+// _DNSServerUDPReadHandler
+//===========================================================================================================================
+
+static OSStatus
+ _DNSServerAnswerQuery(
+ const uint8_t * inQueryPtr,
+ size_t inQueryLen,
+ Boolean inForTCP,
+ uint8_t ** outResponsePtr,
+ size_t * outResponseLen );
+
+#define _DNSServerAnswerQueryForUDP( IN_QUERY_PTR, IN_QUERY_LEN, IN_RESPONSE_PTR, IN_RESPONSE_LEN ) \
+ _DNSServerAnswerQuery( IN_QUERY_PTR, IN_QUERY_LEN, false, IN_RESPONSE_PTR, IN_RESPONSE_LEN )
+
+#define _DNSServerAnswerQueryForTCP( IN_QUERY_PTR, IN_QUERY_LEN, IN_RESPONSE_PTR, IN_RESPONSE_LEN ) \
+ _DNSServerAnswerQuery( IN_QUERY_PTR, IN_QUERY_LEN, true, IN_RESPONSE_PTR, IN_RESPONSE_LEN )
+
+static void _DNSServerUDPDelayedSend( void *inContext );
+
+static void _DNSServerUDPReadHandler( void *inContext )
+{
+ OSStatus err;
+ SocketContext * const sockCtx = (SocketContext *) inContext;
+ DNSServerRef const me = (DNSServerRef) sockCtx->userContext;
+ struct timeval now;
+ ssize_t n;
+ sockaddr_ip clientAddr;
+ socklen_t clientAddrLen;
+ uint8_t * responsePtr = NULL; // malloc'd
+ size_t responseLen;
+ uint8_t msg[ 512 ];
+
+ gettimeofday( &now, NULL );
+
+ // Receive message.
+
+ clientAddrLen = (socklen_t) sizeof( clientAddr );
+ n = recvfrom( sockCtx->sock, (char *) msg, sizeof( msg ), 0, &clientAddr.sa, &clientAddrLen );
+ err = map_socket_value_errno( sockCtx->sock, n >= 0, n );
+ require_noerr( err, exit );
+
+ ds_ulog( kLogLevelInfo, "UDP server received %zd bytes from %##a at %{du:time}.\n", n, &clientAddr, &now );
+
+ if( n < kDNSHeaderLength )
+ {
+ ds_ulog( kLogLevelInfo, "UDP DNS message is too small (%zd < %d).\n", n, kDNSHeaderLength );
+ goto exit;
+ }
+
+ ds_ulog( kLogLevelInfo, "UDP received message:\n\n%1{du:dnsmsg}", msg, (size_t) n );
+
+ // Create response.
+
+ err = _DNSServerAnswerQueryForUDP( msg, (size_t) n, &responsePtr, &responseLen );
+ require_noerr_quiet( err, exit );
+
+ // Schedule response.
+
+ if( me->responseDelayMs > 0 )
+ {
+ DNSDelayedResponse * resp;
+ DNSDelayedResponse ** ptr;
+ DNSDelayedResponse * newResp;
+
+ newResp = (DNSDelayedResponse *) calloc( 1, sizeof( *newResp ) );
+ require_action( newResp, exit, err = kNoMemoryErr );
+
+ SockAddrCopy( &clientAddr, &newResp->clientAddr );
+ newResp->targetTicks = UpTicks() + MillisecondsToUpTicks( (uint64_t) me->responseDelayMs );
+ newResp->msgLen = responseLen;
+ newResp->msgPtr = responsePtr;
+ responsePtr = NULL;
+
+ for( ptr = &me->responseList; ( resp = *ptr ) != NULL; ptr = &resp->next )
+ {
+ if( newResp->targetTicks < resp->targetTicks ) break;
+ }
+
+ newResp->next = resp;
+ *ptr = newResp;
+
+ if( me->responseList == newResp )
+ {
+ dispatch_source_forget( &me->responseTimer );
+
+ err = DispatchTimerCreate( dispatch_time_milliseconds( me->responseDelayMs ), DISPATCH_TIME_FOREVER,
+ ( (uint64_t) me->responseDelayMs ) * kNanosecondsPerMillisecond / 10, me->queue,
+ _DNSServerUDPDelayedSend, NULL, sockCtx, &me->responseTimer );
+ require_noerr( err, exit );
+ dispatch_resume( me->responseTimer );
+ }
+ }
+ else
+ {
+ ds_ulog( kLogLevelInfo, "UDP sending %zu byte response:\n\n%1{du:dnsmsg}", responseLen, responsePtr, responseLen );
+
+ n = sendto( sockCtx->sock, (char *) responsePtr, responseLen, 0, &clientAddr.sa, clientAddrLen );
+ err = map_socket_value_errno( sockCtx->sock, n == (ssize_t) responseLen, n );
+ require_noerr( err, exit );
+ }
+
+exit:
+ FreeNullSafe( responsePtr );
+ return;
+}
+
+static void _DNSServerUDPDelayedSend( void *inContext )
+{
+ OSStatus err;
+ SocketContext * const sockCtx = (SocketContext *) inContext;
+ DNSServerRef const me = (DNSServerRef) sockCtx->userContext;
+ DNSDelayedResponse * resp;
+ ssize_t n;
+ uint64_t nowTicks;
+ DNSDelayedResponse * freeList = NULL;
+
+ dispatch_source_forget( &me->responseTimer );
+
+ nowTicks = UpTicks();
+ while( ( resp = me->responseList ) != NULL )
+ {
+ if( resp->targetTicks > nowTicks ) break;
+ me->responseList = resp->next;
+
+ ds_ulog( kLogLevelInfo, "UDP sending %zu byte response (delayed):\n\n%1{du:dnsmsg}",
+ resp->msgLen, resp->msgPtr, resp->msgLen );
+
+ n = sendto( sockCtx->sock, (char *) resp->msgPtr, resp->msgLen, 0, &resp->clientAddr.sa,
+ SockAddrGetSize( &resp->clientAddr ) );
+ err = map_socket_value_errno( sockCtx->sock, n == (ssize_t) resp->msgLen, n );
+ check_noerr( err );
+
+ resp->next = freeList;
+ freeList = resp;
+ nowTicks = UpTicks();
+ }
+
+ if( ( resp = me->responseList ) != NULL )
+ {
+ uint64_t remainingNs;
+
+ remainingNs = UpTicksToNanoseconds( resp->targetTicks - nowTicks );
+ if( remainingNs > INT64_MAX ) remainingNs = INT64_MAX;
+
+ err = DispatchTimerCreate( dispatch_time( DISPATCH_TIME_NOW, (int64_t) remainingNs ), DISPATCH_TIME_FOREVER, 0,
+ me->queue, _DNSServerUDPDelayedSend, NULL, sockCtx, &me->responseTimer );
+ require_noerr( err, exit );
+ dispatch_resume( me->responseTimer );
+ }
+
+exit:
+ while( ( resp = freeList ) != NULL )
+ {
+ freeList = resp->next;
+ DNSScheduledResponseFree( resp );
+ }
+}
+
+//===========================================================================================================================
+// _DNSServerAnswerQuery
+//===========================================================================================================================
+
+#define kLabelPrefix_Alias "alias"
+#define kLabelPrefix_AliasTTL "alias-ttl"
+#define kLabelPrefix_Count "count"
+#define kLabelPrefix_TTL "ttl"
+#define kLabel_IPv4 "ipv4"
+#define kLabel_IPv6 "ipv6"
+
+#define kMaxAliasTTLCount ( ( kDomainLabelLengthMax - sizeof_string( kLabelPrefix_AliasTTL ) ) / 2 )
+
+static OSStatus
+ _DNSServerInitializeResponseMessage(
+ DataBuffer * inDB,
+ unsigned int inID,
+ unsigned int inFlags,
+ const uint8_t * inQName,
+ unsigned int inQType,
+ unsigned int inQClass );
+static OSStatus
+ _DNSServerAnswerQueryDynamically(
+ const uint8_t * inQName,
+ unsigned int inQType,
+ unsigned int inQClass,
+ Boolean inForTCP,
+ DataBuffer * inDB );
+
+static OSStatus
+ _DNSServerAnswerQuery(
+ const uint8_t * const inQueryPtr,
+ const size_t inQueryLen,
+ Boolean inForTCP,
+ uint8_t ** outResponsePtr,
+ size_t * outResponseLen )
+{
+ OSStatus err;
+ DataBuffer dataBuf;
+ const uint8_t * ptr;
+ const uint8_t * const queryEnd = &inQueryPtr[ inQueryLen ];
+ const DNSHeader * qhdr;
+ unsigned int msgID, qflags, qtype, qclass, rflags;
+ uint8_t qname[ kDomainNameLengthMax ];
+
+ DataBuffer_Init( &dataBuf, NULL, 0, kDNSMaxTCPMessageSize );
+
+ require_action_quiet( inQueryLen >= kDNSHeaderLength, exit, err = kUnderrunErr );
+
+ qhdr = (const DNSHeader *) inQueryPtr;
+ msgID = DNSHeaderGetID( qhdr );
+ qflags = DNSHeaderGetFlags( qhdr );
+
+ // Minimal checking of the query message's header.
+
+ if( ( qflags & kDNSHeaderFlag_Response ) || // The message must be a query, not a response.
+ ( DNSFlagsGetOpCode( qflags ) != kDNSOpCode_Query ) || // OPCODE must be QUERY (standard query).
+ ( DNSHeaderGetQuestionCount( qhdr ) != 1 ) ) // There should be a single question.
+ {
+ err = kRequestErr;
+ goto exit;
+ }
+
+ // Get QNAME.
+
+ ptr = (const uint8_t *) &qhdr[ 1 ];
+ err = DNSMessageExtractDomainName( inQueryPtr, inQueryLen, ptr, qname, &ptr );
+ require_noerr( err, exit );
+
+ // Get QTYPE and QCLASS.
+
+ require_action_quiet( ( queryEnd - ptr ) >= 4, exit, err = kUnderrunErr );
+ qtype = DNSQuestionFixedFieldsGetType( (const DNSQuestionFixedFields *) ptr );
+ qclass = DNSQuestionFixedFieldsGetClass( (const DNSQuestionFixedFields *) ptr );
+ ptr += 4;
+
+ // Create a tentative response message.
+
+ rflags = kDNSHeaderFlag_Response;
+ if( qflags & kDNSHeaderFlag_RecursionDesired ) rflags |= kDNSHeaderFlag_RecursionDesired;
+ DNSFlagsSetOpCode( rflags, kDNSOpCode_Query );
+
+ err = _DNSServerInitializeResponseMessage( &dataBuf, msgID, rflags, qname, qtype, qclass );
+ require_noerr( err, exit );
+
+ err = _DNSServerAnswerQueryDynamically( qname, qtype, qclass, inForTCP, &dataBuf );
+ if( err )
+ {
+ DNSFlagsSetRCode( rflags, kDNSRCode_ServerFailure );
+ err = _DNSServerInitializeResponseMessage( &dataBuf, msgID, rflags, qname, qtype, qclass );
+ require_noerr( err, exit );
+ }
+
+ err = DataBuffer_Detach( &dataBuf, outResponsePtr, outResponseLen );
+ require_noerr( err, exit );
+
+exit:
+ DataBuffer_Free( &dataBuf );
+ return( err );
+}
+
+static OSStatus
+ _DNSServerInitializeResponseMessage(
+ DataBuffer * inDB,
+ unsigned int inID,
+ unsigned int inFlags,
+ const uint8_t * inQName,
+ unsigned int inQType,
+ unsigned int inQClass )
+{
+ OSStatus err;
+ DNSHeader header;
+ DNSQuestionFixedFields fields;
+
+ 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_Append( inDB, inQName, DomainNameLength( inQName ) );
+ require_noerr( err, exit );
+
+ DNSQuestionFixedFieldsInit( &fields, inQType, inQClass );
+ err = DataBuffer_Append( inDB, &fields, sizeof( fields ) );
+ require_noerr( err, exit );
+
+exit:
+ return( err );
+}
+
+static OSStatus
+ _DNSServerAnswerQueryDynamically(
+ const uint8_t * const inQName,
+ const unsigned int inQType,
+ const unsigned int inQClass,
+ const Boolean inForTCP,
+ DataBuffer * const inDB )
+{
+ OSStatus err; // General-purpose error variable.
+ const uint8_t * labelPtr; // QNAME label pointer.
+ size_t labelLen; // QNAME label length.
+ DNSHeader * hdr; // Response header pointer.
+ unsigned int flags; // Response header flags.
+ unsigned int rcode; // Response header response code.
+ unsigned int answerCount = 0; // Number of answers contained in response.
+ int32_t aliasCount = -1; // Arg from "alias" label. Valid values are in [2 .. 2^31 - 1].
+ int count = -1; // First arg from "count" label. Valid values are in [1 .. 255].
+ int randCount = -1; // Second arg from "count" label. Valid values are in [1 .. 255].
+ int32_t ttl = -1; // Arg from "ttl" label. Valid values are in [0 .. 2^31 - 1].
+ uint32_t aliasTTLs[ kMaxAliasTTLCount ]; // Args from "alias-ttl" label. Valid values are in [0 .. 2^31 - 1].
+ int i; // General-purpose array index.
+ Boolean useAliasTTLs = false; // True if QNAME contained a valid "alias-ttl" label.
+ Boolean nameExists = false; // True if name specified by QNAME exists.
+ Boolean nameHasA = false; // True if name specified by QNAME has an A record.
+ Boolean nameHasAAAA = false; // True if name specified by QNAME has a AAAA record.
+ Boolean notImplemented = false; // True if the kind of the query is not supported.
+ Boolean truncated = false; // True if the response message is truncated.
+ uint8_t namePtr[ 2 ]; // Name compression pointer.
+
+ if( inQClass != kDNSServiceClass_IN )
+ {
+ notImplemented = true;
+ goto done;
+ }
+
+ for( labelPtr = inQName; ( labelLen = *labelPtr ) != 0; labelPtr += ( 1 + labelLen ) )
+ {
+ const char * const labelStr = (const char *) &labelPtr[ 1 ];
+ const char * next;
+ long long arg;
+ int n;
+
+ require_action( labelLen <= kDomainNameLengthMax, exit, err = kUnexpectedErr );
+
+ // Check if the first label is a valid alias TTL sequence label.
+
+ if( ( labelPtr == inQName ) && ( strnicmp_prefix( labelStr, labelLen, kLabelPrefix_AliasTTL ) == 0 ) )
+ {
+ const char * src = &labelStr[ sizeof_string( kLabelPrefix_AliasTTL ) ];
+ const char * const end = &labelStr[ labelLen ];
+ int argCount = 0;
+
+ while( src < end )
+ {
+ n = SNScanF( src, (size_t)( end - src ), "-%10lld%#n", &arg, &next );
+ if( n != 1 ) break;
+ if( ( arg < 0 ) || ( arg > INT32_MAX ) ) break; // TTL must be >= 0 and <= (2^31 - 1).
+ aliasTTLs[ argCount++ ] = (uint32_t) arg;
+ src = next;
+ }
+ if( ( argCount > 0 ) && ( src == end ) )
+ {
+ aliasCount = argCount;
+ useAliasTTLs = true;
+ continue;
+ }
+ }
+
+ // Check if the first label is a valid alias label.
+
+ if( ( labelPtr == inQName ) && ( strnicmp_prefix( labelStr, labelLen, kLabelPrefix_Alias ) == 0 ) )
+ {
+ const char * src = &labelStr[ sizeof_string( kLabelPrefix_Alias ) ];
+ const char * const end = &labelStr[ labelLen ];
+
+ if( src == end )
+ {
+ aliasCount = 1;
+ continue;
+ }
+
+ n = SNScanF( src, (size_t)( end - src ), "-%10lld%#n", &arg, &next );
+ if( ( n == 1 ) && ( next == end ) )
+ {
+ if( ( arg < 2 ) || ( arg > INT32_MAX ) ) break; // Alias count must be >= 2 and <= (2^31 - 1).
+ aliasCount = (int32_t) arg;
+ continue;
+ }
+ }
+
+ // Check if the label is a valid count label.
+
+ if( strnicmp_prefix( labelStr, labelLen, kLabelPrefix_Count ) == 0 )
+ {
+ const char * src = &labelStr[ sizeof_string( kLabelPrefix_Count ) ];
+ const char * const end = &labelStr[ labelLen ];
+
+ n = SNScanF( src, (size_t)( end - src ), "-%3lld%#n", &arg, &next );
+ if( n == 1 )
+ {
+ if( count > 0 ) break; // Count cannot be specified more than once.
+ if( ( arg < 1 ) || ( arg > 255 ) ) break; // Count must be >= 1 and <= 255.
+ count = (int) arg;
+
+ src = next;
+ if( src < end )
+ {
+ n = SNScanF( src, (size_t)( end - src ), "-%3lld%#n", &arg, &next );
+ if( ( n != 1 ) || ( next != end ) ) break;
+ if( ( arg < count ) || ( arg > 255 ) ) break; // Rand count must be >= count and <= 255.
+ randCount = (int) arg;
+ }
+ continue;
+ }
+ }
+
+ // Check if the label is a valid tag label.
+
+ if( strnicmp_prefix( labelStr, labelLen, "tag-" ) == 0 ) continue;
+
+ // Check if the label is a valid TTL label.
+
+ if( strnicmp_prefix( labelStr, labelLen, kLabelPrefix_TTL ) == 0 )
+ {
+ const char * src = &labelStr[ sizeof_string( kLabelPrefix_TTL ) ];
+ const char * const end = &labelStr[ labelLen ];
+
+ n = SNScanF( src, (size_t)( end - src ), "-%10lld%#n", &arg, &next );
+ if( ( n == 1 ) && ( next == end ) )
+ {
+ if( ttl >= 0 ) break; // TTL cannot be specified more than once.
+ if( ( arg < 0 ) || ( arg > INT32_MAX ) ) break; // TTL must be >= 0 and <= (2^31 - 1).
+ ttl = (int32_t) arg;
+ continue;
+ }
+ }
+
+ // Check if the label is a valid IPv4 or IPv6 label.
+
+ if( MemIEqual( labelStr, labelLen, kLabel_IPv4, sizeof_string( kLabel_IPv4 ) ) )
+ {
+ if( nameHasA || nameHasAAAA ) break; // Valid names have at most one IPv4 or IPv6 label.
+ nameHasA = true;
+ continue;
+ }
+ if( MemIEqual( labelStr, labelLen, kLabel_IPv6, sizeof_string( kLabel_IPv6 ) ) )
+ {
+ if( nameHasA || nameHasAAAA ) break; // Valid names have at most one IPv4 or IPv6 label.
+ nameHasAAAA = true;
+ continue;
+ }
+
+ // If the remaining labels are equal to "d.test.", the name exists.
+
+ if( DomainNameEqual( labelPtr, (const uint8_t *) "\x01" "d" "\x04" "test" ) ) nameExists = true;
+ break;
+ }
+ require_quiet( nameExists, done );
+
+ // Set default values for count and TTL, if those labels were present.
+
+ if( count <= 0 ) count = 1;
+ check( ( gDNSServer_DefaultTTL >= 0 ) && ( gDNSServer_DefaultTTL <= INT32_MAX ) );
+ if( ttl < 0 ) ttl = gDNSServer_DefaultTTL;
+
+ // Names that don't specify v4 or v6 have both A and AAAA records.
+
+ if( !nameHasA && !nameHasAAAA )
+ {
+ nameHasA = true;
+ nameHasAAAA = true;
+ }
+
+ check( ( count >= 1 ) && ( count <= 255 ) );
+ check( ( randCount <= 0 ) || ( ( randCount >= count ) && ( randCount <= 255 ) ) );
+
+ if( aliasCount > 0 )
+ {
+ size_t nameOffset;
+ uint8_t rdataLabel[ 1 + kDomainLabelLengthMax + 1 ];
+
+ // If aliasCount is non-zero, then the first label of QNAME is either "alias" or "alias-<N>". superPtr is a name
+ // compression pointer to the second label of QNAME, i.e., the immediate superdomain 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.
+
+ const uint8_t superPtr[ 2 ] = { 0xC0, (uint8_t)( kDNSHeaderLength + 1 + inQName[ 0 ] ) };
+
+ // The name of the first CNAME record is equal to QNAME, so nameOffset is set to offset of QNAME.
+
+ nameOffset = kDNSHeaderLength;
+
+ for( i = aliasCount; i >= 1; --i )
+ {
+ size_t nameLen;
+ size_t rdataLen;
+ int j;
+ uint32_t aliasTTL;
+ uint8_t nameLabel[ 1 + kDomainLabelLengthMax + 1 ];
+ DNSRecordFixedFields fields;
+
+ if( nameOffset <= kDNSCompressionOffsetMax )
+ {
+ namePtr[ 0 ] = (uint8_t)( ( ( nameOffset >> 8 ) & 0x3F ) | 0xC0 );
+ namePtr[ 1 ] = (uint8_t)( nameOffset & 0xFF );
+
+ nameLen = sizeof( namePtr );
+ }
+ else
+ {
+ memcpy( nameLabel, rdataLabel, 1 + rdataLabel[ 0 ] );
+ nameLen = 1 + nameLabel[ 0 ] + sizeof( superPtr );
+ }
+
+ if( i >= 2 )
+ {
+ char * dst = (char *) &rdataLabel[ 1 ];
+ char * const end = (char *) &rdataLabel[ countof( rdataLabel ) ];
+
+ if( useAliasTTLs )
+ {
+ err = SNPrintF_Add( &dst, end, kLabelPrefix_AliasTTL );
+ require_noerr( err, exit );
+
+ for( j = aliasCount - ( i - 1 ); j < aliasCount; ++j )
+ {
+ err = SNPrintF_Add( &dst, end, "-%u", aliasTTLs[ j ] );
+ require_noerr( err, exit );
+ }
+ }
+ else
+ {
+ err = SNPrintF_Add( &dst, end, kLabelPrefix_Alias "%?{end}-%u", i == 2, i - 1 );
+ require_noerr( err, exit );
+ }
+ rdataLabel[ 0 ] = (uint8_t)( dst - (char *) &rdataLabel[ 1 ] );
+ rdataLen = 1 + rdataLabel[ 0 ] + sizeof( superPtr );
+ }
+ else
+ {
+ rdataLen = sizeof( superPtr );
+ }
+
+ if( !inForTCP )
+ {
+ size_t recordLen = nameLen + sizeof( fields ) + rdataLen;
+
+ if( ( DataBuffer_GetLen( inDB ) + recordLen ) > kDNSMaxUDPMessageSize )
+ {
+ truncated = true;
+ goto done;
+ }
+ }
+ ++answerCount;
+
+ // Set CNAME record's NAME.
+
+ if( nameOffset <= kDNSCompressionOffsetMax )
+ {
+ err = DataBuffer_Append( inDB, namePtr, sizeof( namePtr ) );
+ require_noerr( err, exit );
+ }
+ else
+ {
+ err = DataBuffer_Append( inDB, nameLabel, 1 + nameLabel[ 0 ] );
+ require_noerr( err, exit );
+
+ err = DataBuffer_Append( inDB, superPtr, sizeof( superPtr ) );
+ require_noerr( err, exit );
+ }
+
+ // Set CNAME record's TYPE, CLASS, TTL, and RDLENGTH.
+
+ aliasTTL = useAliasTTLs ? aliasTTLs[ aliasCount - i ] : ( (uint32_t) gDNSServer_DefaultTTL );
+ DNSRecordFixedFieldsInit( &fields, kDNSServiceType_CNAME, kDNSServiceClass_IN, aliasTTL, rdataLen );
+ err = DataBuffer_Append( inDB, &fields, sizeof( fields ) );
+ 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 );
+
+ // Set CNAME record's RDATA.
+
+ if( i >= 2 )
+ {
+ err = DataBuffer_Append( inDB, rdataLabel, 1 + rdataLabel[ 0 ] );
+ require_noerr( err, exit );
+ }
+ err = DataBuffer_Append( inDB, superPtr, sizeof( superPtr ) );
+ require_noerr( err, exit );
+ }
+
+ namePtr[ 0 ] = superPtr[ 0 ];
+ namePtr[ 1 ] = superPtr[ 1 ];
+ }
+ else
+ {
+ // There are no aliases, so initialize the name compression pointer to point to QNAME.
+
+ namePtr[ 0 ] = 0xC0;
+ namePtr[ 1 ] = kDNSHeaderLength;
+ }
+
+ if( ( ( inQType == kDNSServiceType_A ) && nameHasA ) ||
+ ( ( inQType == kDNSServiceType_AAAA ) && nameHasAAAA ) )
+ {
+ uint8_t * lsb; // Pointer to the least significant byte of record data.
+ size_t recordLen; // Length of the entire record.
+ size_t rdataLen; // Length of record's RDATA.
+ uint8_t rdata[ 16 ]; // A buffer that's big enough for either A or AAAA RDATA.
+ uint8_t randItegers[ 255 ]; // Array for random integers in [1 .. 255].
+ DNSRecordFixedFields fields;
+
+ if( inQType == kDNSServiceType_A )
+ {
+ rdataLen = 4;
+ WriteBig32( rdata, kTestDNSServerBaseAddrV4 );
+ lsb = &rdata[ 3 ];
+ }
+ else
+ {
+ rdataLen = 16;
+ memcpy( rdata, kTestDNSServerBaseAddrV6, 16 );
+ lsb = &rdata[ 15 ];
+ }
+
+ if( randCount > 0 )
+ {
+ // Populate the array with all integers between 1 and <randCount>, inclusive.
+
+ for( i = 0; i < randCount; ++i ) randItegers[ i ] = (uint8_t)( i + 1 );
+
+ // Create a contiguous subarray starting at index 0 that contains <count> 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 < count; ++i )
+ {
+ uint8_t tmp;
+ int j;
+
+ j = (int) RandomRange( i, randCount - 1 );
+ if( i != j )
+ {
+ tmp = randItegers[ i ];
+ randItegers[ i ] = randItegers[ j ];
+ randItegers[ j ] = tmp;
+ }
+ }
+ }
+
+ recordLen = sizeof( namePtr ) + sizeof( fields ) + rdataLen;
+ for( i = 0; i < count; ++i )
+ {
+ if( !inForTCP && ( ( DataBuffer_GetLen( inDB ) + recordLen ) > kDNSMaxUDPMessageSize ) )
+ {
+ truncated = true;
+ goto done;
+ }
+ ++answerCount;
+
+ // Set record NAME.
+
+ err = DataBuffer_Append( inDB, namePtr, sizeof( namePtr ) );
+ require_noerr( err, exit );
+
+ // Set record TYPE, CLASS, TTL, and RDLENGTH.
+
+ DNSRecordFixedFieldsInit( &fields, inQType, kDNSServiceClass_IN, ttl, rdataLen );
+ err = DataBuffer_Append( inDB, &fields, sizeof( fields ) );
+ require_noerr( err, exit );
+
+ // Set record RDATA.
+
+ *lsb = ( randCount > 0 ) ? randItegers[ i ] : ( *lsb + 1 );
+
+ err = DataBuffer_Append( inDB, rdata, rdataLen );
+ require_noerr( err, exit );
+ }
+ }
+
+done:
+ hdr = (DNSHeader *) DataBuffer_GetPtr( inDB );
+ flags = DNSHeaderGetFlags( hdr );
+ if( truncated ) flags |= kDNSHeaderFlag_Truncation;
+ if( notImplemented )
+ {
+ rcode = kDNSRCode_NotImplemented;
+ }
+ else
+ {
+ flags |= kDNSHeaderFlag_AuthAnswer;
+ rcode = nameExists ? kDNSRCode_NoError : kDNSRCode_NXDomain;
+ }
+ DNSFlagsSetRCode( flags, rcode );
+ DNSHeaderSetFlags( hdr, flags );
+ DNSHeaderSetAnswerCount( hdr, answerCount );
+ err = kNoErr;
+
+exit:
+ return( err );
+}
+
+//===========================================================================================================================
+// _DNSServerTCPReadHandler
+//===========================================================================================================================
+
+typedef struct
+{
+ sockaddr_ip clientAddr; // Client's address.
+ dispatch_source_t readSource; // Dispatch read source for client socket.
+ dispatch_source_t writeSource; // Dispatch write source for client socket.
+ size_t offset; // Offset into receive buffer.
+ void * msgPtr; // Pointer to dynamically allocated message buffer.
+ size_t msgLen; // Length of message buffer.
+ Boolean readSuspended; // True if the read source is currently suspended.
+ Boolean writeSuspended; // True if the write source is currently suspended.
+ Boolean receivedLength; // True if receiving DNS message as opposed to the message length.
+ uint8_t lenBuf[ 2 ]; // Buffer for two-octet message length field.
+ iovec_t iov[ 2 ]; // IO vector for writing response message.
+ iovec_t * iovPtr; // Vector pointer for SocketWriteData().
+ int iovCount; // Vector count for SocketWriteData().
+
+} TCPConnectionContext;
+
+static void TCPConnectionStop( TCPConnectionContext *inContext );
+static void TCPConnectionContextFree( TCPConnectionContext *inContext );
+static void TCPConnectionReadHandler( void *inContext );
+static void TCPConnectionWriteHandler( void *inContext );
+
+#define TCPConnectionForget( X ) ForgetCustomEx( X, TCPConnectionStop, TCPConnectionContextFree )
+
+static void _DNSServerTCPReadHandler( void *inContext )
+{
+ OSStatus err;
+ SocketContext * const sockCtx = (SocketContext *) inContext;
+ TCPConnectionContext * connection;
+ socklen_t clientAddrLen;
+ SocketRef newSock = kInvalidSocketRef;
+ SocketContext * newSockCtx = NULL;
+
+ connection = (TCPConnectionContext *) calloc( 1, sizeof( *connection ) );
+ require_action( connection, exit, err = kNoMemoryErr );
+
+ clientAddrLen = (socklen_t) sizeof( connection->clientAddr );
+ newSock = accept( sockCtx->sock, &connection->clientAddr.sa, &clientAddrLen );
+ err = map_socket_creation_errno( newSock );
+ require_noerr( err, exit );
+
+ err = SocketContextCreate( newSock, connection, &newSockCtx );
+ require_noerr( err, exit );
+ newSock = kInvalidSocketRef;
+
+ err = DispatchReadSourceCreate( newSockCtx->sock, NULL, TCPConnectionReadHandler, SocketContextCancelHandler,
+ newSockCtx, &connection->readSource );
+ require_noerr( err, exit );
+ SocketContextRetain( newSockCtx );
+ dispatch_resume( connection->readSource );
+
+ err = DispatchWriteSourceCreate( newSockCtx->sock, NULL, TCPConnectionWriteHandler, SocketContextCancelHandler,
+ newSockCtx, &connection->writeSource );
+ require_noerr( err, exit );
+ SocketContextRetain( newSockCtx );
+ connection->writeSuspended = true;
+ connection = NULL;
+
+exit:
+ ForgetSocket( &newSock );
+ SocketContextRelease( newSockCtx );
+ TCPConnectionForget( &connection );
+}
+
+//===========================================================================================================================
+// TCPConnectionStop
+//===========================================================================================================================
+
+static void TCPConnectionStop( TCPConnectionContext *inContext )
+{
+ dispatch_source_forget_ex( &inContext->readSource, &inContext->readSuspended );
+ dispatch_source_forget_ex( &inContext->writeSource, &inContext->writeSuspended );
+}
+
+//===========================================================================================================================
+// TCPConnectionContextFree
+//===========================================================================================================================
+
+static void TCPConnectionContextFree( TCPConnectionContext *inContext )
+{
+ check( !inContext->readSource );
+ check( !inContext->writeSource );
+ ForgetMem( &inContext->msgPtr );
+ free( inContext );
+}
+
+//===========================================================================================================================
+// TCPConnectionReadHandler
+//===========================================================================================================================
+
+static void TCPConnectionReadHandler( void *inContext )
+{
+ OSStatus err;
+ SocketContext * const sockCtx = (SocketContext *) inContext;
+ TCPConnectionContext * connection = (TCPConnectionContext *) sockCtx->userContext;
+ struct timeval now;
+ uint8_t * responsePtr = NULL; // malloc'd
+ size_t responseLen;
+
+ // Receive message length.
+
+ if( !connection->receivedLength )
+ {
+ err = SocketReadData( sockCtx->sock, connection->lenBuf, sizeof( connection->lenBuf ), &connection->offset );
+ if( err == EWOULDBLOCK ) goto exit;
+ require_noerr( err, exit );
+
+ connection->offset = 0;
+ connection->msgLen = ReadBig16( connection->lenBuf );
+ connection->msgPtr = malloc( connection->msgLen );
+ require_action( connection->msgPtr, exit, err = kNoMemoryErr );
+ connection->receivedLength = true;
+ }
+
+ // Receive message.
+
+ err = SocketReadData( sockCtx->sock, connection->msgPtr, connection->msgLen, &connection->offset );
+ if( err == EWOULDBLOCK ) goto exit;
+ require_noerr( err, exit );
+
+ gettimeofday( &now, NULL );
+ dispatch_suspend( connection->readSource );
+ connection->readSuspended = true;
+
+ ds_ulog( kLogLevelInfo, "TCP server received %zu bytes from %##a at %{du:time}.\n",
+ connection->msgLen, &connection->clientAddr, &now );
+
+ if( connection->msgLen < kDNSHeaderLength )
+ {
+ ds_ulog( kLogLevelInfo, "TCP DNS message is too small (%zu < %d).\n", connection->msgLen, kDNSHeaderLength );
+ goto exit;
+ }
+
+ ds_ulog( kLogLevelInfo, "TCP received message:\n\n%1{du:dnsmsg}", connection->msgPtr, connection->msgLen );
+
+ // Create response.
+
+ err = _DNSServerAnswerQueryForTCP( connection->msgPtr, connection->msgLen, &responsePtr, &responseLen );
+ require_noerr_quiet( err, exit );
+
+ // Send response.
+
+ ds_ulog( kLogLevelInfo, "TCP sending %zu byte response:\n\n%1{du:dnsmsg}", responseLen, responsePtr, responseLen );
+
+ free( connection->msgPtr );
+ connection->msgPtr = responsePtr;
+ connection->msgLen = responseLen;
+ responsePtr = NULL;
+
+ check( connection->msgLen <= UINT16_MAX );
+ WriteBig16( connection->lenBuf, connection->msgLen );
+ connection->iov[ 0 ].iov_base = connection->lenBuf;
+ connection->iov[ 0 ].iov_len = sizeof( connection->lenBuf );
+ connection->iov[ 1 ].iov_base = connection->msgPtr;
+ connection->iov[ 1 ].iov_len = connection->msgLen;
+
+ connection->iovPtr = connection->iov;
+ connection->iovCount = 2;
+
+ check( connection->writeSuspended );
+ dispatch_resume( connection->writeSource );
+ connection->writeSuspended = false;
+
+exit:
+ FreeNullSafe( responsePtr );
+ if( err && ( err != EWOULDBLOCK ) ) TCPConnectionForget( &connection );
+}
+
+//===========================================================================================================================
+// TCPConnectionWriteHandler
+//===========================================================================================================================
+
+static void TCPConnectionWriteHandler( void *inContext )
+{
+ OSStatus err;
+ SocketContext * const sockCtx = (SocketContext *) inContext;
+ TCPConnectionContext * connection = (TCPConnectionContext *) sockCtx->userContext;
+
+ err = SocketWriteData( sockCtx->sock, &connection->iovPtr, &connection->iovCount );
+ if( err == EWOULDBLOCK ) goto exit;
+ check_noerr( err );
+
+ TCPConnectionForget( &connection );
+
+exit:
+ return;
+}
+
+//===========================================================================================================================
+// GAIPerfCmd
+//===========================================================================================================================
+
+#define kGAIPerfStandardTTL ( 1 * kSecondsPerHour )
+
+typedef struct GAITesterPrivate * GAITesterRef;
+typedef struct GAITestCase GAITestCase;
+
+typedef uint32_t GAITesterEventType;
+#define kGAITesterEvent_Started 1
+#define kGAITesterEvent_Stopped 2
+
+typedef struct
+{
+ const char * name; // Domain name that was resolved.
+ int64_t connectionTimeUs; // Time in microseconds that it took to create a DNS-SD connection.
+ int64_t firstTimeUs; // Time in microseconds that it took to get the first address result.
+ int64_t timeUs; // Time in microseconds that it took to get all expected address results.
+
+} GAITestItemResult;
+
+typedef void ( *GAITesterEventHandler_f )( GAITesterEventType inType, void *inContext );
+typedef void
+ ( *GAITesterResultsHandler_f )(
+ const char * inCaseTitle,
+ MicroTime64 inCaseStartTime,
+ MicroTime64 inCaseEndTime,
+ const GAITestItemResult * inResults,
+ size_t inResultCount,
+ size_t inItemCount,
+ 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 enum
+{
+ kGAIPerfOutputFormat_JSON = 1,
+ kGAIPerfOutputFormat_XML = 2,
+ kGAIPerfOutputFormat_Binary = 3
+
+} GAIPerfOutputFormatType;
+
+typedef struct
+{
+ GAITesterRef tester; // GAI tester object.
+ CFMutableArrayRef caseResults; // Array of test case results.
+ char * outputFilePath; // File to write test results to. If NULL, then write to stdout.
+ GAIPerfOutputFormatType outputFormat; // Format of test results output.
+ 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.
+ Boolean gotSignal; // True if SIGINT or SIGTERM was caught.
+ Boolean testerStarted; // True if the GAI tester was started.
+ Boolean appendNewLine; // True if a newline character should be appended to JSON output.
+
+} GAIPerfContext;
+
+static void GAIPerfContextFree( GAIPerfContext *inContext );
+static OSStatus GAIPerfAddAdvancedTestCases( GAIPerfContext *inContext );
+static OSStatus GAIPerfAddBasicTestCases( GAIPerfContext *inContext );
+static void GAIPerfEventHandler( GAITesterEventType inType, void *inContext );
+static void
+ GAIPerfResultsHandler(
+ const char * inCaseTitle,
+ MicroTime64 inCaseStartTime,
+ MicroTime64 inCaseEndTime,
+ const GAITestItemResult * inResults,
+ size_t inResultCount,
+ size_t inItemCount,
+ void * inContext );
+static void GAIPerfSignalHandler( void *inContext );
+
+CFTypeID GAITesterGetTypeID( void );
+static OSStatus
+ GAITesterCreate(
+ dispatch_queue_t inQueue,
+ int inCallDelayMs,
+ int inServerDelayMs,
+ int inServerDefaultTTL,
+ GAITesterRef * outTester );
+static void GAITesterStart( GAITesterRef inTester );
+static void GAITesterStop( GAITesterRef inTester );
+static void GAITesterAddCase( GAITesterRef inTester, GAITestCase *inCase );
+static void
+ GAITesterSetEventHandler(
+ GAITesterRef inTester,
+ GAITesterEventHandler_f inEventHandler,
+ void * inEventContext );
+static void
+ GAITesterSetResultsHandler(
+ GAITesterRef inTester,
+ GAITesterResultsHandler_f inResultsHandler,
+ void * inResultsContext );
+
+static OSStatus GAITestCaseCreate( const char *inTitle, unsigned int inTimeLimitMs, GAITestCase **outSet );
+static void GAITestCaseFree( GAITestCase *inCase );
+static OSStatus
+ GAITestCaseAddItem(
+ GAITestCase * inCase,
+ unsigned int inAliasCount,
+ unsigned int inAddressCount,
+ int inTTL,
+ GAITestAddrType inHasAddrs,
+ GAITestAddrType inWantAddrs,
+ unsigned int inItemCount );
+static OSStatus GAITestCaseAddLocalHostItem( GAITestCase *inCase, GAITestAddrType inWantAddrs, unsigned int inItemCount );
+
+#define kGAIPerfTestSuite_Basic 1
+#define kGAIPerfTestSuite_Advanced 2
+
+static void GAIPerfCmd( void )
+{
+ OSStatus err;
+ GAIPerfContext * context;
+ int suiteValue;
+
+ context = (GAIPerfContext *) calloc( 1, sizeof( *context ) );
+ require_action( context, exit, err = kNoMemoryErr );
+
+ context->caseResults = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
+ require_action( context->caseResults, exit, err = kNoMemoryErr );
+
+ context->outputFormat = (GAIPerfOutputFormatType) CLIArgToValue( "format", gGAIPerf_OutputFormat, &err,
+ "json", kGAIPerfOutputFormat_JSON,
+ "xml", kGAIPerfOutputFormat_XML,
+ "binary", kGAIPerfOutputFormat_Binary,
+ NULL );
+ require_noerr_quiet( err, exit );
+
+ context->callDelayMs = ( gGAIPerf_CallDelayMs >= 0 ) ? (unsigned int) gGAIPerf_CallDelayMs : 0;
+ context->serverDelayMs = ( gGAIPerf_ServerDelayMs >= 0 ) ? (unsigned int) gGAIPerf_ServerDelayMs : 0;
+ context->defaultIterCount = ( gGAIPerf_DefaultIterCount >= 0 ) ? (unsigned int) gGAIPerf_DefaultIterCount : 0;
+ context->appendNewLine = gGAIPerf_OutputAppendNewLine ? true : false;
+
+ if( gGAIPerf_OutputFilePath )
+ {
+ context->outputFilePath = strdup( gGAIPerf_OutputFilePath );
+ require_action( context->outputFilePath, exit, err = kNoMemoryErr );
+ }
+
+ err = GAITesterCreate( dispatch_get_main_queue(), (int) context->callDelayMs, (int) context->serverDelayMs,
+ kGAIPerfStandardTTL, &context->tester );
+ require_noerr( err, exit );
+
+ check( gGAIPerf_TestSuite );
+ suiteValue = CLIArgToValue( "suite", gGAIPerf_TestSuite, &err,
+ "basic", kGAIPerfTestSuite_Basic,
+ "advanced", kGAIPerfTestSuite_Advanced,
+ NULL );
+ require_noerr_quiet( err, exit );
+
+ switch( suiteValue )
+ {
+ case kGAIPerfTestSuite_Basic:
+ err = GAIPerfAddBasicTestCases( context );
+ require_noerr( err, exit );
+ break;
+
+ case kGAIPerfTestSuite_Advanced:
+ err = GAIPerfAddAdvancedTestCases( context );
+ require_noerr( err, exit );
+ break;
+
+ default:
+ err = kValueErr;
+ break;
+ }
+
+ GAITesterSetEventHandler( context->tester, GAIPerfEventHandler, context );
+ GAITesterSetResultsHandler( context->tester, GAIPerfResultsHandler, context );
+
+ signal( SIGINT, SIG_IGN );
+ err = DispatchSignalSourceCreate( SIGINT, GAIPerfSignalHandler, context, &context->sigIntSource );
+ require_noerr( err, exit );
+ dispatch_resume( context->sigIntSource );
+
+ signal( SIGTERM, SIG_IGN );
+ err = DispatchSignalSourceCreate( SIGTERM, GAIPerfSignalHandler, context, &context->sigTermSource );
+ require_noerr( err, exit );
+ dispatch_resume( context->sigTermSource );
+
+ GAITesterStart( context->tester );
+ dispatch_main();
+
+exit:
+ if( context ) GAIPerfContextFree( context );
+ if( err ) exit( 1 );
+}
+
+//===========================================================================================================================
+// GAIPerfContextFree
+//===========================================================================================================================
+
+static void GAIPerfContextFree( GAIPerfContext *inContext )
+{
+ ForgetCF( &inContext->tester );
+ ForgetCF( &inContext->caseResults );
+ 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 );
+static unsigned int
+ _GAIPerfTimeLimitMs(
+ unsigned int inCallDelayMs,
+ unsigned int inServerDelayMs,
+ unsigned int inIterationCount );
+
+#define kGAIPerfAdvancedTestSuite_MaxAliasCount 4
+#define kGAIPerfAdvancedTestSuite_MaxAddrCount 8
+
+static OSStatus GAIPerfAddAdvancedTestCases( GAIPerfContext *inContext )
+{
+ OSStatus err;
+ unsigned int aliasCount, addressCount, timeLimitMs, 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 );
+
+ timeLimitMs = _GAIPerfTimeLimitMs( inContext->callDelayMs, inContext->serverDelayMs,
+ inContext->defaultIterCount );
+ err = GAITestCaseCreate( title, timeLimitMs, &testCase );
+ require_noerr( err, exit );
+
+ for( i = 0; i < inContext->defaultIterCount; ++i )
+ {
+ err = GAITestCaseAddItem( testCase, aliasCount, addressCount, kGAIPerfStandardTTL,
+ kGAITestAddrType_Both, kGAITestAddrType_Both, 1 );
+ require_noerr( err, exit );
+ }
+
+ GAITesterAddCase( inContext->tester, testCase );
+ 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 );
+
+ timeLimitMs = _GAIPerfTimeLimitMs( inContext->callDelayMs, inContext->serverDelayMs, 1 ) +
+ _GAIPerfTimeLimitMs( inContext->callDelayMs, 0, inContext->defaultIterCount );
+ err = GAITestCaseCreate( title, timeLimitMs, &testCase );
+ require_noerr( err, exit );
+
+ err = GAITestCaseAddItem( testCase, aliasCount, addressCount, kGAIPerfStandardTTL,
+ kGAITestAddrType_Both, kGAITestAddrType_Both, inContext->defaultIterCount + 1 );
+ require_noerr( err, exit );
+
+ GAITesterAddCase( inContext->tester, testCase );
+ testCase = NULL;
+ }
+
+ if( aliasCount == 0 ) aliasCount = 1;
+ else aliasCount *= 2;
+ }
+
+ // Finally, add a test case to resolve localhost to its IPv4 and IPv6 addresses.
+
+ _GAIPerfWriteLocalHostTestCaseTitle( title, kGAITestAddrType_Both, inContext->defaultIterCount );
+
+ timeLimitMs = _GAIPerfTimeLimitMs( inContext->callDelayMs, 0, inContext->defaultIterCount );
+ err = GAITestCaseCreate( title, timeLimitMs, &testCase );
+ require_noerr( err, exit );
+
+ err = GAITestCaseAddLocalHostItem( testCase, kGAITestAddrType_Both, inContext->defaultIterCount );
+ require_noerr( err, exit );
+
+ GAITesterAddCase( inContext->tester, testCase );
+ 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 );
+}
+
+//===========================================================================================================================
+// _GAIPerfTimeLimitMs
+//===========================================================================================================================
+
+static unsigned int
+ _GAIPerfTimeLimitMs(
+ unsigned int inCallDelayMs,
+ unsigned int inServerDelayMs,
+ unsigned int inIterationCount )
+{
+ // Allow each iteration 20 ms to complete (in addition to the call and server delay times).
+
+ return( ( inCallDelayMs + inServerDelayMs + 20 ) * 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 timeLimitMs, 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 );
+
+ timeLimitMs = _GAIPerfTimeLimitMs( inContext->callDelayMs, inContext->serverDelayMs, inContext->defaultIterCount );
+ err = GAITestCaseCreate( title, timeLimitMs, &testCase );
+ require_noerr( err, exit );
+
+ for( i = 0; i < inContext->defaultIterCount; ++i )
+ {
+ err = GAITestCaseAddItem( testCase, kGAIPerfBasicTestSuite_AliasCount, kGAIPerfBasicTestSuite_AddrCount,
+ kGAIPerfStandardTTL, kGAITestAddrType_Both, kGAITestAddrType_Both, 1 );
+ require_noerr( err, exit );
+ }
+
+ GAITesterAddCase( inContext->tester, testCase );
+ 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 );
+
+ timeLimitMs = _GAIPerfTimeLimitMs( inContext->callDelayMs, inContext->serverDelayMs, 1 ) +
+ _GAIPerfTimeLimitMs( inContext->callDelayMs, 0, inContext->defaultIterCount );
+ err = GAITestCaseCreate( title, timeLimitMs, &testCase );
+ require_noerr( err, exit );
+
+ err = GAITestCaseAddItem( testCase, kGAIPerfBasicTestSuite_AliasCount, kGAIPerfBasicTestSuite_AddrCount,
+ kGAIPerfStandardTTL, kGAITestAddrType_Both, kGAITestAddrType_Both, inContext->defaultIterCount + 1 );
+ require_noerr( err, exit );
+
+ GAITesterAddCase( inContext->tester, testCase );
+ testCase = NULL;
+
+ // Test Case #3:
+ // Each iteration resolves localhost to its IPv4 and IPv6 addresses.
+
+ _GAIPerfWriteLocalHostTestCaseTitle( title, kGAITestAddrType_Both, inContext->defaultIterCount );
+
+ timeLimitMs = _GAIPerfTimeLimitMs( inContext->callDelayMs, 0, inContext->defaultIterCount );
+ err = GAITestCaseCreate( title, timeLimitMs, &testCase );
+ require_noerr( err, exit );
+
+ err = GAITestCaseAddLocalHostItem( testCase, kGAITestAddrType_Both, inContext->defaultIterCount );
+ require_noerr( err, exit );
+
+ GAITesterAddCase( inContext->tester, testCase );
+ testCase = NULL;
+
+exit:
+ if( testCase ) GAITestCaseFree( testCase );
+ return( err );
+}
+
+//===========================================================================================================================
+// GAIPerfEventHandler
+//===========================================================================================================================
+
+static void _GAIPerfOutputResultsAndExit( GAIPerfContext *inContext ) ATTRIBUTE_NORETURN;
+
+static void GAIPerfEventHandler( GAITesterEventType inType, void *inContext )
+{
+ GAIPerfContext * const context = (GAIPerfContext *) inContext;
+
+ if( inType == kGAITesterEvent_Started )
+ {
+ context->testerStarted = true;
+ }
+ else if( inType == kGAITesterEvent_Stopped )
+ {
+ if( context->gotSignal ) exit( 1 );
+ _GAIPerfOutputResultsAndExit( context );
+ }
+}
+
+//===========================================================================================================================
+// _GAIPerfOutputResultsAndExit
+//===========================================================================================================================
+
+#define kGAIPerfResultsKey_TestCases CFSTR( "testCases" )
+#define kGAIPerfResultsKey_Info CFSTR( "info" )
+
+#define kGAIPerfInfoKey_CallDelay CFSTR( "callDelayMs" )
+#define kGAIPerfInfoKey_ServerDelay CFSTR( "serverDelayMs" )
+
+static void _GAIPerfOutputResultsAndExit( GAIPerfContext *inContext )
+{
+ OSStatus err;
+ CFPropertyListRef plist = NULL;
+ CFDataRef results = NULL;
+ FILE * file = NULL;
+
+ err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &plist,
+ "{"
+ "%kO=%O"
+ "%kO="
+ "{"
+ "%kO=%lli"
+ "%kO=%lli"
+ "}"
+ "}",
+ kGAIPerfResultsKey_TestCases, inContext->caseResults,
+ kGAIPerfResultsKey_Info,
+ kGAIPerfInfoKey_CallDelay, (int64_t) inContext->callDelayMs,
+ kGAIPerfInfoKey_ServerDelay, (int64_t) inContext->serverDelayMs );
+ require_noerr( err, exit );
+
+ // Convert results to a specific format.
+
+ switch( inContext->outputFormat )
+ {
+ case kGAIPerfOutputFormat_JSON:
+ results = CFCreateJSONData( plist, kJSONFlags_None, NULL );
+ require_action( results, exit, err = kUnknownErr );
+ break;
+
+ case kGAIPerfOutputFormat_XML:
+ results = CFPropertyListCreateData( NULL, plist, kCFPropertyListXMLFormat_v1_0, 0, NULL );
+ require_action( results, exit, err = kUnknownErr );
+ break;
+
+ case kGAIPerfOutputFormat_Binary:
+ results = CFPropertyListCreateData( NULL, plist, kCFPropertyListBinaryFormat_v1_0, 0, NULL );
+ require_action( results, exit, err = kUnknownErr );
+ break;
+
+ default:
+ err = kValueErr;
+ goto exit;
+ }
+
+ // Write formatted results to file or stdout.
+
+ if( inContext->outputFilePath )
+ {
+ file = fopen( inContext->outputFilePath, "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( err, exit );
+
+ // Write a trailing newline for JSON-formatted results if requested.
+
+ if( ( inContext->outputFormat == kGAIPerfOutputFormat_JSON ) && inContext->appendNewLine )
+ {
+ err = WriteANSIFile( file, "\n", 1 );
+ require_noerr( err, exit );
+ }
+
+exit:
+ CFReleaseNullSafe( plist );
+ CFReleaseNullSafe( results );
+ if( file && ( file != stdout ) ) fclose( file );
+ GAIPerfContextFree( inContext );
+ exit( err ? 1 : 0 );
+}
+
+//===========================================================================================================================
+// GAIPerfResultsHandler
+//===========================================================================================================================
+
+// Keys for test case dictionary
+
+#define kGAIPerfTestCaseKey_Title CFSTR( "title" )
+#define kGAIPerfTestCaseKey_StartTime CFSTR( "startTimeUs" )
+#define kGAIPerfTestCaseKey_EndTime CFSTR( "endTimeUs" )
+#define kGAIPerfTestCaseKey_Results CFSTR( "results" )
+#define kGAIPerfTestCaseKey_FirstStats CFSTR( "firstStats" )
+#define kGAIPerfTestCaseKey_ConnectionStats CFSTR( "connectionStats" )
+#define kGAIPerfTestCaseKey_Stats CFSTR( "stats" )
+#define kGAIPerfTestCaseKey_TimedOut CFSTR( "timedOut" )
+
+// 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,
+ MicroTime64 inCaseStartTime,
+ MicroTime64 inCaseEndTime,
+ const GAITestItemResult * inResults,
+ size_t inResultCount,
+ size_t inItemCount,
+ void * inContext )
+{
+ OSStatus err;
+ GAIPerfContext * const context = (GAIPerfContext *) inContext;
+ int namesAreDynamic, namesAreUnique;
+ const char * ptr;
+ size_t count, startIndex;
+ CFMutableArrayRef results = NULL;
+ GAIPerfStats stats, firstStats, connStats;
+ double sum, firstSum, connSum, value, diff;
+ size_t keyValueLen, i;
+ char keyValue[ 16 ]; // Size must be at least strlen( "name=dynamic" ) + 1 bytes.
+
+ // 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;
+ }
+
+ if( namesAreDynamic && !namesAreUnique && ( inItemCount > 0 ) )
+ {
+ count = ( inResultCount > 0 ) ? ( inResultCount - 1 ) : 0;
+ startIndex = 1;
+ }
+ else
+ {
+ count = inResultCount;
+ startIndex = 0;
+ }
+
+ results = CFArrayCreateMutable( NULL, (CFIndex) count, &kCFTypeArrayCallBacks );
+ require_action( results, exit, err = kNoMemoryErr );
+
+ GAIPerfStatsInit( &stats );
+ GAIPerfStatsInit( &firstStats );
+ GAIPerfStatsInit( &connStats );
+
+ sum = 0.0;
+ firstSum = 0.0;
+ connSum = 0.0;
+ for( i = startIndex; i < count; ++i )
+ {
+ value = (double) inResults[ i ].timeUs;
+ if( value < stats.min ) stats.min = value;
+ if( value > stats.max ) stats.max = value;
+ sum += value;
+
+ value = (double) inResults[ i ].firstTimeUs;
+ if( value < firstStats.min ) firstStats.min = value;
+ if( value > firstStats.max ) firstStats.max = value;
+ firstSum += value;
+
+ value = (double) inResults[ i ].connectionTimeUs;
+ if( value < connStats.min ) connStats.min = value;
+ if( value > connStats.max ) connStats.max = value;
+ connSum += value;
+
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, results,
+ "{"
+ "%kO=%s"
+ "%kO=%lli"
+ "%kO=%lli"
+ "%kO=%lli"
+ "}",
+ kGAIPerfTestCaseResultKey_Name, inResults[ i ].name,
+ kGAIPerfTestCaseResultKey_ConnectionTime, inResults[ i ].connectionTimeUs,
+ kGAIPerfTestCaseResultKey_FirstTime, inResults[ i ].firstTimeUs,
+ kGAIPerfTestCaseResultKey_Time, inResults[ i ].timeUs );
+ require_noerr( err, exit );
+ }
+
+ if( count > 0 )
+ {
+ stats.mean = sum / count;
+ firstStats.mean = firstSum / count;
+ connStats.mean = connSum / count;
+
+ sum = 0.0;
+ firstSum = 0.0;
+ connSum = 0.0;
+ for( i = startIndex; i < count; ++i )
+ {
+ diff = stats.mean - (double) inResults[ i ].timeUs;
+ sum += ( diff * diff );
+
+ diff = firstStats.mean - (double) inResults[ i ].firstTimeUs;
+ firstSum += ( diff * diff );
+
+ diff = connStats.mean - (double) inResults[ i ].connectionTimeUs;
+ connSum += ( diff * diff );
+ }
+ stats.stdDev = sqrt( sum / count );
+ firstStats.stdDev = sqrt( firstSum / count );
+ connStats.stdDev = sqrt( connSum / count );
+ }
+
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, context->caseResults,
+ "{"
+ "%kO=%s"
+ "%kO=%lli"
+ "%kO=%lli"
+ "%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"
+ "}"
+ "%kO=%b"
+ "}",
+ kGAIPerfTestCaseKey_Title, inCaseTitle,
+ kGAIPerfTestCaseKey_StartTime, (int64_t) inCaseStartTime,
+ kGAIPerfTestCaseKey_EndTime, (int64_t) inCaseEndTime,
+ 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,
+ kGAIPerfTestCaseKey_TimedOut, ( inResultCount < inItemCount ) ? true : false );
+ require_noerr( err, exit );
+
+exit:
+ CFReleaseNullSafe( results );
+}
+
+//===========================================================================================================================
+// GAIPerfSignalHandler
+//===========================================================================================================================
+
+static void GAIPerfSignalHandler( void *inContext )
+{
+ GAIPerfContext * const context = (GAIPerfContext *) inContext;
+
+ context->gotSignal = true;
+ if( context->tester && context->testerStarted )
+ {
+ GAITesterStop( context->tester );
+ }
+ else
+ {
+ exit( 1 );
+ }
+}
+
+//===========================================================================================================================
+// GAITesterCreate
+//===========================================================================================================================
+
+typedef enum
+{
+ kGAITestConnType_UseMainConnection = 1,
+ kGAITestConnType_OwnSharedConnection = 2
+
+} GAITestConnType;
+
+typedef struct GAITestItem GAITestItem;
+struct GAITestItem
+{
+ GAITestItem * next; // Next test item in list.
+ char * name; // Domain name to resolve.
+ int64_t connectionTimeUs; // Time in microseconds that it took to create a DNS-SD connection.
+ int64_t firstTimeUs; // Time in microseconds that it took to get the first address result.
+ int64_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.
+ 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.
+ unsigned int timeLimitMs; // Time limit in milliseconds for the test case's completion.
+};
+
+struct GAITesterPrivate
+{
+ CFRuntimeBase base; // CF object base.
+ dispatch_queue_t queue; // Serial work queue.
+ DNSServiceRef mainRef; // Reference to the main shared DNS-SD connection.
+ DNSServiceRef opRef; // 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.
+ MicroTime64 caseStartTime; // Start time of current test case in Unix time as microseconds.
+ MicroTime64 caseEndTime; // End time of current test case in Unix time as microseconds.
+ Boolean started; // True if the tester has been successfully started.
+ Boolean stopped; // True if the tester has been stopped.
+ int callDelayMs; // Amount of time to wait before calling DNSServiceGetAddrInfo().
+ dispatch_source_t caseTimer; // Timer for enforcing a test case time limits.
+ 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.
+ GAITesterEventHandler_f eventHandler; // User's event handler.
+ void * eventContext; // User's event handler context.
+ GAITesterResultsHandler_f resultsHandler; // User's results handler.
+ void * resultsContext; // User's results handler context.
+
+ // 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 _GAITesterRun( void *inContext );
+static OSStatus _GAITesterCreatePacketCapture( pcap_t **outPCap );
+static void _GAITesterTimeout( void *inContext );
+static void _GAITesterAdvanceCurrentItem( GAITesterRef inTester );
+static void _GAITesterAdvanceCurrentSet( GAITesterRef inTester );
+static void _GAITesterInitializeCurrentTest( GAITesterRef inTester );
+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, Boolean inTimedOut );
+
+#define ForgetPacketCapture( X ) ForgetCustom( X, pcap_close )
+
+static OSStatus
+ GAITestItemCreate(
+ const char * inName,
+ unsigned int inAddressCount,
+ GAITestAddrType inHasAddrs,
+ GAITestAddrType inWantAddrs,
+ GAITestItem ** outItem );
+static OSStatus GAITestItemDuplicate( const GAITestItem *inItem, GAITestItem **outItem );
+static void GAITestItemFree( GAITestItem *inItem );
+
+static OSStatus
+ GAITesterCreate(
+ dispatch_queue_t inQueue,
+ int inCallDelayMs,
+ int inServerDelayMs,
+ int inServerDefaultTTL,
+ GAITesterRef * outTester )
+{
+ OSStatus err;
+ GAITesterRef obj = NULL;
+
+ CF_OBJECT_CREATE( GAITester, obj, err, exit );
+
+ ReplaceDispatchQueue( &obj->queue, inQueue );
+ obj->callDelayMs = inCallDelayMs;
+ obj->serverPID = -1;
+ obj->serverDelayMs = inServerDelayMs;
+ obj->serverDefaultTTL = inServerDefaultTTL;
+
+ *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->opRef );
+ check( !me->mainRef );
+ check( !me->caseTimer );
+ 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 );
+
+static void GAITesterStart( GAITesterRef me )
+{
+ CFRetain( me );
+ dispatch_async_f( me->queue, me, _GAITesterStart );
+}
+
+extern char ** environ;
+
+static void _GAITesterStart( void *inContext )
+{
+ OSStatus err;
+ GAITesterRef const me = (GAITesterRef) inContext;
+ char * argv[ 4 ];
+ char * ptr;
+ char * end;
+ char command[ 128 ];
+
+ ptr = &command[ 0 ];
+ end = &command[ countof( command ) ];
+ SNPrintF_Add( &ptr, end, "dnssdutil server --loopback --followPID %lld", (int64_t) getpid() );
+ if( me->serverDefaultTTL >= 0 ) SNPrintF_Add( &ptr, end, " --defaultTTL %d", me->serverDefaultTTL );
+ if( me->serverDelayMs >= 0 ) SNPrintF_Add( &ptr, end, " --responseDelay %d", me->serverDelayMs );
+
+ argv[ 0 ] = "/bin/sh";
+ argv[ 1 ] = "-c";
+ argv[ 2 ] = command;
+ argv[ 3 ] = NULL;
+ err = posix_spawn( &me->serverPID, argv[ 0 ], NULL, NULL, argv, environ );
+ require_noerr( err, exit );
+
+ me->currentCase = me->caseList;
+ me->currentItem = me->currentCase ? me->currentCase->itemList : NULL;
+ _GAITesterInitializeCurrentTest( me );
+
+ // Hack: The first tester run is delayed for three seconds to allow the test DNS server to start up.
+ // A better way to handle this is to issue an asynchronous query for something in the d.test. domain. As soon as an
+ // expected response is received, the server can be considered to be up and running.
+
+ CFRetain( me );
+ dispatch_after_f( dispatch_time_seconds( 3 ), me->queue, me, _GAITesterRun );
+
+ CFRetain( me );
+ me->started = true;
+ if( me->eventHandler ) me->eventHandler( kGAITesterEvent_Started, me->eventContext );
+
+exit:
+ if( err ) _GAITesterStop( me );
+ CFRelease( me );
+}
+
+//===========================================================================================================================
+// 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 );
+ CFRelease( me );
+}
+
+static void _GAITesterStop( GAITesterRef me )
+{
+ OSStatus err;
+
+ DNSServiceForget( &me->opRef );
+ DNSServiceForget( &me->mainRef );
+ ForgetPacketCapture( &me->pcap );
+ dispatch_source_forget( &me->caseTimer );
+ if( me->serverPID != -1 )
+ {
+ err = kill( me->serverPID, SIGTERM );
+ err = map_global_noerr_errno( err );
+ check_noerr( err );
+ }
+
+ if( !me->stopped )
+ {
+ me->stopped = true;
+ if( me->eventHandler ) me->eventHandler( kGAITesterEvent_Stopped, me->eventContext );
+ if( me->started ) CFRelease( me );
+ }
+}
+
+//===========================================================================================================================
+// GAITesterAddCase
+//===========================================================================================================================
+
+static void GAITesterAddCase( GAITesterRef me, GAITestCase *inCase )
+{
+ GAITestCase ** ptr;
+
+ for( ptr = &me->caseList; *ptr != NULL; ptr = &( *ptr )->next ) {}
+ *ptr = inCase;
+}
+
+//===========================================================================================================================
+// GAITesterSetEventHandler
+//===========================================================================================================================
+
+static void GAITesterSetEventHandler( GAITesterRef me, GAITesterEventHandler_f inEventHandler, void *inEventContext )
+{
+ me->eventHandler = inEventHandler;
+ me->eventContext = inEventContext;
+}
+
+//===========================================================================================================================
+// GAITesterSetResultsHandler
+//===========================================================================================================================
+
+static void GAITesterSetResultsHandler( GAITesterRef me, GAITesterResultsHandler_f inResultsHandler, void *inResultsContext )
+{
+ me->resultsHandler = inResultsHandler;
+ me->resultsContext = inResultsContext;
+}
+
+//===========================================================================================================================
+// _GAITesterRun
+//===========================================================================================================================
+
+static void _GAITesterRun( void *inContext )
+{
+ OSStatus err;
+ GAITesterRef const me = (GAITesterRef) inContext;
+ GAITestItem * item;
+ GAITestItemResult * results = NULL;
+
+ require_action_quiet( !me->stopped, exit, err = kNoErr );
+
+ for( ;; )
+ {
+ item = me->currentItem;
+ if( item )
+ {
+ DNSServiceProtocol protocols;
+
+ check( !me->opRef );
+ check( ( me->bitmapV4 != 0 ) || ( me->bitmapV6 != 0 ) );
+
+ // 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 );
+ usleep( kMicrosecondsPerSecond );
+
+ // Start a packet capture.
+
+ check( !me->pcap );
+ err = _GAITesterCreatePacketCapture( &me->pcap );
+ require_noerr( err, exit );
+
+ // Start the test case time limit timer.
+
+ check( !me->caseTimer );
+ if( me->currentCase->timeLimitMs > 0 )
+ {
+ const int64_t timeLimitSecs = ( me->currentCase->timeLimitMs + 999 ) / 1000;
+
+ err = DispatchTimerCreate( dispatch_time_seconds( timeLimitSecs ), DISPATCH_TIME_FOREVER,
+ ( (uint64_t) timeLimitSecs ) * kNanosecondsPerSecond / 10,
+ me->queue, _GAITesterTimeout, NULL, me, &me->caseTimer );
+ require_noerr( err, exit );
+ dispatch_resume( me->caseTimer );
+ }
+
+ me->caseStartTime = GetCurrentMicroTime();
+ }
+
+ // Call DNSServiceGetAddrInfo().
+
+ if( me->callDelayMs > 0 ) usleep( ( (useconds_t) me->callDelayMs ) * kMicrosecondsPerMillisecond );
+
+ protocols = 0;
+ if( item->wantV4 ) protocols |= kDNSServiceProtocol_IPv4;
+ if( item->wantV6 ) protocols |= kDNSServiceProtocol_IPv6;
+
+ check( !me->mainRef );
+ me->startTicks = UpTicks();
+
+ err = DNSServiceCreateConnection( &me->mainRef );
+ require_noerr( err, exit );
+
+ err = DNSServiceSetDispatchQueue( me->mainRef, me->queue );
+ require_noerr( err, exit );
+
+ me->connTicks = UpTicks();
+
+ me->opRef = me->mainRef;
+ err = DNSServiceGetAddrInfo( &me->opRef, kDNSServiceFlagsShareConnection | kDNSServiceFlagsReturnIntermediates,
+ kDNSServiceInterfaceIndexAny, protocols, item->name, _GAITesterGetAddrInfoCallback, me );
+ require_noerr( err, exit );
+ break;
+ }
+ else
+ {
+ // No more test items means that this test case has completed (or timed out).
+
+ me->caseEndTime = GetCurrentMicroTime();
+ dispatch_source_forget( &me->caseTimer );
+ ForgetPacketCapture( &me->pcap );
+
+ if( me->resultsHandler )
+ {
+ size_t resultCount, itemCount, i;
+ int timedOut;
+
+ itemCount = 0;
+ resultCount = 0;
+ timedOut = false;
+ for( item = me->currentCase->itemList; item; item = item->next )
+ {
+ if( !timedOut )
+ {
+ if( item->timeUs < 0 )
+ {
+ timedOut = true;
+ }
+ else
+ {
+ ++resultCount;
+ }
+ }
+ ++itemCount;
+ }
+ if( resultCount > 0 )
+ {
+ results = (GAITestItemResult *) calloc( resultCount, sizeof( *results ) );
+ require_action( results, exit, err = kNoMemoryErr );
+
+ item = me->currentCase->itemList;
+ for( i = 0; i < resultCount; ++i )
+ {
+ results[ i ].name = item->name;
+ results[ i ].connectionTimeUs = item->connectionTimeUs;
+ results[ i ].firstTimeUs = item->firstTimeUs;
+ results[ i ].timeUs = item->timeUs;
+ item = item->next;
+ }
+ }
+ me->resultsHandler( me->currentCase->title, me->caseStartTime, me->caseEndTime, results, resultCount,
+ itemCount, me->resultsContext );
+ ForgetMem( &results );
+ }
+
+ _GAITesterAdvanceCurrentSet( me );
+ require_action_quiet( me->currentCase, exit, err = kEndingErr );
+ }
+ }
+
+exit:
+ FreeNullSafe( results );
+ if( err ) _GAITesterStop( me );
+ CFRelease( me );
+}
+
+//===========================================================================================================================
+// _GAITesterCreatePacketCapture
+//===========================================================================================================================
+
+static OSStatus _GAITesterCreatePacketCapture( pcap_t **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, pcap_geterr( pcap ) );
+
+ err = pcap_compile( pcap, &program, "udp port 53", 1, PCAP_NETMASK_UNKNOWN );
+ 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 );
+}
+
+//===========================================================================================================================
+// _GAITesterTimeout
+//===========================================================================================================================
+
+static void _GAITesterTimeout( void *inContext )
+{
+ GAITesterRef const me = (GAITesterRef) inContext;
+
+ dispatch_source_forget( &me->caseTimer );
+
+ _GAITesterCompleteCurrentTest( me, true );
+}
+
+//===========================================================================================================================
+// _GAITesterAdvanceCurrentItem
+//===========================================================================================================================
+
+static void _GAITesterAdvanceCurrentItem( GAITesterRef me )
+{
+ if( me->currentItem )
+ {
+ me->currentItem = me->currentItem->next;
+ _GAITesterInitializeCurrentTest( me );
+ }
+}
+
+//===========================================================================================================================
+// _GAITesterAdvanceCurrentSet
+//===========================================================================================================================
+
+static void _GAITesterAdvanceCurrentSet( GAITesterRef me )
+{
+ if( me->currentCase )
+ {
+ me->caseStartTime = 0;
+ me->caseEndTime = 0;
+ me->currentCase = me->currentCase->next;
+ if( me->currentCase )
+ {
+ me->currentItem = me->currentCase->itemList;
+ _GAITesterInitializeCurrentTest( me );
+ }
+ }
+}
+
+//===========================================================================================================================
+// _GAITesterInitializeCurrentTest
+//===========================================================================================================================
+
+static void _GAITesterInitializeCurrentTest( GAITesterRef me )
+{
+ GAITestItem * const item = me->currentItem;
+
+ if( item )
+ {
+ check( item->addressCount > 0 );
+ if( item->wantV4 )
+ {
+ me->bitmapV4 = item->hasV4 ? ( ( UINT64_C( 1 ) << item->addressCount ) - 1 ) : 1;
+ }
+ else
+ {
+ me->bitmapV4 = 0;
+ }
+
+ if( item->wantV6 )
+ {
+ me->bitmapV6 = item->hasV6 ? ( ( UINT64_C( 1 ) << item->addressCount ) - 1 ) : 1;
+ }
+ else
+ {
+ me->bitmapV6 = 0;
+ }
+ me->gotFirstResult = false;
+ }
+}
+
+//===========================================================================================================================
+// _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 )
+{
+ 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;
+ unsigned int addrOffset;
+
+ Unused( inSDRef );
+ Unused( inInterfaceIndex );
+ Unused( inHostname );
+ Unused( inTTL );
+
+ nowTicks = UpTicks();
+
+ require_quiet( inFlags & kDNSServiceFlagsAdd, exit );
+ require_quiet( !inError || ( inError == kDNSServiceErr_NoSuchRecord ), exit );
+
+ bitmapPtr = NULL;
+ bitmask = 0;
+ if( ( sip->sa.sa_family == AF_INET ) && item->wantV4 )
+ {
+ if( item->hasV4 )
+ {
+ if( !inError )
+ {
+ const uint32_t addrV4 = ntohl( sip->v4.sin_addr.s_addr );
+
+ if( strcasecmp( item->name, "localhost." ) == 0 )
+ {
+ if( addrV4 == INADDR_LOOPBACK )
+ {
+ bitmask = 1;
+ bitmapPtr = &me->bitmapV4;
+ }
+ }
+ else
+ {
+ addrOffset = addrV4 - kTestDNSServerBaseAddrV4;
+ if( ( addrOffset >= 1 ) && ( addrOffset <= item->addressCount ) )
+ {
+ bitmask = UINT64_C( 1 ) << ( addrOffset - 1 );
+ bitmapPtr = &me->bitmapV4;
+ }
+ }
+ }
+ }
+ else if( inError == kDNSServiceErr_NoSuchRecord )
+ {
+ bitmask = 1;
+ bitmapPtr = &me->bitmapV4;
+ }
+ }
+ else if( ( sip->sa.sa_family == AF_INET6 ) && item->wantV6 )
+ {
+ if( item->hasV6 )
+ {
+ if( !inError )
+ {
+ 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;
+ bitmapPtr = &me->bitmapV6;
+ }
+ }
+ else if( memcmp( addrV6, kTestDNSServerBaseAddrV6, 15 ) == 0 )
+ {
+ addrOffset = addrV6[ 15 ];
+ if( ( addrOffset >= 1 ) && ( addrOffset <= item->addressCount ) )
+ {
+ bitmask = UINT64_C( 1 ) << ( addrOffset - 1 );
+ bitmapPtr = &me->bitmapV6;
+ }
+ }
+ }
+ }
+ else if( inError == kDNSServiceErr_NoSuchRecord )
+ {
+ bitmask = 1;
+ bitmapPtr = &me->bitmapV6;
+ }
+ }
+
+ if( bitmapPtr && ( *bitmapPtr & bitmask ) )
+ {
+ *bitmapPtr &= ~bitmask;
+ if( !me->gotFirstResult )
+ {
+ me->firstTicks = nowTicks;
+ me->gotFirstResult = true;
+ }
+
+ if( ( me->bitmapV4 == 0 ) && ( me->bitmapV6 == 0 ) )
+ {
+ me->endTicks = nowTicks;
+ _GAITesterCompleteCurrentTest( me, false );
+ }
+ }
+
+exit:
+ return;
+}
+
+//===========================================================================================================================
+// _GAITesterCompleteCurrentTest
+//===========================================================================================================================
+
+static OSStatus
+ _GAITesterGetDNSMessageFromPacket(
+ const uint8_t * inPacketPtr,
+ size_t inPacketLen,
+ const uint8_t ** outMsgPtr,
+ size_t * outMsgLen );
+
+static void _GAITesterCompleteCurrentTest( GAITesterRef me, Boolean inTimedOut )
+{
+ OSStatus err;
+ GAITestItem * item;
+ struct timeval * tsQA = NULL;
+ struct timeval * tsQAAAA = NULL;
+ struct timeval * tsRA = NULL;
+ struct timeval * tsRAAAA = NULL;
+ struct timeval timeStamps[ 4 ];
+ struct timeval * tsPtr = &timeStamps[ 0 ];
+ struct timeval * tsQ;
+ struct timeval * tsR;
+ int64_t idleTimeUs;
+ uint8_t name[ kDomainNameLengthMax ];
+
+ DNSServiceForget( &me->opRef );
+ DNSServiceForget( &me->mainRef );
+
+ if( inTimedOut )
+ {
+ for( item = me->currentItem; item; item = item->next )
+ {
+ item->firstTimeUs = -1;
+ item->timeUs = -1;
+ }
+ me->currentItem = NULL;
+
+ CFRetain( me );
+ dispatch_async_f( me->queue, me, _GAITesterRun );
+ return;
+ }
+
+ item = me->currentItem;
+ err = DomainNameFromString( name, item->name, NULL );
+ require_noerr( err, exit );
+
+ 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;
+ const DNSQuestionFixedFields * qfields;
+ unsigned int qtype;
+ 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( DNSMessageExtractDomainName( msgPtr, msgLen, ptr, qname, &ptr ) != kNoErr ) continue;
+ if( !DomainNameEqual( qname, name ) ) continue;
+
+ qfields = (const DNSQuestionFixedFields *) ptr;
+ if( DNSQuestionFixedFieldsGetClass( qfields ) != kDNSServiceClass_IN ) continue;
+
+ qtype = DNSQuestionFixedFieldsGetType( qfields );
+ 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;
+ }
+ }
+ }
+
+ if( tsQA && tsQAAAA ) tsQ = TIMEVAL_GT( *tsQA, *tsQAAAA ) ? tsQA : tsQAAAA;
+ else tsQ = tsQA ? tsQA : tsQAAAA;
+
+ if( tsRA && tsRAAAA ) tsR = TIMEVAL_LT( *tsRA, *tsRAAAA ) ? tsRA : tsRAAAA;
+ else tsR = tsQA ? tsQA : tsQAAAA;
+
+ if( tsQ && tsR )
+ {
+ idleTimeUs = TIMEVAL_USEC64_DIFF( *tsR, *tsQ );
+ if( idleTimeUs < 0 ) idleTimeUs = 0;
+ }
+ else
+ {
+ idleTimeUs = 0;
+ }
+
+ item->connectionTimeUs = (int64_t) UpTicksToMicroseconds( me->connTicks - me->startTicks );
+ item->firstTimeUs = (int64_t)( UpTicksToMicroseconds( me->firstTicks - me->connTicks ) - (uint64_t) idleTimeUs );
+ item->timeUs = (int64_t)( UpTicksToMicroseconds( me->endTicks - me->connTicks ) - (uint64_t) idleTimeUs );
+
+ _GAITesterAdvanceCurrentItem( me );
+ CFRetain( me );
+ dispatch_async_f( me->queue, me, _GAITesterRun );
+
+exit:
+ if( err ) _GAITesterStop( 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, unsigned int inTimeLimitMs, GAITestCase **outSet )
+{
+ 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 );
+
+ obj->timeLimitMs = inTimeLimitMs;
+
+ *outSet = 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
+//===========================================================================================================================
+
+// 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 kUniqueStringCharSet "abcdefghijklmnopqrstuvwxyz0123456789"
+#define kUniqueStringCharSetLen sizeof_string( kUniqueStringCharSet )
+#define kUniqueStringLen 6
+
+static OSStatus
+ GAITestCaseAddItem(
+ GAITestCase * inCase,
+ unsigned int inAliasCount,
+ unsigned int inAddressCount,
+ int inTTL,
+ GAITestAddrType inHasAddrs,
+ GAITestAddrType inWantAddrs,
+ 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 uniqueStr[ kUniqueStringLen + 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.
+
+ RandomString( kUniqueStringCharSet, kUniqueStringCharSetLen, kUniqueStringLen, kUniqueStringLen, uniqueStr );
+ SNPrintF_Add( &ptr, end, "tag-%s.", uniqueStr );
+
+ // Add IPv4 or IPv6 label if necessary.
+
+ switch( inHasAddrs )
+ {
+ case kGAITestAddrType_IPv4:
+ SNPrintF_Add( &ptr, end, "ipv4." );
+ break;
+
+ case kGAITestAddrType_IPv6:
+ SNPrintF_Add( &ptr, end, "ipv6." );
+ break;
+ }
+
+ // Add d.test. labels.
+
+ SNPrintF_Add( &ptr, end, "d.test." );
+
+ // Create item.
+
+ err = GAITestItemCreate( name, inAddressCount, inHasAddrs, inWantAddrs, &item );
+ require_noerr( err, exit );
+
+ newItemList = item;
+ itemPtr = &item->next;
+
+ // Create repeat items.
+
+ for( i = 1; i < inItemCount; ++i )
+ {
+ err = GAITestItemDuplicate( 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 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, &item );
+ require_noerr( err, exit );
+
+ newItemList = item;
+ itemPtr = &item->next;
+
+ // Create repeat items.
+
+ for( i = 1; i < inItemCount; ++i )
+ {
+ err = GAITestItemDuplicate( 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,
+ 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;
+
+ *outItem = obj;
+ obj = NULL;
+ err = kNoErr;
+
+exit:
+ if( obj ) GAITestItemFree( obj );
+ return( err );
+}
+
+//===========================================================================================================================
+// GAITestItemDuplicate
+//===========================================================================================================================
+
+static OSStatus GAITestItemDuplicate( 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 );
+}
+
+//===========================================================================================================================
+// 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, 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;
+
+ memset( &mcastAddr4, 0, sizeof( mcastAddr4 ) );
+ SIN_LEN_SET( &mcastAddr4 );
+ mcastAddr4.sin_family = AF_INET;
+ mcastAddr4.sin_port = htons( kSSDPPort );
+ mcastAddr4.sin_addr.s_addr = htonl( 0xEFFFFFFA ); // 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.
+