Merge "Fix replying to queries via unicast" into main

This commit is contained in:
Remi NGUYEN VAN 2024-01-16 07:43:52 +00:00 • committed by Gerrit Code Review
commit 0c905a152e
10 changed files with 453 additions and 75 deletions

View file

@ -176,7 +176,7 @@ public class NsdService extends INsdManager.Stub {
"mdns_advertiser_allowlist_";
private static final String MDNS_ALLOWLIST_FLAG_SUFFIX = "_version";
private static final String FORCE_ENABLE_FLAG_FOR_TEST_PREFIX = "test_";
@VisibleForTesting
static final String MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF =
@ -1739,6 +1739,10 @@ public class NsdService extends INsdManager.Stub {
mContext, MdnsFeatureFlags.NSD_LIMIT_LABEL_COUNT))
.setIsKnownAnswerSuppressionEnabled(mDeps.isFeatureEnabled(
mContext, MdnsFeatureFlags.NSD_KNOWN_ANSWER_SUPPRESSION))
.setIsUnicastReplyEnabled(mDeps.isFeatureEnabled(
mContext, MdnsFeatureFlags.NSD_UNICAST_REPLY_ENABLED))
.setOverrideProvider(flag -> mDeps.isFeatureEnabled(
mContext, FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag))
.build();
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider,

View file

@ -15,6 +15,9 @@
*/
package com.android.server.connectivity.mdns;
import android.annotation.NonNull;
import android.annotation.Nullable;
/**
* The class that contains mDNS feature flags;
*/
@ -46,6 +49,14 @@ public class MdnsFeatureFlags {
*/
public static final String NSD_KNOWN_ANSWER_SUPPRESSION = "nsd_known_answer_suppression";
/**
* A feature flag to control whether unicast replies should be enabled.
*
* <p>Enabling this feature causes replies to queries with the Query Unicast (QU) flag set to be
* sent unicast instead of multicast, as per RFC6762 5.4.
*/
public static final String NSD_UNICAST_REPLY_ENABLED = "nsd_unicast_reply_enabled";
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
@ -61,6 +72,36 @@ public class MdnsFeatureFlags {
// Flag for known-answer suppression
public final boolean mIsKnownAnswerSuppressionEnabled;
// Flag to enable replying unicast to queries requesting unicast replies
public final boolean mIsUnicastReplyEnabled;
@Nullable
private final FlagOverrideProvider mOverrideProvider;
/**
* A provider that can indicate whether a flag should be force-enabled for testing purposes.
*/
public interface FlagOverrideProvider {
/**
* Indicates whether the flag should be force-enabled for testing purposes.
*/
boolean isForceEnabledForTest(@NonNull String flag);
}
/**
* Indicates whether the flag should be force-enabled for testing purposes.
*/
private boolean isForceEnabledForTest(@NonNull String flag) {
return mOverrideProvider != null && mOverrideProvider.isForceEnabledForTest(flag);
}
/**
* Indicates whether {@link #NSD_UNICAST_REPLY_ENABLED} is enabled, including for testing.
*/
public boolean isUnicastReplyEnabled() {
return mIsUnicastReplyEnabled || isForceEnabledForTest(NSD_UNICAST_REPLY_ENABLED);
}
/**
* The constructor for {@link MdnsFeatureFlags}.
*/
@ -68,12 +109,16 @@ public class MdnsFeatureFlags {
boolean includeInetAddressRecordsInProbing,
boolean isExpiredServicesRemovalEnabled,
boolean isLabelCountLimitEnabled,
boolean isKnownAnswerSuppressionEnabled) {
boolean isKnownAnswerSuppressionEnabled,
boolean isUnicastReplyEnabled,
@Nullable FlagOverrideProvider overrideProvider) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
mIsExpiredServicesRemovalEnabled = isExpiredServicesRemovalEnabled;
mIsLabelCountLimitEnabled = isLabelCountLimitEnabled;
mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
mIsUnicastReplyEnabled = isUnicastReplyEnabled;
mOverrideProvider = overrideProvider;
}
@ -90,6 +135,8 @@ public class MdnsFeatureFlags {
private boolean mIsExpiredServicesRemovalEnabled;
private boolean mIsLabelCountLimitEnabled;
private boolean mIsKnownAnswerSuppressionEnabled;
private boolean mIsUnicastReplyEnabled;
private FlagOverrideProvider mOverrideProvider;
/**
* The constructor for {@link Builder}.
@ -100,6 +147,8 @@ public class MdnsFeatureFlags {
mIsExpiredServicesRemovalEnabled = false;
mIsLabelCountLimitEnabled = true; // Default enabled.
mIsKnownAnswerSuppressionEnabled = false;
mIsUnicastReplyEnabled = true;
mOverrideProvider = null;
}
/**
@ -153,6 +202,27 @@ public class MdnsFeatureFlags {
return this;
}
/**
* Set whether the unicast reply feature is enabled.
*
* @see #NSD_UNICAST_REPLY_ENABLED
*/
public Builder setIsUnicastReplyEnabled(boolean isUnicastReplyEnabled) {
mIsUnicastReplyEnabled = isUnicastReplyEnabled;
return this;
}
/**
* Set a {@link FlagOverrideProvider} to be used by {@link #isForceEnabledForTest(String)}.
*
* If non-null, features that use {@link #isForceEnabledForTest(String)} will use that
* provider to query whether the flag should be force-enabled.
*/
public Builder setOverrideProvider(@Nullable FlagOverrideProvider overrideProvider) {
mOverrideProvider = overrideProvider;
return this;
}
/**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
@ -161,7 +231,9 @@ public class MdnsFeatureFlags {
mIncludeInetAddressRecordsInProbing,
mIsExpiredServicesRemovalEnabled,
mIsLabelCountLimitEnabled,
mIsKnownAnswerSuppressionEnabled);
mIsKnownAnswerSuppressionEnabled,
mIsUnicastReplyEnabled,
mOverrideProvider);
}
}
}

View file

@ -175,6 +175,16 @@ public abstract class MdnsRecord {
return (cls & FLAG_CACHE_FLUSH) != 0;
}
/**
* For questions, returns whether a unicast reply was requested.
*
* In practice this is identical to {@link #getCacheFlush()}, as the "cache flush" flag in
* replies is the same as "unicast reply requested" in questions.
*/
public final boolean isUnicastReplyRequested() {
return (cls & MdnsConstants.QCLASS_UNICAST) != 0;
}
/**
* Returns the record's remaining TTL.
*

View file

@ -499,26 +499,30 @@ public class MdnsRecordRepository {
@Nullable
public MdnsReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
final long now = SystemClock.elapsedRealtime();
final boolean replyUnicast = (packet.flags & MdnsConstants.QCLASS_UNICAST) != 0;
// Use LinkedHashSet for preserving the insert order of the RRs, so that RRs of the same
// service or host are grouped together (which is more developer-friendly).
final Set<RecordInfo<?>> answerInfo = new LinkedHashSet<>();
final Set<RecordInfo<?>> additionalAnswerInfo = new LinkedHashSet<>();
// Reply unicast if the feature is enabled AND all replied questions request unicast
final boolean replyUnicastEnabled = mMdnsFeatureFlags.isUnicastReplyEnabled();
boolean replyUnicast = replyUnicastEnabled;
for (MdnsRecord question : packet.questions) {
// Add answers from general records
addReplyFromService(question, mGeneralRecords, null /* servicePtrRecord */,
null /* serviceSrvRecord */, null /* serviceTxtRecord */, replyUnicast, now,
answerInfo, additionalAnswerInfo, Collections.emptyList());
if (addReplyFromService(question, mGeneralRecords, null /* servicePtrRecord */,
null /* serviceSrvRecord */, null /* serviceTxtRecord */, replyUnicastEnabled,
now, answerInfo, additionalAnswerInfo, Collections.emptyList())) {
replyUnicast &= question.isUnicastReplyRequested();
}
// Add answers from each service
for (int i = 0; i < mServices.size(); i++) {
final ServiceRegistration registration = mServices.valueAt(i);
if (registration.exiting || registration.isProbing) continue;
if (addReplyFromService(question, registration.allRecords, registration.ptrRecords,
registration.srvRecord, registration.txtRecord, replyUnicast, now,
registration.srvRecord, registration.txtRecord, replyUnicastEnabled, now,
answerInfo, additionalAnswerInfo, packet.answers)) {
replyUnicast &= question.isUnicastReplyRequested();
registration.repliedServiceCount++;
registration.sentPacketCount++;
}
@ -570,6 +574,12 @@ public class MdnsRecordRepository {
// Determine the send destination
final InetSocketAddress dest;
if (replyUnicast) {
// As per RFC6762 5.4, "if the responder has not multicast that record recently (within
// one quarter of its TTL), then the responder SHOULD instead multicast the response so
// as to keep all the peer caches up to date": this SHOULD is not implemented to
// minimize latency for queriers who have just started, so they did not receive previous
// multicast responses. Unicast replies are faster as they do not need to wait for the
// beacon interval on Wi-Fi.
dest = src;
} else if (src.getAddress() instanceof Inet4Address) {
dest = IPV4_SOCKET_ADDR;
@ -608,7 +618,7 @@ public class MdnsRecordRepository {
@Nullable List<RecordInfo<MdnsPointerRecord>> servicePtrRecords,
@Nullable RecordInfo<MdnsServiceRecord> serviceSrvRecord,
@Nullable RecordInfo<MdnsTextRecord> serviceTxtRecord,
boolean replyUnicast, long now, @NonNull Set<RecordInfo<?>> answerInfo,
boolean replyUnicastEnabled, long now, @NonNull Set<RecordInfo<?>> answerInfo,
@NonNull Set<RecordInfo<?>> additionalAnswerInfo,
@NonNull List<MdnsRecord> knownAnswerRecords) {
boolean hasDnsSdPtrRecordAnswer = false;
@ -659,7 +669,8 @@ public class MdnsRecordRepository {
// TODO: responses to probe queries should bypass this check and only ensure the
// reply is sent 250ms after the last sent time (RFC 6762 p.15)
if (!replyUnicast && info.lastAdvertisedTimeMs > 0L
if (!(replyUnicastEnabled && question.isUnicastReplyRequested())
&& info.lastAdvertisedTimeMs > 0L
&& now - info.lastAdvertisedTimeMs < MIN_MULTICAST_REPLY_INTERVAL_MS) {
continue;
}

View file

@ -0,0 +1,86 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.testutils
import android.net.MacAddress
import android.util.Log
import com.android.net.module.util.Ipv6Utils
import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
import com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA
import com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED
import com.android.net.module.util.Struct
import com.android.net.module.util.structs.Icmpv6Header
import com.android.net.module.util.structs.Ipv6Header
import com.android.net.module.util.structs.LlaOption
import com.android.net.module.util.structs.NsHeader
import com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH
import java.lang.IllegalArgumentException
import java.net.Inet6Address
import java.nio.ByteBuffer
private const val NS_TYPE = 135.toShort()
/**
* A class that can be used to reply to Neighbor Solicitation packets on a [TapPacketReader].
*/
class NSResponder(
reader: TapPacketReader,
table: Map<Inet6Address, MacAddress>,
name: String = NSResponder::class.java.simpleName
) : PacketResponder(reader, Icmpv6Filter(), name) {
companion object {
private val TAG = NSResponder::class.simpleName
}
// Copy the map if not already immutable (toMap) to make sure it is not modified
private val table = table.toMap()
override fun replyToPacket(packet: ByteArray, reader: TapPacketReader) {
if (packet.size < IPV6_HEADER_LENGTH) {
return
}
val buf = ByteBuffer.wrap(packet, ETHER_HEADER_LEN, packet.size - ETHER_HEADER_LEN)
val ipv6Header = parseOrLog(Ipv6Header::class.java, buf) ?: return
val icmpHeader = parseOrLog(Icmpv6Header::class.java, buf) ?: return
if (icmpHeader.type != NS_TYPE) {
return
}
val ns = parseOrLog(NsHeader::class.java, buf) ?: return
val replyMacAddr = table[ns.target] ?: return
val slla = parseOrLog(LlaOption::class.java, buf) ?: return
val requesterMac = slla.linkLayerAddress
val tlla = LlaOption.build(ICMPV6_ND_OPTION_TLLA.toByte(), replyMacAddr)
reader.sendResponse(Ipv6Utils.buildNaPacket(
replyMacAddr /* srcMac */,
requesterMac /* dstMac */,
ns.target /* srcIp */,
ipv6Header.srcIp /* dstIp */,
NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED,
ns.target,
tlla))
}
private fun <T> parseOrLog(clazz: Class<T>, buf: ByteBuffer): T? where T : Struct {
return try {
Struct.parse(clazz, buf)
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Invalid ${clazz.simpleName} in ICMPv6 packet", e)
null
}
}
}

View file

@ -22,12 +22,12 @@ import android.provider.DeviceConfig
import android.util.Log
import com.android.modules.utils.build.SdkLevel
import com.android.testutils.FunctionalUtils.ThrowingRunnable
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
private val TAG = DeviceConfigRule::class.simpleName
@ -147,11 +147,11 @@ class DeviceConfigRule @JvmOverloads constructor(
return tryTest {
runAsShell(*readWritePermissions) {
DeviceConfig.addOnPropertiesChangedListener(
DeviceConfig.NAMESPACE_CONNECTIVITY,
namespace,
inlineExecutor,
listener)
DeviceConfig.setProperty(
DeviceConfig.NAMESPACE_CONNECTIVITY,
namespace,
key,
value,
false /* makeDefault */)

View file

@ -110,6 +110,12 @@ class ArpRequestFilter : Predicate<ByteArray> {
override fun test(t: ByteArray) = impl.test(t)
}
class Icmpv6Filter : Predicate<ByteArray> {
private val impl = OffsetFilter(ETHER_TYPE_OFFSET, 0x86.toByte(), 0xdd.toByte() /* IPv6 */).and(
OffsetFilter(IPV6_PROTOCOL_OFFSET, 58 /* ICMPv6 */))
override fun test(t: ByteArray) = impl.test(t)
}
/**
* A [Predicate] that matches ethernet-encapped DHCP packets sent from a DHCP client.
*/

View file

@ -23,11 +23,15 @@ import android.os.Process
import com.android.net.module.util.ArrayTrackRecord
import com.android.net.module.util.DnsPacket
import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN
import com.android.net.module.util.NetworkStackConstants.IPV6_DST_ADDR_OFFSET
import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
import com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN
import com.android.net.module.util.TrackRecord
import com.android.testutils.IPv6UdpFilter
import com.android.testutils.TapPacketReader
import java.net.Inet6Address
import java.net.InetAddress
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@ -236,19 +240,28 @@ class NsdServiceInfoCallbackRecord : NsdManager.ServiceInfoCallback,
private fun getMdnsPayload(packet: ByteArray) = packet.copyOfRange(
ETHER_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN, packet.size)
private fun getDstAddr(packet: ByteArray): Inet6Address {
val v6AddrPos = ETHER_HEADER_LEN + IPV6_DST_ADDR_OFFSET
return Inet6Address.getByAddress(packet.copyOfRange(v6AddrPos, v6AddrPos + IPV6_ADDR_LEN))
as Inet6Address
}
fun TapPacketReader.pollForMdnsPacket(
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS,
predicate: (TestDnsPacket) -> Boolean
): TestDnsPacket? {
val mdnsProbeFilter = IPv6UdpFilter(srcPort = MDNS_PORT, dstPort = MDNS_PORT).and {
val dst = getDstAddr(it)
val mdnsPayload = getMdnsPayload(it)
try {
predicate(TestDnsPacket(mdnsPayload))
predicate(TestDnsPacket(mdnsPayload, dst))
} catch (e: DnsPacket.ParseException) {
false
}
}
return poll(timeoutMs, mdnsProbeFilter)?.let { TestDnsPacket(getMdnsPayload(it)) }
return poll(timeoutMs, mdnsProbeFilter)?.let {
TestDnsPacket(getMdnsPayload(it), getDstAddr(it))
}
}
fun TapPacketReader.pollForProbe(
@ -281,7 +294,7 @@ fun TapPacketReader.pollForReply(
it.isReplyFor("$serviceName.$serviceType.local")
}
class TestDnsPacket(data: ByteArray) : DnsPacket(data) {
class TestDnsPacket(data: ByteArray, val dstAddr: InetAddress) : DnsPacket(data) {
val header: DnsHeader
get() = mHeader
val records: Array<List<DnsRecord>>
@ -290,9 +303,10 @@ class TestDnsPacket(data: ByteArray) : DnsPacket(data) {
it.dName == name && it.nsType == DnsResolver.TYPE_ANY
}
fun isReplyFor(name: String): Boolean = mRecords[ANSECTION].any {
it.dName == name && it.nsType == DnsResolver.TYPE_SRV
}
fun isReplyFor(name: String, type: Int = DnsResolver.TYPE_SRV): Boolean =
mRecords[ANSECTION].any {
it.dName == name && it.nsType == type
}
fun isQueryFor(name: String, vararg requiredTypes: Int): Boolean = requiredTypes.all { type ->
mRecords[QDSECTION].any {

View file

@ -61,6 +61,7 @@ import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.platform.test.annotations.AppModeFull
import android.provider.DeviceConfig.NAMESPACE_TETHERING
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants.AF_INET6
@ -69,6 +70,7 @@ import android.system.OsConstants.ENETUNREACH
import android.system.OsConstants.ETH_P_IPV6
import android.system.OsConstants.IPPROTO_IPV6
import android.system.OsConstants.IPPROTO_UDP
import android.system.OsConstants.RT_SCOPE_LINK
import android.system.OsConstants.SOCK_DGRAM
import android.util.Log
import androidx.test.filters.SmallTest
@ -78,12 +80,14 @@ import com.android.compatibility.common.util.PropertyUtil
import com.android.modules.utils.build.SdkLevel.isAtLeastU
import com.android.net.module.util.DnsPacket
import com.android.net.module.util.HexDump
import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN
import com.android.net.module.util.PacketBuilder
import com.android.testutils.ConnectivityModuleTest
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.DeviceConfigRule
import com.android.testutils.NSResponder
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TapPacketReader
@ -133,6 +137,7 @@ private const val DBG = false
private const val TEST_PORT = 12345
private const val MDNS_PORT = 5353.toShort()
private val multicastIpv6Addr = parseNumericAddress("ff02::fb") as Inet6Address
private val testSrcAddr = parseNumericAddress("2001:db8::123") as Inet6Address
@AppModeFull(reason = "Socket cannot bind in instant app mode")
@RunWith(DevSdkIgnoreRunner::class)
@ -144,6 +149,9 @@ class NsdManagerTest {
@get:Rule
val ignoreRule = DevSdkIgnoreRule()
@get:Rule
val deviceConfigRule = DeviceConfigRule()
private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
private val nsdManager by lazy {
context.getSystemService(NsdManager::class.java) ?: fail("Could not get NsdManager service")
@ -682,7 +690,7 @@ class NsdManagerTest {
assertEquals(OffloadEngine.OFFLOAD_TYPE_REPLY.toLong(), serviceInfo.offloadType)
val offloadPayload = serviceInfo.offloadPayload
assertNotNull(offloadPayload)
val dnsPacket = TestDnsPacket(offloadPayload)
val dnsPacket = TestDnsPacket(offloadPayload, dstAddr = multicastIpv6Addr)
assertEquals(0x8400, dnsPacket.header.flags)
assertEquals(0, dnsPacket.records[DnsPacket.QDSECTION].size)
assertTrue(dnsPacket.records[DnsPacket.ANSECTION].size >= 5)
@ -1286,7 +1294,8 @@ class NsdManagerTest {
// Resolve service on testNetwork1
val resolveRecord = NsdResolveRecord()
val packetReader = TapPacketReader(Handler(handlerThread.looper),
testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */
)
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
@ -1349,6 +1358,68 @@ class NsdManagerTest {
serviceResolved.serviceInfo.hostAddresses.toSet())
}
@Test
fun testUnicastReplyUsedWhenQueryUnicastFlagSet() {
// The flag may be removed in the future but unicast replies should be enabled by default
// in that case. The rule will reset flags automatically on teardown.
deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_unicast_reply_enabled", "1")
val si = makeTestServiceInfo(testNetwork1.network)
// Register service on testNetwork1
val registrationRecord = NsdRegistrationRecord()
var nsResponder: NSResponder? = null
tryTest {
registerService(registrationRecord, si)
val packetReader = TapPacketReader(Handler(handlerThread.looper),
testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
/*
Send a "query unicast" query.
Generated with:
scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR', qclass=0x8001)
)).hex()
*/
val mdnsPayload = HexDump.hexStringToByteArray("0000000000010000000000000d5f6e6d74313" +
"233343536373839045f746370056c6f63616c00000c8001")
replaceServiceNameAndTypeWithTestSuffix(mdnsPayload)
val testSrcAddr = makeLinkLocalAddressOfOtherDeviceOnPrefix(testNetwork1.network)
nsResponder = NSResponder(packetReader, mapOf(
testSrcAddr to MacAddress.fromString("01:02:03:04:05:06")
)).apply { start() }
packetReader.sendResponse(buildMdnsPacket(mdnsPayload, testSrcAddr))
// The reply is sent unicast to the source address. There may be announcements sent
// multicast around this time, so filter by destination address.
val reply = packetReader.pollForMdnsPacket { pkt ->
pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
pkt.dstAddr == testSrcAddr
}
assertNotNull(reply)
} cleanup {
nsResponder?.stop()
nsdManager.unregisterService(registrationRecord)
registrationRecord.expectCallback<ServiceUnregistered>()
}
}
private fun makeLinkLocalAddressOfOtherDeviceOnPrefix(network: Network): Inet6Address {
val lp = cm.getLinkProperties(network) ?: fail("No LinkProperties for net $network")
// Expect to have a /64 link-local address
val linkAddr = lp.linkAddresses.firstOrNull {
it.isIPv6 && it.scope == RT_SCOPE_LINK && it.prefixLength == 64
} ?: fail("No /64 link-local address found in ${lp.linkAddresses} for net $network")
// Add one to the device address to simulate the address of another device on the prefix
val addrBytes = linkAddr.address.address
addrBytes[IPV6_ADDR_LEN - 1]++
return Inet6Address.getByAddress(addrBytes) as Inet6Address
}
private fun buildConflictingAnnouncement(): ByteBuffer {
/*
Generated with:
@ -1393,7 +1464,10 @@ class NsdManagerTest {
replaceAll(buffer, source, replacement)
}
private fun buildMdnsPacket(mdnsPayload: ByteArray): ByteBuffer {
private fun buildMdnsPacket(
mdnsPayload: ByteArray,
srcAddr: Inet6Address = testSrcAddr
): ByteBuffer {
val packetBuffer = PacketBuilder.allocate(true /* hasEther */, IPPROTO_IPV6,
IPPROTO_UDP, mdnsPayload.size)
val packetBuilder = PacketBuilder(packetBuffer)
@ -1408,7 +1482,7 @@ class NsdManagerTest {
0x60000000, // version=6, traffic class=0x0, flowlabel=0x0
IPPROTO_UDP.toByte(),
64 /* hop limit */,
parseNumericAddress("2001:db8::123") as Inet6Address /* srcIp */,
srcAddr,
multicastIpv6Addr /* dstIp */)
packetBuilder.writeUdpHeader(MDNS_PORT /* srcPort */, MDNS_PORT /* dstPort */)
packetBuffer.put(mdnsPayload)

View file

@ -24,7 +24,6 @@ import android.os.HandlerThread
import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
import com.android.server.connectivity.mdns.MdnsRecord.TYPE_A
import com.android.server.connectivity.mdns.MdnsRecord.TYPE_AAAA
import com.android.server.connectivity.mdns.MdnsRecord.TYPE_ANY
import com.android.server.connectivity.mdns.MdnsRecord.TYPE_PTR
import com.android.server.connectivity.mdns.MdnsRecord.TYPE_SRV
import com.android.server.connectivity.mdns.MdnsRecord.TYPE_TXT
@ -95,7 +94,6 @@ class MdnsRecordRepositoryTest {
override fun getInterfaceInetAddresses(iface: NetworkInterface) =
Collections.enumeration(TEST_ADDRESSES.map { it.address })
}
private val flags = MdnsFeatureFlags.newBuilder().build()
@Before
fun setUp() {
@ -108,9 +106,19 @@ class MdnsRecordRepositoryTest {
thread.join()
}
private fun makeFlags(
includeInetAddressesInProbing: Boolean = false,
isKnownAnswerSuppressionEnabled: Boolean = false,
unicastReplyEnabled: Boolean = true
) = MdnsFeatureFlags.Builder()
.setIncludeInetAddressRecordsInProbing(includeInetAddressesInProbing)
.setIsKnownAnswerSuppressionEnabled(isKnownAnswerSuppressionEnabled)
.setIsUnicastReplyEnabled(unicastReplyEnabled)
.build()
@Test
fun testAddServiceAndProbe() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
assertEquals(0, repository.servicesCount)
assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
assertEquals(1, repository.servicesCount)
@ -144,7 +152,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testAddAndConflicts() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
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)
@ -156,7 +164,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testAddAndUpdates() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(IllegalArgumentException::class) {
@ -190,7 +198,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testInvalidReuseOfServiceId() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(IllegalArgumentException::class) {
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2)
@ -199,7 +207,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testHasActiveService() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
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)
@ -216,7 +224,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testExitAnnouncements() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
@ -246,7 +254,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testExitAnnouncements_WithSubtypes() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
@ -288,7 +296,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testExitingServiceReAdded() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
repository.exitService(TEST_SERVICE_ID_1)
@ -303,7 +311,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testOnProbingSucceeded() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
@ -435,7 +443,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetOffloadPacket() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
val serviceType = arrayOf("_testservice", "_tcp", "local")
@ -497,7 +505,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetReplyCaseInsensitive() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
val questionsCaseInSensitive = listOf(
MdnsPointerRecord(arrayOf("_TESTSERVICE", "_TCP", "local"), false /* isUnicast */))
@ -517,7 +525,7 @@ class MdnsRecordRepositoryTest {
private fun makeQuery(vararg queries: Pair<Int, Array<String>>): MdnsPacket {
val questions = queries.map { (type, name) -> makeQuestionRecord(name, type) }
return MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
listOf() /* authorityRecords */, listOf() /* additionalRecords */)
listOf() /* authorityRecords */, listOf() /* additionalRecords */)
}
private fun makeQuestionRecord(name: Array<String>, type: Int): MdnsRecord {
@ -532,7 +540,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetReply_singlePtrQuestion_returnsSrvTxtAddressNsecRecords() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
@ -563,7 +571,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetReply_singleSubtypePtrQuestion_returnsSrvTxtAddressNsecRecords() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
@ -596,7 +604,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetReply_duplicatePtrQuestions_doesNotReturnDuplicateRecords() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
@ -629,7 +637,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetReply_multiplePtrQuestionsWithSubtype_doesNotReturnDuplicateRecords() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
@ -665,7 +673,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetReply_txtQuestion_returnsNoNsecRecord() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
@ -682,7 +690,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetReply_AAAAQuestionButNoIpv6Address_returnsNsecRecord() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(
TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE),
listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
@ -701,7 +709,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetReply_ptrAndSrvQuestions_doesNotReturnSrvRecordInAdditionalAnswerSection() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
@ -723,7 +731,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetReply_srvTxtAddressQuestions_returnsAllRecordsInAnswerSectionExceptNsec() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
@ -757,7 +765,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetReply_queryWithIpv4Address_replyWithIpv4Address() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
@ -771,7 +779,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetReply_queryWithIpv6Address_replyWithIpv6Address() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
@ -785,7 +793,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetConflictingServices() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
@ -813,7 +821,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetConflictingServicesCaseInsensitive() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
@ -841,7 +849,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetConflictingServices_IdenticalService() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
@ -870,7 +878,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetConflictingServicesCaseInsensitive_IdenticalService() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
@ -899,7 +907,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testGetServiceRepliedRequestsCount() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
// Verify that there is no packet replied.
assertEquals(MdnsConstants.NO_PACKET,
@ -924,7 +932,7 @@ class MdnsRecordRepositoryTest {
@Test
fun testIncludeInetAddressRecordsInProbing() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
MdnsFeatureFlags.newBuilder().setIncludeInetAddressRecordsInProbing(true).build())
makeFlags(includeInetAddressesInProbing = true))
repository.updateAddresses(TEST_ADDRESSES)
assertEquals(0, repository.servicesCount)
assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
@ -990,7 +998,7 @@ class MdnsRecordRepositoryTest {
expectReply: Boolean
) {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
MdnsFeatureFlags.newBuilder().setIsKnownAnswerSuppressionEnabled(true).build())
makeFlags(isKnownAnswerSuppressionEnabled = true))
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
val query = MdnsPacket(0 /* flags */, questions, knownAnswers,
listOf() /* authorityRecords */, listOf() /* additionalRecords */)
@ -1222,23 +1230,109 @@ class MdnsRecordRepositoryTest {
MdnsPointerRecord(queriedName, false /* isUnicast */),
MdnsServiceRecord(serviceName, false /* isUnicast */))
val knownAnswers = listOf(
MdnsPointerRecord(
queriedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
LONG_TTL - 1000L,
serviceName),
MdnsServiceRecord(
serviceName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
SHORT_TTL - 15_000L,
0 /* servicePriority */,
0 /* serviceWeight */,
TEST_PORT,
TEST_HOSTNAME))
doGetReplyWithAnswersTest(questions, knownAnswers, listOf() /* replyAnswers */,
listOf() /* additionalAnswers */, false /* expectReply */)
MdnsPointerRecord(
queriedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
LONG_TTL - 1000L,
serviceName
),
MdnsServiceRecord(
serviceName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
SHORT_TTL - 15_000L,
0 /* servicePriority */,
0 /* serviceWeight */,
TEST_PORT,
TEST_HOSTNAME
)
)
doGetReplyWithAnswersTest(
questions, knownAnswers, listOf() /* replyAnswers */,
listOf() /* additionalAnswers */, false /* expectReply */
)
}
@Test
fun testReplyUnicastToQueryUnicastQuestions() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
// Ask for 2 services, only the first one is known and requests unicast reply
val questions = listOf(
MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), true /* isUnicast */),
MdnsPointerRecord(arrayOf("_otherservice", "_tcp", "local"), true /* isUnicast */))
val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
listOf() /* authorityRecords */, listOf() /* additionalRecords */)
val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
// Reply to the question and verify it is sent to the source.
val reply = repository.getReply(query, src)
assertNotNull(reply)
assertEquals(src, reply.destination)
}
@Test
fun testReplyMulticastToQueryUnicastAndMulticastMixedQuestions() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, NsdServiceInfo().apply {
serviceType = "_otherservice._tcp"
serviceName = "OtherTestService"
port = TEST_PORT
})
// Ask for 2 services, both are known and only the first one requests unicast reply
val questions = listOf(
MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), true /* isUnicast */),
MdnsPointerRecord(arrayOf("_otherservice", "_tcp", "local"), false /* isUnicast */))
val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
listOf() /* authorityRecords */, listOf() /* additionalRecords */)
val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
// Reply to the question and verify it is sent multicast.
val reply = repository.getReply(query, src)
assertNotNull(reply)
assertEquals(MdnsConstants.getMdnsIPv6Address(), reply.destination.address)
}
@Test
fun testReplyMulticastWhenNoUnicastQueryMatches() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
// Ask for 2 services, the first one requests a unicast reply but is unknown
val questions = listOf(
MdnsPointerRecord(arrayOf("_otherservice", "_tcp", "local"), true /* isUnicast */),
MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), false /* isUnicast */))
val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
listOf() /* authorityRecords */, listOf() /* additionalRecords */)
val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
// Reply to the question and verify it is sent multicast.
val reply = repository.getReply(query, src)
assertNotNull(reply)
assertEquals(MdnsConstants.getMdnsIPv6Address(), reply.destination.address)
}
@Test
fun testReplyMulticastWhenUnicastFeatureDisabled() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
makeFlags(unicastReplyEnabled = false))
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
// The service is known and requests unicast reply, but the feature is disabled
val questions = listOf(
MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), true /* isUnicast */))
val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
listOf() /* authorityRecords */, listOf() /* additionalRecords */)
val src = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
// Reply to the question and verify it is sent multicast.
val reply = repository.getReply(query, src)
assertNotNull(reply)
assertEquals(MdnsConstants.getMdnsIPv6Address(), reply.destination.address)
}
}
@ -1250,6 +1344,13 @@ private fun MdnsRecordRepository.initWithService(
): AnnouncementInfo {
updateAddresses(addresses)
serviceInfo.setSubtypes(subtypes)
return addServiceAndFinishProbing(serviceId, serviceInfo)
}
private fun MdnsRecordRepository.addServiceAndFinishProbing(
serviceId: Int,
serviceInfo: NsdServiceInfo
): AnnouncementInfo {
addService(serviceId, serviceInfo)
val probingInfo = setServiceProbing(serviceId)
assertNotNull(probingInfo)