diff --git a/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt b/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt index 1f82a35dc5..e49c0c7a3d 100644 --- a/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt +++ b/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt @@ -18,72 +18,56 @@ package com.android.testutils import android.net.DnsResolver import android.net.InetAddresses -import android.os.Looper +import android.net.Network import android.os.Handler +import android.os.Looper import com.android.internal.annotations.GuardedBy -import java.net.InetAddress -import java.util.concurrent.Executor -import org.mockito.invocation.InvocationOnMock +import com.android.net.module.util.DnsPacket import org.mockito.Mockito.any import org.mockito.Mockito.anyInt import org.mockito.Mockito.doAnswer +import org.mockito.invocation.InvocationOnMock +import org.mockito.stubbing.Answer +import java.net.InetAddress +import java.net.UnknownHostException +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor +import java.util.concurrent.TimeUnit -const val TYPE_UNSPECIFIED = -1 -// TODO: Integrate with NetworkMonitorTest. -class FakeDns(val mockResolver: DnsResolver) { - class DnsEntry(val hostname: String, val type: Int, val addresses: List) { - fun match(host: String, type: Int) = hostname.equals(host) && type == type - } +// Nonexistent DNS query type to represent "A and/or AAAA queries". +// TODO: deduplicate this with DnsUtils.TYPE_ADDRCONFIG. +private const val TYPE_ADDRCONFIG = -1 - @GuardedBy("answers") - val answers = ArrayList() +class FakeDns(val network: Network, val dnsResolver: DnsResolver) { + private val HANDLER_TIMEOUT_MS = 1000 - fun getAnswer(hostname: String, type: Int): DnsEntry? = synchronized(answers) { - return answers.firstOrNull { it.match(hostname, type) } - } - - fun setAnswer(hostname: String, answer: Array, type: Int) = synchronized(answers) { - val ans = DnsEntry(hostname, type, generateAnswer(answer)) - // Replace or remove the existing one. - when (val index = answers.indexOfFirst { it.match(hostname, type) }) { - -1 -> answers.add(ans) - else -> answers[index] = ans + /** Data class to record the Dns entry. */ + class DnsEntry (val hostname: String, val type: Int, val answerSupplier: AnswerSupplier) { + // Full match or partial match that target host contains the entry hostname to support + // random private dns probe hostname. + fun matches(hostname: String, type: Int): Boolean { + return hostname.endsWith(this.hostname) && type == this.type } } - private fun generateAnswer(answer: Array) = - answer.filterNotNull().map { InetAddresses.parseNumericAddress(it) } + /** + * Whether queries on [network] will be answered when private DNS is enabled. Queries that + * bypass private DNS by using [network.privateDnsBypassingCopy] are always answered. + */ + var nonBypassPrivateDnsWorking: Boolean = true - fun startMocking() { - // Mock DnsResolver.query() w/o type - doAnswer { - mockAnswer(it, 1, -1, 3, 5) - }.`when`(mockResolver).query(any() /* network */, any() /* domain */, anyInt() /* flags */, - any() /* executor */, any() /* cancellationSignal */, any() /*callback*/) - // Mock DnsResolver.query() w/ type - doAnswer { - mockAnswer(it, 1, 2, 4, 6) - }.`when`(mockResolver).query(any() /* network */, any() /* domain */, anyInt() /* nsType */, - anyInt() /* flags */, any() /* executor */, any() /* cancellationSignal */, - any() /*callback*/) + @GuardedBy("answers") + private val answers = mutableListOf() + + interface AnswerSupplier { + /** Supplies the answer to one DnsResolver query method call. */ + @Throws(DnsResolver.DnsException::class) + fun get(): Array? } - private fun mockAnswer( - it: InvocationOnMock, - posHos: Int, - posType: Int, - posExecutor: Int, - posCallback: Int - ) { - val hostname = it.arguments[posHos] as String - val executor = it.arguments[posExecutor] as Executor - val callback = it.arguments[posCallback] as DnsResolver.Callback> - var type = if (posType != -1) it.arguments[posType] as Int else TYPE_UNSPECIFIED - val answer = getAnswer(hostname, type) - - if (answer != null && !answer.addresses.isNullOrEmpty()) { - Handler(Looper.getMainLooper()).post({ executor.execute({ - callback.onAnswer(answer.addresses, 0); }) }) + private class InstantAnswerSupplier(val answers: Array?) : AnswerSupplier { + override fun get(): Array? { + return answers } } @@ -91,4 +75,177 @@ class FakeDns(val mockResolver: DnsResolver) { fun clearAll() = synchronized(answers) { answers.clear() } + + /** Returns the answer for a given name and type on the given mock network. */ + private fun getAnswer(mockNetwork: Network, hostname: String, type: Int): + CompletableFuture?> { + if (!checkQueryNetwork(mockNetwork)) { + return CompletableFuture.completedFuture(null) + } + val answerSupplier: AnswerSupplier? = synchronized(answers) { + answers.firstOrNull({e: DnsEntry -> e.matches(hostname, type)})?.answerSupplier + } + if (answerSupplier == null) { + return CompletableFuture.completedFuture(null) + } + if (answerSupplier is InstantAnswerSupplier) { + // Save latency waiting for a query thread if the answer is hardcoded. + return CompletableFuture.completedFuture?>(answerSupplier.get()) + } + val answerFuture = CompletableFuture?>() + // Don't worry about ThreadLeadMonitor: these threads terminate immediately, so they won't + // leak, and ThreadLeakMonitor won't monitor them anyway, since they have one-time names + // such as "Thread-42". + Thread { + try { + answerFuture.complete(answerSupplier.get()) + } catch (e: DnsResolver.DnsException) { + answerFuture.completeExceptionally(e) + } + }.start() + return answerFuture + } + + /** Sets the answer for a given name and type. */ + fun setAnswer(hostname: String, answer: Array?, type: Int) = setAnswer( + hostname, InstantAnswerSupplier(answer), type) + + /** Sets the answer for a given name and type. */ + fun setAnswer( + hostname: String, answerSupplier: AnswerSupplier, type: Int) = synchronized (answers) { + val ans = DnsEntry(hostname, type, answerSupplier) + // Replace or remove the existing one. + when (val index = answers.indexOfFirst { it.matches(hostname, type) }) { + -1 -> answers.add(ans) + else -> answers[index] = ans + } + } + + private fun checkQueryNetwork(mockNetwork: Network): Boolean { + // Queries on the wrong network do not work. + // Queries that bypass private DNS work. + // Queries that do not bypass private DNS work only if nonBypassPrivateDnsWorking is true. + return mockNetwork == network.privateDnsBypassingCopy || + mockNetwork == network && nonBypassPrivateDnsWorking + } + + /** Simulates a getAllByName call for the specified name on the specified mock network. */ + private fun getAllByName(mockNetwork: Network, hostname: String): Array? { + val answer = stringsToInetAddresses(queryAllTypes(mockNetwork, hostname) + .get(HANDLER_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)) + if (answer == null || answer.size == 0) { + throw UnknownHostException(hostname) + } + return answer.toTypedArray() + } + + // Regardless of the type, depends on what the responses contained in the network. + private fun queryAllTypes( + mockNetwork: Network, hostname: String + ): CompletableFuture?> { + val aFuture = getAnswer(mockNetwork, hostname, DnsResolver.TYPE_A) + .exceptionally { emptyArray() } + val aaaaFuture = getAnswer(mockNetwork, hostname, DnsResolver.TYPE_AAAA) + .exceptionally { emptyArray() } + val combinedFuture = CompletableFuture?>() + aFuture.thenAcceptBoth(aaaaFuture) { res1: Array?, res2: Array? -> + var answer: Array = arrayOf() + if (res1 != null) answer += res1 + if (res2 != null) answer += res2 + combinedFuture.complete(answer) + } + return combinedFuture + } + + /** Starts mocking DNS queries. */ + fun startMocking() { + // Queries on mNetwork using getAllByName. + doAnswer { + getAllByName(it.mock as Network, it.getArgument(0)) + }.`when`(network).getAllByName(any()) + + // Queries on mCleartextDnsNetwork using DnsResolver#query. + doAnswer { + mockQuery(it, posNetwork = 0, posHostname = 1, posExecutor = 3, posCallback = 5, + posType = -1) + }.`when`(dnsResolver).query(any(), any(), anyInt(), any(), any(), any()) + + // Queries on mCleartextDnsNetwork using DnsResolver#query with QueryType. + doAnswer { + mockQuery(it, posNetwork = 0, posHostname = 1, posExecutor = 4, posCallback = 6, + posType = 2) + }.`when`(dnsResolver).query(any(), any(), anyInt(), anyInt(), any(), any(), any()) + + // Queries using rawQuery. Currently, mockQuery only supports TYPE_SVCB. + doAnswer { + mockQuery(it, posNetwork = 0, posHostname = 1, posExecutor = 5, posCallback = 7, + posType = 3) + }.`when`(dnsResolver).rawQuery(any(), any(), anyInt(), anyInt(), anyInt(), any(), any(), + any()) + } + + private fun stringsToInetAddresses(addrs: Array?): List? { + if (addrs == null) return null + val out: MutableList = ArrayList() + for (addr in addrs) { + out.add(InetAddresses.parseNumericAddress(addr)) + } + return out + } + + // Mocks all the DnsResolver query methods used in this test. + private fun mockQuery( + invocation: InvocationOnMock, posNetwork: Int, posHostname: Int, + posExecutor: Int, posCallback: Int, posType: Int + ): Answer<*>? { + val hostname = invocation.getArgument(posHostname) + val executor = invocation.getArgument(posExecutor) + val network = invocation.getArgument(posNetwork) + val qtype = if (posType != -1) invocation.getArgument(posType) else TYPE_ADDRCONFIG + val answerFuture: CompletableFuture?> = if (posType != -1) getAnswer( + network, + hostname, + invocation.getArgument(posType) + ) else queryAllTypes(network, hostname) + + // Discriminate between different callback types to avoid unchecked cast warnings when + // calling the onAnswer methods. + val inetAddressCallback: DnsResolver.Callback> = + invocation.getArgument(posCallback) + val byteArrayCallback: DnsResolver.Callback = + invocation.getArgument(posCallback) + val callback: DnsResolver.Callback<*> = invocation.getArgument(posCallback) + + answerFuture.whenComplete { answer: Array?, exception: Throwable? -> + // Use getMainLooper() because that's what android.net.DnsResolver currently uses. + Handler(Looper.getMainLooper()).post { + executor.execute { + if (exception != null) { + if (exception !is DnsResolver.DnsException) { + throw java.lang.AssertionError( + "Test error building DNS response", + exception + ) + } + callback.onError((exception as DnsResolver.DnsException?)!!) + return@execute + } + if (answer != null && answer.size > 0) { + when (qtype) { + DnsResolver.TYPE_A, DnsResolver.TYPE_AAAA, TYPE_ADDRCONFIG -> + inetAddressCallback.onAnswer(stringsToInetAddresses(answer)!!, 0) + DnsPacket.TYPE_SVCB -> + byteArrayCallback.onAnswer( + DnsSvcbUtils.makeSvcbResponse(hostname, answer), 0) + else -> throw UnsupportedOperationException( + "Unsupported qtype $qtype, update this fake" + ) + } + } + } + } + } + // If the future does not complete or has no answer do nothing. The timeout should fire. + return null + } }