Merge changes from topics "ddr-tests-in-networkstack", "move-fakedns" into main am: 3627907075 am: 3add91cc16

Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/3261692

Change-Id: I7fcb65fb0c9feaeae3c1bd92b52bac56fb4d9069
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java
new file mode 100644
index 0000000..8608344
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2024 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 static android.net.DnsResolver.CLASS_IN;
+
+import static com.android.net.module.util.DnsPacket.TYPE_SVCB;
+import static com.android.net.module.util.DnsPacketUtils.DnsRecordParser.domainNameToLabels;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
+
+import static org.junit.Assert.fail;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import android.net.InetAddresses;
+
+import androidx.annotation.NonNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DnsSvcbUtils {
+    private static final Pattern SVC_PARAM_PATTERN = Pattern.compile("([a-z0-9-]+)=?(.*)");
+
+    /**
+     * Returns a DNS SVCB response with given hostname `hostname` and given SVCB records
+     * `records`. Each record must contain the service priority, the target name, and the service
+     * parameters.
+     *     E.g. "1 doh.google alpn=h2,h3 port=443 ipv4hint=192.0.2.1 dohpath=/dns-query{?dns}"
+     */
+    @NonNull
+    public static byte[] makeSvcbResponse(String hostname, String[] records) throws IOException {
+        if (records == null) throw new NullPointerException();
+        if (!hostname.startsWith("_dns.")) throw new UnsupportedOperationException();
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        // Write DNS header.
+        os.write(shortsToByteArray(
+                0x1234,         /* Transaction ID */
+                0x8100,         /* Flags */
+                1,              /* qdcount */
+                records.length, /* ancount */
+                0,              /* nscount */
+                0               /* arcount */
+        ));
+        // Write Question.
+        // - domainNameToLabels() doesn't support the hostname starting with "_", so divide
+        //   the writing into two steps.
+        os.write(new byte[] { 0x04, '_', 'd', 'n', 's' });
+        os.write(domainNameToLabels(hostname.substring(5)));
+        os.write(shortsToByteArray(TYPE_SVCB, CLASS_IN));
+        // Write Answer section.
+        for (String r : records) {
+            os.write(makeSvcbRecord(r));
+        }
+        return os.toByteArray();
+    }
+
+    @NonNull
+    private static byte[] makeSvcbRecord(String representation) throws IOException {
+        if (representation == null) return new byte[0];
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        os.write(shortsToByteArray(
+                0xc00c, /* Pointer to qname in question section */
+                TYPE_SVCB,
+                CLASS_IN,
+                0, 16, /* TTL = 16 */
+                0 /* Data Length = 0 */
+
+        ));
+        final String[] strings = representation.split(" +");
+        // SvcPriority and TargetName are mandatory in the representation.
+        if (strings.length < 3) {
+            fail("Invalid SVCB representation: " + representation);
+        }
+        // Write SvcPriority, TargetName, and SvcParams.
+        os.write(shortsToByteArray(Short.parseShort(strings[0])));
+        os.write(domainNameToLabels(strings[1]));
+        for (int i = 2; i < strings.length; i++) {
+            try {
+                os.write(svcParamToByteArray(strings[i]));
+            } catch (UnsupportedEncodingException e) {
+                throw new IOException(e);
+            }
+        }
+        // Update rdata length.
+        final byte[] out = os.toByteArray();
+        ByteBuffer.wrap(out).putShort(10, (short) (out.length - 12));
+        return out;
+    }
+
+    @NonNull
+    private static byte[] svcParamToByteArray(String svcParam) throws IOException {
+        final Matcher matcher = SVC_PARAM_PATTERN.matcher(svcParam);
+        if (!matcher.matches() || matcher.groupCount() != 2) {
+            fail("Invalid SvcParam: " + svcParam);
+        }
+        final String svcParamkey = matcher.group(1);
+        final String svcParamValue = matcher.group(2);
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        os.write(svcParamKeyToBytes(svcParamkey));
+        switch (svcParamkey) {
+            case "mandatory":
+                final String[] keys = svcParamValue.split(",");
+                os.write(shortsToByteArray(keys.length));
+                for (String v : keys) {
+                    os.write(svcParamKeyToBytes(v));
+                }
+                break;
+            case "alpn":
+                os.write(shortsToByteArray((svcParamValue.length() + 1)));
+                for (String v : svcParamValue.split(",")) {
+                    os.write(v.length());
+                    // TODO: support percent-encoding per RFC 7838.
+                    os.write(v.getBytes(US_ASCII));
+                }
+                break;
+            case "no-default-alpn":
+                os.write(shortsToByteArray(0));
+                break;
+            case "port":
+                os.write(shortsToByteArray(2));
+                os.write(shortsToByteArray(Short.parseShort(svcParamValue)));
+                break;
+            case "ipv4hint":
+                final String[] v4Addrs = svcParamValue.split(",");
+                os.write(shortsToByteArray((v4Addrs.length * IPV4_ADDR_LEN)));
+                for (String v : v4Addrs) {
+                    os.write(InetAddresses.parseNumericAddress(v).getAddress());
+                }
+                break;
+            case "ech":
+                os.write(shortsToByteArray(svcParamValue.length()));
+                os.write(svcParamValue.getBytes(US_ASCII));  // base64 encoded
+                break;
+            case "ipv6hint":
+                final String[] v6Addrs = svcParamValue.split(",");
+                os.write(shortsToByteArray((v6Addrs.length * IPV6_ADDR_LEN)));
+                for (String v : v6Addrs) {
+                    os.write(InetAddresses.parseNumericAddress(v).getAddress());
+                }
+                break;
+            case "dohpath":
+                os.write(shortsToByteArray(svcParamValue.length()));
+                // TODO: support percent-encoding, since this is a URI template.
+                os.write(svcParamValue.getBytes(US_ASCII));
+                break;
+            default:
+                os.write(shortsToByteArray(svcParamValue.length()));
+                os.write(svcParamValue.getBytes(US_ASCII));
+                break;
+        }
+        return os.toByteArray();
+    }
+
+    @NonNull
+    private static byte[] svcParamKeyToBytes(String key) {
+        switch (key) {
+            case "mandatory": return shortsToByteArray(0);
+            case "alpn": return shortsToByteArray(1);
+            case "no-default-alpn": return shortsToByteArray(2);
+            case "port": return shortsToByteArray(3);
+            case "ipv4hint": return shortsToByteArray(4);
+            case "ech": return shortsToByteArray(5);
+            case "ipv6hint": return shortsToByteArray(6);
+            case "dohpath": return shortsToByteArray(7);
+            default:
+                if (!key.startsWith("key")) fail("Invalid SvcParamKey " + key);
+                return shortsToByteArray(Short.parseShort(key.substring(3)));
+        }
+    }
+
+    @NonNull
+    private static byte[] shortsToByteArray(int... values) {
+        final ByteBuffer out = ByteBuffer.allocate(values.length * 2);
+        for (int value: values) {
+            if (value < 0 || value > 0xffff) {
+                throw new AssertionError("not an unsigned short: " + value);
+            }
+            out.putShort((short) value);
+        }
+        return out.array();
+    }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt b/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
index 1f82a35..e49c0c7 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/FakeDns.kt
@@ -18,72 +18,56 @@
 
 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 @@
     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
+    }
 }