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
This commit is contained in:
Yuyang Huang 2025-04-30 18:30:13 +09:00
commit e41b66393c
9 changed files with 423 additions and 74 deletions

View file

@ -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 = {

View file

@ -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);

View file

@ -1027,17 +1027,13 @@ public class MdnsAdvertiser {
});
}
private List<String> 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<String> 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,

View file

@ -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);
}
}
}

View file

@ -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) {

View file

@ -202,6 +202,8 @@ public class MdnsRecordRepository {
public final List<RecordInfo<MdnsInetAddressRecord>> 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<RecordInfo<?>> getFilteredAllRecords() {
return CollectionUtils.filter(allRecords,
info -> !mAdvertisingOptions.skipSubtypeAnnouncements() || !isSubtypePtrRecord(
info));
}
/**
* Get filtered ptr records of the service registration.
*/
List<RecordInfo<MdnsPointerRecord>> 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<RecordInfo<MdnsPointerRecord>> filteredRecords = CollectionUtils.filter(
registration.getFilteredPtrRecords(), r -> r.lastSentTimeMs != 0L);
if (filteredRecords.isEmpty()) {
return null;
}
registration.exiting = true;
final List<MdnsRecord> expiredRecords = CollectionUtils.map(registration.ptrRecords,
final List<MdnsRecord> 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<RecordInfo<?>> 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<MdnsPointerRecord> 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;

View file

@ -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
)
}

View file

@ -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

View file

@ -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<String> = setOf(),
addresses: List<LinkAddress> = TEST_ADDRESSES
addresses: List<LinkAddress> = 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)