From e41b66393cfbab36fcfa464dcb6a6ec545cb5100 Mon Sep 17 00:00:00 2001 From: Yuyang Huang Date: Wed, 30 Apr 2025 18:30:13 +0900 Subject: [PATCH] Skip subtype PTR records in announcements/offload Skip including the subtype PTR query for both announcement and offload packets. This is necessary because devices like Googlecast have numerous subtypes. Including these subtypes in the packets can easily exceed the MTU limit. Furthermore, the Wi-Fi firmware has limited RAM available for hardware offload, and including subtypes would consume excessive memory resources needed for the offload code. Test: TH Flag: EXEMPT bug fix Change-Id: I2575659983579da0bbf4ff104ba5b8111ee9585e --- .../android/net/nsd/AdvertisingRequest.java | 6 + .../src/com/android/server/NsdService.java | 6 +- .../connectivity/mdns/MdnsAdvertiser.java | 16 +- .../mdns/MdnsAdvertisingOptions.java | 47 ++- .../mdns/MdnsInterfaceAdvertiser.java | 2 +- .../mdns/MdnsRecordRepository.java | 79 +++-- .../connectivity/mdns/MdnsAdvertiserTest.kt | 4 +- .../mdns/MdnsInterfaceAdvertiserTest.kt | 6 +- .../mdns/MdnsRecordRepositoryTest.kt | 331 ++++++++++++++++-- 9 files changed, 423 insertions(+), 74 deletions(-) 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)