diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java index a62df65901..4b1833ad8b 100644 --- a/framework-t/src/android/net/nsd/AdvertisingRequest.java +++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java @@ -52,6 +52,12 @@ public final class AdvertisingRequest implements Parcelable { */ public static final long FLAG_SKIP_PROBING = 1 << 1; + /** + * Don't announce the subtype of the service. + * @hide + */ + public static final long FLAG_SKIP_SUBTYPE_ANNOUNCEMENTS = 1L << 63; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @LongDef(flag = true, prefix = {"FLAG_"}, value = { diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java index 23899150bf..17139a9889 100644 --- a/service-t/src/com/android/server/NsdService.java +++ b/service-t/src/com/android/server/NsdService.java @@ -24,6 +24,7 @@ import static android.net.NetworkCapabilities.TRANSPORT_VPN; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; import static android.net.nsd.AdvertisingRequest.FLAG_SKIP_PROBING; +import static android.net.nsd.AdvertisingRequest.FLAG_SKIP_SUBTYPE_ANNOUNCEMENTS; import static android.net.nsd.NsdManager.MDNS_DISCOVERY_MANAGER_EVENT; import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT; import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED; @@ -1048,12 +1049,15 @@ public class NsdService extends INsdManager.Stub { serviceInfo.setSubtypes(subtypes); maybeStartMonitoringSockets(); final boolean skipProbing = (advertisingRequest.getFlags() - & FLAG_SKIP_PROBING) > 0; + & FLAG_SKIP_PROBING) != 0; + final boolean skipSubtypeAnnouncements = (advertisingRequest.getFlags() + & FLAG_SKIP_SUBTYPE_ANNOUNCEMENTS) != 0; final MdnsAdvertisingOptions mdnsAdvertisingOptions = MdnsAdvertisingOptions.newBuilder() .setIsOnlyUpdate(isUpdateOnly) .setSkipProbing(skipProbing) .setTtl(advertisingRequest.getTtl()) + .setSkipSubtypeAnnouncements(skipSubtypeAnnouncements) .build(); mAdvertiser.addOrUpdateService(transactionId, serviceInfo, mdnsAdvertisingOptions, clientInfo.mUid); diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java index e251ec2375..bd7b8b585f 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java @@ -1027,17 +1027,13 @@ public class MdnsAdvertiser { }); } - private List getOffloadSubtype(@NonNull NsdServiceInfo nsdServiceInfo) { - // Workaround: Google Cast doesn't announce subtypes per DNS-SD/mDNS spec. - // Thus, subtypes aren't offloaded; only "_googlecast._tcp" is. - // Subtype responses occur when hardware offload is off. - // This solution works because Google Cast doesn't follow the intended usage of subtypes in - // the spec, as it always discovers for both the subtype+base type, and only uses the mDNS - // subtype as an optimization. - if (nsdServiceInfo.getServiceType().equals("_googlecast._tcp")) { + private List getOffloadSubtype(@NonNull Registration registration) { + final NsdServiceInfo serviceInfo = registration.getServiceInfo(); + // If skipSubtypeAnnouncements is set, we should not offload subtypes. + if (registration.mAdvertisingOptions.skipSubtypeAnnouncements()) { return new ArrayList<>(); } - return new ArrayList<>(nsdServiceInfo.getSubtypes()); + return new ArrayList<>(serviceInfo.getSubtypes()); } private OffloadServiceInfoWrapper createOffloadService(int serviceId, @@ -1050,7 +1046,7 @@ public class MdnsAdvertiser { final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo( new OffloadServiceInfo.Key(nsdServiceInfo.getServiceName(), nsdServiceInfo.getServiceType()), - getOffloadSubtype(nsdServiceInfo), + getOffloadSubtype(registration), String.join(".", mDeviceHostName), rawOffloadPacket, priority, diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java index 5133d4f730..250e61fd6e 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java @@ -35,14 +35,17 @@ public class MdnsAdvertisingOptions { @Nullable private final Duration mTtl; private final boolean mSkipProbing; + private final boolean mSkipSubtypeAnnouncements; /** * Parcelable constructs for a {@link MdnsAdvertisingOptions}. */ - MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl, boolean skipProbing) { + MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl, boolean skipProbing, + boolean skipSubtypeAnnouncements) { this.mIsOnlyUpdate = isOnlyUpdate; this.mTtl = ttl; this.mSkipProbing = skipProbing; + this.mSkipSubtypeAnnouncements = skipSubtypeAnnouncements; } /** @@ -84,6 +87,13 @@ public class MdnsAdvertisingOptions { return mTtl; } + /** + * Returns {@code true} if subtype announcements should be skipped. + */ + public boolean skipSubtypeAnnouncements() { + return mSkipSubtypeAnnouncements; + } + @Override public boolean equals(Object other) { if (this == other) { @@ -93,19 +103,36 @@ public class MdnsAdvertisingOptions { } else { final MdnsAdvertisingOptions otherOptions = (MdnsAdvertisingOptions) other; return mIsOnlyUpdate == otherOptions.mIsOnlyUpdate + && mSkipProbing == otherOptions.mSkipProbing + && mSkipSubtypeAnnouncements == otherOptions.mSkipSubtypeAnnouncements && Objects.equals(mTtl, otherOptions.mTtl); } } @Override public int hashCode() { - return Objects.hash(mIsOnlyUpdate, mTtl); + return Objects.hash(mIsOnlyUpdate, mTtl, mSkipProbing, mSkipSubtypeAnnouncements); } @Override public String toString() { - return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + ", mTtl=" + mTtl - + '}'; + final StringBuilder sb = new StringBuilder(); + sb.append("MdnsAdvertisingOptions{"); + if (mIsOnlyUpdate) { + sb.append(" isOnlyUpdate"); + } + if (mSkipProbing) { + sb.append(" skipProbing"); + } + if (mSkipSubtypeAnnouncements) { + sb.append(" skipSubtypeAnnouncements"); + } + if (mTtl != null) { + sb.append(" ttl="); + sb.append(mTtl); + } + sb.append(" }"); + return sb.toString(); } /** @@ -114,6 +141,7 @@ public class MdnsAdvertisingOptions { public static final class Builder { private boolean mIsOnlyUpdate = false; private boolean mSkipProbing = false; + private boolean mSkipSubtypeAnnouncements = false; @Nullable private Duration mTtl; @@ -144,11 +172,20 @@ public class MdnsAdvertisingOptions { return this; } + /** + * Sets whether to skip subtype announcements. + */ + public Builder setSkipSubtypeAnnouncements(boolean skipSubtypeAnnouncements) { + this.mSkipSubtypeAnnouncements = skipSubtypeAnnouncements; + return this; + } + /** * Builds a {@link MdnsAdvertisingOptions} with the arguments supplied to this builder. */ public MdnsAdvertisingOptions build() { - return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl, mSkipProbing); + return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl, mSkipProbing, + mSkipSubtypeAnnouncements); } } } diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java index b9b09ed0c8..b5c3468343 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java @@ -276,7 +276,7 @@ public class MdnsInterfaceAdvertiser implements MulticastPacketReader.PacketHand public void addService(int id, NsdServiceInfo service, @NonNull MdnsAdvertisingOptions advertisingOptions) throws NameConflictException { final int replacedExitingService = - mRecordRepository.addService(id, service, advertisingOptions.getTtl()); + mRecordRepository.addService(id, service, advertisingOptions); // Cancel announcements for the existing service. This only happens for exiting services // (so cancelling exiting announcements), as per RecordRepository.addService. if (replacedExitingService >= 0) { diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java index 73a6f2f78b..411130b702 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java @@ -202,6 +202,8 @@ public class MdnsRecordRepository { public final List> addressRecords; @NonNull public final NsdServiceInfo serviceInfo; + @NonNull + private final MdnsAdvertisingOptions mAdvertisingOptions; /** * Whether the service is sending exit announcements and will be destroyed soon. @@ -223,9 +225,6 @@ public class MdnsRecordRepository { */ private boolean isProbing; - @Nullable - private Duration ttl; - /** * Create a ServiceRegistration with only update the subType. */ @@ -234,17 +233,38 @@ public class MdnsRecordRepository { NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo); newServiceInfo.setSubtypes(newSubtypes); return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo, - repliedServiceCount, sentPacketCount, exiting, isProbing, ttl, + repliedServiceCount, sentPacketCount, exiting, isProbing, mAdvertisingOptions, featureFlags); } + /** + * Get filtered all records of the service registration. + */ + List> getFilteredAllRecords() { + return CollectionUtils.filter(allRecords, + info -> !mAdvertisingOptions.skipSubtypeAnnouncements() || !isSubtypePtrRecord( + info)); + } + + /** + * Get filtered ptr records of the service registration. + */ + List> getFilteredPtrRecords() { + return CollectionUtils.filter(ptrRecords, + info -> !mAdvertisingOptions.skipSubtypeAnnouncements() || !isSubtypePtrRecord( + info)); + } + + /** * Create a ServiceRegistration for dns-sd service registration (RFC6763). */ ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo, int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing, - @Nullable Duration ttl, @NonNull MdnsFeatureFlags featureFlags) { + @NonNull MdnsAdvertisingOptions advertisingOptions, + @NonNull MdnsFeatureFlags featureFlags) { this.serviceInfo = serviceInfo; + this.mAdvertisingOptions = advertisingOptions; final long nonNameRecordsTtlMillis; final long nameRecordsTtlMillis; @@ -253,6 +273,7 @@ public class MdnsRecordRepository { // This is typically useful for SRP (Service Registration Protocol: // https://datatracker.ietf.org/doc/html/draft-ietf-dnssd-srp-24) Advertising Proxy // where all records in a single SRP are required the same TTL. + final Duration ttl = advertisingOptions.getTtl(); if (ttl != null) { nonNameRecordsTtlMillis = ttl.toMillis(); nameRecordsTtlMillis = ttl.toMillis(); @@ -398,10 +419,11 @@ public class MdnsRecordRepository { * @param serviceInfo Service to advertise */ ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo, - int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl, + int repliedServiceCount, int sentPacketCount, + @NonNull MdnsAdvertisingOptions advertisingOptions, @NonNull MdnsFeatureFlags featureFlags) { this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount, - false /* exiting */, true /* isProbing */, ttl, featureFlags); + false /* exiting */, true /* isProbing */, advertisingOptions, featureFlags); } void setProbing(boolean probing) { @@ -467,12 +489,13 @@ public class MdnsRecordRepository { * This may remove/replace any existing service that used the name added but is exiting. * @param serviceId A unique service ID. * @param serviceInfo Service info to add. - * @param ttl the TTL duration for all records of {@code serviceInfo} or {@code null} + * @param advertisingOptions the advertiser options for this service. * @return If the added service replaced another with a matching name (which was exiting), the * ID of the replaced service. * @throws NameConflictException There is already a (non-exiting) service using the name. */ - public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable Duration ttl) + public int addService(int serviceId, NsdServiceInfo serviceInfo, + @NonNull MdnsAdvertisingOptions advertisingOptions) throws NameConflictException { if (mServices.contains(serviceId)) { throw new IllegalArgumentException( @@ -488,7 +511,7 @@ public class MdnsRecordRepository { final ServiceRegistration registration = new ServiceRegistration( mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */, - NO_PACKET /* sentPacketCount */, ttl, + NO_PACKET /* sentPacketCount */, advertisingOptions, mMdnsFeatureFlags); mServices.put(serviceId, registration); @@ -588,14 +611,14 @@ public class MdnsRecordRepository { if (registration == null) return null; if (registration.exiting) return null; - // Send exit (TTL 0) for the PTR records, if at least one was sent (in particular don't send - // if still probing) - if (CollectionUtils.all(registration.ptrRecords, r -> r.lastSentTimeMs == 0L)) { + registration.exiting = true; + final List> filteredRecords = CollectionUtils.filter( + registration.getFilteredPtrRecords(), r -> r.lastSentTimeMs != 0L); + if (filteredRecords.isEmpty()) { return null; } - - registration.exiting = true; - final List expiredRecords = CollectionUtils.map(registration.ptrRecords, + final List expiredRecords = CollectionUtils.map( + filteredRecords, r -> new MdnsPointerRecord( r.record.getName(), 0L /* receiptTimeMillis */, @@ -1100,6 +1123,14 @@ public class MdnsRecordRepository { return makeAnnouncementInfo(serviceId, registration); } + private static boolean isSubtypePtrRecord(@NonNull RecordInfo info) { + if (!(info.record instanceof MdnsPointerRecord)) { + return false; + } + final String[] recordName = info.record.getName(); + return recordName.length > 4; + } + /** * Make the announcement info of the given service ID. * @@ -1132,13 +1163,15 @@ public class MdnsRecordRepository { }); } + final List> filteredAllRecords = registration.getFilteredAllRecords(); // All service records - for (RecordInfo info : registration.allRecords) { + for (RecordInfo info : filteredAllRecords) { answersSet.add(info.record); } + // ignore all the PTR query with subtype addNsecRecordsForUniqueNames(additionalAnswers, - mGeneralRecords.iterator(), registration.allRecords.iterator()); + mGeneralRecords.iterator(), filteredAllRecords.iterator()); return new MdnsAnnouncer.AnnouncementInfo(serviceId, new ArrayList<>(answersSet), additionalAnswers); @@ -1158,6 +1191,10 @@ public class MdnsRecordRepository { // Adds all PTR, SRV, TXT, A/AAAA records. for (RecordInfo ptrRecord : registration.ptrRecords) { + if (registration.mAdvertisingOptions.skipSubtypeAnnouncements() && isSubtypePtrRecord( + ptrRecord)) { + continue; + } answers.add(ptrRecord.record); } if (registration.srvRecord != null) { @@ -1424,8 +1461,8 @@ public class MdnsRecordRepository { if (existing == null) return null; final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo, - existing.repliedServiceCount, existing.sentPacketCount, existing.ttl, - mMdnsFeatureFlags); + existing.repliedServiceCount, existing.sentPacketCount, + existing.mAdvertisingOptions, mMdnsFeatureFlags); mServices.put(serviceId, newService); return makeProbingInfo(serviceId, newService); } @@ -1438,7 +1475,7 @@ public class MdnsRecordRepository { if (registration == null) return; final long now = mDeps.elapsedRealTime(); - for (RecordInfo record : registration.allRecords) { + for (RecordInfo record : registration.getFilteredAllRecords()) { record.lastSentTimeMs = now; record.lastAdvertisedOnIpv4TimeMs = now; record.lastAdvertisedOnIpv6TimeMs = now; diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt index 087617ad62..e08ce84109 100644 --- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt +++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt @@ -576,14 +576,14 @@ class MdnsAdvertiserTest { } @Test - fun testAddService_NoSubtypeForGoogleCastOffload() { + fun testAddService_NoSubtypeForOffloadWhenSkipAnnouncements() { val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context) postSync { advertiser.addOrUpdateService( SERVICE_ID_1, GOOGLECAST_SERVICE, - DEFAULT_ADVERTISING_OPTION, + MdnsAdvertisingOptions.newBuilder().setSkipSubtypeAnnouncements(true).build(), TEST_CLIENT_UID_1 ) } diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt index dc62c8474b..4c003c54e9 100644 --- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt +++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt @@ -575,7 +575,11 @@ class MdnsInterfaceAdvertiserTest { doReturn(testProbingInfo).`when`(repository).setServiceProbing(serviceId) advertiser.addService(serviceId, serviceInfo, MdnsAdvertisingOptions.getDefaultOptions()) - verify(repository).addService(serviceId, serviceInfo, null /* ttl */) + verify(repository).addService( + serviceId, + serviceInfo, + MdnsAdvertisingOptions.newBuilder().build() + ) verify(prober).startProbing(testProbingInfo) return testProbingInfo diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt index 38b333c0ae..ddd8d176c1 100644 --- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt +++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt @@ -85,6 +85,12 @@ private val TEST_TEMPORARY_IPV6_ADDRESS = LinkAddress( 0 /* lifetime */ ) +private val GOOGLECAST_SERVICE = NsdServiceInfo().apply { + serviceType = "_googlecast._tcp" + serviceName = "MyTestService" + port = TEST_PORT +} + private val TEST_SERVICE_1 = NsdServiceInfo().apply { serviceType = "_testservice._tcp" serviceName = "MyTestService" @@ -147,6 +153,8 @@ private val TEST_PUBLIC_KEY_2 = hexStringToByteArray( "270962d4" ) +private val DEFAULT_OPTION = MdnsAdvertisingOptions.newBuilder().build() + @RunWith(DevSdkIgnoreRunner::class) @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2) class MdnsRecordRepositoryTest { @@ -195,10 +203,13 @@ class MdnsRecordRepositoryTest { @Test fun testAddServiceAndProbe() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) + val option = MdnsAdvertisingOptions.newBuilder() + .setTtl(Duration.ofSeconds(50)) + .build() assertEquals(0, repository.servicesCount) assertEquals( -1, - repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, Duration.ofSeconds(50)) + repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, option) ) assertEquals(1, repository.servicesCount) @@ -238,10 +249,10 @@ class MdnsRecordRepositoryTest { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1) assertFailsWith(NameConflictException::class) { - repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* ttl */) + repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, DEFAULT_OPTION) } assertFailsWith(NameConflictException::class) { - repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, null /* ttl */) + repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, DEFAULT_OPTION) } } @@ -288,9 +299,9 @@ class MdnsRecordRepositoryTest { @Test fun testInvalidReuseOfServiceId() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) - repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */) + repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, DEFAULT_OPTION) assertFailsWith(IllegalArgumentException::class) { - repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* ttl */) + repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, DEFAULT_OPTION) } } @@ -299,7 +310,7 @@ class MdnsRecordRepositoryTest { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) assertFalse(repository.hasActiveService(TEST_SERVICE_ID_1)) - repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */) + repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, DEFAULT_OPTION) assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1)) val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1) @@ -391,6 +402,258 @@ class MdnsRecordRepositoryTest { assertEquals(0, repository.servicesCount) } + @Test + fun testAnnouncementsWithOutSubtypes() { + val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) + val option = MdnsAdvertisingOptions.newBuilder().setSkipSubtypeAnnouncements(true).build() + val announcementInfo = repository.initWithService( + TEST_SERVICE_ID_1, + GOOGLECAST_SERVICE, + setOf(TEST_SUBTYPE, TEST_SUBTYPE2), + option = option + ) + repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */) + val packet = announcementInfo.getPacket(0) + + assertEquals(0, packet.transactionId) + assertEquals(0x8400 /* response, authoritative */, packet.flags) + assertEquals(0, packet.questions.size) + assertEquals(0, packet.authorityRecords.size) + + val serviceType = arrayOf("_googlecast", "_tcp", "local") + val serviceName = arrayOf("MyTestService", "_googlecast", "_tcp", "local") + val v4AddrRev = getReverseDnsAddress(TEST_ADDRESSES[0].address) + val v6Addr1Rev = getReverseDnsAddress(TEST_ADDRESSES[1].address) + val v6Addr2Rev = getReverseDnsAddress(TEST_ADDRESSES[2].address) + + assertThat(packet.answers).containsExactly( + // Reverse address and address records for the hostname + MdnsPointerRecord( + v4AddrRev, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + TEST_HOSTNAME + ), + MdnsInetAddressRecord( + TEST_HOSTNAME, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + TEST_ADDRESSES[0].address + ), + MdnsPointerRecord( + v6Addr1Rev, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + TEST_HOSTNAME + ), + MdnsInetAddressRecord( + TEST_HOSTNAME, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + TEST_ADDRESSES[1].address + ), + MdnsPointerRecord( + v6Addr2Rev, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + TEST_HOSTNAME + ), + MdnsInetAddressRecord( + TEST_HOSTNAME, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + TEST_ADDRESSES[2].address + ), + // Service registration records (RFC6763) + MdnsPointerRecord( + serviceType, + 0L /* receiptTimeMillis */, + // Not a unique name owned by the announcer, so cacheFlush=false + false /* cacheFlush */, + 4500000L /* ttlMillis */, + serviceName + ), + MdnsServiceRecord( + serviceName, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + 0 /* servicePriority */, + 0 /* serviceWeight */, + TEST_PORT /* servicePort */, + TEST_HOSTNAME + ), + MdnsTextRecord( + serviceName, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 4500000L /* ttlMillis */, + emptyList() /* entries */ + ), + // Service type enumeration record (RFC6763 9.) + MdnsPointerRecord( + arrayOf("_services", "_dns-sd", "_udp", "local"), + 0L /* receiptTimeMillis */, + false /* cacheFlush */, + 4500000L /* ttlMillis */, + serviceType + )) + + assertContentEquals(listOf( + MdnsNsecRecord( + v4AddrRev, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + v4AddrRev, + intArrayOf(TYPE_PTR) + ), + MdnsNsecRecord( + TEST_HOSTNAME, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + TEST_HOSTNAME, + intArrayOf(TYPE_A, TYPE_AAAA) + ), + MdnsNsecRecord( + v6Addr1Rev, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + v6Addr1Rev, + intArrayOf(TYPE_PTR) + ), + MdnsNsecRecord( + v6Addr2Rev, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + v6Addr2Rev, + intArrayOf(TYPE_PTR) + ), + MdnsNsecRecord( + serviceName, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 4500000L /* ttlMillis */, + serviceName, + intArrayOf(TYPE_TXT, TYPE_SRV) + ) + ), packet.additionalRecords) + } + + @Test + fun testExitAnnouncementsWithOutSubtypes() { + val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) + val option = MdnsAdvertisingOptions.newBuilder().setSkipSubtypeAnnouncements(true).build() + repository.initWithService( + TEST_SERVICE_ID_1, + TEST_SERVICE_1, + setOf(TEST_SUBTYPE, TEST_SUBTYPE2), + option = option + ) + repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */) + + val exitAnnouncement = repository.exitService(TEST_SERVICE_ID_1) + assertNotNull(exitAnnouncement) + assertEquals(1, repository.servicesCount) + val packet = exitAnnouncement.getPacket(0) + + assertEquals(0, packet.transactionId) + assertEquals(0x8400 /* response, authoritative */, packet.flags) + assertEquals(0, packet.questions.size) + assertEquals(0, packet.authorityRecords.size) + assertEquals(0, packet.additionalRecords.size) + + assertThat(packet.answers).containsExactly( + MdnsPointerRecord( + arrayOf("_testservice", "_tcp", "local"), + 0L /* receiptTimeMillis */, + false /* cacheFlush */, + 0L /* ttlMillis */, + arrayOf("MyTestService", "_testservice", "_tcp", "local") + ), + ) + } + + @Test + fun testGetOffloadPacketWithoutSubtype() { + val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) + val ipAddress = TEST_ADDRESSES.toMutableList() + ipAddress.add(TEST_TEMPORARY_IPV6_ADDRESS) + val option = MdnsAdvertisingOptions.newBuilder().setSkipSubtypeAnnouncements(true).build() + repository.initWithService( + TEST_SERVICE_ID_1, + GOOGLECAST_SERVICE, + setOf(TEST_SUBTYPE, TEST_SUBTYPE2), + addresses = ipAddress, + option = option + ) + val serviceName = arrayOf("MyTestService", "_googlecast", "_tcp", "local") + val serviceType = arrayOf("_googlecast", "_tcp", "local") + val offloadPacket = repository.getOffloadPacket(TEST_SERVICE_ID_1) + assertEquals(0, offloadPacket.transactionId) + assertEquals(0x8400, offloadPacket.flags) + assertEquals(0, offloadPacket.questions.size) + assertEquals(0, offloadPacket.additionalRecords.size) + assertEquals(0, offloadPacket.authorityRecords.size) + assertContentEquals(listOf( + MdnsPointerRecord( + serviceType, + 0L /* receiptTimeMillis */, + // Not a unique name owned by the announcer, so cacheFlush=false + false /* cacheFlush */, + 4500000L /* ttlMillis */, + serviceName + ), + MdnsServiceRecord( + serviceName, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + 0 /* servicePriority */, + 0 /* serviceWeight */, + TEST_PORT /* servicePort */, + TEST_HOSTNAME + ), + MdnsTextRecord( + serviceName, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 4500000L /* ttlMillis */, + emptyList() /* entries */ + ), + MdnsInetAddressRecord( + TEST_HOSTNAME, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + TEST_ADDRESSES[0].address + ), + MdnsInetAddressRecord( + TEST_HOSTNAME, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + TEST_ADDRESSES[1].address + ), + MdnsInetAddressRecord( + TEST_HOSTNAME, + 0L /* receiptTimeMillis */, + true /* cacheFlush */, + 120000L /* ttlMillis */, + TEST_ADDRESSES[2].address + ), + ), offloadPacket.answers) + } + @Test fun testExitingServiceReAdded() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) @@ -400,7 +663,7 @@ class MdnsRecordRepositoryTest { assertEquals( TEST_SERVICE_ID_1, - repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* ttl */) + repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, DEFAULT_OPTION) ) assertEquals(1, repository.servicesCount) @@ -1188,7 +1451,7 @@ class MdnsRecordRepositoryTest { subtypes = setOf(), listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)) ) - repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */) + repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, DEFAULT_OPTION) val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353) val query = makeQuery(TYPE_AAAA to TEST_CUSTOM_HOST_1_NAME) @@ -1498,7 +1761,7 @@ class MdnsRecordRepositoryTest { repository.addService( TEST_SERVICE_CUSTOM_HOST_ID_1, TEST_SERVICE_CUSTOM_HOST_1, - null /* ttl */ + DEFAULT_OPTION ) repository.removeService(TEST_CUSTOM_HOST_ID_1) repository.removeService(TEST_SERVICE_CUSTOM_HOST_ID_1) @@ -1744,8 +2007,8 @@ class MdnsRecordRepositoryTest { @Test fun testGetConflictingServices() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) - repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */) - repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */) + repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, DEFAULT_OPTION) + repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, DEFAULT_OPTION) val packet = MdnsPacket( 0 /* flags */, @@ -1785,8 +2048,8 @@ class MdnsRecordRepositoryTest { @Test fun testGetConflictingServicesCaseInsensitive() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) - repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */) - repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */) + repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, DEFAULT_OPTION) + repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, DEFAULT_OPTION) val packet = MdnsPacket( 0 /* flags */, @@ -1826,8 +2089,8 @@ class MdnsRecordRepositoryTest { @Test fun testGetConflictingServices_customHosts_differentAddresses() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) - repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */) - repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */) + repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, DEFAULT_OPTION) + repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, DEFAULT_OPTION) val packet = MdnsPacket( 0, /* flags */ @@ -1861,8 +2124,8 @@ class MdnsRecordRepositoryTest { @Test fun testGetConflictingServices_customHosts_moreAddressesThanUs_conflict() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) - repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */) - repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */) + repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, DEFAULT_OPTION) + repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, DEFAULT_OPTION) val packet = MdnsPacket( 0, /* flags */ @@ -1931,7 +2194,7 @@ class MdnsRecordRepositoryTest { @Test fun testGetConflictingServices_customHostsReplyHasSameNameRecord_conflictDuringProbing() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) - repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */) + repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, DEFAULT_OPTION) repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2) val packet = MdnsPacket( @@ -2035,7 +2298,7 @@ class MdnsRecordRepositoryTest { serviceName = "MyTestService" port = TEST_PORT publicKey = TEST_PUBLIC_KEY - }, null /* ttl */) + }, DEFAULT_OPTION) val otherTtlMillis = 1234L val packet = MdnsPacket( @@ -2069,7 +2332,7 @@ class MdnsRecordRepositoryTest { serviceName = "MyTestService" port = TEST_PORT publicKey = TEST_PUBLIC_KEY - }, null /* null */) + }, DEFAULT_OPTION) val otherTtlMillis = 1234L val packet = MdnsPacket( @@ -2137,7 +2400,7 @@ class MdnsRecordRepositoryTest { repository.addService(TEST_SERVICE_ID_1, NsdServiceInfo().apply { hostname = "MyHost" publicKey = TEST_PUBLIC_KEY - }, null /* ttl */) + }, DEFAULT_OPTION) val otherTtlMillis = 1234L val packet = MdnsPacket( @@ -2172,7 +2435,7 @@ class MdnsRecordRepositoryTest { parseNumericAddress("2001:db8::1"), parseNumericAddress("2001:db8::2")) publicKey = TEST_PUBLIC_KEY - }, null /* ttl */) + }, DEFAULT_OPTION) val otherTtlMillis = 1234L val packet = MdnsPacket( @@ -2214,7 +2477,7 @@ class MdnsRecordRepositoryTest { port = TEST_PORT hostname = "MyHost" publicKey = TEST_PUBLIC_KEY - }, null /* ttl */) + }, DEFAULT_OPTION) // Although there's a KEY RR in the second registration being probed, it shouldn't conflict // with an address record which is from a probed registration in the repository. @@ -2241,8 +2504,8 @@ class MdnsRecordRepositoryTest { @Test fun testGetConflictingServices_IdenticalService() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) - repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */) - repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */) + repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, DEFAULT_OPTION) + repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, DEFAULT_OPTION) val otherTtlMillis = 1234L val packet = MdnsPacket( @@ -2278,8 +2541,8 @@ class MdnsRecordRepositoryTest { @Test fun testGetConflictingServicesCaseInsensitive_IdenticalService() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags()) - repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */) - repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */) + repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, DEFAULT_OPTION) + repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, DEFAULT_OPTION) val otherTtlMillis = 1234L val packet = MdnsPacket( @@ -2382,7 +2645,7 @@ class MdnsRecordRepositoryTest { assertEquals(0, repository.servicesCount) assertEquals( -1, - repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */) + repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, DEFAULT_OPTION) ) assertEquals(1, repository.servicesCount) @@ -2939,7 +3202,7 @@ class MdnsRecordRepositoryTest { repository.addService( TEST_SERVICE_CUSTOM_HOST_ID_1, TEST_SERVICE_CUSTOM_HOST_NO_ADDRESSES, - null + DEFAULT_OPTION ) repository.setServiceProbing(TEST_SERVICE_CUSTOM_HOST_ID_1) repository.removeService(TEST_CUSTOM_HOST_ID_1) @@ -3083,18 +3346,20 @@ private fun MdnsRecordRepository.initWithService( serviceId: Int, serviceInfo: NsdServiceInfo, subtypes: Set = setOf(), - addresses: List = TEST_ADDRESSES + addresses: List = TEST_ADDRESSES, + option: MdnsAdvertisingOptions = DEFAULT_OPTION ): AnnouncementInfo { updateAddresses(addresses) serviceInfo.setSubtypes(subtypes) - return addServiceAndFinishProbing(serviceId, serviceInfo) + return addServiceAndFinishProbing(serviceId, serviceInfo, option) } private fun MdnsRecordRepository.addServiceAndFinishProbing( serviceId: Int, - serviceInfo: NsdServiceInfo + serviceInfo: NsdServiceInfo, + option: MdnsAdvertisingOptions = DEFAULT_OPTION ): AnnouncementInfo { - addService(serviceId, serviceInfo, null /* ttl */) + addService(serviceId, serviceInfo, option) val probingInfo = setServiceProbing(serviceId) assertNotNull(probingInfo) return onProbingSucceeded(probingInfo)