Snap for 12391343 from f1b7825cd024dcfca7b12c97371132818995685c to 24Q4-release

Change-Id: I13692f5b19806b7a4d25024bab06c7e5d645aa7e
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 3b197fc..0c05354 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -98,7 +98,6 @@
     ],
     canned_fs_config: "canned_fs_config",
     bpfs: [
-        "block.o",
         "clatd.o",
         "dscpPolicy.o",
         "netd.o",
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index 69f1cb5..f369458 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -215,7 +215,7 @@
  * is the name of the program, and tracepoint is the type.
  *
  * However, be aware that you should not be directly using the SECTION() macro.
- * Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE/CRITICAL macros.
+ * Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE macros.
  *
  * Programs shipped inside the tethering apex should be limited to networking stuff,
  * as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
@@ -1105,30 +1105,22 @@
     return 0;
 }
 
-int loadProg(const char* const elfPath, bool* const isCritical, const unsigned int bpfloader_ver,
+int loadProg(const char* const elfPath, const unsigned int bpfloader_ver,
              const char* const prefix) {
     vector<char> license;
-    vector<char> critical;
     vector<codeSection> cs;
     vector<unique_fd> mapFds;
     int ret;
 
-    if (!isCritical) return -1;
-    *isCritical = false;
-
     ifstream elfFile(elfPath, ios::in | ios::binary);
     if (!elfFile.is_open()) return -1;
 
-    ret = readSectionByName("critical", elfFile, critical);
-    *isCritical = !ret;
-
     ret = readSectionByName("license", elfFile, license);
     if (ret) {
         ALOGE("Couldn't find license in %s", elfPath);
         return ret;
     } else {
-        ALOGD("Loading %s%s ELF object %s with license %s",
-              *isCritical ? "critical for " : "optional", *isCritical ? (char*)critical.data() : "",
+        ALOGD("Loading ELF object %s with license %s",
               elfPath, (char*)license.data());
     }
 
@@ -1230,10 +1222,9 @@
             string progPath(location.dir);
             progPath += s;
 
-            bool critical;
-            int ret = loadProg(progPath.c_str(), &critical, bpfloader_ver, location.prefix);
+            int ret = loadProg(progPath.c_str(), bpfloader_ver, location.prefix);
             if (ret) {
-                if (critical) retVal = ret;
+                retVal = ret;
                 ALOGE("Failed to load object: %s, ret: %s", progPath.c_str(), std::strerror(-ret));
             } else {
                 ALOGD("Loaded object: %s", progPath.c_str());
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index 9682545..15ab19c 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -142,11 +142,9 @@
     }
 
     if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
-        RETURN_IF_NOT_OK(attachProgramToCgroup(
-                "/sys/fs/bpf/netd_readonly/prog_block_bind4_block_port",
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND4_PROG_PATH,
                 cg_fd, BPF_CGROUP_INET4_BIND));
-        RETURN_IF_NOT_OK(attachProgramToCgroup(
-                "/sys/fs/bpf/netd_readonly/prog_block_bind6_block_port",
+        RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_BIND6_PROG_PATH,
                 cg_fd, BPF_CGROUP_INET6_BIND));
 
         // This should trivially pass, since we just attached up above,
diff --git a/bpf/progs/Android.bp b/bpf/progs/Android.bp
index dc1f56d..52eb1b3 100644
--- a/bpf/progs/Android.bp
+++ b/bpf/progs/Android.bp
@@ -64,12 +64,6 @@
 // bpf kernel programs
 //
 bpf {
-    name: "block.o",
-    srcs: ["block.c"],
-    sub_dir: "net_shared",
-}
-
-bpf {
     name: "dscpPolicy.o",
     srcs: ["dscpPolicy.c"],
     sub_dir: "net_shared",
diff --git a/bpf/progs/block.c b/bpf/progs/block.c
deleted file mode 100644
index 0e2dba9..0000000
--- a/bpf/progs/block.c
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2022 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.
- */
-
-// The resulting .o needs to load on Android T+
-#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
-
-#include "bpf_net_helpers.h"
-
-DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t,
-        1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM)
-
-static inline __always_inline int block_port(struct bpf_sock_addr *ctx) {
-    if (!ctx->user_port) return BPF_ALLOW;
-
-    switch (ctx->protocol) {
-        case IPPROTO_TCP:
-        case IPPROTO_MPTCP:
-        case IPPROTO_UDP:
-        case IPPROTO_UDPLITE:
-        case IPPROTO_DCCP:
-        case IPPROTO_SCTP:
-            break;
-        default:
-            return BPF_ALLOW; // unknown protocols are allowed
-    }
-
-    int key = ctx->user_port >> 6;
-    int shift = ctx->user_port & 63;
-
-    uint64_t *val = bpf_blocked_ports_map_lookup_elem(&key);
-    // Lookup should never fail in reality, but if it does return here to keep the
-    // BPF verifier happy.
-    if (!val) return BPF_ALLOW;
-
-    if ((*val >> shift) & 1) return BPF_DISALLOW;
-    return BPF_ALLOW;
-}
-
-// the program need to be accessible/loadable by netd (from netd updatable plugin)
-#define DEFINE_NETD_RO_BPF_PROG(SECTION_NAME, the_prog, min_kver) \
-    DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, min_kver, KVER_INF,  \
-                        BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
-                        "", "netd_readonly/", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
-
-DEFINE_NETD_RO_BPF_PROG("bind4/block_port", bind4_block_port, KVER_4_19)
-(struct bpf_sock_addr *ctx) {
-    return block_port(ctx);
-}
-
-DEFINE_NETD_RO_BPF_PROG("bind6/block_port", bind6_block_port, KVER_4_19)
-(struct bpf_sock_addr *ctx) {
-    return block_port(ctx);
-}
-
-LICENSE("Apache 2.0");
-CRITICAL("ConnectivityNative");
diff --git a/bpf/progs/netd.c b/bpf/progs/netd.c
index 4248a46..8804ad5 100644
--- a/bpf/progs/netd.c
+++ b/bpf/progs/netd.c
@@ -69,6 +69,8 @@
 // TODO: consider whether we can merge some of these maps
 // for example it might be possible to merge 2 or 3 of:
 //   uid_counterset_map + uid_owner_map + uid_permission_map
+DEFINE_BPF_MAP_NO_NETD(blocked_ports_map, ARRAY, int, uint64_t,
+                       1024 /* 64K ports -> 1024 u64s */)
 DEFINE_BPF_MAP_RW_NETD(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE)
 DEFINE_BPF_MAP_NO_NETD(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE)
 DEFINE_BPF_MAP_NO_NETD(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE)
@@ -670,6 +672,43 @@
     return BPF_ALLOW;
 }
 
+static inline __always_inline int block_port(struct bpf_sock_addr *ctx) {
+    if (!ctx->user_port) return BPF_ALLOW;
+
+    switch (ctx->protocol) {
+        case IPPROTO_TCP:
+        case IPPROTO_MPTCP:
+        case IPPROTO_UDP:
+        case IPPROTO_UDPLITE:
+        case IPPROTO_DCCP:
+        case IPPROTO_SCTP:
+            break;
+        default:
+            return BPF_ALLOW; // unknown protocols are allowed
+    }
+
+    int key = ctx->user_port >> 6;
+    int shift = ctx->user_port & 63;
+
+    uint64_t *val = bpf_blocked_ports_map_lookup_elem(&key);
+    // Lookup should never fail in reality, but if it does return here to keep the
+    // BPF verifier happy.
+    if (!val) return BPF_ALLOW;
+
+    if ((*val >> shift) & 1) return BPF_DISALLOW;
+    return BPF_ALLOW;
+}
+
+DEFINE_NETD_BPF_PROG_KVER("bind4/inet4_bind", AID_ROOT, AID_ROOT, inet4_bind, KVER_4_19)
+(struct bpf_sock_addr *ctx) {
+    return block_port(ctx);
+}
+
+DEFINE_NETD_BPF_PROG_KVER("bind6/inet6_bind", AID_ROOT, AID_ROOT, inet6_bind, KVER_4_19)
+(struct bpf_sock_addr *ctx) {
+    return block_port(ctx);
+}
+
 DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", AID_ROOT, AID_ROOT, inet4_connect, KVER_4_14)
 (struct bpf_sock_addr *ctx) {
     return check_localhost(ctx);
diff --git a/bpf/progs/netd.h b/bpf/progs/netd.h
index 4877a4b..be7c311 100644
--- a/bpf/progs/netd.h
+++ b/bpf/progs/netd.h
@@ -157,6 +157,8 @@
 
 #define CGROUP_INET_CREATE_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsock_inet_create"
 #define CGROUP_INET_RELEASE_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsockrelease_inet_release"
+#define CGROUP_BIND4_PROG_PATH BPF_NETD_PATH "prog_netd_bind4_inet4_bind"
+#define CGROUP_BIND6_PROG_PATH BPF_NETD_PATH "prog_netd_bind6_inet6_bind"
 #define CGROUP_CONNECT4_PROG_PATH BPF_NETD_PATH "prog_netd_connect4_inet4_connect"
 #define CGROUP_CONNECT6_PROG_PATH BPF_NETD_PATH "prog_netd_connect6_inet6_connect"
 #define CGROUP_UDP4_RECVMSG_PROG_PATH BPF_NETD_PATH "prog_netd_recvmsg4_udp4_recvmsg"
diff --git a/bpf/tests/mts/bpf_existence_test.cpp b/bpf/tests/mts/bpf_existence_test.cpp
index f3c6907..eaa6373 100644
--- a/bpf/tests/mts/bpf_existence_test.cpp
+++ b/bpf/tests/mts/bpf_existence_test.cpp
@@ -82,13 +82,13 @@
 
 // Provided by *current* mainline module for T+ devices
 static const set<string> MAINLINE_FOR_T_PLUS = {
-    SHARED "map_block_blocked_ports_map",
     SHARED "map_clatd_clat_egress4_map",
     SHARED "map_clatd_clat_ingress6_map",
     SHARED "map_dscpPolicy_ipv4_dscp_policies_map",
     SHARED "map_dscpPolicy_ipv6_dscp_policies_map",
     SHARED "map_dscpPolicy_socket_policy_cache_map",
     NETD "map_netd_app_uid_stats_map",
+    NETD "map_netd_blocked_ports_map",
     NETD "map_netd_configuration_map",
     NETD "map_netd_cookie_tag_map",
     NETD "map_netd_data_saver_enabled_map",
@@ -119,8 +119,8 @@
 
 // Provided by *current* mainline module for T+ devices with 5.4+ kernels
 static const set<string> MAINLINE_FOR_T_4_19_PLUS = {
-    NETD_RO "prog_block_bind4_block_port",
-    NETD_RO "prog_block_bind6_block_port",
+    NETD "prog_netd_bind4_inet4_bind",
+    NETD "prog_netd_bind6_inet6_bind",
 };
 
 // Provided by *current* mainline module for T+ devices with 5.15+ kernels
diff --git a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
index 241d5fa..9cca078 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
@@ -41,10 +41,7 @@
   // The task runner is sequential so these can't run on top of each other.
   runner->PostDelayedTask([=, this]() { PollAndSchedule(runner, poll_ms); }, poll_ms);
 
-  if (mMutex.try_lock()) {
-    ConsumeAllLocked();
-    mMutex.unlock();
-  }
+  ConsumeAll();
 }
 
 bool NetworkTracePoller::Start(uint32_t pollMs) {
@@ -76,7 +73,10 @@
     return false;
   }
 
-  mRingBuffer = std::move(*rb);
+  {
+    std::scoped_lock<std::mutex> block(mBufferMutex);
+    mRingBuffer = std::move(*rb);
+  }
 
   auto res = mConfigurationMap.writeValue(0, true, BPF_ANY);
   if (!res.ok()) {
@@ -114,10 +114,14 @@
   // Drain remaining events from the ring buffer now that tracing is disabled.
   // This prevents the next trace from seeing stale events and allows writing
   // the last batch of events to Perfetto.
-  ConsumeAllLocked();
+  ConsumeAll();
 
   mTaskRunner.reset();
-  mRingBuffer.reset();
+
+  {
+    std::scoped_lock<std::mutex> block(mBufferMutex);
+    mRingBuffer.reset();
+  }
 
   return res.ok();
 }
@@ -145,22 +149,20 @@
 }
 
 bool NetworkTracePoller::ConsumeAll() {
-  std::scoped_lock<std::mutex> lock(mMutex);
-  return ConsumeAllLocked();
-}
-
-bool NetworkTracePoller::ConsumeAllLocked() {
-  if (mRingBuffer == nullptr) {
-    ALOGW("Tracing is not active");
-    return false;
-  }
-
   std::vector<PacketTrace> packets;
-  base::Result<int> ret = mRingBuffer->ConsumeAll(
-      [&](const PacketTrace& pkt) { packets.push_back(pkt); });
-  if (!ret.ok()) {
-    ALOGW("Failed to poll ringbuf: %s", ret.error().message().c_str());
-    return false;
+  {
+    std::scoped_lock<std::mutex> lock(mBufferMutex);
+    if (mRingBuffer == nullptr) {
+      ALOGW("Tracing is not active");
+      return false;
+    }
+
+    base::Result<int> ret = mRingBuffer->ConsumeAll(
+        [&](const PacketTrace& pkt) { packets.push_back(pkt); });
+    if (!ret.ok()) {
+      ALOGW("Failed to poll ringbuf: %s", ret.error().message().c_str());
+      return false;
+    }
   }
 
   ATRACE_INT("NetworkTracePackets", packets.size());
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
index 092ab64..72fa66e 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
@@ -50,7 +50,7 @@
   bool Stop() EXCLUDES(mMutex);
 
   // Consumes all available events from the ringbuffer.
-  bool ConsumeAll() EXCLUDES(mMutex);
+  bool ConsumeAll() EXCLUDES(mBufferMutex);
 
  private:
   // Poll the ring buffer for new data and schedule another run of ourselves
@@ -59,15 +59,19 @@
   // and thus a deadlock while resetting the TaskRunner. The runner pointer is
   // always valid within tasks run by that runner.
   void PollAndSchedule(perfetto::base::TaskRunner* runner, uint32_t poll_ms);
-  bool ConsumeAllLocked() REQUIRES(mMutex);
 
   // Record sparse iface stats via atrace. This queries the per-iface stats maps
   // for any iface present in the vector of packets. This is inexact, but should
   // have sufficient coverage given these are cumulative counters.
-  void TraceIfaces(const std::vector<PacketTrace>& packets) REQUIRES(mMutex);
+  static void TraceIfaces(const std::vector<PacketTrace>& packets);
 
   std::mutex mMutex;
 
+  // The mBufferMutex protects the ring buffer. This allows separate protected
+  // access of mTaskRunner in Stop (to terminate) and mRingBuffer in ConsumeAll.
+  // Without this separation, Stop() can deadlock.
+  std::mutex mBufferMutex;
+
   // Records the number of successfully started active sessions so that only the
   // first active session attempts setup and only the last cleans up. Note that
   // the session count will remain zero if Start fails. It is expected that Stop
@@ -78,10 +82,10 @@
   uint32_t mPollMs GUARDED_BY(mMutex);
 
   // The function to process PacketTrace, typically a Perfetto sink.
-  EventSink mCallback GUARDED_BY(mMutex);
+  const EventSink mCallback;
 
   // The BPF ring buffer handle.
-  std::unique_ptr<BpfRingbuf<PacketTrace>> mRingBuffer GUARDED_BY(mMutex);
+  std::unique_ptr<BpfRingbuf<PacketTrace>> mRingBuffer GUARDED_BY(mBufferMutex);
 
   // The packet tracing config map (really a 1-element array).
   BpfMap<uint32_t, bool> mConfigurationMap GUARDED_BY(mMutex);
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
index 917ad4d..7a008c6 100644
--- a/service/src/com/android/server/connectivity/ConnectivityNativeService.java
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -43,7 +43,7 @@
     private static final String TAG = ConnectivityNativeService.class.getSimpleName();
 
     private static final String BLOCKED_PORTS_MAP_PATH =
-            "/sys/fs/bpf/net_shared/map_block_blocked_ports_map";
+            "/sys/fs/bpf/netd_shared/map_netd_blocked_ports_map";
 
     private final Context mContext;
 
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
+    }
 }
diff --git a/thread/docs/build-an-android-border-router.md b/thread/docs/build-an-android-border-router.md
index 257999b..f90a23b 100644
--- a/thread/docs/build-an-android-border-router.md
+++ b/thread/docs/build-an-android-border-router.md
@@ -169,7 +169,7 @@
     user thread_network
 ```
 
-For real RCP devices, it supports both SPI and UART interace and you can
+For real RCP devices, it supports both SPI and UART interfaces and you can
 specify the device with the schema `spinel+spi://`, `spinel+hdlc+uart://` and
 `spinel+socket://` respectively.