diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java index 6c25d76d0b..9b2f80b3ac 100644 --- a/service-t/src/com/android/server/NsdService.java +++ b/service-t/src/com/android/server/NsdService.java @@ -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, diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java index 1ad47a30b7..946616226d 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java @@ -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. + * + *

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); } } } diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java index 28bd1b4d23..4b43989a53 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java @@ -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. * 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 6b6632cd6e..585b09791f 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java @@ -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> answerInfo = new LinkedHashSet<>(); final Set> 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> servicePtrRecords, @Nullable RecordInfo serviceSrvRecord, @Nullable RecordInfo serviceTxtRecord, - boolean replyUnicast, long now, @NonNull Set> answerInfo, + boolean replyUnicastEnabled, long now, @NonNull Set> answerInfo, @NonNull Set> additionalAnswerInfo, @NonNull List 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; } diff --git a/staticlibs/testutils/devicetests/NSResponder.kt b/staticlibs/testutils/devicetests/NSResponder.kt new file mode 100644 index 0000000000..f7619cdac3 --- /dev/null +++ b/staticlibs/testutils/devicetests/NSResponder.kt @@ -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, + 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 parseOrLog(clazz: Class, 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 + } + } +} diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt index 3d98cc381b..68248ca5ee 100644 --- a/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt +++ b/staticlibs/testutils/devicetests/com/android/testutils/DeviceConfigRule.kt @@ -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 */) diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt b/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt index 1bb6d683e2..a73a58a40a 100644 --- a/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt +++ b/staticlibs/testutils/hostdevice/com/android/testutils/PacketFilter.kt @@ -110,6 +110,12 @@ class ArpRequestFilter : Predicate { override fun test(t: ByteArray) = impl.test(t) } +class Icmpv6Filter : Predicate { + 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. */ diff --git a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt index eef3f8724b..5ba6c4c720 100644 --- a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt +++ b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt @@ -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> @@ -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 { diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt index a04020149b..1309e79872 100644 --- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt +++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt @@ -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() + } + } + + 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) 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 4b1f16690f..1edc80691d 100644 --- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt +++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt @@ -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>): 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, 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)