Move to one unified implementation of FakeDns.

Take the functionality from the (better) FakeDns in NetworkStack,
move it to the FakeDns in staticlibs, and port it to kotlin.

Test: test-only change
Change-Id: I6d0b4a348ca86ab69785cdf722c259ef6a92ce4b
This commit is contained in:
Lorenzo Colitti 2024-09-10 19:15:03 +09:00
commit 5355fce9b2

View file

@ -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<InetAddress>) {
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<DnsEntry>()
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<String>, 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<String>) =
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<DnsEntry>()
interface AnswerSupplier {
/** Supplies the answer to one DnsResolver query method call. */
@Throws(DnsResolver.DnsException::class)
fun get(): Array<String>?
}
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<List<InetAddress>>
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<String>?) : AnswerSupplier {
override fun get(): Array<String>? {
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<Array<String>?> {
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<Array<String>?>(answerSupplier.get())
}
val answerFuture = CompletableFuture<Array<String>?>()
// 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<String>?, 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<InetAddress>? {
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<Array<String>?> {
val aFuture = getAnswer(mockNetwork, hostname, DnsResolver.TYPE_A)
.exceptionally { emptyArray() }
val aaaaFuture = getAnswer(mockNetwork, hostname, DnsResolver.TYPE_AAAA)
.exceptionally { emptyArray() }
val combinedFuture = CompletableFuture<Array<String>?>()
aFuture.thenAcceptBoth(aaaaFuture) { res1: Array<String>?, res2: Array<String>? ->
var answer: Array<String> = 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<String>?): List<InetAddress>? {
if (addrs == null) return null
val out: MutableList<InetAddress> = 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<String>(posHostname)
val executor = invocation.getArgument<Executor>(posExecutor)
val network = invocation.getArgument<Network>(posNetwork)
val qtype = if (posType != -1) invocation.getArgument(posType) else TYPE_ADDRCONFIG
val answerFuture: CompletableFuture<Array<String>?> = 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<List<InetAddress>> =
invocation.getArgument(posCallback)
val byteArrayCallback: DnsResolver.Callback<ByteArray> =
invocation.getArgument(posCallback)
val callback: DnsResolver.Callback<*> = invocation.getArgument(posCallback)
answerFuture.whenComplete { answer: Array<String>?, 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
}
}