+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, results,
+ "{"
+ "%kO=%s" // name
+ "%kO=%lli" // connectionTimeUs
+ "%kO=%lli" // firstTimeUs
+ "%kO=%lli" // timeUs
+ "%kO=%lli" // error
+ "}",
+ kGAIPerfTestCaseResultKey_Name, result->name,
+ kGAIPerfTestCaseResultKey_ConnectionTime, (int64_t) result->connectionTimeUs,
+ kGAIPerfTestCaseResultKey_FirstTime, (int64_t) result->firstTimeUs,
+ kGAIPerfTestCaseResultKey_Time, (int64_t) result->timeUs,
+ CFSTR( "error" ), (int64_t) result->error );
+ require_noerr( err, exit );
+
+ if( !result->error )
+ {
+ value = (double) result->timeUs;
+ if( value < stats.min ) stats.min = value;
+ if( value > stats.max ) stats.max = value;
+ sum += value;
+
+ value = (double) result->firstTimeUs;
+ if( value < firstStats.min ) firstStats.min = value;
+ if( value > firstStats.max ) firstStats.max = value;
+ firstSum += value;
+
+ value = (double) result->connectionTimeUs;
+ if( value < connStats.min ) connStats.min = value;
+ if( value > connStats.max ) connStats.max = value;
+ connSum += value;
+
+ ++count;
+ }
+ else
+ {
+ context->testFailed = true;
+ }
+ }
+
+ if( count > 0 )
+ {
+ stats.mean = sum / count;
+ firstStats.mean = firstSum / count;
+ connStats.mean = connSum / count;
+
+ sum = 0.0;
+ firstSum = 0.0;
+ connSum = 0.0;
+ for( i = startIndex; i < inResultCount; ++i )
+ {
+ double diff;
+
+ result = &inResultArray[ i ];
+ if( result->error ) continue;
+
+ diff = stats.mean - (double) result->timeUs;
+ sum += ( diff * diff );
+
+ diff = firstStats.mean - (double) result->firstTimeUs;
+ firstSum += ( diff * diff );
+
+ diff = connStats.mean - (double) result->connectionTimeUs;
+ connSum += ( diff * diff );
+ }
+ stats.stdDev = sqrt( sum / count );
+ firstStats.stdDev = sqrt( firstSum / count );
+ connStats.stdDev = sqrt( connSum / count );
+ }
+
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, context->testCaseResults,
+ "{"
+ "%kO=%s"
+ "%kO=%s"
+ "%kO=%s"
+ "%kO=%O"
+ "%kO="
+ "{"
+ "%kO=%lli"
+ "%kO=%f"
+ "%kO=%f"
+ "%kO=%f"
+ "%kO=%f"
+ "}"
+ "%kO="
+ "{"
+ "%kO=%lli"
+ "%kO=%f"
+ "%kO=%f"
+ "%kO=%f"
+ "%kO=%f"
+ "}"
+ "%kO="
+ "{"
+ "%kO=%lli"
+ "%kO=%f"
+ "%kO=%f"
+ "%kO=%f"
+ "%kO=%f"
+ "}"
+ "}",
+ kGAIPerfTestCaseKey_Title, inCaseTitle,
+ kGAIPerfTestCaseKey_StartTime, _NanoTime64ToDateString( inCaseStartTime, startTimeStr, sizeof( startTimeStr ) ),
+ kGAIPerfTestCaseKey_EndTime, _NanoTime64ToDateString( inCaseEndTime, endTimeStr, sizeof( endTimeStr ) ),
+ kGAIPerfTestCaseKey_Results, results,
+ kGAIPerfTestCaseKey_Stats,
+ kGAIPerfTestCaseStatsKey_Count, (int64_t) count,
+ kGAIPerfTestCaseStatsKey_Min, stats.min,
+ kGAIPerfTestCaseStatsKey_Max, stats.max,
+ kGAIPerfTestCaseStatsKey_Mean, stats.mean,
+ kGAIPerfTestCaseStatsKey_StdDev, stats.stdDev,
+ kGAIPerfTestCaseKey_FirstStats,
+ kGAIPerfTestCaseStatsKey_Count, (int64_t) count,
+ kGAIPerfTestCaseStatsKey_Min, firstStats.min,
+ kGAIPerfTestCaseStatsKey_Max, firstStats.max,
+ kGAIPerfTestCaseStatsKey_Mean, firstStats.mean,
+ kGAIPerfTestCaseStatsKey_StdDev, firstStats.stdDev,
+ kGAIPerfTestCaseKey_ConnectionStats,
+ kGAIPerfTestCaseStatsKey_Count, (int64_t) count,
+ kGAIPerfTestCaseStatsKey_Min, connStats.min,
+ kGAIPerfTestCaseStatsKey_Max, connStats.max,
+ kGAIPerfTestCaseStatsKey_Mean, connStats.mean,
+ kGAIPerfTestCaseStatsKey_StdDev, connStats.stdDev );
+ require_noerr( err, exit );
+
+exit:
+ CFReleaseNullSafe( results );
+ if( err ) exit( 1 );
+}
+
+//===========================================================================================================================
+// GAIPerfSignalHandler
+//===========================================================================================================================
+
+static void GAIPerfSignalHandler( void *inContext )
+{
+ GAIPerfContext * const context = (GAIPerfContext *) inContext;
+
+ if( !context->tester ) exit( 1 );
+ GAITesterStop( context->tester );
+ context->tester = NULL;
+}
+
+//===========================================================================================================================
+// GAITesterCreate
+//===========================================================================================================================
+
+// A character set of lower-case alphabet characters and digits and a string length of six allows for 36^6 = 2,176,782,336
+// possible strings to use in the Tag label.
+
+#define kGAITesterTagStringLen 6
+
+typedef struct GAITestItem GAITestItem;
+struct GAITestItem
+{
+ GAITestItem * next; // Next test item in list.
+ char * name; // Domain name to resolve.
+ uint64_t connectionTimeUs; // Time in microseconds that it took to create a DNS-SD connection.
+ uint64_t firstTimeUs; // Time in microseconds that it took to get the first address result.
+ uint64_t timeUs; // Time in microseconds that it took to get all expected address results.
+ unsigned int addressCount; // Address count of the domain name, i.e., the Count label argument.
+ OSStatus error; // Current status/error.
+ unsigned int timeLimitMs; // Time limit in milliseconds for the test item's completion.
+ Boolean hasV4; // True if the domain name has one or more IPv4 addresses.
+ Boolean hasV6; // True if the domain name has one or more IPv6 addresses.
+ Boolean wantV4; // True if DNSServiceGetAddrInfo() should be called to get IPv4 addresses.
+ Boolean wantV6; // True if DNSServiceGetAddrInfo() should be called to get IPv6 addresses.
+};
+
+struct GAITestCase
+{
+ GAITestCase * next; // Next test case in list.
+ GAITestItem * itemList; // List of test items.
+ char * title; // Title of the test case.
+};
+
+struct GAITesterPrivate
+{
+ CFRuntimeBase base; // CF object base.
+ dispatch_queue_t queue; // Serial work queue.
+ DNSServiceRef connection; // Reference to the shared DNS-SD connection.
+ DNSServiceRef getAddrInfo; // Reference to the current DNSServiceGetAddrInfo operation.
+ GAITestCase * caseList; // List of test cases.
+ GAITestCase * currentCase; // Pointer to the current test case.
+ GAITestItem * currentItem; // Pointer to the current test item.
+ NanoTime64 caseStartTime; // Start time of current test case in Unix time as nanoseconds.
+ NanoTime64 caseEndTime; // End time of current test case in Unix time as nanoseconds.
+ int callDelayMs; // Amount of time to wait before calling DNSServiceGetAddrInfo().
+ Boolean skipPathEval; // True if DNSServiceGetAddrInfo() path evaluation is to be skipped.
+ Boolean stopped; // True if the tester has been stopped.
+ Boolean badUDPMode; // True if the test DNS server is to run in Bad UDP mode.
+ dispatch_source_t timer; // Timer for enforcing a test item's time limit.
+ pcap_t * pcap; // Captures traffic between mDNSResponder and test DNS server.
+ pid_t serverPID; // PID of the test DNS server.
+ int serverDelayMs; // Additional time to have the server delay its responses by.
+ int serverDefaultTTL; // Default TTL for the server's records.
+ GAITesterStopHandler_f stopHandler; // User's stop handler.
+ void * stopContext; // User's event handler context.
+ GAITesterResultsHandler_f resultsHandler; // User's results handler.
+ void * resultsContext; // User's results handler context.
+
+ // Variables for current test item.
+
+ uint64_t bitmapV4; // Bitmap of IPv4 results that have yet to be received.
+ uint64_t bitmapV6; // Bitmap of IPv6 results that have yet to be received.
+ uint64_t startTicks; // Start ticks of DNSServiceGetAddrInfo().
+ uint64_t connTicks; // Ticks when the connection was created.
+ uint64_t firstTicks; // Ticks when the first DNSServiceGetAddrInfo result was received.
+ uint64_t endTicks; // Ticks when the last DNSServiceGetAddrInfo result was received.
+ Boolean gotFirstResult; // True if the first result has been received.
+};
+
+CF_CLASS_DEFINE( GAITester );
+
+static void _GAITesterStartNextTest( GAITesterRef inTester );
+static OSStatus _GAITesterCreatePacketCapture( pcap_t **outPCap );
+static void _GAITesterFirstGAITimeout( void *inContext );
+static void _GAITesterTimeout( void *inContext );
+static void DNSSD_API
+ _GAITesterFirstGAICallback(
+ DNSServiceRef inSDRef,
+ DNSServiceFlags inFlags,
+ uint32_t inInterfaceIndex,
+ DNSServiceErrorType inError,
+ const char * inHostname,
+ const struct sockaddr * inSockAddr,
+ uint32_t inTTL,
+ void * inContext );
+static void DNSSD_API
+ _GAITesterGetAddrInfoCallback(
+ DNSServiceRef inSDRef,
+ DNSServiceFlags inFlags,
+ uint32_t inInterfaceIndex,
+ DNSServiceErrorType inError,
+ const char * inHostname,
+ const struct sockaddr * inSockAddr,
+ uint32_t inTTL,
+ void * inContext );
+static void _GAITesterCompleteCurrentTest( GAITesterRef inTester, OSStatus inError );
+
+#define ForgetPacketCapture( X ) ForgetCustom( X, pcap_close )
+
+static OSStatus
+ GAITestItemCreate(
+ const char * inName,
+ unsigned int inAddressCount,
+ GAITestAddrType inHasAddrs,
+ GAITestAddrType inWantAddrs,
+ unsigned int inTimeLimitMs,
+ GAITestItem ** outItem );
+static OSStatus GAITestItemDup( const GAITestItem *inItem, GAITestItem **outItem );
+static void GAITestItemFree( GAITestItem *inItem );
+
+static OSStatus
+ GAITesterCreate(
+ dispatch_queue_t inQueue,
+ int inCallDelayMs,
+ int inServerDelayMs,
+ int inServerDefaultTTL,
+ Boolean inSkipPathEvaluation,
+ Boolean inBadUDPMode,
+ GAITesterRef * outTester )
+{
+ OSStatus err;
+ GAITesterRef obj = NULL;
+
+ CF_OBJECT_CREATE( GAITester, obj, err, exit );
+
+ ReplaceDispatchQueue( &obj->queue, inQueue );
+ obj->callDelayMs = inCallDelayMs;
+ obj->serverPID = -1;
+ obj->serverDelayMs = inServerDelayMs;
+ obj->serverDefaultTTL = inServerDefaultTTL;
+ obj->skipPathEval = inSkipPathEvaluation;
+ obj->badUDPMode = inBadUDPMode;
+
+ *outTester = obj;
+ obj = NULL;
+ err = kNoErr;
+
+exit:
+ CFReleaseNullSafe( obj );
+ return( err );
+}
+
+//===========================================================================================================================
+// _GAITesterFinalize
+//===========================================================================================================================
+
+static void _GAITesterFinalize( CFTypeRef inObj )
+{
+ GAITesterRef const me = (GAITesterRef) inObj;
+ GAITestCase * testCase;
+
+ check( !me->getAddrInfo );
+ check( !me->connection );
+ check( !me->timer );
+ dispatch_forget( &me->queue );
+ while( ( testCase = me->caseList ) != NULL )
+ {
+ me->caseList = testCase->next;
+ GAITestCaseFree( testCase );
+ }
+}
+
+//===========================================================================================================================
+// GAITesterStart
+//===========================================================================================================================
+
+static void _GAITesterStart( void *inContext );
+static void _GAITesterStop( GAITesterRef me, OSStatus inError );
+
+static void GAITesterStart( GAITesterRef me )
+{
+ CFRetain( me );
+ dispatch_async_f( me->queue, me, _GAITesterStart );
+}
+
+#define kGAITesterFirstGAITimeoutSecs 4
+
+static void _GAITesterStart( void *inContext )
+{
+ OSStatus err;
+ GAITesterRef const me = (GAITesterRef) inContext;
+ DNSServiceFlags flags;
+ char name[ 64 ];
+ char tag[ kGAITesterTagStringLen + 1 ];
+
+ err = SpawnCommand( &me->serverPID, "dnssdutil server --loopback --follow %lld%?s%?d%?s%?d%?s",
+ (int64_t) getpid(),
+ me->serverDefaultTTL >= 0, " --defaultTTL ",
+ me->serverDefaultTTL >= 0, me->serverDefaultTTL,
+ me->serverDelayMs >= 0, " --responseDelay ",
+ me->serverDelayMs >= 0, me->serverDelayMs,
+ me->badUDPMode, " --badUDPMode" );
+ require_noerr_quiet( err, exit );
+
+ SNPrintF( name, sizeof( name ), "tag-gaitester-probe-%s.ipv4.d.test",
+ _RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
+
+ flags = 0;
+ if( me->skipPathEval ) flags |= kDNSServiceFlagsPathEvaluationDone;
+
+ err = DNSServiceGetAddrInfo( &me->getAddrInfo, flags, kDNSServiceInterfaceIndexAny, kDNSServiceProtocol_IPv4, name,
+ _GAITesterFirstGAICallback, me );
+ require_noerr( err, exit );
+
+ err = DNSServiceSetDispatchQueue( me->getAddrInfo, me->queue );
+ require_noerr( err, exit );
+
+ err = DispatchTimerOneShotCreate( dispatch_time_seconds( kGAITesterFirstGAITimeoutSecs ),
+ UINT64_C_safe( kGAITesterFirstGAITimeoutSecs ) * kNanosecondsPerSecond / 10, me->queue,
+ _GAITesterFirstGAITimeout, me, &me->timer );
+ require_noerr( err, exit );
+ dispatch_resume( me->timer );
+
+exit:
+ if( err ) _GAITesterStop( me, err );
+}
+
+//===========================================================================================================================
+// GAITesterStop
+//===========================================================================================================================
+
+static void _GAITesterUserStop( void *inContext );
+
+static void GAITesterStop( GAITesterRef me )
+{
+ CFRetain( me );
+ dispatch_async_f( me->queue, me, _GAITesterUserStop );
+}
+
+static void _GAITesterUserStop( void *inContext )
+{
+ GAITesterRef const me = (GAITesterRef) inContext;
+
+ _GAITesterStop( me, kCanceledErr );
+ CFRelease( me );
+}
+
+static void _GAITesterStop( GAITesterRef me, OSStatus inError )
+{
+ OSStatus err;
+
+ ForgetPacketCapture( &me->pcap );
+ dispatch_source_forget( &me->timer );
+ DNSServiceForget( &me->getAddrInfo );
+ DNSServiceForget( &me->connection );
+ if( me->serverPID != -1 )
+ {
+ err = kill( me->serverPID, SIGTERM );
+ err = map_global_noerr_errno( err );
+ check_noerr( err );
+ me->serverPID = -1;
+ }
+
+ if( !me->stopped )
+ {
+ me->stopped = true;
+ if( me->stopHandler ) me->stopHandler( me->stopContext, inError );
+ CFRelease( me );
+ }
+}
+
+//===========================================================================================================================
+// GAITesterAddTestCase
+//===========================================================================================================================
+
+static OSStatus GAITesterAddTestCase( GAITesterRef me, GAITestCase *inCase )
+{
+ OSStatus err;
+ GAITestCase ** ptr;
+
+ require_action_quiet( inCase->itemList, exit, err = kCountErr );
+
+ for( ptr = &me->caseList; *ptr; ptr = &( *ptr )->next ) {}
+ *ptr = inCase;
+ err = kNoErr;
+
+exit:
+ return( err );
+}
+
+//===========================================================================================================================
+// GAITesterSetStopHandler
+//===========================================================================================================================
+
+static void GAITesterSetStopHandler( GAITesterRef me, GAITesterStopHandler_f inStopHandler, void *inStopContext )
+{
+ me->stopHandler = inStopHandler;
+ me->stopContext = inStopContext;
+}
+
+//===========================================================================================================================
+// GAITesterSetResultsHandler
+//===========================================================================================================================
+
+static void GAITesterSetResultsHandler( GAITesterRef me, GAITesterResultsHandler_f inResultsHandler, void *inResultsContext )
+{
+ me->resultsHandler = inResultsHandler;
+ me->resultsContext = inResultsContext;
+}
+
+//===========================================================================================================================
+// _GAITesterStartNextTest
+//===========================================================================================================================
+
+static void _GAITesterStartNextTest( GAITesterRef me )
+{
+ OSStatus err;
+ GAITestItem * item;
+ DNSServiceFlags flags;
+ DNSServiceProtocol protocols;
+ int done = false;
+
+ if( me->currentItem ) me->currentItem = me->currentItem->next;
+
+ if( !me->currentItem )
+ {
+ if( me->currentCase )
+ {
+ // No more test items means that the current test case has completed.
+
+ me->caseEndTime = NanoTimeGetCurrent();
+
+ if( me->resultsHandler )
+ {
+ size_t resultCount, i;
+ GAITestItemResult * resultArray;
+
+ resultCount = 0;
+ for( item = me->currentCase->itemList; item; item = item->next ) ++resultCount;
+ check( resultCount > 0 );
+
+ resultArray = (GAITestItemResult *) calloc( resultCount, sizeof( *resultArray ) );
+ require_action( resultArray, exit, err = kNoMemoryErr );
+
+ item = me->currentCase->itemList;
+ for( i = 0; i < resultCount; ++i )
+ {
+ resultArray[ i ].name = item->name;
+ resultArray[ i ].connectionTimeUs = item->connectionTimeUs;
+ resultArray[ i ].firstTimeUs = item->firstTimeUs;
+ resultArray[ i ].timeUs = item->timeUs;
+ resultArray[ i ].error = item->error;
+ item = item->next;
+ }
+ me->resultsHandler( me->currentCase->title, me->caseStartTime, me->caseEndTime, resultArray, resultCount,
+ me->resultsContext );
+ ForgetMem( &resultArray );
+ }
+
+ me->currentCase = me->currentCase->next;
+ if( !me->currentCase )
+ {
+ done = true;
+ err = kNoErr;
+ goto exit;
+ }
+ }
+ else
+ {
+ me->currentCase = me->caseList;
+ }
+ require_action_quiet( me->currentCase->itemList, exit, err = kInternalErr );
+ me->currentItem = me->currentCase->itemList;
+ }
+
+ item = me->currentItem;
+ check( ( item->addressCount >= 1 ) && ( item->addressCount <= 64 ) );
+
+ if( !item->wantV4 ) me->bitmapV4 = 0;
+ else if( !item->hasV4 ) me->bitmapV4 = 1;
+ else if( item->addressCount < 64 ) me->bitmapV4 = ( UINT64_C( 1 ) << item->addressCount ) - 1;
+ else me->bitmapV4 = ~UINT64_C( 0 );
+
+ if( !item->wantV6 ) me->bitmapV6 = 0;
+ else if( !item->hasV6 ) me->bitmapV6 = 1;
+ else if( item->addressCount < 64 ) me->bitmapV6 = ( UINT64_C( 1 ) << item->addressCount ) - 1;
+ else me->bitmapV6 = ~UINT64_C( 0 );
+ check( ( me->bitmapV4 != 0 ) || ( me->bitmapV6 != 0 ) );
+ me->gotFirstResult = false;
+
+ // Perform preliminary tasks if this is the start of a new test case.
+
+ if( item == me->currentCase->itemList )
+ {
+ // Flush mDNSResponder's cache.
+
+ err = systemf( NULL, "killall -HUP mDNSResponder" );
+ require_noerr( err, exit );
+ sleep( 1 );
+
+ me->caseStartTime = NanoTimeGetCurrent();
+ me->caseEndTime = kNanoTime_Invalid;
+ }
+
+ // Start a packet capture.
+
+ check( !me->pcap );
+ err = _GAITesterCreatePacketCapture( &me->pcap );
+ require_noerr( err, exit );
+
+ // Start timer for test item's time limit.
+
+ check( !me->timer );
+ if( item->timeLimitMs > 0 )
+ {
+ unsigned int timeLimitMs;
+
+ timeLimitMs = item->timeLimitMs;
+ if( me->callDelayMs > 0 ) timeLimitMs += (unsigned int) me->callDelayMs;
+ if( me->serverDelayMs > 0 ) timeLimitMs += (unsigned int) me->serverDelayMs;
+
+ err = DispatchTimerCreate( dispatch_time_milliseconds( timeLimitMs ), DISPATCH_TIME_FOREVER,
+ ( (uint64_t) timeLimitMs ) * kNanosecondsPerMillisecond / 10,
+ me->queue, _GAITesterTimeout, NULL, me, &me->timer );
+ require_noerr( err, exit );
+ dispatch_resume( me->timer );
+ }
+
+ // Call DNSServiceGetAddrInfo().
+
+ if( me->callDelayMs > 0 ) usleep( ( (useconds_t) me->callDelayMs ) * kMicrosecondsPerMillisecond );
+
+ flags = kDNSServiceFlagsShareConnection | kDNSServiceFlagsReturnIntermediates;
+ if( me->skipPathEval ) flags |= kDNSServiceFlagsPathEvaluationDone;
+
+ protocols = 0;
+ if( item->wantV4 ) protocols |= kDNSServiceProtocol_IPv4;
+ if( item->wantV6 ) protocols |= kDNSServiceProtocol_IPv6;
+
+ me->startTicks = UpTicks();
+
+ check( !me->connection );
+ err = DNSServiceCreateConnection( &me->connection );
+ require_noerr( err, exit );
+
+ err = DNSServiceSetDispatchQueue( me->connection, me->queue );
+ require_noerr( err, exit );
+
+ me->connTicks = UpTicks();
+
+ check( !me->getAddrInfo );
+ me->getAddrInfo = me->connection;
+ err = DNSServiceGetAddrInfo( &me->getAddrInfo, flags, kDNSServiceInterfaceIndexAny, protocols, item->name,
+ _GAITesterGetAddrInfoCallback, me );
+ require_noerr( err, exit );
+
+exit:
+ if( err || done ) _GAITesterStop( me, err );
+}
+
+//===========================================================================================================================
+// _GAITesterCreatePacketCapture
+//===========================================================================================================================
+
+static OSStatus _GAITesterCreatePacketCapture( 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, errBuf );
+
+ 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 );
+}
+
+//===========================================================================================================================
+// _GAITesterFirstGAITimeout
+//===========================================================================================================================
+
+static void _GAITesterFirstGAITimeout( void *inContext )
+{
+ GAITesterRef const me = (GAITesterRef) inContext;
+
+ _GAITesterStop( me, kNoResourcesErr );
+}
+
+//===========================================================================================================================
+// _GAITesterTimeout
+//===========================================================================================================================
+
+static void _GAITesterTimeout( void *inContext )
+{
+ GAITesterRef const me = (GAITesterRef) inContext;
+
+ _GAITesterCompleteCurrentTest( me, kTimeoutErr );
+}
+
+//===========================================================================================================================
+// _GAITesterFirstGAICallback
+//===========================================================================================================================
+
+static void DNSSD_API
+ _GAITesterFirstGAICallback(
+ DNSServiceRef inSDRef,
+ DNSServiceFlags inFlags,
+ uint32_t inInterfaceIndex,
+ DNSServiceErrorType inError,
+ const char * inHostname,
+ const struct sockaddr * inSockAddr,
+ uint32_t inTTL,
+ void * inContext )
+{
+ GAITesterRef const me = (GAITesterRef) inContext;
+
+ Unused( inSDRef );
+ Unused( inInterfaceIndex );
+ Unused( inHostname );
+ Unused( inSockAddr );
+ Unused( inTTL );
+
+ if( ( inFlags & kDNSServiceFlagsAdd ) && !inError )
+ {
+ dispatch_source_forget( &me->timer );
+ DNSServiceForget( &me->getAddrInfo );
+
+ _GAITesterStartNextTest( me );
+ }
+}
+
+//===========================================================================================================================
+// _GAITesterGetAddrInfoCallback
+//===========================================================================================================================
+
+static void DNSSD_API
+ _GAITesterGetAddrInfoCallback(
+ DNSServiceRef inSDRef,
+ DNSServiceFlags inFlags,
+ uint32_t inInterfaceIndex,
+ DNSServiceErrorType inError,
+ const char * inHostname,
+ const struct sockaddr * inSockAddr,
+ uint32_t inTTL,
+ void * inContext )
+{
+ OSStatus err;
+ GAITesterRef const me = (GAITesterRef) inContext;
+ GAITestItem * const item = me->currentItem;
+ const sockaddr_ip * const sip = (const sockaddr_ip *) inSockAddr;
+ uint64_t nowTicks;
+ uint64_t * bitmapPtr;
+ uint64_t bitmask;
+ int hasAddr;
+
+ Unused( inSDRef );
+ Unused( inInterfaceIndex );
+ Unused( inHostname );
+ Unused( inTTL );
+
+ nowTicks = UpTicks();
+
+ require_action_quiet( inFlags & kDNSServiceFlagsAdd, exit, err = kFlagErr );
+
+ // Check if we were expecting an IP address result of this type.
+
+ if( sip->sa.sa_family == AF_INET )
+ {
+ bitmapPtr = &me->bitmapV4;
+ hasAddr = item->hasV4;
+ }
+ else if( sip->sa.sa_family == AF_INET6 )
+ {
+ bitmapPtr = &me->bitmapV6;
+ hasAddr = item->hasV6;
+ }
+ else
+ {
+ err = kTypeErr;
+ goto exit;
+ }
+
+ bitmask = 0;
+ if( hasAddr )
+ {
+ uint32_t addrOffset;
+
+ require_noerr_action_quiet( inError, exit, err = inError );
+
+ if( sip->sa.sa_family == AF_INET )
+ {
+ const uint32_t addrV4 = ntohl( sip->v4.sin_addr.s_addr );
+
+ if( strcasecmp( item->name, "localhost." ) == 0 )
+ {
+ if( addrV4 == INADDR_LOOPBACK ) bitmask = 1;
+ }
+ else
+ {
+ addrOffset = addrV4 - kDNSServerBaseAddrV4;
+ if( ( addrOffset >= 1 ) && ( addrOffset <= item->addressCount ) )
+ {
+ bitmask = UINT64_C( 1 ) << ( addrOffset - 1 );
+ }
+ }
+ }
+ else
+ {
+ const uint8_t * const addrV6 = sip->v6.sin6_addr.s6_addr;
+
+ if( strcasecmp( item->name, "localhost." ) == 0 )
+ {
+ if( memcmp( addrV6, in6addr_loopback.s6_addr, 16 ) == 0 ) bitmask = 1;
+ }
+ else if( memcmp( addrV6, kDNSServerBaseAddrV6, 15 ) == 0 )
+ {
+ addrOffset = addrV6[ 15 ];
+ if( ( addrOffset >= 1 ) && ( addrOffset <= item->addressCount ) )
+ {
+ bitmask = UINT64_C( 1 ) << ( addrOffset - 1 );
+ }
+ }
+ }
+ }
+ else
+ {
+ require_action_quiet( inError == kDNSServiceErr_NoSuchRecord, exit, err = inError ? inError : kUnexpectedErr );
+ bitmask = 1;
+ }
+ require_action_quiet( bitmask != 0, exit, err = kValueErr );
+ require_action_quiet( *bitmapPtr & bitmask, exit, err = kDuplicateErr );
+
+ *bitmapPtr &= ~bitmask;
+ if( !me->gotFirstResult )
+ {
+ me->firstTicks = nowTicks;
+ me->gotFirstResult = true;
+ }
+ err = kNoErr;
+
+exit:
+ if( err || ( ( me->bitmapV4 == 0 ) && ( me->bitmapV6 == 0 ) ) )
+ {
+ me->endTicks = nowTicks;
+ _GAITesterCompleteCurrentTest( me, err );
+ }
+}
+
+//===========================================================================================================================
+// _GAITesterCompleteCurrentTest
+//===========================================================================================================================
+
+static OSStatus
+ _GAITesterGetDNSMessageFromPacket(
+ const uint8_t * inPacketPtr,
+ size_t inPacketLen,
+ const uint8_t ** outMsgPtr,
+ size_t * outMsgLen );
+
+static void _GAITesterCompleteCurrentTest( GAITesterRef me, OSStatus inError )
+{
+ OSStatus err;
+ GAITestItem * const item = me->currentItem;
+ struct timeval timeStamps[ 4 ];
+ struct timeval * tsPtr;
+ struct timeval * tsQA = NULL;
+ struct timeval * tsQAAAA = NULL;
+ struct timeval * tsRA = NULL;
+ struct timeval * tsRAAAA = NULL;
+ struct timeval * t1;
+ struct timeval * t2;
+ int64_t idleTimeUs;
+ uint8_t name[ kDomainNameLengthMax ];
+
+ dispatch_source_forget( &me->timer );
+ DNSServiceForget( &me->getAddrInfo );
+ DNSServiceForget( &me->connection );
+
+ item->error = inError;
+ if( item->error )
+ {
+ err = kNoErr;
+ goto exit;
+ }
+
+ err = DomainNameFromString( name, item->name, NULL );
+ require_noerr( err, exit );
+
+ tsPtr = &timeStamps[ 0 ];
+ for( ;; )
+ {
+ int status;
+ struct pcap_pkthdr * pktHdr;
+ const uint8_t * packet;
+ const uint8_t * msgPtr;
+ size_t msgLen;
+ const DNSHeader * hdr;
+ unsigned int flags;
+ const uint8_t * ptr;
+ uint16_t qtype, qclass;
+ uint8_t qname[ kDomainNameLengthMax ];
+
+ status = pcap_next_ex( me->pcap, &pktHdr, &packet );
+ if( status != 1 ) break;
+ if( _GAITesterGetDNSMessageFromPacket( packet, pktHdr->caplen, &msgPtr, &msgLen ) != kNoErr ) continue;
+ if( msgLen < kDNSHeaderLength ) continue;
+
+ hdr = (const DNSHeader *) msgPtr;
+ flags = DNSHeaderGetFlags( hdr );
+ if( DNSFlagsGetOpCode( flags ) != kDNSOpCode_Query ) continue;
+ if( DNSHeaderGetQuestionCount( hdr ) < 1 ) continue;
+
+ ptr = (const uint8_t *) &hdr[ 1 ];
+ if( DNSMessageExtractQuestion( msgPtr, msgLen, ptr, qname, &qtype, &qclass, NULL ) != kNoErr ) continue;
+ if( qclass != kDNSServiceClass_IN ) continue;
+ if( !DomainNameEqual( qname, name ) ) continue;
+
+ if( item->wantV4 && ( qtype == kDNSServiceType_A ) )
+ {
+ if( flags & kDNSHeaderFlag_Response )
+ {
+ if( tsQA && !tsRA )
+ {
+ tsRA = tsPtr++;
+ *tsRA = pktHdr->ts;
+ }
+ }
+ else if( !tsQA )
+ {
+ tsQA = tsPtr++;
+ *tsQA = pktHdr->ts;
+ }
+ }
+ else if( item->wantV6 && ( qtype == kDNSServiceType_AAAA ) )
+ {
+ if( flags & kDNSHeaderFlag_Response )
+ {
+ if( tsQAAAA && !tsRAAAA )
+ {
+ tsRAAAA = tsPtr++;
+ *tsRAAAA = pktHdr->ts;
+ }
+ }
+ else if( !tsQAAAA )
+ {
+ tsQAAAA = tsPtr++;
+ *tsQAAAA = pktHdr->ts;
+ }
+ }
+ }
+
+ // t1 is the time when the last query was sent.
+
+ if( tsQA && tsQAAAA ) t1 = TIMEVAL_GT( *tsQA, *tsQAAAA ) ? tsQA : tsQAAAA;
+ else t1 = tsQA ? tsQA : tsQAAAA;
+
+ // t2 is when the first response was received.
+
+ if( tsRA && tsRAAAA ) t2 = TIMEVAL_LT( *tsRA, *tsRAAAA ) ? tsRA : tsRAAAA;
+ else t2 = tsRA ? tsRA : tsRAAAA;
+
+ if( t1 && t2 )
+ {
+ idleTimeUs = TIMEVAL_USEC64_DIFF( *t2, *t1 );
+ if( idleTimeUs < 0 ) idleTimeUs = 0;
+ }
+ else
+ {
+ idleTimeUs = 0;
+ }
+
+ item->connectionTimeUs = UpTicksToMicroseconds( me->connTicks - me->startTicks );
+ item->firstTimeUs = UpTicksToMicroseconds( me->firstTicks - me->connTicks ) - (uint64_t) idleTimeUs;
+ item->timeUs = UpTicksToMicroseconds( me->endTicks - me->connTicks ) - (uint64_t) idleTimeUs;
+
+exit:
+ ForgetPacketCapture( &me->pcap );
+ if( err ) _GAITesterStop( me, err );
+ else _GAITesterStartNextTest( me );
+}
+
+//===========================================================================================================================
+// _GAITesterGetDNSMessageFromPacket
+//===========================================================================================================================
+
+#define kHeaderSizeNullLink 4
+#define kHeaderSizeIPv4Min 20
+#define kHeaderSizeIPv6 40
+#define kHeaderSizeUDP 8
+
+#define kIPProtocolUDP 0x11
+
+static OSStatus
+ _GAITesterGetDNSMessageFromPacket(
+ const uint8_t * inPacketPtr,
+ size_t inPacketLen,
+ const uint8_t ** outMsgPtr,
+ size_t * outMsgLen )
+{
+ OSStatus err;
+ const uint8_t * nullLink;
+ uint32_t addressFamily;
+ const uint8_t * ip;
+ int ipHeaderLen;
+ int protocol;
+ const uint8_t * msg;
+ const uint8_t * const end = &inPacketPtr[ inPacketLen ];
+
+ nullLink = &inPacketPtr[ 0 ];
+ require_action_quiet( ( end - nullLink ) >= kHeaderSizeNullLink, exit, err = kUnderrunErr );
+ addressFamily = ReadHost32( &nullLink[ 0 ] );
+
+ ip = &nullLink[ kHeaderSizeNullLink ];
+ if( addressFamily == AF_INET )
+ {
+ require_action_quiet( ( end - ip ) >= kHeaderSizeIPv4Min, exit, err = kUnderrunErr );
+ ipHeaderLen = ( ip[ 0 ] & 0x0F ) * 4;
+ protocol = ip[ 9 ];
+ }
+ else if( addressFamily == AF_INET6 )
+ {
+ require_action_quiet( ( end - ip ) >= kHeaderSizeIPv6, exit, err = kUnderrunErr );
+ ipHeaderLen = kHeaderSizeIPv6;
+ protocol = ip[ 6 ];
+ }
+ else
+ {
+ err = kTypeErr;
+ goto exit;
+ }
+ require_action_quiet( protocol == kIPProtocolUDP, exit, err = kTypeErr );
+ require_action_quiet( ( end - ip ) >= ( ipHeaderLen + kHeaderSizeUDP ), exit, err = kUnderrunErr );
+
+ msg = &ip[ ipHeaderLen + kHeaderSizeUDP ];
+
+ *outMsgPtr = msg;
+ *outMsgLen = (size_t)( end - msg );
+ err = kNoErr;
+
+exit:
+ return( err );
+}
+
+//===========================================================================================================================
+// GAITestCaseCreate
+//===========================================================================================================================
+
+static OSStatus GAITestCaseCreate( const char *inTitle, GAITestCase **outCase )
+{
+ OSStatus err;
+ GAITestCase * obj;
+
+ obj = (GAITestCase *) calloc( 1, sizeof( *obj ) );
+ require_action( obj, exit, err = kNoMemoryErr );
+
+ obj->title = strdup( inTitle );
+ require_action( obj->title, exit, err = kNoMemoryErr );
+
+ *outCase = obj;
+ obj = NULL;
+ err = kNoErr;
+
+exit:
+ if( obj ) GAITestCaseFree( obj );
+ return( err );
+}
+
+//===========================================================================================================================
+// GAITestCaseFree
+//===========================================================================================================================
+
+static void GAITestCaseFree( GAITestCase *inCase )
+{
+ GAITestItem * item;
+
+ while( ( item = inCase->itemList ) != NULL )
+ {
+ inCase->itemList = item->next;
+ GAITestItemFree( item );
+ }
+ ForgetMem( &inCase->title );
+ free( inCase );
+}
+
+//===========================================================================================================================
+// GAITestCaseAddItem
+//===========================================================================================================================
+
+static OSStatus
+ GAITestCaseAddItem(
+ GAITestCase * inCase,
+ unsigned int inAliasCount,
+ unsigned int inAddressCount,
+ int inTTL,
+ GAITestAddrType inHasAddrs,
+ GAITestAddrType inWantAddrs,
+ unsigned int inTimeLimitMs,
+ unsigned int inItemCount )
+{
+ OSStatus err;
+ GAITestItem * item;
+ GAITestItem * item2;
+ GAITestItem * newItemList = NULL;
+ GAITestItem ** itemPtr;
+ char * ptr;
+ char * end;
+ unsigned int i;
+ char name[ 64 ];
+ char tag[ kGAITesterTagStringLen + 1 ];
+
+ require_action_quiet( inItemCount > 0, exit, err = kNoErr );
+
+ // Limit address count to 64 because we use 64-bit bitmaps for keeping track of addresses.
+
+ require_action_quiet( ( inAddressCount >= 1 ) && ( inAddressCount <= 64 ), exit, err = kCountErr );
+ require_action_quiet( ( inAliasCount >= 0 ) && ( inAliasCount <= INT32_MAX ), exit, err = kCountErr );
+ require_action_quiet( GAITestAddrTypeIsValid( inHasAddrs ), exit, err = kValueErr );
+
+ ptr = &name[ 0 ];
+ end = &name[ countof( name ) ];
+
+ // Add Alias label.
+
+ if( inAliasCount == 1 ) SNPrintF_Add( &ptr, end, "alias." );
+ else if( inAliasCount >= 2 ) SNPrintF_Add( &ptr, end, "alias-%u.", inAliasCount );
+
+ // Add Count label.
+
+ SNPrintF_Add( &ptr, end, "count-%u.", inAddressCount );
+
+ // Add TTL label.
+
+ if( inTTL >= 0 ) SNPrintF_Add( &ptr, end, "ttl-%d.", inTTL );
+
+ // Add Tag label.
+
+ SNPrintF_Add( &ptr, end, "tag-%s.",
+ _RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
+
+ // Add IPv4 or IPv6 label if necessary.
+
+ if( inHasAddrs == kGAITestAddrType_IPv4 ) SNPrintF_Add( &ptr, end, "ipv4." );
+ else if( inHasAddrs == kGAITestAddrType_IPv6 ) SNPrintF_Add( &ptr, end, "ipv6." );
+
+ // Finally, add the d.test. labels.
+
+ SNPrintF_Add( &ptr, end, "d.test." );
+
+ // Create item.
+
+ err = GAITestItemCreate( name, inAddressCount, inHasAddrs, inWantAddrs, inTimeLimitMs, &item );
+ require_noerr( err, exit );
+
+ newItemList = item;
+ itemPtr = &item->next;
+
+ // Create repeat items.
+
+ for( i = 1; i < inItemCount; ++i )
+ {
+ err = GAITestItemDup( item, &item2 );
+ require_noerr( err, exit );
+
+ *itemPtr = item2;
+ itemPtr = &item2->next;
+ }
+
+ // Append to test case's item list.
+
+ for( itemPtr = &inCase->itemList; *itemPtr; itemPtr = &( *itemPtr )->next ) {}
+ *itemPtr = newItemList;
+ newItemList = NULL;
+
+exit:
+ while( ( item = newItemList ) != NULL )
+ {
+ newItemList = item->next;
+ GAITestItemFree( item );
+ }
+ return( err );
+}
+
+//===========================================================================================================================
+// GAITestCaseAddLocalHostItem
+//===========================================================================================================================
+
+static OSStatus
+ GAITestCaseAddLocalHostItem(
+ GAITestCase * inCase,
+ GAITestAddrType inWantAddrs,
+ unsigned int inTimeLimitMs,
+ unsigned int inItemCount )
+{
+ OSStatus err;
+ GAITestItem * item;
+ GAITestItem * item2;
+ GAITestItem * newItemList = NULL;
+ GAITestItem ** itemPtr;
+ unsigned int i;
+
+ require_action_quiet( inItemCount > 1, exit, err = kNoErr );
+
+ err = GAITestItemCreate( "localhost.", 1, kGAITestAddrType_Both, inWantAddrs, inTimeLimitMs, &item );
+ require_noerr( err, exit );
+
+ newItemList = item;
+ itemPtr = &item->next;
+
+ // Create repeat items.
+
+ for( i = 1; i < inItemCount; ++i )
+ {
+ err = GAITestItemDup( item, &item2 );
+ require_noerr( err, exit );
+
+ *itemPtr = item2;
+ itemPtr = &item2->next;
+ }
+
+ for( itemPtr = &inCase->itemList; *itemPtr; itemPtr = &( *itemPtr )->next ) {}
+ *itemPtr = newItemList;
+ newItemList = NULL;
+
+exit:
+ while( ( item = newItemList ) != NULL )
+ {
+ newItemList = item->next;
+ GAITestItemFree( item );
+ }
+ return( err );
+}
+
+//===========================================================================================================================
+// GAITestItemCreate
+//===========================================================================================================================
+
+static OSStatus
+ GAITestItemCreate(
+ const char * inName,
+ unsigned int inAddressCount,
+ GAITestAddrType inHasAddrs,
+ GAITestAddrType inWantAddrs,
+ unsigned int inTimeLimitMs,
+ GAITestItem ** outItem )
+{
+ OSStatus err;
+ GAITestItem * obj = NULL;
+
+ require_action_quiet( inAddressCount >= 1, exit, err = kCountErr );
+ require_action_quiet( GAITestAddrTypeIsValid( inHasAddrs ), exit, err = kValueErr );
+ require_action_quiet( GAITestAddrTypeIsValid( inWantAddrs ), exit, err = kValueErr );
+
+ obj = (GAITestItem *) calloc( 1, sizeof( *obj ) );
+ require_action( obj, exit, err = kNoMemoryErr );
+
+ obj->name = strdup( inName );
+ require_action( obj->name, exit, err = kNoMemoryErr );
+
+ obj->addressCount = inAddressCount;
+ obj->hasV4 = ( inHasAddrs & kGAITestAddrType_IPv4 ) ? true : false;
+ obj->hasV6 = ( inHasAddrs & kGAITestAddrType_IPv6 ) ? true : false;
+ obj->wantV4 = ( inWantAddrs & kGAITestAddrType_IPv4 ) ? true : false;
+ obj->wantV6 = ( inWantAddrs & kGAITestAddrType_IPv6 ) ? true : false;
+ obj->error = kInProgressErr;
+ obj->timeLimitMs = inTimeLimitMs;
+
+ *outItem = obj;
+ obj = NULL;
+ err = kNoErr;
+
+exit:
+ if( obj ) GAITestItemFree( obj );
+ return( err );
+}
+
+//===========================================================================================================================
+// GAITestItemDup
+//===========================================================================================================================
+
+static OSStatus GAITestItemDup( const GAITestItem *inItem, GAITestItem **outItem )
+{
+ OSStatus err;
+ GAITestItem * obj;
+
+ obj = (GAITestItem *) calloc( 1, sizeof( *obj ) );
+ require_action( obj, exit, err = kNoMemoryErr );
+
+ *obj = *inItem;
+ obj->next = NULL;
+ if( inItem->name )
+ {
+ obj->name = strdup( inItem->name );
+ require_action( obj->name, exit, err = kNoMemoryErr );
+ }
+
+ *outItem = obj;
+ obj = NULL;
+ err = kNoErr;
+
+exit:
+ if( obj ) GAITestItemFree( obj );
+ return( err );
+}
+
+//===========================================================================================================================
+// GAITestItemFree
+//===========================================================================================================================
+
+static void GAITestItemFree( GAITestItem *inItem )
+{
+ ForgetMem( &inItem->name );
+ free( inItem );
+}
+
+//===========================================================================================================================
+// MDNSDiscoveryTestCmd
+//===========================================================================================================================
+
+#define kMDNSDiscoveryTestFirstQueryTimeoutSecs 4
+
+typedef struct
+{
+ DNSServiceRef query; // Reference to DNSServiceQueryRecord for replier's "about" TXT record.
+ dispatch_source_t queryTimer; // Used to time out the "about" TXT record query.
+ NanoTime64 startTime; // When the test started.
+ NanoTime64 endTime; // When the test ended.
+ pid_t replierPID; // PID of mDNS replier.
+ uint32_t ifIndex; // Index of interface to run the replier on.
+ unsigned int instanceCount; // Desired number of service instances.
+ unsigned int txtSize; // Desired size of each service instance's TXT record data.
+ unsigned int recordCountA; // Desired number of A records per replier hostname.
+ unsigned int recordCountAAAA; // Desired number of AAAA records per replier hostname.
+ unsigned int maxDropCount; // Replier's --maxDropCount option argument.
+ double ucastDropRate; // Replier's probability of dropping a unicast response.
+ double mcastDropRate; // Replier's probability of dropping a multicast query or response.
+ Boolean noAdditionals; // True if the replier is to not include additional records in responses.
+ Boolean useIPv4; // True if the replier is to use IPv4.
+ Boolean useIPv6; // True if the replier is to use IPv6.
+ Boolean flushedCache; // True if mDNSResponder's record cache was flushed before testing.
+ char * replierCommand; // Command used to run the replier.
+ char * serviceType; // Type of services to browse for.
+ ServiceBrowserRef browser; // Service browser.
+ unsigned int browseTimeSecs; // Amount of time to spend browsing in seconds.
+ const char * outputFilePath; // File to write test results to. If NULL, then write to stdout.
+ OutputFormatType outputFormat; // Format of test results output.
+ Boolean outputAppendNewline; // True if a newline character should be appended to JSON output.
+ char hostname[ 32 + 1 ]; // Base hostname that the replier is to use for instance and host names.
+ char tag[ 4 + 1 ]; // Tag that the replier is to use in its service types.
+
+} MDNSDiscoveryTestContext;
+
+static OSStatus GetAnyMDNSInterface( char inNameBuf[ IF_NAMESIZE + 1 ], uint32_t *outIndex );
+static void _MDNSDiscoveryTestFirstQueryTimeout( void *inContext );
+static void DNSSD_API
+ _MDNSDiscoveryTestAboutQueryCallback(
+ DNSServiceRef inSDRef,
+ DNSServiceFlags inFlags,
+ uint32_t inInterfaceIndex,
+ DNSServiceErrorType inError,
+ const char * inFullName,
+ uint16_t inType,
+ uint16_t inClass,
+ uint16_t inRDataLen,
+ const void * inRDataPtr,
+ uint32_t inTTL,
+ void * inContext );
+static void
+ _MDNSDiscoveryTestServiceBrowserCallback(
+ ServiceBrowserResults * inResults,
+ OSStatus inError,
+ void * inContext );
+static Boolean _MDNSDiscoveryTestTXTRecordIsValid( const uint8_t *inRecordName, const uint8_t *inTXTPtr, size_t inTXTLen );
+
+static void MDNSDiscoveryTestCmd( void )
+{
+ OSStatus err;
+ MDNSDiscoveryTestContext * context;
+ char queryName[ sizeof_field( MDNSDiscoveryTestContext, hostname ) + 15 ];
+
+ context = (MDNSDiscoveryTestContext *) calloc( 1, sizeof( *context ) );
+ require_action( context, exit, err = kNoMemoryErr );
+
+ err = CheckIntegerArgument( gMDNSDiscoveryTest_InstanceCount, "instance count", 1, UINT16_MAX );
+ require_noerr_quiet( err, exit );
+
+ err = CheckIntegerArgument( gMDNSDiscoveryTest_TXTSize, "TXT size", 1, UINT16_MAX );
+ require_noerr_quiet( err, exit );
+
+ err = CheckIntegerArgument( gMDNSDiscoveryTest_BrowseTimeSecs, "browse time (seconds)", 1, INT_MAX );
+ require_noerr_quiet( err, exit );
+
+ err = CheckIntegerArgument( gMDNSDiscoveryTest_RecordCountA, "A record count", 0, 64 );
+ require_noerr_quiet( err, exit );
+
+ err = CheckIntegerArgument( gMDNSDiscoveryTest_RecordCountAAAA, "AAAA record count", 0, 64 );
+ require_noerr_quiet( err, exit );
+
+ err = CheckDoubleArgument( gMDNSDiscoveryTest_UnicastDropRate, "unicast drop rate", 0.0, 1.0 );
+ require_noerr_quiet( err, exit );
+
+ err = CheckDoubleArgument( gMDNSDiscoveryTest_MulticastDropRate, "multicast drop rate", 0.0, 1.0 );
+ require_noerr_quiet( err, exit );
+
+ err = CheckIntegerArgument( gMDNSDiscoveryTest_MaxDropCount, "drop count", 0, 255 );
+ require_noerr_quiet( err, exit );
+
+ context->replierPID = -1;
+ context->instanceCount = (unsigned int) gMDNSDiscoveryTest_InstanceCount;
+ context->txtSize = (unsigned int) gMDNSDiscoveryTest_TXTSize;
+ context->browseTimeSecs = (unsigned int) gMDNSDiscoveryTest_BrowseTimeSecs;
+ context->recordCountA = (unsigned int) gMDNSDiscoveryTest_RecordCountA;
+ context->recordCountAAAA = (unsigned int) gMDNSDiscoveryTest_RecordCountAAAA;
+ context->ucastDropRate = gMDNSDiscoveryTest_UnicastDropRate;
+ context->mcastDropRate = gMDNSDiscoveryTest_MulticastDropRate;
+ context->maxDropCount = (unsigned int) gMDNSDiscoveryTest_MaxDropCount;
+ context->outputFilePath = gMDNSDiscoveryTest_OutputFilePath;
+ context->outputAppendNewline = gMDNSDiscoveryTest_OutputAppendNewline ? true : false;
+ context->noAdditionals = gMDNSDiscoveryTest_NoAdditionals ? true : false;
+ context->useIPv4 = ( gMDNSDiscoveryTest_UseIPv4 || !gMDNSDiscoveryTest_UseIPv6 ) ? true : false;
+ context->useIPv6 = ( gMDNSDiscoveryTest_UseIPv6 || !gMDNSDiscoveryTest_UseIPv4 ) ? true : false;
+
+ if( gMDNSDiscoveryTest_Interface )
+ {
+ err = InterfaceIndexFromArgString( gMDNSDiscoveryTest_Interface, &context->ifIndex );
+ require_noerr_quiet( err, exit );
+ }
+ else
+ {
+ err = GetAnyMDNSInterface( NULL, &context->ifIndex );
+ require_noerr_quiet( err, exit );
+ }
+
+ context->outputFormat = (OutputFormatType) CLIArgToValue( "format", gMDNSDiscoveryTest_OutputFormat, &err,
+ kOutputFormatStr_JSON, kOutputFormatType_JSON,
+ kOutputFormatStr_XML, kOutputFormatType_XML,
+ kOutputFormatStr_Binary, kOutputFormatType_Binary,
+ NULL );
+ require_noerr_quiet( err, exit );
+
+ if( gMDNSDiscoveryTest_FlushCache )
+ {
+ err = CheckRootUser();
+ require_noerr_quiet( err, exit );
+
+ err = systemf( NULL, "killall -HUP mDNSResponder" );
+ require_noerr( err, exit );
+ sleep( 1 );
+ context->flushedCache = true;
+ }
+
+ _RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( context->hostname ) - 1,
+ context->hostname );
+ _RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( context->tag ) - 1, context->tag );
+
+ ASPrintF( &context->serviceType, "_t-%s-%u-%u._tcp", context->tag, context->txtSize, context->instanceCount );
+ require_action( context->serviceType, exit, err = kUnknownErr );
+
+ err = ASPrintF( &context->replierCommand,
+ "dnssdutil mdnsreplier --follow %lld --interface %u --hostname %s --tag %s --maxInstanceCount %u "
+ "--countA %u --countAAAA %u --udrop %.1f --mdrop %.1f --maxDropCount %u %?s%?s%?s",
+ (int64_t) getpid(),
+ context->ifIndex,
+ context->hostname,
+ context->tag,
+ context->instanceCount,
+ context->recordCountA,
+ context->recordCountAAAA,
+ context->ucastDropRate,
+ context->mcastDropRate,
+ context->maxDropCount,
+ context->noAdditionals, " --noAdditionals",
+ context->useIPv4, " --ipv4",
+ context->useIPv6, " --ipv6" );
+ require_action_quiet( context->replierCommand, exit, err = kUnknownErr );
+
+ err = SpawnCommand( &context->replierPID, "%s", context->replierCommand );
+ require_noerr_quiet( err, exit );
+
+ // Query for the replier's about TXT record. A response means it's fully up and running.
+
+ SNPrintF( queryName, sizeof( queryName ), "about.%s.local.", context->hostname );
+ err = DNSServiceQueryRecord( &context->query, kDNSServiceFlagsForceMulticast, context->ifIndex, queryName,
+ kDNSServiceType_TXT, kDNSServiceClass_IN, _MDNSDiscoveryTestAboutQueryCallback, context );
+ require_noerr( err, exit );
+
+ err = DNSServiceSetDispatchQueue( context->query, dispatch_get_main_queue() );
+ require_noerr( err, exit );
+
+ err = DispatchTimerCreate( dispatch_time_seconds( kMDNSDiscoveryTestFirstQueryTimeoutSecs ),
+ DISPATCH_TIME_FOREVER, UINT64_C_safe( kMDNSDiscoveryTestFirstQueryTimeoutSecs ) * kNanosecondsPerSecond / 10, NULL,
+ _MDNSDiscoveryTestFirstQueryTimeout, NULL, context, &context->queryTimer );
+ require_noerr( err, exit );
+ dispatch_resume( context->queryTimer );
+
+ context->startTime = NanoTimeGetCurrent();
+ dispatch_main();
+
+exit:
+ exit( 1 );
+}
+
+//===========================================================================================================================
+// _MDNSDiscoveryTestFirstQueryTimeout
+//===========================================================================================================================
+
+static void _MDNSDiscoveryTestFirstQueryTimeout( void *inContext )
+{
+ MDNSDiscoveryTestContext * const context = (MDNSDiscoveryTestContext *) inContext;
+
+ dispatch_source_forget( &context->queryTimer );
+
+ FPrintF( stderr, "error: Query for mdnsreplier's \"about\" TXT record timed out.\n" );
+ exit( 1 );
+}
+
+//===========================================================================================================================
+// _MDNSDiscoveryTestAboutQueryCallback
+//===========================================================================================================================
+
+static void DNSSD_API
+ _MDNSDiscoveryTestAboutQueryCallback(
+ DNSServiceRef inSDRef,
+ DNSServiceFlags inFlags,
+ uint32_t inInterfaceIndex,
+ DNSServiceErrorType inError,
+ const char * inFullName,
+ uint16_t inType,
+ uint16_t inClass,
+ uint16_t inRDataLen,
+ const void * inRDataPtr,
+ uint32_t inTTL,
+ void * inContext )
+{
+ OSStatus err;
+ MDNSDiscoveryTestContext * const context = (MDNSDiscoveryTestContext *) inContext;
+
+ Unused( inSDRef );
+ Unused( inInterfaceIndex );
+ Unused( inFullName );
+ Unused( inType );
+ Unused( inClass );
+ Unused( inRDataLen );
+ Unused( inRDataPtr );
+ Unused( inTTL );
+
+ err = inError;
+ require_noerr( err, exit );
+ require_quiet( inFlags & kDNSServiceFlagsAdd, exit );
+
+ DNSServiceForget( &context->query );
+ dispatch_source_forget( &context->queryTimer );
+
+ err = ServiceBrowserCreate( dispatch_get_main_queue(), 0, "local.", context->browseTimeSecs, false, &context->browser );
+ require_noerr( err, exit );
+
+ err = ServiceBrowserAddServiceType( context->browser, context->serviceType );
+ require_noerr( err, exit );
+
+ ServiceBrowserSetCallback( context->browser, _MDNSDiscoveryTestServiceBrowserCallback, context );
+ ServiceBrowserStart( context->browser );
+
+exit:
+ if( err ) exit( 1 );
+}
+
+//===========================================================================================================================
+// _MDNSDiscoveryTestServiceBrowserCallback
+//===========================================================================================================================
+
+#define kMDNSDiscoveryTestResultsKey_ReplierInfo CFSTR( "replierInfo" )
+#define kMDNSDiscoveryTestResultsKey_StartTime CFSTR( "startTime" )
+#define kMDNSDiscoveryTestResultsKey_EndTime CFSTR( "endTime" )
+#define kMDNSDiscoveryTestResultsKey_BrowseTimeSecs CFSTR( "browseTimeSecs" )
+#define kMDNSDiscoveryTestResultsKey_ServiceType CFSTR( "serviceType" )
+#define kMDNSDiscoveryTestResultsKey_FlushedCache CFSTR( "flushedCache" )
+#define kMDNSDiscoveryTestResultsKey_UnexpectedInstances CFSTR( "unexpectedInstances" )
+#define kMDNSDiscoveryTestResultsKey_MissingInstances CFSTR( "missingInstances" )
+#define kMDNSDiscoveryTestResultsKey_IncorrectInstances CFSTR( "incorrectInstances" )
+#define kMDNSDiscoveryTestResultsKey_Success CFSTR( "success" )
+#define kMDNSDiscoveryTestResultsKey_TotalResolveTime CFSTR( "totalResolveTimeUs" )
+
+#define kMDNSDiscoveryTestReplierInfoKey_Command CFSTR( "command" )
+#define kMDNSDiscoveryTestReplierInfoKey_InstanceCount CFSTR( "instanceCount" )
+#define kMDNSDiscoveryTestReplierInfoKey_TXTSize CFSTR( "txtSize" )
+#define kMDNSDiscoveryTestReplierInfoKey_RecordCountA CFSTR( "recordCountA" )
+#define kMDNSDiscoveryTestReplierInfoKey_RecordCountAAAA CFSTR( "recordCountAAAA" )
+#define kMDNSDiscoveryTestReplierInfoKey_Hostname CFSTR( "hostname" )
+#define kMDNSDiscoveryTestReplierInfoKey_NoAdditionals CFSTR( "noAdditionals" )
+#define kMDNSDiscoveryTestReplierInfoKey_UnicastDropRate CFSTR( "ucastDropRate" )
+#define kMDNSDiscoveryTestReplierInfoKey_MulticastDropRate CFSTR( "mcastDropRate" )
+#define kMDNSDiscoveryTestReplierInfoKey_MaxDropCount CFSTR( "maxDropCount" )
+
+#define kMDNSDiscoveryTestUnexpectedInstanceKey_Name CFSTR( "name" )
+#define kMDNSDiscoveryTestUnexpectedInstanceKey_InterfaceIndex CFSTR( "interfaceIndex" )
+
+#define kMDNSDiscoveryTestIncorrectInstanceKey_Name CFSTR( "name" )
+#define kMDNSDiscoveryTestIncorrectInstanceKey_DidResolve CFSTR( "didResolve" )
+#define kMDNSDiscoveryTestIncorrectInstanceKey_BadHostname CFSTR( "badHostname" )
+#define kMDNSDiscoveryTestIncorrectInstanceKey_BadPort CFSTR( "badPort" )
+#define kMDNSDiscoveryTestIncorrectInstanceKey_BadTXT CFSTR( "badTXT" )
+#define kMDNSDiscoveryTestIncorrectInstanceKey_UnexpectedAddrs CFSTR( "unexpectedAddrs" )
+#define kMDNSDiscoveryTestIncorrectInstanceKey_MissingAddrs CFSTR( "missingAddrs" )
+
+static void _MDNSDiscoveryTestServiceBrowserCallback( ServiceBrowserResults *inResults, OSStatus inError, void *inContext )
+{
+ OSStatus err;
+ MDNSDiscoveryTestContext * const context = (MDNSDiscoveryTestContext *) inContext;
+ const SBRDomain * domain;
+ const SBRServiceType * type;
+ const SBRServiceInstance * instance;
+ const SBRServiceInstance ** instanceArray = NULL;
+ const SBRIPAddress * ipaddr;
+ size_t hostnameLen;
+ const char * ptr;
+ const char * end;
+ unsigned int i;
+ uint32_t u32;
+ CFMutableArrayRef unexpectedInstances;
+ CFMutableArrayRef missingInstances;
+ CFMutableArrayRef incorrectInstances;
+ CFMutableDictionaryRef plist = NULL;
+ CFMutableDictionaryRef badDict = NULL;
+ CFMutableArrayRef unexpectedAddrs = NULL;
+ CFMutableArrayRef missingAddrs = NULL;
+ uint64_t maxResolveTimeUs;
+ int success = false;
+ char startTimeStr[ 32 ];
+ char endTimeStr[ 32 ];
+
+ context->endTime = NanoTimeGetCurrent();
+
+ err = inError;
+ require_noerr( err, exit );
+
+ _NanoTime64ToDateString( context->startTime, startTimeStr, sizeof( startTimeStr ) );
+ _NanoTime64ToDateString( context->endTime, endTimeStr, sizeof( endTimeStr ) );
+ err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &plist,
+ "{"
+ "%kO="
+ "{"
+ "%kO=%s" // replierCommand
+ "%kO=%lli" // txtSize
+ "%kO=%lli" // instanceCount
+ "%kO=%lli" // recordCountA
+ "%kO=%lli" // recordCountAAAA
+ "%kO=%s" // hostname
+ "%kO=%b" // noAdditionals
+ "%kO=%f" // ucastDropRate
+ "%kO=%f" // mcastDropRate
+ "%kO=%i" // maxDropCount
+ "}"
+ "%kO=%s" // startTime
+ "%kO=%s" // endTime
+ "%kO=%lli" // browseTimeSecs
+ "%kO=%s" // serviceType
+ "%kO=%b" // flushedCache
+ "%kO=[%@]" // unexpectedInstances
+ "%kO=[%@]" // missingInstances
+ "%kO=[%@]" // incorrectInstances
+ "}",
+ kMDNSDiscoveryTestResultsKey_ReplierInfo,
+ kMDNSDiscoveryTestReplierInfoKey_Command, context->replierCommand,
+ kMDNSDiscoveryTestReplierInfoKey_InstanceCount, (int64_t) context->instanceCount,
+ kMDNSDiscoveryTestReplierInfoKey_TXTSize, (int64_t) context->txtSize,
+ kMDNSDiscoveryTestReplierInfoKey_RecordCountA, (int64_t) context->recordCountA,
+ kMDNSDiscoveryTestReplierInfoKey_RecordCountAAAA, (int64_t) context->recordCountAAAA,
+ kMDNSDiscoveryTestReplierInfoKey_Hostname, context->hostname,
+ kMDNSDiscoveryTestReplierInfoKey_NoAdditionals, context->noAdditionals,
+ kMDNSDiscoveryTestReplierInfoKey_UnicastDropRate, context->ucastDropRate,
+ kMDNSDiscoveryTestReplierInfoKey_MulticastDropRate, context->mcastDropRate,
+ kMDNSDiscoveryTestReplierInfoKey_MaxDropCount, context->maxDropCount,
+ kMDNSDiscoveryTestResultsKey_StartTime, startTimeStr,
+ kMDNSDiscoveryTestResultsKey_EndTime, endTimeStr,
+ kMDNSDiscoveryTestResultsKey_BrowseTimeSecs, (int64_t) context->browseTimeSecs,
+ kMDNSDiscoveryTestResultsKey_ServiceType, context->serviceType,
+ kMDNSDiscoveryTestResultsKey_FlushedCache, context->flushedCache,
+ kMDNSDiscoveryTestResultsKey_UnexpectedInstances, &unexpectedInstances,
+ kMDNSDiscoveryTestResultsKey_MissingInstances, &missingInstances,
+ kMDNSDiscoveryTestResultsKey_IncorrectInstances, &incorrectInstances );
+ require_noerr( err, exit );
+
+ for( domain = inResults->domainList; domain && ( strcasecmp( domain->name, "local.") != 0 ); domain = domain->next ) {}
+ require_action( domain, exit, err = kInternalErr );
+
+ for( type = domain->typeList; type && ( strcasecmp( type->name, context->serviceType ) != 0 ); type = type->next ) {}
+ require_action( type, exit, err = kInternalErr );
+
+ instanceArray = (const SBRServiceInstance **) calloc( context->instanceCount, sizeof( *instanceArray ) );
+ require_action( instanceArray, exit, err = kNoMemoryErr );
+
+ hostnameLen = strlen( context->hostname );
+ for( instance = type->instanceList; instance; instance = instance->next )
+ {
+ unsigned int instanceNumber = 0;
+
+ if( strcmp_prefix( instance->name, context->hostname ) == 0 )
+ {
+ ptr = &instance->name[ hostnameLen ];
+ if( ( ptr[ 0 ] == ' ' ) && ( ptr[ 1 ] == '(' ) )
+ {
+ ptr += 2;
+ for( end = ptr; isdigit_safe( *end ); ++end ) {}
+ if( DecimalTextToUInt32( ptr, end, &u32, &ptr ) == kNoErr )
+ {
+ if( ( u32 >= 2 ) && ( u32 <= context->instanceCount ) && ( ptr[ 0 ] == ')' ) && ( ptr[ 1 ] == '\0' ) )
+ {
+ instanceNumber = u32;
+ }
+ }
+ }
+ else if( *ptr == '\0' )
+ {
+ instanceNumber = 1;
+ }
+ }
+ if( ( instanceNumber != 0 ) && ( instance->ifIndex == context->ifIndex ) )
+ {
+ check( !instanceArray[ instanceNumber - 1 ] );
+ instanceArray[ instanceNumber - 1 ] = instance;
+ }
+ else
+ {
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, unexpectedInstances,
+ "{"
+ "%kO=%s"
+ "%kO=%lli"
+ "}",
+ kMDNSDiscoveryTestUnexpectedInstanceKey_Name, instance->name,
+ kMDNSDiscoveryTestUnexpectedInstanceKey_InterfaceIndex, (int64_t) instance->ifIndex );
+ require_noerr( err, exit );
+ }
+ }
+
+ maxResolveTimeUs = 0;
+ for( i = 1; i <= context->instanceCount; ++i )
+ {
+ int isHostnameValid;
+ int isTXTValid;
+
+ instance = instanceArray[ i - 1 ];
+ if( !instance )
+ {
+ if( i == 1 )
+ {
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, missingInstances, "%s", context->hostname );
+ require_noerr( err, exit );
+ }
+ else
+ {
+ char * instanceName = NULL;
+
+ ASPrintF( &instanceName, "%s (%u)", context->hostname, i );
+ require_action( instanceName, exit, err = kUnknownErr );
+
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, missingInstances, "%s", instanceName );
+ free( instanceName );
+ require_noerr( err, exit );
+ }
+ continue;
+ }
+
+ if( !instance->hostname )
+ {
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, incorrectInstances,
+ "{"
+ "%kO=%s"
+ "%kO=%b"
+ "}",
+ kMDNSDiscoveryTestIncorrectInstanceKey_Name, instance->name,
+ kMDNSDiscoveryTestIncorrectInstanceKey_DidResolve, false );
+ require_noerr( err, exit );
+ continue;
+ }
+
+ badDict = CFDictionaryCreateMutable( NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks );
+ require_action( badDict, exit, err = kNoMemoryErr );
+
+ isHostnameValid = false;
+ if( strcmp_prefix( instance->hostname, context->hostname ) == 0 )
+ {
+ ptr = &instance->hostname[ hostnameLen ];
+ if( i == 1 )
+ {
+ if( strcmp( ptr, ".local." ) == 0 ) isHostnameValid = true;
+ }
+ else if( *ptr == '-' )
+ {
+ ++ptr;
+ for( end = ptr; isdigit_safe( *end ); ++end ) {}
+ if( DecimalTextToUInt32( ptr, end, &u32, &ptr ) == kNoErr )
+ {
+ if( ( u32 == i ) && ( strcmp( ptr, ".local." ) == 0 ) ) isHostnameValid = true;
+ }
+ }
+ }
+ if( !isHostnameValid )
+ {
+ err = CFDictionarySetCString( badDict, kMDNSDiscoveryTestIncorrectInstanceKey_BadHostname, instance->hostname,
+ kSizeCString );
+ require_noerr( err, exit );
+ }
+
+ if( instance->port != (uint16_t)( kMDNSReplierPortBase + context->txtSize ) )
+ {
+ err = CFDictionarySetInt64( badDict, kMDNSDiscoveryTestIncorrectInstanceKey_BadPort, instance->port );
+ require_noerr( err, exit );
+ }
+
+ isTXTValid = false;
+ if( instance->txtLen == context->txtSize )
+ {
+ uint8_t name[ kDomainNameLengthMax ];
+
+ err = DomainNameFromString( name, instance->name, NULL );
+ require_noerr( err, exit );
+
+ err = DomainNameAppendString( name, type->name, NULL );
+ require_noerr( err, exit );
+
+ err = DomainNameAppendString( name, "local", NULL );
+ require_noerr( err, exit );
+
+ if( _MDNSDiscoveryTestTXTRecordIsValid( name, instance->txtPtr, instance->txtLen ) ) isTXTValid = true;
+ }
+ if( !isTXTValid )
+ {
+ char * hexStr = NULL;
+
+ ASPrintF( &hexStr, "%.4H", instance->txtPtr, (int) instance->txtLen, (int) instance->txtLen );
+ require_action( hexStr, exit, err = kUnknownErr );
+
+ err = CFDictionarySetCString( badDict, kMDNSDiscoveryTestIncorrectInstanceKey_BadTXT, hexStr, kSizeCString );
+ free( hexStr );
+ require_noerr( err, exit );
+ }
+
+ if( isHostnameValid )
+ {
+ uint64_t addrV4Bitmap, addrV6Bitmap, bitmask, resolveTimeUs;
+ unsigned int j;
+ uint8_t addrV4[ 4 ];
+ uint8_t addrV6[ 16 ];
+
+ if( context->recordCountA < 64 ) addrV4Bitmap = ( UINT64_C( 1 ) << context->recordCountA ) - 1;
+ else addrV4Bitmap = ~UINT64_C( 0 );
+
+ if( context->recordCountAAAA < 64 ) addrV6Bitmap = ( UINT64_C( 1 ) << context->recordCountAAAA ) - 1;
+ else addrV6Bitmap = ~UINT64_C( 0 );
+
+ addrV4[ 0 ] = 0;
+ WriteBig16( &addrV4[ 1 ], i );
+ addrV4[ 3 ] = 0;
+
+ memcpy( addrV6, kMDNSReplierBaseAddrV6, 16 );
+ WriteBig16( &addrV6[ 12 ], i );
+
+ unexpectedAddrs = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
+ require_action( unexpectedAddrs, exit, err = kNoMemoryErr );
+
+ resolveTimeUs = 0;
+ for( ipaddr = instance->ipaddrList; ipaddr; ipaddr = ipaddr->next )
+ {
+ const uint8_t * addrPtr;
+ unsigned int lsb;
+ int isAddrValid = false;
+
+ if( ipaddr->sip.sa.sa_family == AF_INET )
+ {
+ addrPtr = (const uint8_t *) &ipaddr->sip.v4.sin_addr.s_addr;
+ lsb = addrPtr[ 3 ];
+ if( ( memcmp( addrPtr, addrV4, 3 ) == 0 ) && ( lsb >= 1 ) && ( lsb <= context->recordCountA ) )
+ {
+ bitmask = UINT64_C( 1 ) << ( lsb - 1 );
+ addrV4Bitmap &= ~bitmask;
+ isAddrValid = true;
+ }
+ }
+ else if( ipaddr->sip.sa.sa_family == AF_INET6 )
+ {
+ addrPtr = ipaddr->sip.v6.sin6_addr.s6_addr;
+ lsb = addrPtr[ 15 ];
+ if( ( memcmp( addrPtr, addrV6, 15 ) == 0 ) && ( lsb >= 1 ) && ( lsb <= context->recordCountAAAA ) )
+ {
+ bitmask = UINT64_C( 1 ) << ( lsb - 1 );
+ addrV6Bitmap &= ~bitmask;
+ isAddrValid = true;
+ }
+ }
+ if( isAddrValid )
+ {
+ if( ipaddr->resolveTimeUs > resolveTimeUs ) resolveTimeUs = ipaddr->resolveTimeUs;
+ }
+ else
+ {
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, unexpectedAddrs, "%##a", &ipaddr->sip );
+ require_noerr( err, exit );
+ }
+ }
+
+ resolveTimeUs += ( instance->discoverTimeUs + instance->resolveTimeUs );
+ if( resolveTimeUs > maxResolveTimeUs ) maxResolveTimeUs = resolveTimeUs;
+
+ if( CFArrayGetCount( unexpectedAddrs ) > 0 )
+ {
+ CFDictionarySetValue( badDict, kMDNSDiscoveryTestIncorrectInstanceKey_UnexpectedAddrs, unexpectedAddrs );
+ }
+ ForgetCF( &unexpectedAddrs );
+
+ missingAddrs = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
+ require_action( missingAddrs, exit, err = kNoMemoryErr );
+
+ for( j = 1; addrV4Bitmap != 0; ++j )
+ {
+ bitmask = UINT64_C( 1 ) << ( j - 1 );
+ if( addrV4Bitmap & bitmask )
+ {
+ addrV4Bitmap &= ~bitmask;
+ addrV4[ 3 ] = (uint8_t) j;
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, missingAddrs, "%.4a", addrV4 );
+ require_noerr( err, exit );
+ }
+ }
+ for( j = 1; addrV6Bitmap != 0; ++j )
+ {
+ bitmask = UINT64_C( 1 ) << ( j - 1 );
+ if( addrV6Bitmap & bitmask )
+ {
+ addrV6Bitmap &= ~bitmask;
+ addrV6[ 15 ] = (uint8_t) j;
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, missingAddrs, "%.16a", addrV6 );
+ require_noerr( err, exit );
+ }
+ }
+
+ if( CFArrayGetCount( missingAddrs ) > 0 )
+ {
+ CFDictionarySetValue( badDict, kMDNSDiscoveryTestIncorrectInstanceKey_MissingAddrs, missingAddrs );
+ }
+ ForgetCF( &missingAddrs );
+ }
+
+ if( CFDictionaryGetCount( badDict ) > 0 )
+ {
+ err = CFDictionarySetCString( badDict, kMDNSDiscoveryTestIncorrectInstanceKey_Name, instance->name,
+ kSizeCString );
+ require_noerr( err, exit );
+
+ CFDictionarySetBoolean( badDict, kMDNSDiscoveryTestIncorrectInstanceKey_DidResolve, true );
+ CFArrayAppendValue( incorrectInstances, badDict );
+ }
+ ForgetCF( &badDict );
+ }
+
+ if( ( CFArrayGetCount( unexpectedInstances ) == 0 ) &&
+ ( CFArrayGetCount( missingInstances ) == 0 ) &&
+ ( CFArrayGetCount( incorrectInstances ) == 0 ) )
+ {
+ err = CFDictionarySetInt64( plist, kMDNSDiscoveryTestResultsKey_TotalResolveTime, (int64_t) maxResolveTimeUs );
+ require_noerr( err, exit );
+ success = true;
+ }
+ else
+ {
+ success = false;
+ }
+ CFDictionarySetBoolean( plist, kMDNSDiscoveryTestResultsKey_Success, success );
+
+ err = OutputPropertyList( plist, context->outputFormat, context->outputAppendNewline, context->outputFilePath );
+ require_noerr_quiet( err, exit );
+
+exit:
+ ForgetCF( &context->browser );
+ if( context->replierPID != -1 )
+ {
+ kill( context->replierPID, SIGTERM );
+ context->replierPID = -1;
+ }
+ FreeNullSafe( instanceArray );
+ CFReleaseNullSafe( plist );
+ CFReleaseNullSafe( badDict );
+ CFReleaseNullSafe( unexpectedAddrs );
+ CFReleaseNullSafe( missingAddrs );
+ exit( err ? 1 : ( success ? 0 : 2 ) );
+}
+
+//===========================================================================================================================
+// _MDNSDiscoveryTestTXTRecordIsValid
+//===========================================================================================================================
+
+static Boolean _MDNSDiscoveryTestTXTRecordIsValid( const uint8_t *inRecordName, const uint8_t *inTXTPtr, size_t inTXTLen )
+{
+ uint32_t hash;
+ int n;
+ const uint8_t * ptr;
+ size_t i, wholeCount, remCount;
+ uint8_t txtStr[ 16 ];
+
+ if( inTXTLen == 0 ) return( false );
+
+ hash = FNV1( inRecordName, DomainNameLength( inRecordName ) );
+
+ txtStr[ 0 ] = 15;
+ n = MemPrintF( &txtStr[ 1 ], 15, "hash=0x%08X", hash );
+ check( n == 15 );
+
+ ptr = inTXTPtr;
+ wholeCount = inTXTLen / 16;
+ for( i = 0; i < wholeCount; ++i )
+ {
+ if( memcmp( ptr, txtStr, 16 ) != 0 ) return( false );
+ ptr += 16;
+ }
+
+ remCount = inTXTLen % 16;
+ if( remCount > 0 )
+ {
+ txtStr[ 0 ] = (uint8_t)( remCount - 1 );
+ if( memcmp( ptr, txtStr, remCount ) != 0 ) return( false );
+ ptr += remCount;
+ }
+ check( ptr == &inTXTPtr[ inTXTLen ] );
+ return( true );
+}
+
+//===========================================================================================================================
+// DotLocalTestCmd
+//===========================================================================================================================
+
+#define kDotLocalTestPreparationTimeLimitSecs 5
+#define kDotLocalTestSubTestDurationSecs 5
+
+// Constants for SRV record query subtest.
+
+#define kDotLocalTestSRV_Priority 1
+#define kDotLocalTestSRV_Weight 0
+#define kDotLocalTestSRV_Port 80
+#define kDotLocalTestSRV_TargetName ( (const uint8_t *) "\x03" "www" "\x07" "example" "\x03" "com" )
+#define kDotLocalTestSRV_TargetStr "www.example.com."
+#define kDotLocalTestSRV_ResultStr "1 0 80 " kDotLocalTestSRV_TargetStr
+
+typedef enum
+{
+ kDotLocalTestState_Unset = 0,
+ kDotLocalTestState_Preparing = 1,
+ kDotLocalTestState_GAIMDNSOnly = 2,
+ kDotLocalTestState_GAIDNSOnly = 3,
+ kDotLocalTestState_GAIBoth = 4,
+ kDotLocalTestState_GAINeither = 5,
+ kDotLocalTestState_GAINoSuchRecord = 6,
+ kDotLocalTestState_QuerySRV = 7,
+ kDotLocalTestState_Done = 8
+
+} DotLocalTestState;
+
+typedef struct
+{
+ const char * testDesc; // Description of the current subtest.
+ char * queryName; // Query name for GetAddrInfo or QueryRecord operation.
+ dispatch_source_t timer; // Timer used for limiting the time for each subtest.
+ NanoTime64 startTime; // Timestamp of when the subtest started.
+ NanoTime64 endTime; // Timestamp of when the subtest ended.
+ CFMutableArrayRef correctResults; // Operation results that were expected.
+ CFMutableArrayRef duplicateResults; // Operation results that were expected, but were already received.
+ CFMutableArrayRef unexpectedResults; // Operation results that were unexpected.
+ OSStatus error; // Subtest's error code.
+ uint32_t addrDNSv4; // If hasDNSv4 is true, the expected DNS IPv4 address for queryName.
+ uint32_t addrMDNSv4; // If hasMDNSv4 is true, the expected MDNS IPv4 address for queryName.
+ uint8_t addrDNSv6[ 16 ]; // If hasDNSv6 is true, the expected DNS IPv6 address for queryName.
+ uint8_t addrMDNSv6[ 16 ]; // If hasMDNSv6 is true, the expected MDNS IPv6 address for queryName.
+ Boolean hasDNSv4; // True if queryName has a DNS IPv4 address.
+ Boolean hasDNSv6; // True if queryName has a DNS IPv6 address.
+ Boolean hasMDNSv4; // True if queryName has an MDNS IPv4 address.
+ Boolean hasMDNSv6; // True if queryName has an MDNS IPv6 address.
+ Boolean needDNSv4; // True if operation is expecting, but hasn't received a DNS IPv4 result.
+ Boolean needDNSv6; // True if operation is expecting, but hasn't received a DNS IPv6 result.
+ Boolean needMDNSv4; // True if operation is expecting, but hasn't received an MDNS IPv4 result.
+ Boolean needMDNSv6; // True if operation is expecting, but hasn't received an MDNS IPv6 result.
+ Boolean needSRV; // True if operation is expecting, but hasn't received an SRV result.
+
+} DotLocalSubtest;
+
+typedef struct
+{
+ dispatch_source_t timer; // Timer used for limiting the time for each state/subtest.
+ DotLocalSubtest * subtest; // Current subtest's state.
+ DNSServiceRef connection; // Shared connection for DNS-SD operations.
+ DNSServiceRef op; // Reference for the current DNS-SD operation.
+ DNSServiceRef op2; // Reference for mdnsreplier probe query used during preparing state.
+ DNSRecordRef localSOARef; // Reference returned by DNSServiceRegisterRecord() for local. SOA record.
+ char * replierCmd; // Command used to invoke the mdnsreplier.
+ char * serverCmd; // Command used to invoke the test DNS server.
+ CFMutableArrayRef reportsGAI; // Reports for subtests that use DNSServiceGetAddrInfo.
+ CFMutableArrayRef reportsQuerySRV; // Reports for subtests that use DNSServiceQueryRecord for SRV records.
+ NanoTime64 startTime; // Timestamp for when the test started.
+ NanoTime64 endTime; // Timestamp for when the test ended.
+ DotLocalTestState state; // The test's current state.
+ pid_t replierPID; // PID of spawned mdnsreplier.
+ pid_t serverPID; // PID of spawned test DNS server.
+ uint32_t ifIndex; // Interface index used for mdnsreplier.
+ char * outputFilePath; // File to write test results to. If NULL, then write to stdout.
+ OutputFormatType outputFormat; // Format of test results output.
+ Boolean appendNewline; // True if a newline character should be appended to JSON output.
+ Boolean registeredSOA; // True if the dummy local. SOA record was successfully registered.
+ Boolean serverIsReady; // True if response was received for test DNS server probe query.
+ Boolean replierIsReady; // True if response was received for mdnsreplier probe query.
+ Boolean testFailed; // True if at least one subtest failed.
+ char labelStr[ 20 + 1 ]; // Unique label string used for for making the query names used by subtests.
+ // The format of this string is "dotlocal-test-<six random chars>".
+} DotLocalTestContext;
+
+static void _DotLocalTestStateMachine( DotLocalTestContext *inContext );
+static void DNSSD_API
+ _DotLocalTestProbeQueryRecordCallback(
+ DNSServiceRef inSDRef,
+ DNSServiceFlags inFlags,
+ uint32_t inInterfaceIndex,
+ DNSServiceErrorType inError,
+ const char * inFullName,
+ uint16_t inType,
+ uint16_t inClass,
+ uint16_t inRDataLen,
+ const void * inRDataPtr,
+ uint32_t inTTL,
+ void * inContext );
+static void DNSSD_API
+ _DotLocalTestRegisterRecordCallback(
+ DNSServiceRef inSDRef,
+ DNSRecordRef inRecordRef,
+ DNSServiceFlags inFlags,
+ DNSServiceErrorType inError,
+ void * inContext );
+static void _DotLocalTestTimerHandler( void *inContext );
+static void DNSSD_API
+ _DotLocalTestGAICallback(
+ DNSServiceRef inSDRef,
+ DNSServiceFlags inFlags,
+ uint32_t inInterfaceIndex,
+ DNSServiceErrorType inError,
+ const char * inHostname,
+ const struct sockaddr * inSockAddr,
+ uint32_t inTTL,
+ void * inContext );
+static void DNSSD_API
+ _DotLocalTestQueryRecordCallback(
+ DNSServiceRef inSDRef,
+ DNSServiceFlags inFlags,
+ uint32_t inInterfaceIndex,
+ DNSServiceErrorType inError,
+ const char * inFullName,
+ uint16_t inType,
+ uint16_t inClass,
+ uint16_t inRDataLen,
+ const void * inRDataPtr,
+ uint32_t inTTL,
+ void * inContext );
+
+static void DotLocalTestCmd( void )
+{
+ OSStatus err;
+ DotLocalTestContext * context;
+ uint8_t * rdataPtr;
+ size_t rdataLen;
+ DNSServiceFlags flags;
+ char queryName[ 64 ];
+ char randBuf[ 6 + 1 ]; // Large enough for four and six character random strings below.
+
+ context = (DotLocalTestContext *) calloc( 1, sizeof( *context ) );
+ require_action( context, exit, err = kNoMemoryErr );
+
+ context->startTime = NanoTimeGetCurrent();
+ context->endTime = kNanoTime_Invalid;
+
+ context->state = kDotLocalTestState_Preparing;
+
+ if( gDotLocalTest_Interface )
+ {
+ err = InterfaceIndexFromArgString( gDotLocalTest_Interface, &context->ifIndex );
+ require_noerr_quiet( err, exit );
+ }
+ else
+ {
+ err = GetAnyMDNSInterface( NULL, &context->ifIndex );
+ require_noerr_quiet( err, exit );
+ }
+
+ if( gDotLocalTest_OutputFilePath )
+ {
+ context->outputFilePath = strdup( gDotLocalTest_OutputFilePath );
+ require_action( context->outputFilePath, exit, err = kNoMemoryErr );
+ }
+
+ context->outputFormat = (OutputFormatType) CLIArgToValue( "format", gDotLocalTest_OutputFormat, &err,
+ kOutputFormatStr_JSON, kOutputFormatType_JSON,
+ kOutputFormatStr_XML, kOutputFormatType_XML,
+ kOutputFormatStr_Binary, kOutputFormatType_Binary,
+ NULL );
+ require_noerr_quiet( err, exit );
+
+ context->appendNewline = gDotLocalTest_OutputAppendNewline ? true : false;
+
+ context->reportsGAI = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
+ require_action( context->reportsGAI, exit, err = kNoMemoryErr );
+
+ context->reportsQuerySRV = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
+ require_action( context->reportsQuerySRV, exit, err = kNoMemoryErr );
+
+ SNPrintF( context->labelStr, sizeof( context->labelStr ), "dotlocal-test-%s",
+ _RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, 6, randBuf ) );
+
+ // Spawn an mdnsreplier.
+
+ err = ASPrintF( &context->replierCmd,
+ "dnssdutil mdnsreplier --follow %lld --interface %u --hostname %s --tag %s --maxInstanceCount 2 --countA 1"
+ " --countAAAA 1",
+ (int64_t) getpid(), context->ifIndex, context->labelStr,
+ _RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, 4, randBuf ) );
+ require_action_quiet( context->replierCmd, exit, err = kUnknownErr );
+
+ err = SpawnCommand( &context->replierPID, "%s", context->replierCmd );
+ require_noerr( err, exit );
+
+ // Spawn a test DNS server
+
+ err = ASPrintF( &context->serverCmd,
+ "dnssdutil server --loopback --follow %lld --port 0 --defaultTTL 300 --domain %s.local.",
+ (int64_t) getpid(), context->labelStr );
+ require_action_quiet( context->serverCmd, exit, err = kUnknownErr );
+
+ err = SpawnCommand( &context->serverPID, "%s", context->serverCmd );
+ require_noerr( err, exit );
+
+ // Create a shared DNS-SD connection.
+
+ err = DNSServiceCreateConnection( &context->connection );
+ require_noerr( err, exit );
+
+ err = DNSServiceSetDispatchQueue( context->connection, dispatch_get_main_queue() );
+ require_noerr( err, exit );
+
+ // Create probe query for DNS server, i.e., query for any name that has an A record.
+
+ SNPrintF( queryName, sizeof( queryName ), "tag-dotlocal-test-probe.ipv4.%s.local.", context->labelStr );
+
+ flags = kDNSServiceFlagsShareConnection;
+#if( TARGET_OS_WATCH )
+ flags |= kDNSServiceFlagsPathEvaluationDone;
+#endif
+
+ context->op = context->connection;
+ err = DNSServiceQueryRecord( &context->op, flags, kDNSServiceInterfaceIndexAny, queryName, kDNSServiceType_A,
+ kDNSServiceClass_IN, _DotLocalTestProbeQueryRecordCallback, context );
+ require_noerr( err, exit );
+
+ // Create probe query for mdnsreplier's "about" TXT record.
+
+ SNPrintF( queryName, sizeof( queryName ), "about.%s.local.", context->labelStr );
+
+ flags = kDNSServiceFlagsShareConnection | kDNSServiceFlagsForceMulticast;
+#if( TARGET_OS_WATCH )
+ flags |= kDNSServiceFlagsPathEvaluationDone;
+#endif
+
+ context->op2 = context->connection;
+ err = DNSServiceQueryRecord( &context->op2, flags, context->ifIndex, queryName, kDNSServiceType_TXT, kDNSServiceClass_IN,
+ _DotLocalTestProbeQueryRecordCallback, context );
+ require_noerr( err, exit );
+
+ // Register a dummy local. SOA record.
+
+ err = CreateSOARecordData( kRootLabel, kRootLabel, 1976040101, 1 * kSecondsPerDay, 2 * kSecondsPerHour,
+ 1000 * kSecondsPerHour, 2 * kSecondsPerDay, &rdataPtr, &rdataLen );
+ require_noerr( err, exit );
+
+ err = DNSServiceRegisterRecord( context->connection, &context->localSOARef, kDNSServiceFlagsUnique,
+ kDNSServiceInterfaceIndexLocalOnly, "local.", kDNSServiceType_SOA, kDNSServiceClass_IN, 1,
+ rdataPtr, 1 * kSecondsPerHour, _DotLocalTestRegisterRecordCallback, context );
+ require_noerr( err, exit );
+
+ // Start timer for probe responses and SOA record registration.
+
+ err = DispatchTimerOneShotCreate( dispatch_time_seconds( kDotLocalTestPreparationTimeLimitSecs ),
+ INT64_C_safe( kDotLocalTestPreparationTimeLimitSecs ) * kNanosecondsPerSecond / 10, dispatch_get_main_queue(),
+ _DotLocalTestTimerHandler, context, &context->timer );
+ require_noerr( err, exit );
+ dispatch_resume( context->timer );
+
+ dispatch_main();
+
+exit:
+ if( err ) ErrQuit( 1, "error: %#m\n", err );
+}
+
+//===========================================================================================================================
+// _DotLocalTestStateMachine
+//===========================================================================================================================
+
+static OSStatus _DotLocalSubtestCreate( DotLocalSubtest **outSubtest );
+static void _DotLocalSubtestFree( DotLocalSubtest *inSubtest );
+static OSStatus _DotLocalTestStartSubtest( DotLocalTestContext *inContext );
+static OSStatus _DotLocalTestFinalizeSubtest( DotLocalTestContext *inContext );
+static void _DotLocalTestFinalizeAndExit( DotLocalTestContext *inContext ) ATTRIBUTE_NORETURN;
+
+static void _DotLocalTestStateMachine( DotLocalTestContext *inContext )
+{
+ OSStatus err;
+ DotLocalTestState nextState;
+
+ DNSServiceForget( &inContext->op );
+ DNSServiceForget( &inContext->op2 );
+ dispatch_source_forget( &inContext->timer );
+
+ switch( inContext->state )
+ {
+ case kDotLocalTestState_Preparing: nextState = kDotLocalTestState_GAIMDNSOnly; break;
+ case kDotLocalTestState_GAIMDNSOnly: nextState = kDotLocalTestState_GAIDNSOnly; break;
+ case kDotLocalTestState_GAIDNSOnly: nextState = kDotLocalTestState_GAIBoth; break;
+ case kDotLocalTestState_GAIBoth: nextState = kDotLocalTestState_GAINeither; break;
+ case kDotLocalTestState_GAINeither: nextState = kDotLocalTestState_GAINoSuchRecord; break;
+ case kDotLocalTestState_GAINoSuchRecord: nextState = kDotLocalTestState_QuerySRV; break;
+ case kDotLocalTestState_QuerySRV: nextState = kDotLocalTestState_Done; break;
+ default: err = kStateErr; goto exit;
+ }
+
+ if( inContext->state == kDotLocalTestState_Preparing )
+ {
+ if( !inContext->registeredSOA || !inContext->serverIsReady || !inContext->replierIsReady )
+ {
+ FPrintF( stderr, "Preparation timed out: Registered SOA? %s. Server ready? %s. mdnsreplier ready? %s.\n",
+ YesNoStr( inContext->registeredSOA ),
+ YesNoStr( inContext->serverIsReady ),
+ YesNoStr( inContext->replierIsReady ) );
+ err = kNotPreparedErr;
+ goto exit;
+ }
+ }
+ else
+ {
+ err = _DotLocalTestFinalizeSubtest( inContext );
+ require_noerr( err, exit );
+ }
+
+ inContext->state = nextState;
+ if( inContext->state == kDotLocalTestState_Done ) _DotLocalTestFinalizeAndExit( inContext );
+ err = _DotLocalTestStartSubtest( inContext );
+
+exit:
+ if( err ) ErrQuit( 1, "error: %#m\n", err );
+}
+
+//===========================================================================================================================
+// _DotLocalSubtestCreate
+//===========================================================================================================================
+
+static OSStatus _DotLocalSubtestCreate( DotLocalSubtest **outSubtest )
+{
+ OSStatus err;
+ DotLocalSubtest * obj;
+
+ obj = (DotLocalSubtest *) calloc( 1, sizeof( *obj ) );
+ require_action( obj, exit, err = kNoMemoryErr );
+
+ obj->correctResults = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
+ require_action( obj->correctResults, exit, err = kNoMemoryErr );
+
+ obj->duplicateResults = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
+ require_action( obj->duplicateResults, exit, err = kNoMemoryErr );
+
+ obj->unexpectedResults = CFArrayCreateMutable( NULL, 0, &kCFTypeArrayCallBacks );
+ require_action( obj->unexpectedResults, exit, err = kNoMemoryErr );
+
+ *outSubtest = obj;
+ obj = NULL;
+ err = kNoErr;
+
+exit:
+ if( obj ) _DotLocalSubtestFree( obj );
+ return( err );
+}
+
+//===========================================================================================================================
+// _DotLocalSubtestFree
+//===========================================================================================================================
+
+static void _DotLocalSubtestFree( DotLocalSubtest *inSubtest )
+{
+ ForgetMem( &inSubtest->queryName );
+ ForgetCF( &inSubtest->correctResults );
+ ForgetCF( &inSubtest->duplicateResults );
+ ForgetCF( &inSubtest->unexpectedResults );
+ free( inSubtest );
+}
+
+//===========================================================================================================================
+// _DotLocalTestStartSubtest
+//===========================================================================================================================
+
+static OSStatus _DotLocalTestStartSubtest( DotLocalTestContext *inContext )
+{
+ OSStatus err;
+ DotLocalSubtest * subtest = NULL;
+ DNSServiceRef op = NULL;
+ DNSServiceFlags flags;
+
+ err = _DotLocalSubtestCreate( &subtest );
+ require_noerr( err, exit );
+
+ if( inContext->state == kDotLocalTestState_GAIMDNSOnly )
+ {
+ ASPrintF( &subtest->queryName, "%s-2.local.", inContext->labelStr );
+ require_action_quiet( subtest->queryName, exit, err = kNoMemoryErr );
+
+ subtest->hasMDNSv4 = subtest->needMDNSv4 = true;
+ subtest->hasMDNSv6 = subtest->needMDNSv6 = true;
+
+ subtest->addrMDNSv4 = htonl( 0x00000201 ); // 0.0.2.1
+ memcpy( subtest->addrMDNSv6, kMDNSReplierBaseAddrV6, 16 ); // 2001:db8:2::2:1
+ subtest->addrMDNSv6[ 13 ] = 2;
+ subtest->addrMDNSv6[ 15 ] = 1;
+
+ subtest->testDesc = kDotLocalTestSubtestDesc_GAIMDNSOnly;
+ }
+
+ else if( inContext->state == kDotLocalTestState_GAIDNSOnly )
+ {
+ ASPrintF( &subtest->queryName, "tag-dns-only.%s.local.", inContext->labelStr );
+ require_action_quiet( subtest->queryName, exit, err = kNoMemoryErr );
+
+ subtest->hasDNSv4 = subtest->needDNSv4 = true;
+ subtest->hasDNSv6 = subtest->needDNSv6 = true;
+
+ subtest->addrDNSv4 = htonl( kDNSServerBaseAddrV4 + 1 ); // 203.0.113.1
+ memcpy( subtest->addrDNSv6, kDNSServerBaseAddrV6, 16 ); // 2001:db8:1::1
+ subtest->addrDNSv6[ 15 ] = 1;
+
+ subtest->testDesc = kDotLocalTestSubtestDesc_GAIDNSOnly;
+ }
+
+ else if( inContext->state == kDotLocalTestState_GAIBoth )
+ {
+ ASPrintF( &subtest->queryName, "%s.local.", inContext->labelStr );
+ require_action_quiet( subtest->queryName, exit, err = kNoMemoryErr );
+
+ subtest->hasDNSv4 = subtest->needDNSv4 = true;
+ subtest->hasDNSv6 = subtest->needDNSv6 = true;
+ subtest->hasMDNSv4 = subtest->needMDNSv4 = true;
+ subtest->hasMDNSv6 = subtest->needMDNSv6 = true;
+
+ subtest->addrDNSv4 = htonl( kDNSServerBaseAddrV4 + 1 ); // 203.0.113.1
+ memcpy( subtest->addrDNSv6, kDNSServerBaseAddrV6, 16 ); // 2001:db8:1::1
+ subtest->addrDNSv6[ 15 ] = 1;
+
+ subtest->addrMDNSv4 = htonl( 0x00000101 ); // 0.0.1.1
+ memcpy( subtest->addrMDNSv6, kMDNSReplierBaseAddrV6, 16 ); // 2001:db8:2::1:1
+ subtest->addrMDNSv6[ 13 ] = 1;
+ subtest->addrMDNSv6[ 15 ] = 1;
+
+ subtest->testDesc = kDotLocalTestSubtestDesc_GAIBoth;
+ }
+
+ else if( inContext->state == kDotLocalTestState_GAINeither )
+ {
+ ASPrintF( &subtest->queryName, "doesnotexit-%s.local.", inContext->labelStr );
+ require_action_quiet( subtest->queryName, exit, err = kNoMemoryErr );
+
+ subtest->testDesc = kDotLocalTestSubtestDesc_GAINeither;
+ }
+
+ else if( inContext->state == kDotLocalTestState_GAINoSuchRecord )
+ {
+ ASPrintF( &subtest->queryName, "doesnotexit-dns.%s.local.", inContext->labelStr );
+ require_action_quiet( subtest->queryName, exit, err = kNoMemoryErr );
+
+ subtest->hasDNSv4 = subtest->needDNSv4 = true;
+ subtest->hasDNSv6 = subtest->needDNSv6 = true;
+ subtest->testDesc = kDotLocalTestSubtestDesc_GAINoSuchRecord;
+ }
+
+ else if( inContext->state == kDotLocalTestState_QuerySRV )
+ {
+ ASPrintF( &subtest->queryName, "_http._tcp.srv-%u-%u-%u.%s%s.local.",
+ kDotLocalTestSRV_Priority, kDotLocalTestSRV_Weight, kDotLocalTestSRV_Port, kDotLocalTestSRV_TargetStr,
+ inContext->labelStr );
+ require_action_quiet( subtest->queryName, exit, err = kNoMemoryErr );
+
+ subtest->needSRV = true;
+ subtest->testDesc = kDotLocalTestSubtestDesc_QuerySRV;
+ }
+
+ else
+ {
+ err = kStateErr;
+ goto exit;
+ }
+
+ // Start new operation.
+
+ flags = kDNSServiceFlagsShareConnection | kDNSServiceFlagsReturnIntermediates;
+#if( TARGET_OS_WATCH )
+ flags |= kDNSServiceFlagsPathEvaluationDone;
+#endif
+
+ subtest->startTime = NanoTimeGetCurrent();
+ subtest->endTime = kNanoTime_Invalid;
+
+ if( inContext->state == kDotLocalTestState_QuerySRV )
+ {
+ op = inContext->connection;
+ err = DNSServiceQueryRecord( &op, flags, kDNSServiceInterfaceIndexAny, subtest->queryName,
+ kDNSServiceType_SRV, kDNSServiceClass_IN, _DotLocalTestQueryRecordCallback, inContext );
+ require_noerr( err, exit );
+ }
+ else
+ {
+ op = inContext->connection;
+ err = DNSServiceGetAddrInfo( &op, flags, kDNSServiceInterfaceIndexAny,
+ kDNSServiceProtocol_IPv4 | kDNSServiceProtocol_IPv6, subtest->queryName, _DotLocalTestGAICallback, inContext );
+ require_noerr( err, exit );
+ }
+
+ // Start timer.
+
+ check( !inContext->timer );
+ err = DispatchTimerOneShotCreate( dispatch_time_seconds( kDotLocalTestSubTestDurationSecs ),
+ INT64_C_safe( kDotLocalTestSubTestDurationSecs ) * kNanosecondsPerSecond / 10, dispatch_get_main_queue(),
+ _DotLocalTestTimerHandler, inContext, &inContext->timer );
+ require_noerr( err, exit );
+ dispatch_resume( inContext->timer );
+
+ check( !inContext->op );
+ inContext->op = op;
+ op = NULL;
+
+ check( !inContext->subtest );
+ inContext->subtest = subtest;
+ subtest = NULL;
+
+exit:
+ if( subtest ) _DotLocalSubtestFree( subtest );
+ if( op ) DNSServiceRefDeallocate( op );
+ return( err );
+}
+
+//===========================================================================================================================
+// _DotLocalTestFinalizeSubtest
+//===========================================================================================================================
+
+#define kDotLocalTestReportKey_StartTime CFSTR( "startTime" ) // String.
+#define kDotLocalTestReportKey_EndTime CFSTR( "endTime" ) // String.
+#define kDotLocalTestReportKey_Success CFSTR( "success" ) // Boolean.
+#define kDotLocalTestReportKey_MDNSReplierCmd CFSTR( "replierCmd" ) // String.
+#define kDotLocalTestReportKey_DNSServerCmd CFSTR( "serverCmd" ) // String.
+#define kDotLocalTestReportKey_GetAddrInfoTests CFSTR( "testsGAI" ) // Array of Dictionaries.
+#define kDotLocalTestReportKey_QuerySRVTests CFSTR( "testsQuerySRV" ) // Array of Dictionaries.
+#define kDotLocalTestReportKey_Description CFSTR( "description" ) // String.
+#define kDotLocalTestReportKey_QueryName CFSTR( "queryName" ) // String.
+#define kDotLocalTestReportKey_Error CFSTR( "error" ) // Integer.
+#define kDotLocalTestReportKey_Results CFSTR( "results" ) // Dictionary of Arrays.
+#define kDotLocalTestReportKey_CorrectResults CFSTR( "correct" ) // Array of Strings
+#define kDotLocalTestReportKey_DuplicateResults CFSTR( "duplicates" ) // Array of Strings.
+#define kDotLocalTestReportKey_UnexpectedResults CFSTR( "unexpected" ) // Array of Strings.
+#define kDotLocalTestReportKey_MissingResults CFSTR( "missing" ) // Array of Strings.
+
+static OSStatus _DotLocalTestFinalizeSubtest( DotLocalTestContext *inContext )
+{
+ OSStatus err;
+ DotLocalSubtest * subtest;
+ CFMutableDictionaryRef reportDict;
+ CFMutableDictionaryRef resultsDict;
+ CFMutableArrayRef missingResults, reportArray;
+ char startTimeStr[ 32 ];
+ char endTimeStr[ 32 ];
+
+ subtest = inContext->subtest;
+ inContext->subtest = NULL;
+
+ subtest->endTime = NanoTimeGetCurrent();
+ _NanoTime64ToDateString( subtest->startTime, startTimeStr, sizeof( startTimeStr ) );
+ _NanoTime64ToDateString( subtest->endTime, endTimeStr, sizeof( endTimeStr ) );
+
+ reportDict = NULL;
+ err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &reportDict,
+ "{"
+ "%kO=%s" // startTime
+ "%kO=%s" // endTime
+ "%kO=%s" // queryName
+ "%kO=%s" // description
+ "%kO={%@}" // results
+ "}",
+ kDotLocalTestReportKey_StartTime, startTimeStr,
+ kDotLocalTestReportKey_EndTime, endTimeStr,
+ kDotLocalTestReportKey_QueryName, subtest->queryName,
+ kDotLocalTestReportKey_Description, subtest->testDesc,
+ kDotLocalTestReportKey_Results, &resultsDict );
+ require_noerr( err, exit );
+
+ missingResults = NULL;
+ switch( inContext->state )
+ {
+ case kDotLocalTestState_GAIMDNSOnly:
+ case kDotLocalTestState_GAIDNSOnly:
+ case kDotLocalTestState_GAIBoth:
+ case kDotLocalTestState_GAINeither:
+ if( subtest->needDNSv4 || subtest->needDNSv6 || subtest->needMDNSv4 || subtest->needMDNSv6 )
+ {
+ err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &missingResults,
+ "["
+ "%.4a" // Expected DNS IPv4 address
+ "%.16a" // Expected DNS IPv6 address
+ "%.4a" // Expected MDNS IPv4 address
+ "%.16a" // Expected MDNS IPv6 address
+ "]",
+ subtest->needDNSv4 ? &subtest->addrDNSv4 : NULL,
+ subtest->needDNSv6 ? subtest->addrDNSv6 : NULL,
+ subtest->needMDNSv4 ? &subtest->addrMDNSv4 : NULL,
+ subtest->needMDNSv6 ? subtest->addrMDNSv6 : NULL );
+ require_noerr( err, exit );
+ }
+ break;
+
+ case kDotLocalTestState_QuerySRV:
+ if( subtest->needSRV )
+ {
+ err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &missingResults,
+ "["
+ "%s" // Expected SRV record data as a string.
+ "]",
+ kDotLocalTestSRV_ResultStr );
+ require_noerr( err, exit );
+ }
+ break;
+
+ case kDotLocalTestState_GAINoSuchRecord:
+ if( subtest->needDNSv4 || subtest->needDNSv6 )
+ {
+ err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &missingResults,
+ "["
+ "%s" // No Such Record (A)
+ "%s" // No Such Record (AAAA)
+ "]",
+ subtest->needDNSv4 ? kNoSuchRecordAStr : NULL,
+ subtest->needDNSv6 ? kNoSuchRecordAAAAStr : NULL );
+ require_noerr( err, exit );
+ }
+ break;
+
+ default:
+ err = kStateErr;
+ goto exit;
+ }
+
+ CFDictionarySetValue( resultsDict, kDotLocalTestReportKey_CorrectResults, subtest->correctResults );
+
+ if( missingResults )
+ {
+ CFDictionarySetValue( resultsDict, kDotLocalTestReportKey_MissingResults, missingResults );
+ ForgetCF( &missingResults );
+ if( !subtest->error ) subtest->error = kNotFoundErr;
+ }
+
+ if( CFArrayGetCount( subtest->unexpectedResults ) > 0 )
+ {
+ CFDictionarySetValue( resultsDict, kDotLocalTestReportKey_UnexpectedResults, subtest->unexpectedResults );
+ if( !subtest->error ) subtest->error = kUnexpectedErr;
+ }
+
+ if( CFArrayGetCount( subtest->duplicateResults ) > 0 )
+ {
+ CFDictionarySetValue( resultsDict, kDotLocalTestReportKey_DuplicateResults, subtest->duplicateResults );
+ if( !subtest->error ) subtest->error = kDuplicateErr;
+ }
+
+ if( subtest->error ) inContext->testFailed = true;
+ err = CFDictionarySetInt64( reportDict, kDotLocalTestReportKey_Error, subtest->error );
+ require_noerr( err, exit );
+
+ reportArray = ( inContext->state == kDotLocalTestState_QuerySRV ) ? inContext->reportsQuerySRV : inContext->reportsGAI;
+ CFArrayAppendValue( reportArray, reportDict );
+
+exit:
+ _DotLocalSubtestFree( subtest );
+ CFReleaseNullSafe( reportDict );
+ return( err );
+}
+
+//===========================================================================================================================
+// _DotLocalTestFinalizeAndExit
+//===========================================================================================================================
+
+static void _DotLocalTestFinalizeAndExit( DotLocalTestContext *inContext )
+{
+ OSStatus err;
+ CFPropertyListRef plist;
+ char startTimeStr[ 32 ];
+ char endTimeStr[ 32 ];
+
+ check( !inContext->subtest );
+ inContext->endTime = NanoTimeGetCurrent();
+
+ if( inContext->replierPID != -1 )
+ {
+ kill( inContext->replierPID, SIGTERM );
+ inContext->replierPID = -1;
+ }
+ if( inContext->serverPID != -1 )
+ {
+ kill( inContext->serverPID, SIGTERM );
+ inContext->serverPID = -1;
+ }
+ err = DNSServiceRemoveRecord( inContext->connection, inContext->localSOARef, 0 );
+ require_noerr( err, exit );
+
+ _NanoTime64ToDateString( inContext->startTime, startTimeStr, sizeof( startTimeStr ) );
+ _NanoTime64ToDateString( inContext->endTime, endTimeStr, sizeof( endTimeStr ) );
+
+ err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &plist,
+ "{"
+ "%kO=%s" // startTime
+ "%kO=%s" // endTime
+ "%kO=%O" // testsGAI
+ "%kO=%O" // testsQuerySRV
+ "%kO=%b" // success
+ "%kO=%s" // replierCmd
+ "%kO=%s" // serverCmd
+ "}",
+ kDotLocalTestReportKey_StartTime, startTimeStr,
+ kDotLocalTestReportKey_EndTime, endTimeStr,
+ kDotLocalTestReportKey_GetAddrInfoTests, inContext->reportsGAI,
+ kDotLocalTestReportKey_QuerySRVTests, inContext->reportsQuerySRV,
+ kDotLocalTestReportKey_Success, inContext->testFailed ? false : true,
+ kDotLocalTestReportKey_MDNSReplierCmd, inContext->replierCmd,
+ kDotLocalTestReportKey_DNSServerCmd, inContext->serverCmd );
+ require_noerr( err, exit );
+
+ ForgetCF( &inContext->reportsGAI );
+ ForgetCF( &inContext->reportsQuerySRV );
+
+ err = OutputPropertyList( plist, inContext->outputFormat, inContext->appendNewline, inContext->outputFilePath );
+ CFRelease( plist );
+ require_noerr( err, exit );
+
+ exit( inContext->testFailed ? 2 : 0 );
+
+exit:
+ ErrQuit( 1, "error: %#m\n", err );
+}
+
+//===========================================================================================================================
+// _DotLocalTestProbeQueryRecordCallback
+//===========================================================================================================================
+
+static void DNSSD_API
+ _DotLocalTestProbeQueryRecordCallback(
+ DNSServiceRef inSDRef,
+ DNSServiceFlags inFlags,
+ uint32_t inInterfaceIndex,
+ DNSServiceErrorType inError,
+ const char * inFullName,
+ uint16_t inType,
+ uint16_t inClass,
+ uint16_t inRDataLen,
+ const void * inRDataPtr,
+ uint32_t inTTL,
+ void * inContext )
+{
+ DotLocalTestContext * const context = (DotLocalTestContext *) inContext;
+
+ Unused( inInterfaceIndex );
+ Unused( inFullName );
+ Unused( inType );
+ Unused( inClass );
+ Unused( inRDataLen );
+ Unused( inRDataPtr );
+ Unused( inTTL );
+
+ check( context->state == kDotLocalTestState_Preparing );
+
+ require_quiet( ( inFlags & kDNSServiceFlagsAdd ) && !inError, exit );
+
+ if( inSDRef == context->op )
+ {
+ DNSServiceForget( &context->op );
+ context->serverIsReady = true;
+ }
+ else if( inSDRef == context->op2 )
+ {
+ DNSServiceForget( &context->op2 );
+ context->replierIsReady = true;
+ }
+
+ if( context->registeredSOA && context->serverIsReady && context->replierIsReady )
+ {
+ _DotLocalTestStateMachine( context );
+ }
+
+exit:
+ return;
+}
+
+//===========================================================================================================================
+// _DotLocalTestRegisterRecordCallback
+//===========================================================================================================================
+
+static void DNSSD_API
+ _DotLocalTestRegisterRecordCallback(
+ DNSServiceRef inSDRef,
+ DNSRecordRef inRecordRef,
+ DNSServiceFlags inFlags,
+ DNSServiceErrorType inError,
+ void * inContext )
+{
+ DotLocalTestContext * const context = (DotLocalTestContext *) inContext;
+
+ Unused( inSDRef );
+ Unused( inRecordRef );
+ Unused( inFlags );
+
+ if( inError ) ErrQuit( 1, "error: local. SOA record registration failed: %#m\n", inError );
+
+ if( !context->registeredSOA )
+ {
+ context->registeredSOA = true;
+ if( context->serverIsReady && context->replierIsReady ) _DotLocalTestStateMachine( context );
+ }
+}
+
+//===========================================================================================================================
+// _DotLocalTestTimerHandler
+//===========================================================================================================================
+
+static void _DotLocalTestTimerHandler( void *inContext )
+{
+ _DotLocalTestStateMachine( (DotLocalTestContext *) inContext );
+}
+
+//===========================================================================================================================
+// _DotLocalTestGAICallback
+//===========================================================================================================================
+
+static void DNSSD_API
+ _DotLocalTestGAICallback(
+ DNSServiceRef inSDRef,
+ DNSServiceFlags inFlags,
+ uint32_t inInterfaceIndex,
+ DNSServiceErrorType inError,
+ const char * inHostname,
+ const struct sockaddr * inSockAddr,
+ uint32_t inTTL,
+ void * inContext )
+{
+ OSStatus err;
+ DotLocalTestContext * const context = (DotLocalTestContext *) inContext;
+ DotLocalSubtest * const subtest = context->subtest;
+ const sockaddr_ip * const sip = (const sockaddr_ip *) inSockAddr;
+
+ Unused( inSDRef );
+ Unused( inInterfaceIndex );
+ Unused( inHostname );
+ Unused( inTTL );
+
+ require_action_quiet( inFlags & kDNSServiceFlagsAdd, exit, err = kFlagErr );
+ require_action_quiet( ( sip->sa.sa_family == AF_INET ) || ( sip->sa.sa_family == AF_INET6 ), exit, err = kTypeErr );
+
+ if( context->state == kDotLocalTestState_GAINoSuchRecord )
+ {
+ if( inError == kDNSServiceErr_NoSuchRecord )
+ {
+ CFMutableArrayRef array = NULL;
+ const char * noSuchRecordStr;
+
+ if( sip->sa.sa_family == AF_INET )
+ {
+ array = subtest->needDNSv4 ? subtest->correctResults : subtest->duplicateResults;
+ subtest->needDNSv4 = false;
+
+ noSuchRecordStr = kNoSuchRecordAStr;
+ }
+ else
+ {
+ array = subtest->needDNSv6 ? subtest->correctResults : subtest->duplicateResults;
+ subtest->needDNSv6 = false;
+
+ noSuchRecordStr = kNoSuchRecordAAAAStr;
+ }
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, array, "%s", noSuchRecordStr );
+ require_noerr( err, fatal );
+ }
+ else if( !inError )
+ {
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, subtest->unexpectedResults, "%##a", sip );
+ require_noerr( err, fatal );
+ }
+ else
+ {
+ err = inError;
+ goto exit;
+ }
+ }
+ else
+ {
+ if( !inError )
+ {
+ CFMutableArrayRef array = NULL;
+
+ if( sip->sa.sa_family == AF_INET )
+ {
+ const uint32_t addrV4 = sip->v4.sin_addr.s_addr;
+
+ if( subtest->hasDNSv4 && ( addrV4 == subtest->addrDNSv4 ) )
+ {
+ array = subtest->needDNSv4 ? subtest->correctResults : subtest->duplicateResults;
+ subtest->needDNSv4 = false;
+ }
+ else if( subtest->hasMDNSv4 && ( addrV4 == subtest->addrMDNSv4 ) )
+ {
+ array = subtest->needMDNSv4 ? subtest->correctResults : subtest->duplicateResults;
+ subtest->needMDNSv4 = false;
+ }
+ }
+ else
+ {
+ const uint8_t * const addrV6 = sip->v6.sin6_addr.s6_addr;
+
+ if( subtest->hasDNSv6 && ( memcmp( addrV6, subtest->addrDNSv6, 16 ) == 0 ) )
+ {
+ array = subtest->needDNSv6 ? subtest->correctResults : subtest->duplicateResults;
+ subtest->needDNSv6 = false;
+ }
+ else if( subtest->hasMDNSv6 && ( memcmp( addrV6, subtest->addrMDNSv6, 16 ) == 0 ) )
+ {
+ array = subtest->needMDNSv6 ? subtest->correctResults : subtest->duplicateResults;
+ subtest->needMDNSv6 = false;
+ }
+ }
+ if( !array ) array = subtest->unexpectedResults;
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, array, "%##a", sip );
+ require_noerr( err, fatal );
+ }
+ else if( inError == kDNSServiceErr_NoSuchRecord )
+ {
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, subtest->unexpectedResults, "%s",
+ ( sip->sa.sa_family == AF_INET ) ? kNoSuchRecordAStr : kNoSuchRecordAAAAStr );
+ require_noerr( err, fatal );
+ }
+ else
+ {
+ err = inError;
+ goto exit;
+ }
+ }
+
+exit:
+ if( err )
+ {
+ subtest->error = err;
+ _DotLocalTestStateMachine( context );
+ }
+ return;
+
+fatal:
+ ErrQuit( 1, "error: %#m\n", err );
+}
+
+//===========================================================================================================================
+// _DotLocalTestQueryRecordCallback
+//===========================================================================================================================
+
+static void DNSSD_API
+ _DotLocalTestQueryRecordCallback(
+ DNSServiceRef inSDRef,
+ DNSServiceFlags inFlags,
+ uint32_t inInterfaceIndex,
+ DNSServiceErrorType inError,
+ const char * inFullName,
+ uint16_t inType,
+ uint16_t inClass,
+ uint16_t inRDataLen,
+ const void * inRDataPtr,
+ uint32_t inTTL,
+ void * inContext )
+{
+ OSStatus err;
+ DotLocalTestContext * const context = (DotLocalTestContext *) inContext;
+ DotLocalSubtest * const subtest = context->subtest;
+ const SRVRecordDataFixedFields * fields;
+ const uint8_t * target;
+ const uint8_t * ptr;
+ const uint8_t * end;
+ char * rdataStr;
+ unsigned int priority, weight, port;
+ CFMutableArrayRef array;
+
+ Unused( inSDRef );
+ Unused( inInterfaceIndex );
+ Unused( inFullName );
+ Unused( inTTL );
+
+ check( context->state == kDotLocalTestState_QuerySRV );
+
+ err = inError;
+ require_noerr_quiet( err, exit );
+ require_action_quiet( inFlags & kDNSServiceFlagsAdd, exit, err = kFlagErr );
+ require_action_quiet( ( inType == kDNSServiceType_SRV ) && ( inClass == kDNSServiceClass_IN ), exit, err = kTypeErr );
+ require_action_quiet( inRDataLen > sizeof( SRVRecordDataFixedFields ), exit, err = kSizeErr );
+
+ fields = (const SRVRecordDataFixedFields *) inRDataPtr;
+ SRVRecordDataFixedFieldsGet( fields, &priority, &weight, &port );
+ target = (const uint8_t *) &fields[ 1 ];
+ end = ( (const uint8_t *) inRDataPtr ) + inRDataLen;
+ for( ptr = target; ( ptr < end ) && ( *ptr != 0 ); ptr += ( 1 + *ptr ) ) {}
+
+ if( ( priority == kDotLocalTestSRV_Priority ) &&
+ ( weight == kDotLocalTestSRV_Weight ) &&
+ ( port == kDotLocalTestSRV_Port ) &&
+ ( ptr < end ) && DomainNameEqual( target, kDotLocalTestSRV_TargetName ) )
+ {
+ array = subtest->needSRV ? subtest->correctResults : subtest->duplicateResults;
+ subtest->needSRV = false;
+ }
+ else
+ {
+ array = subtest->unexpectedResults;
+ }
+
+ rdataStr = NULL;
+ DNSRecordDataToString( inRDataPtr, inRDataLen, kDNSServiceType_SRV, NULL, 0, &rdataStr );
+ if( !rdataStr )
+ {
+ ASPrintF( &rdataStr, "%#H", inRDataPtr, inRDataLen, inRDataLen );
+ require_action( rdataStr, fatal, err = kNoMemoryErr );
+ }
+
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, array, "%s", rdataStr );
+ free( rdataStr );
+ require_noerr( err, fatal );
+
+exit:
+ if( err )
+ {
+ subtest->error = err;
+ _DotLocalTestStateMachine( context );
+ }
+ return;
+
+fatal:
+ ErrQuit( 1, "error: %#m\n", err );
+}
+
+//===========================================================================================================================
+// ProbeConflictTestCmd
+//===========================================================================================================================
+
+#define kProbeConflictTestService_DefaultName "name"
+#define kProbeConflictTestService_Port 60000
+
+#define kProbeConflictTestTXTPtr "\x13" "PROBE-CONFLICT-TEST"
+#define kProbeConflictTestTXTLen sizeof_string( kProbeConflictTestTXTPtr )
+
+typedef struct
+{
+ const char * description;
+ const char * program;
+ Boolean expectsRename;
+
+} ProbeConflictTestCase;
+
+#define kPCTProgPreWait "wait 1000;" // Wait 1 second before sending gratuitous response.
+#define kPCTProgPostWait "wait 8000;" // Wait 8 seconds after sending gratuitous response.
+ // This allows ~2.75 seconds for probing and ~5 seconds for a rename.
+
+static const ProbeConflictTestCase kProbeConflictTestCases[] =
+{
+ // No conflicts
+
+ { "No probe conflicts.", kPCTProgPreWait "probes n-n-n;" "send;" kPCTProgPostWait, false },
+
+ // One multicast probe conflict
+
+ { "One multicast probe conflict (1).", kPCTProgPreWait "probes m;" "send;" kPCTProgPostWait, false },
+ { "One multicast probe conflict (2).", kPCTProgPreWait "probes n-m;" "send;" kPCTProgPostWait, false },
+ { "One multicast probe conflict (3).", kPCTProgPreWait "probes n-n-m;" "send;" kPCTProgPostWait, false },
+
+ // One unicast probe conflict
+
+ { "One unicast probe conflict (1).", kPCTProgPreWait "probes u;" "send;" kPCTProgPostWait, true },
+ { "One unicast probe conflict (2).", kPCTProgPreWait "probes n-u;" "send;" kPCTProgPostWait, true },
+ { "One unicast probe conflict (3).", kPCTProgPreWait "probes n-n-u;" "send;" kPCTProgPostWait, true },
+
+ // One multicast and one unicast probe conflict
+
+ { "Multicast and unicast probe conflict (1).", kPCTProgPreWait "probes m-u;" "send;" kPCTProgPostWait, true },
+ { "Multicast and unicast probe conflict (2).", kPCTProgPreWait "probes m-n-u;" "send;" kPCTProgPostWait, true },
+ { "Multicast and unicast probe conflict (3).", kPCTProgPreWait "probes m-n-n-u;" "send;" kPCTProgPostWait, true },
+ { "Multicast and unicast probe conflict (4).", kPCTProgPreWait "probes n-m-u;" "send;" kPCTProgPostWait, true },
+ { "Multicast and unicast probe conflict (5).", kPCTProgPreWait "probes n-m-n-u;" "send;" kPCTProgPostWait, true },
+ { "Multicast and unicast probe conflict (6).", kPCTProgPreWait "probes n-m-n-n-u;" "send;" kPCTProgPostWait, true },
+ { "Multicast and unicast probe conflict (7).", kPCTProgPreWait "probes n-n-m-u;" "send;" kPCTProgPostWait, true },
+ { "Multicast and unicast probe conflict (8).", kPCTProgPreWait "probes n-n-m-n-u;" "send;" kPCTProgPostWait, true },
+ { "Multicast and unicast probe conflict (9).", kPCTProgPreWait "probes n-n-m-n-n-u;" "send;" kPCTProgPostWait, true },
+
+ // Two multicast probe conflicts
+
+ { "Two multicast probe conflicts (1).", kPCTProgPreWait "probes m-m;" "send;" kPCTProgPostWait, true },
+ { "Two multicast probe conflicts (2).", kPCTProgPreWait "probes m-n-m;" "send;" kPCTProgPostWait, true },
+ { "Two multicast probe conflicts (3).", kPCTProgPreWait "probes m-n-n-m;" "send;" kPCTProgPostWait, true },
+ { "Two multicast probe conflicts (4).", kPCTProgPreWait "probes n-m-m;" "send;" kPCTProgPostWait, true },
+ { "Two multicast probe conflicts (5).", kPCTProgPreWait "probes n-m-n-m-n;" "send;" kPCTProgPostWait, true },
+ { "Two multicast probe conflicts (6).", kPCTProgPreWait "probes n-m-n-n-m;" "send;" kPCTProgPostWait, true },
+ { "Two multicast probe conflicts (7).", kPCTProgPreWait "probes n-n-m-m;" "send;" kPCTProgPostWait, true },
+ { "Two multicast probe conflicts (8).", kPCTProgPreWait "probes n-n-m-n-m;" "send;" kPCTProgPostWait, true },
+ { "Two multicast probe conflicts (9).", kPCTProgPreWait "probes n-n-m-n-n-m;" "send;" kPCTProgPostWait, true },
+};
+
+#define kProbeConflictTestCaseCount countof( kProbeConflictTestCases )
+
+typedef struct
+{
+ DNSServiceRef registration; // Test service registration.
+ NanoTime64 testStartTime; // Test's start time.
+ NanoTime64 startTime; // Current test case's start time.
+ MDNSColliderRef collider; // mDNS collider object.
+ CFMutableArrayRef results; // Array of test case results.
+ char * serviceName; // Test service's instance name as a string. (malloced)
+ char * serviceType; // Test service's service type as a string. (malloced)
+ uint8_t * recordName; // FQDN of collider's record (same as test service's SRV+TXT records). (malloced)
+ unsigned int testCaseIndex; // Index of the current test case.
+ uint32_t ifIndex; // Index of the interface that the collider is to operate on.
+ char * outputFilePath; // File to write test results to. If NULL, then write to stdout. (malloced)
+ OutputFormatType outputFormat; // Format of test report output.
+ Boolean appendNewline; // True if a newline character should be appended to JSON output.
+ Boolean registered; // True if the test service instance is currently registered.
+ Boolean testFailed; // True if at least one test case failed.
+
+} ProbeConflictTestContext;
+
+static void DNSSD_API
+ _ProbeConflictTestRegisterCallback(
+ DNSServiceRef inSDRef,
+ DNSServiceFlags inFlags,
+ DNSServiceErrorType inError,
+ const char * inName,
+ const char * inType,
+ const char * inDomain,
+ void * inContext );
+static void _ProbeConflictTestColliderStopHandler( void *inContext, OSStatus inError );
+static OSStatus _ProbeConflictTestStartNextTest( ProbeConflictTestContext *inContext );
+static OSStatus _ProbeConflictTestStopCurrentTest( ProbeConflictTestContext *inContext, Boolean inRenamed );
+static void _ProbeConflictTestFinalizeAndExit( ProbeConflictTestContext *inContext ) ATTRIBUTE_NORETURN;
+
+static void ProbeConflictTestCmd( void )
+{
+ OSStatus err;
+ ProbeConflictTestContext * context;
+ const char * serviceName;
+ char tag[ 6 + 1 ];
+
+ context = (ProbeConflictTestContext *) calloc( 1, sizeof( *context ) );
+ require_action( context, exit, err = kNoMemoryErr );
+
+ if( gProbeConflictTest_Interface )
+ {
+ err = InterfaceIndexFromArgString( gProbeConflictTest_Interface, &context->ifIndex );
+ require_noerr_quiet( err, exit );
+ }
+ else
+ {
+ err = GetAnyMDNSInterface( NULL, &context->ifIndex );
+ require_noerr_quiet( err, exit );
+ }
+
+ if( gProbeConflictTest_OutputFilePath )
+ {
+ context->outputFilePath = strdup( gProbeConflictTest_OutputFilePath );
+ require_action( context->outputFilePath, exit, err = kNoMemoryErr );
+ }
+
+ context->appendNewline = gProbeConflictTest_OutputAppendNewline ? true : false;
+ context->outputFormat = (OutputFormatType) CLIArgToValue( "format", gProbeConflictTest_OutputFormat, &err,
+ kOutputFormatStr_JSON, kOutputFormatType_JSON,
+ kOutputFormatStr_XML, kOutputFormatType_XML,
+ kOutputFormatStr_Binary, kOutputFormatType_Binary,
+ NULL );
+ require_noerr_quiet( err, exit );
+
+ context->results = CFArrayCreateMutable( NULL, kProbeConflictTestCaseCount, &kCFTypeArrayCallBacks );
+ require_action( context->results, exit, err = kNoMemoryErr );
+
+ serviceName = gProbeConflictTest_UseComputerName ? NULL : kProbeConflictTestService_DefaultName;
+
+ ASPrintF( &context->serviceType, "_pctest-%s._udp",
+ _RandomStringExact( kLowerAlphaNumericCharSet, kLowerAlphaNumericCharSetSize, sizeof( tag ) - 1, tag ) );
+ require_action( context->serviceType, exit, err = kNoMemoryErr );
+
+ context->testStartTime = NanoTimeGetCurrent();
+ err = DNSServiceRegister( &context->registration, 0, context->ifIndex, serviceName, context->serviceType, "local.",
+ NULL, htons( kProbeConflictTestService_Port ), 0, NULL, _ProbeConflictTestRegisterCallback, context );
+ require_noerr( err, exit );
+
+ err = DNSServiceSetDispatchQueue( context->registration, dispatch_get_main_queue() );
+ require_noerr( err, exit );
+
+ dispatch_main();
+
+exit:
+ exit( 1 );
+}
+
+//===========================================================================================================================
+// _ProbeConflictTestRegisterCallback
+//===========================================================================================================================
+
+static void DNSSD_API
+ _ProbeConflictTestRegisterCallback(
+ DNSServiceRef inSDRef,
+ DNSServiceFlags inFlags,
+ DNSServiceErrorType inError,
+ const char * inName,
+ const char * inType,
+ const char * inDomain,
+ void * inContext )
+{
+ OSStatus err;
+ ProbeConflictTestContext * const context = (ProbeConflictTestContext *) inContext;
+
+ Unused( inSDRef );
+ Unused( inType );
+ Unused( inDomain );
+
+ err = inError;
+ require_noerr( err, exit );
+
+ if( !context->registered )
+ {
+ if( inFlags & kDNSServiceFlagsAdd )
+ {
+ uint8_t * ptr;
+ size_t recordNameLen;
+ unsigned int len;
+ uint8_t name[ kDomainNameLengthMax ];
+
+ context->registered = true;
+
+ FreeNullSafe( context->serviceName );
+ context->serviceName = strdup( inName );
+ require_action( context->serviceName, exit, err = kNoMemoryErr );
+
+ err = DomainNameFromString( name, context->serviceName, NULL );
+ require_noerr( err, exit );
+
+ err = DomainNameAppendString( name, context->serviceType, NULL );
+ require_noerr( err, exit );
+
+ err = DomainNameAppendString( name, "local", NULL );
+ require_noerr( err, exit );
+
+ ForgetMem( &context->recordName );
+ err = DomainNameDup( name, &context->recordName, &recordNameLen );
+ require_noerr( err, exit );
+ require_fatal( recordNameLen > 0, "Record name length is zero." ); // Prevents dubious static analyzer warning.
+
+ // Make the first label all caps so that it's easier to spot in system logs.
+
+ ptr = context->recordName;
+ for( len = *ptr++; len > 0; --len, ++ptr ) *ptr = (uint8_t) toupper_safe( *ptr );
+
+ err = _ProbeConflictTestStartNextTest( context );
+ require_noerr( err, exit );
+ }
+ }
+ else
+ {
+ if( !( inFlags & kDNSServiceFlagsAdd ) )
+ {
+ context->registered = false;
+ err = _ProbeConflictTestStopCurrentTest( context, true );
+ require_noerr( err, exit );
+ }
+ }
+ err = kNoErr;
+
+exit:
+ if( err ) exit( 1 );
+}
+
+//===========================================================================================================================
+// _ProbeConflictTestColliderStopHandler
+//===========================================================================================================================
+
+static void _ProbeConflictTestColliderStopHandler( void *inContext, OSStatus inError )
+{
+ OSStatus err;
+ ProbeConflictTestContext * const context = (ProbeConflictTestContext *) inContext;
+
+ err = inError;
+ require_noerr_quiet( err, exit );
+
+ ForgetCF( &context->collider );
+
+ err = _ProbeConflictTestStopCurrentTest( context, false );
+ require_noerr( err, exit );
+
+ err = _ProbeConflictTestStartNextTest( context );
+ require_noerr( err, exit );
+
+exit:
+ if( err ) exit( 1 );
+}
+
+//===========================================================================================================================
+// _ProbeConflictTestStartNextTest
+//===========================================================================================================================
+
+static OSStatus _ProbeConflictTestStartNextTest( ProbeConflictTestContext *inContext )
+{
+ OSStatus err;
+ const ProbeConflictTestCase * testCase;
+
+ check( !inContext->collider );
+
+ if( inContext->testCaseIndex < kProbeConflictTestCaseCount )
+ {
+ testCase = &kProbeConflictTestCases[ inContext->testCaseIndex ];
+ }
+ else
+ {
+ _ProbeConflictTestFinalizeAndExit( inContext );
+ }
+
+ err = MDNSColliderCreate( dispatch_get_main_queue(), &inContext->collider );
+ require_noerr( err, exit );
+
+ err = MDNSColliderSetProgram( inContext->collider, testCase->program );
+ require_noerr( err, exit );
+
+ err = MDNSColliderSetRecord( inContext->collider, inContext->recordName, kDNSServiceType_TXT,
+ kProbeConflictTestTXTPtr, kProbeConflictTestTXTLen );
+ require_noerr( err, exit );
+
+ MDNSColliderSetProtocols( inContext->collider, kMDNSColliderProtocol_IPv4 );
+ MDNSColliderSetInterfaceIndex( inContext->collider, inContext->ifIndex );
+ MDNSColliderSetStopHandler( inContext->collider, _ProbeConflictTestColliderStopHandler, inContext );
+
+ inContext->startTime = NanoTimeGetCurrent();
+ err = MDNSColliderStart( inContext->collider );
+ require_noerr( err, exit );
+
+exit:
+ return( err );
+}
+
+//===========================================================================================================================
+// _ProbeConflictTestStopCurrentTest
+//===========================================================================================================================
+
+#define kProbeConflictTestCaseResultKey_Description CFSTR( "description" )
+#define kProbeConflictTestCaseResultKey_StartTime CFSTR( "startTime" )
+#define kProbeConflictTestCaseResultKey_EndTime CFSTR( "endTime" )
+#define kProbeConflictTestCaseResultKey_ExpectedRename CFSTR( "expectedRename" )
+#define kProbeConflictTestCaseResultKey_ServiceName CFSTR( "serviceName" )
+#define kProbeConflictTestCaseResultKey_Passed CFSTR( "passed" )
+
+static OSStatus _ProbeConflictTestStopCurrentTest( ProbeConflictTestContext *inContext, Boolean inRenamed )
+{
+ OSStatus err;
+ const ProbeConflictTestCase * testCase;
+ NanoTime64 endTime;
+ Boolean passed;
+ char startTimeStr[ 32 ];
+ char endTimeStr[ 32 ];
+
+ endTime = NanoTimeGetCurrent();
+
+ if( inContext->collider )
+ {
+ MDNSColliderSetStopHandler( inContext->collider, NULL, NULL );
+ MDNSColliderStop( inContext->collider );
+ CFRelease( inContext->collider );
+ inContext->collider = NULL;
+ }
+
+ testCase = &kProbeConflictTestCases[ inContext->testCaseIndex ];
+ passed = ( ( testCase->expectsRename && inRenamed ) || ( !testCase->expectsRename && !inRenamed ) ) ? true : false;
+ if( !passed ) inContext->testFailed = true;
+
+ _NanoTime64ToDateString( inContext->startTime, startTimeStr, sizeof( startTimeStr ) );
+ _NanoTime64ToDateString( endTime, endTimeStr, sizeof( endTimeStr ) );
+
+ err = CFPropertyListAppendFormatted( kCFAllocatorDefault, inContext->results,
+ "{"
+ "%kO=%s" // description
+ "%kO=%b" // expectedRename
+ "%kO=%s" // startTime
+ "%kO=%s" // endTime
+ "%kO=%s" // serviceName
+ "%kO=%b" // passed
+ "}",
+ kProbeConflictTestCaseResultKey_Description, testCase->description,
+ kProbeConflictTestCaseResultKey_ExpectedRename, testCase->expectsRename,
+ kProbeConflictTestCaseResultKey_StartTime, startTimeStr,
+ kProbeConflictTestCaseResultKey_EndTime, endTimeStr,
+ kProbeConflictTestCaseResultKey_ServiceName, inContext->serviceName,
+ kProbeConflictTestCaseResultKey_Passed, passed );
+ require_noerr( err, exit );
+
+ ++inContext->testCaseIndex;
+
+exit:
+ return( err );
+}
+
+//===========================================================================================================================
+// _ProbeConflictTestFinalizeAndExit
+//===========================================================================================================================
+
+#define kProbeConflictTestReportKey_StartTime CFSTR( "startTime" )
+#define kProbeConflictTestReportKey_EndTime CFSTR( "endTime" )
+#define kProbeConflictTestReportKey_ServiceType CFSTR( "serviceType" )
+#define kProbeConflictTestReportKey_Results CFSTR( "results" )
+#define kProbeConflictTestReportKey_Passed CFSTR( "passed" )
+
+static void _ProbeConflictTestFinalizeAndExit( ProbeConflictTestContext *inContext )
+{
+ OSStatus err;
+ CFPropertyListRef plist;
+ NanoTime64 endTime;
+ char startTimeStr[ 32 ];
+ char endTimeStr[ 32 ];
+
+ endTime = NanoTimeGetCurrent();
+
+ check( !inContext->collider );
+
+ _NanoTime64ToDateString( inContext->testStartTime, startTimeStr, sizeof( startTimeStr ) );
+ _NanoTime64ToDateString( endTime, endTimeStr, sizeof( endTimeStr ) );
+
+ err = CFPropertyListCreateFormatted( kCFAllocatorDefault, &plist,
+ "{"
+ "%kO=%s" // startTime
+ "%kO=%s" // endTime
+ "%kO=%s" // serviceType
+ "%kO=%O" // results
+ "%kO=%b" // passed
+ "}",
+ kProbeConflictTestReportKey_StartTime, startTimeStr,
+ kProbeConflictTestReportKey_EndTime, endTimeStr,
+ kProbeConflictTestReportKey_ServiceType, inContext->serviceType,
+ kProbeConflictTestReportKey_Results, inContext->results,
+ kProbeConflictTestReportKey_Passed, inContext->testFailed ? false : true );
+ require_noerr( err, exit );
+ ForgetCF( &inContext->results );
+
+ err = OutputPropertyList( plist, inContext->outputFormat, inContext->appendNewline, inContext->outputFilePath );
+ CFRelease( plist );
+ require_noerr( err, exit );
+
+ exit( inContext->testFailed ? 2 : 0 );
+
+exit:
+ ErrQuit( 1, "error: %#m\n", err );
+}
+
+//===========================================================================================================================
+// 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 );