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:
parent
93f2efbe86
commit
5355fce9b2
1 changed files with 202 additions and 45 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue