Link Rust DoH into DnsResolver with default off

Expect no behavior changes since DoH is disabled.

Test: atest
Bug: 155855709
Change-Id: Ie99cc4c4035c9bfda4a125f5ebf57e2e2f9d2036
diff --git a/Android.bp b/Android.bp
index 2e264b5..5e2f35c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -192,6 +192,7 @@
         "libbase",
         "libcutils",
         "libnetdutils",
+        "libdoh_ffi",
         "libprotobuf-cpp-lite",
         "libstatslog_resolv",
         "libstatspush_compat",
diff --git a/DnsResolver.cpp b/DnsResolver.cpp
index fcd4293..82366e8 100644
--- a/DnsResolver.cpp
+++ b/DnsResolver.cpp
@@ -81,6 +81,7 @@
     auto& dnsTlsDispatcher = DnsTlsDispatcher::getInstance();
     auto& privateDnsConfiguration = PrivateDnsConfiguration::getInstance();
     privateDnsConfiguration.setObserver(&dnsTlsDispatcher);
+    if (isDoHEnabled()) privateDnsConfiguration.initDoh();
 }
 
 bool DnsResolver::start() {
diff --git a/PrivateDnsConfiguration.cpp b/PrivateDnsConfiguration.cpp
index b09c7ce..802728f 100644
--- a/PrivateDnsConfiguration.cpp
+++ b/PrivateDnsConfiguration.cpp
@@ -21,18 +21,22 @@
 #include <android-base/format.h>
 #include <android-base/logging.h>
 #include <android-base/stringprintf.h>
+#include <netdutils/Slice.h>
 #include <netdutils/ThreadUtil.h>
 #include <sys/socket.h>
 
 #include "DnsTlsTransport.h"
 #include "ResolverEventReporter.h"
+#include "doh.h"
 #include "netd_resolv/resolv.h"
+#include "resolv_private.h"
 #include "util.h"
 
 using aidl::android::net::resolv::aidl::IDnsResolverUnsolicitedEventListener;
 using aidl::android::net::resolv::aidl::PrivateDnsValidationEventParcel;
 using android::base::StringPrintf;
 using android::netdutils::setThreadName;
+using android::netdutils::Slice;
 using std::chrono::milliseconds;
 
 namespace android {
@@ -238,9 +242,9 @@
 }
 
 void PrivateDnsConfiguration::sendPrivateDnsValidationEvent(const ServerIdentity& identity,
-                                                            unsigned netId, bool success) {
+                                                            unsigned netId, bool success) const {
     LOG(DEBUG) << "Sending validation " << (success ? "success" : "failure") << " event on netId "
-               << netId << " for " << identity.sockaddr.ip().toString() << " with hostname {"
+               << netId << " for " << identity.sockaddr.toString() << " with hostname {"
                << identity.provider << "}";
     // Send a validation event to NetdEventListenerService.
     const auto& listeners = ResolverEventReporter::getInstance().getListeners();
@@ -313,7 +317,9 @@
     }
 
     // Send private dns validation result to listeners.
-    sendPrivateDnsValidationEvent(identity, netId, success);
+    if (needReportEvent(netId, identity, success)) {
+        sendPrivateDnsValidationEvent(identity, netId, success);
+    }
 
     if (success) {
         updateServerState(identity, Validation::success, netId);
@@ -411,5 +417,134 @@
     dw.blankline();
 }
 
+void PrivateDnsConfiguration::initDoh() {
+    std::lock_guard guard(mPrivateDnsLock);
+    initDohLocked();
+}
+
+void PrivateDnsConfiguration::initDohLocked() {
+    if (mDohDispatcher != nullptr) return;
+    mDohDispatcher = doh_dispatcher_new(
+            [](uint32_t net_id, bool success, const char* ip_addr, const char* host) {
+                android::net::PrivateDnsConfiguration::getInstance().onDohStatusUpdate(
+                        net_id, success, ip_addr, host);
+            });
+}
+
+int PrivateDnsConfiguration::setDoh(int32_t netId, uint32_t mark,
+                                    const std::vector<std::string>& servers,
+                                    const std::string& name, const std::string& caCert) {
+    if (servers.empty()) return 0;
+    LOG(DEBUG) << "PrivateDnsConfiguration::setDoh(" << netId << ", 0x" << std::hex << mark
+               << std::dec << ", " << servers.size() << ", " << name << ")";
+    std::lock_guard guard(mPrivateDnsLock);
+
+    initDohLocked();
+
+    // TODO: 1. Improve how to choose the server
+    // TODO: 2. Support multiple servers
+    for (const auto& entry : mAvailableDoHProviders) {
+        const auto& doh = entry.getDohIdentity(servers, name);
+        if (!doh.ok()) continue;
+
+        auto it = mDohTracker.find(netId);
+        // Skip if the same server already exists and its status == success.
+        if (it != mDohTracker.end() && it->second == doh.value() &&
+            it->second.status == Validation::success) {
+            return 0;
+        }
+        const auto& [dohIt, _] = mDohTracker.insert_or_assign(netId, doh.value());
+        const auto& dohId = dohIt->second;
+
+        RecordEntry record(netId, {netdutils::IPSockAddr::toIPSockAddr(dohId.ipAddr, 443), name},
+                           dohId.status);
+        mPrivateDnsLog.push(std::move(record));
+        return doh_net_new(mDohDispatcher, netId, dohId.httpsTemplate.c_str(), dohId.host.c_str(),
+                           dohId.ipAddr.c_str(), mark, caCert.c_str(), 3000);
+    }
+
+    LOG(INFO) << __func__ << "No suitable DoH server found";
+    return 0;
+}
+
+void PrivateDnsConfiguration::clearDoh(unsigned netId) {
+    LOG(DEBUG) << "PrivateDnsConfiguration::clearDoh (" << netId << ")";
+    std::lock_guard guard(mPrivateDnsLock);
+    if (mDohDispatcher != nullptr) doh_net_delete(mDohDispatcher, netId);
+    mDohTracker.erase(netId);
+}
+
+ssize_t PrivateDnsConfiguration::dohQuery(unsigned netId, const Slice query, const Slice answer,
+                                          uint64_t timeoutMs) {
+    {
+        std::lock_guard guard(mPrivateDnsLock);
+        // It's safe because mDohDispatcher won't be deleted after initializing.
+        if (mDohDispatcher == nullptr) return RESULT_CAN_NOT_SEND;
+    }
+    return doh_query(mDohDispatcher, netId, query.base(), query.size(), answer.base(),
+                     answer.size(), timeoutMs);
+}
+
+void PrivateDnsConfiguration::onDohStatusUpdate(uint32_t netId, bool success, const char* ipAddr,
+                                                const char* host) {
+    LOG(INFO) << __func__ << netId << ", " << success << ", " << ipAddr << ", " << host;
+    std::lock_guard guard(mPrivateDnsLock);
+    // Update the server status.
+    auto it = mDohTracker.find(netId);
+    if (it == mDohTracker.end() || (it->second.ipAddr != ipAddr && it->second.host != host)) {
+        LOG(WARNING) << __func__ << "obsolete event";
+        return;
+    }
+    Validation status = success ? Validation::success : Validation::fail;
+    it->second.status = status;
+    // Send the events to registered listeners.
+    ServerIdentity identity = {netdutils::IPSockAddr::toIPSockAddr(ipAddr, 443), host};
+    if (needReportEvent(netId, identity, success)) {
+        sendPrivateDnsValidationEvent(identity, netId, success);
+    }
+    // Add log.
+    RecordEntry record(netId, identity, status);
+    mPrivateDnsLog.push(std::move(record));
+}
+
+bool PrivateDnsConfiguration::needReportEvent(uint32_t netId, ServerIdentity identity,
+                                              bool success) const {
+    // If the result is success or DoH is not enable, no concern to report the events.
+    if (success || !isDoHEnabled()) return true;
+    // If the result is failure, check another transport's status to determine if we should report
+    // the event.
+    switch (identity.sockaddr.port()) {
+        // DoH
+        case 443: {
+            auto netPair = mPrivateDnsTransports.find(netId);
+            if (netPair == mPrivateDnsTransports.end()) return true;
+            for (const auto& [id, server] : netPair->second) {
+                if ((identity.sockaddr.ip() == id.sockaddr.ip()) &&
+                    (identity.sockaddr.port() != id.sockaddr.port()) &&
+                    (server->validationState() == Validation::success)) {
+                    LOG(DEBUG) << __func__
+                               << "skip reporting DoH validation failure event, server addr: " +
+                                          identity.sockaddr.ip().toString();
+                    return false;
+                }
+            }
+            break;
+        }
+        // DoT
+        case 853: {
+            auto it = mDohTracker.find(netId);
+            if (it == mDohTracker.end()) return true;
+            if (it->second == identity && it->second.status == Validation::success) {
+                LOG(DEBUG) << __func__
+                           << "skip reporting DoT validation failure event, server addr: " +
+                                      identity.sockaddr.ip().toString();
+                return false;
+            }
+            break;
+        }
+    }
+    return true;
+}
+
 }  // namespace net
 }  // namespace android
diff --git a/PrivateDnsConfiguration.h b/PrivateDnsConfiguration.h
index f7e6592..cd3205f 100644
--- a/PrivateDnsConfiguration.h
+++ b/PrivateDnsConfiguration.h
@@ -16,20 +16,25 @@
 
 #pragma once
 
+#include <array>
 #include <list>
 #include <map>
 #include <mutex>
 #include <vector>
 
+#include <android-base/format.h>
+#include <android-base/logging.h>
 #include <android-base/result.h>
 #include <android-base/thread_annotations.h>
 #include <netdutils/BackoffSequence.h>
 #include <netdutils/DumpWriter.h>
 #include <netdutils/InternetAddresses.h>
+#include <netdutils/Slice.h>
 
 #include "DnsTlsServer.h"
 #include "LockedQueue.h"
 #include "PrivateDnsValidationObserver.h"
+#include "doh.h"
 
 namespace android {
 namespace net {
@@ -61,6 +66,8 @@
 
         explicit ServerIdentity(const IPrivateDnsServer& server)
             : sockaddr(server.addr()), provider(server.provider()) {}
+        ServerIdentity(const netdutils::IPSockAddr& addr, const std::string& host)
+            : sockaddr(addr), provider(host) {}
 
         bool operator<(const ServerIdentity& other) const {
             return std::tie(sockaddr, provider) < std::tie(other.sockaddr, other.provider);
@@ -79,10 +86,20 @@
     int set(int32_t netId, uint32_t mark, const std::vector<std::string>& servers,
             const std::string& name, const std::string& caCert) EXCLUDES(mPrivateDnsLock);
 
+    void initDoh() EXCLUDES(mPrivateDnsLock);
+
+    int setDoh(int32_t netId, uint32_t mark, const std::vector<std::string>& servers,
+               const std::string& name, const std::string& caCert) EXCLUDES(mPrivateDnsLock);
+
     PrivateDnsStatus getStatus(unsigned netId) const EXCLUDES(mPrivateDnsLock);
 
     void clear(unsigned netId) EXCLUDES(mPrivateDnsLock);
 
+    void clearDoh(unsigned netId) EXCLUDES(mPrivateDnsLock);
+
+    ssize_t dohQuery(unsigned netId, const netdutils::Slice query, const netdutils::Slice answer,
+                     uint64_t timeoutMs) EXCLUDES(mPrivateDnsLock);
+
     // Request the server to be revalidated on a connection tagged with |mark|.
     // Returns a Result to indicate if the request is accepted.
     base::Result<void> requestValidation(unsigned netId, const ServerIdentity& identity,
@@ -92,6 +109,9 @@
 
     void dump(netdutils::DumpWriter& dw) const;
 
+    void onDohStatusUpdate(uint32_t netId, bool success, const char* ipAddr, const char* host)
+            EXCLUDES(mPrivateDnsLock);
+
   private:
     typedef std::map<ServerIdentity, std::unique_ptr<IPrivateDnsServer>> PrivateDnsTracker;
 
@@ -105,8 +125,8 @@
     bool recordPrivateDnsValidation(const ServerIdentity& identity, unsigned netId, bool success,
                                     bool isRevalidation) EXCLUDES(mPrivateDnsLock);
 
-    void sendPrivateDnsValidationEvent(const ServerIdentity& identity, unsigned netId, bool success)
-            REQUIRES(mPrivateDnsLock);
+    void sendPrivateDnsValidationEvent(const ServerIdentity& identity, unsigned netId,
+                                       bool success) const REQUIRES(mPrivateDnsLock);
 
     // Decide if a validation for |server| is needed. Note that servers that have failed
     // multiple validation attempts but for which there is still a validating
@@ -123,6 +143,8 @@
     base::Result<IPrivateDnsServer*> getPrivateDnsLocked(const ServerIdentity& identity,
                                                          unsigned netId) REQUIRES(mPrivateDnsLock);
 
+    void initDohLocked() REQUIRES(mPrivateDnsLock);
+
     mutable std::mutex mPrivateDnsLock;
     std::map<unsigned, PrivateDnsMode> mPrivateDnsModes GUARDED_BY(mPrivateDnsLock);
 
@@ -135,9 +157,14 @@
     void notifyValidationStateUpdate(const netdutils::IPSockAddr& sockaddr, Validation validation,
                                      uint32_t netId) const REQUIRES(mPrivateDnsLock);
 
+    bool needReportEvent(uint32_t netId, ServerIdentity identity, bool success) const
+            REQUIRES(mPrivateDnsLock);
+
     // TODO: fix the reentrancy problem.
     PrivateDnsValidationObserver* mObserver GUARDED_BY(mPrivateDnsLock);
 
+    DohDispatcher* mDohDispatcher;
+
     friend class PrivateDnsConfigurationTest;
 
     // It's not const because PrivateDnsConfigurationTest needs to override it.
@@ -147,6 +174,58 @@
                     .withInitialRetransmissionTime(std::chrono::seconds(60))
                     .withMaximumRetransmissionTime(std::chrono::seconds(3600));
 
+    struct DohIdentity {
+        std::string httpsTemplate;
+        std::string ipAddr;
+        std::string host;
+        Validation status;
+        bool operator<(const DohIdentity& other) const {
+            return std::tie(ipAddr, host) < std::tie(other.ipAddr, other.host);
+        }
+        bool operator==(const DohIdentity& other) const {
+            return std::tie(ipAddr, host) == std::tie(other.ipAddr, other.host);
+        }
+        bool operator<(const ServerIdentity& other) const {
+            std::string otherIp = other.sockaddr.ip().toString();
+            return std::tie(ipAddr, host) < std::tie(otherIp, other.provider);
+        }
+        bool operator==(const ServerIdentity& other) const {
+            std::string otherIp = other.sockaddr.ip().toString();
+            return std::tie(ipAddr, host) == std::tie(otherIp, other.provider);
+        }
+    };
+
+    struct DohProviderEntry {
+        std::string provider;
+        std::set<std::string> ips;
+        std::string host;
+        std::string httpsTemplate;
+        base::Result<DohIdentity> getDohIdentity(const std::vector<std::string>& ips,
+                                                 const std::string& host) const {
+            if (!host.empty() && this->host != host) return Errorf("host {} not matched", host);
+            for (const auto& ip : ips) {
+                if (this->ips.find(ip) == this->ips.end()) continue;
+                LOG(INFO) << fmt::format("getDohIdentity: {} {}", ip, host);
+                // Only pick the first one for now.
+                return DohIdentity{httpsTemplate, ip, host, Validation::in_process};
+            }
+            return Errorf("server not matched");
+        };
+    };
+
+    // TODO: Move below DoH relevant stuff into Rust implementation.
+    std::map<unsigned, DohIdentity> mDohTracker GUARDED_BY(mPrivateDnsLock);
+    std::array<DohProviderEntry, 2> mAvailableDoHProviders = {{
+            {"Google",
+             {"2001:4860:4860::8888", "2001:4860:4860::8844", "8.8.8.8", "8.8.4.4"},
+             "dns.google",
+             "https://dns.google/dns-query"},
+            {"Cloudflare",
+             {"2606:4700::6810:f8f9", "2606:4700::6810:f9f9", "104.16.248.249", "104.16.249.249"},
+             "cloudflare-dns.com",
+             "https://cloudflare-dns.com/dns-query"},
+    }};
+
     struct RecordEntry {
         RecordEntry(uint32_t netId, const ServerIdentity& identity, Validation state)
             : netId(netId), serverIdentity(identity), state(state) {}
diff --git a/ResolverController.cpp b/ResolverController.cpp
index c983a15..9e693d3 100644
--- a/ResolverController.cpp
+++ b/ResolverController.cpp
@@ -34,6 +34,7 @@
 #include "ResolverStats.h"
 #include "resolv_cache.h"
 #include "stats.h"
+#include "util.h"
 
 using aidl::android::net::ResolverParamsParcel;
 using aidl::android::net::resolv::aidl::IDnsResolverUnsolicitedEventListener;
@@ -169,6 +170,7 @@
     resolv_delete_cache_for_net(netId);
     mDns64Configuration.stopPrefixDiscovery(netId);
     PrivateDnsConfiguration::getInstance().clear(netId);
+    if (isDoHEnabled()) PrivateDnsConfiguration::getInstance().clearDoh(netId);
 
     // Don't get this instance in PrivateDnsConfiguration. It's probe to deadlock.
     DnsTlsDispatcher::getInstance().forceCleanup(netId);
@@ -206,9 +208,9 @@
     // through a different network. For example, on a VPN with no DNS servers (Do53), if the VPN
     // applies to UID 0, dns_mark is assigned for default network rathan the VPN. (note that it's
     // possible that a VPN doesn't have any DNS servers but DoT servers in DNS strict mode)
-    const int err = PrivateDnsConfiguration::getInstance().set(
-            resolverParams.netId, netcontext.app_mark, tlsServers, resolverParams.tlsName,
-            resolverParams.caCertificate);
+    int err = PrivateDnsConfiguration::getInstance().set(resolverParams.netId, netcontext.app_mark,
+                                                         tlsServers, resolverParams.tlsName,
+                                                         resolverParams.caCertificate);
 
     if (err != 0) {
         return err;
@@ -225,6 +227,15 @@
         return err;
     }
 
+    if (isDoHEnabled())
+        err = PrivateDnsConfiguration::getInstance().setDoh(
+                resolverParams.netId, netcontext.app_mark, tlsServers, resolverParams.tlsName,
+                resolverParams.caCertificate);
+
+    if (err != 0) {
+        return err;
+    }
+
     res_params res_params = {};
     res_params.sample_validity = resolverParams.sampleValiditySeconds;
     res_params.success_threshold = resolverParams.successThreshold;
diff --git a/res_send.cpp b/res_send.cpp
index 46cd31d..a0c3091 100644
--- a/res_send.cpp
+++ b/res_send.cpp
@@ -110,6 +110,7 @@
 #include "netd_resolv/resolv.h"
 #include "private/android_filesystem_config.h"
 
+#include "doh.h"
 #include "res_comp.h"
 #include "res_debug.h"
 #include "resolv_cache.h"
@@ -125,6 +126,7 @@
 using android::net::CacheStatus;
 using android::net::DnsQueryEvent;
 using android::net::DnsTlsDispatcher;
+using android::net::DnsTlsServer;
 using android::net::DnsTlsTransport;
 using android::net::IpVersion;
 using android::net::IV_IPV4;
@@ -139,6 +141,7 @@
 using android::net::PrivateDnsMode;
 using android::net::PrivateDnsModes;
 using android::net::PrivateDnsStatus;
+using android::net::PROTO_DOH;
 using android::net::PROTO_MDNS;
 using android::net::PROTO_TCP;
 using android::net::PROTO_UDP;
@@ -164,8 +167,11 @@
 static int connect_with_timeout(int sock, const struct sockaddr* nsap, socklen_t salen,
                                 const struct timespec timeout);
 static int retrying_poll(const int sock, short events, const struct timespec* finish);
-static int res_tls_send(ResState*, const Slice query, const Slice answer, int* rcode,
-                        bool* fallback);
+static int res_private_dns_send(ResState*, const Slice query, const Slice answer, int* rcode,
+                                bool* fallback);
+static int res_tls_send(const std::list<DnsTlsServer>& tlsServers, ResState*, const Slice query,
+                        const Slice answer, int* rcode, PrivateDnsMode mode);
+static ssize_t res_doh_send(ResState*, const Slice query, const Slice answer, int* rcode);
 
 NsType getQueryType(const uint8_t* msg, size_t msgLen) {
     ns_msg handle;
@@ -507,14 +513,14 @@
     if (sleepTimeMs != 0ms) {
         std::this_thread::sleep_for(sleepTimeMs);
     }
-    // DoT
+    // Private DNS
     if (!(statp->netcontext_flags & NET_CONTEXT_FLAG_USE_LOCAL_NAMESERVERS) &&
         !isMdnsResolution(statp->flags)) {
         bool fallback = false;
-        int resplen = res_tls_send(statp, Slice(const_cast<uint8_t*>(buf), buflen),
-                                   Slice(ans, anssiz), rcode, &fallback);
+        int resplen = res_private_dns_send(statp, Slice(const_cast<uint8_t*>(buf), buflen),
+                                           Slice(ans, anssiz), rcode, &fallback);
         if (resplen > 0) {
-            LOG(DEBUG) << __func__ << ": got answer from DoT";
+            LOG(DEBUG) << __func__ << ": got answer from Private DNS";
             res_pquery(ans, resplen);
             if (cache_status == RESOLV_CACHE_NOTFOUND) {
                 resolv_cache_add(statp->netid, buf, buflen, ans, resplen);
@@ -1314,60 +1320,108 @@
     }
 }
 
-static int res_tls_send(ResState* statp, const Slice query, const Slice answer, int* rcode,
-                        bool* fallback) {
-    int resplen = 0;
+static int res_private_dns_send(ResState* statp, const Slice query, const Slice answer, int* rcode,
+                                bool* fallback) {
     const unsigned netId = statp->netid;
 
     auto& privateDnsConfiguration = PrivateDnsConfiguration::getInstance();
     PrivateDnsStatus privateDnsStatus = privateDnsConfiguration.getStatus(netId);
     statp->event->set_private_dns_modes(convertEnumType(privateDnsStatus.mode));
 
-    if (privateDnsStatus.mode == PrivateDnsMode::OFF) {
-        *fallback = true;
-        return -1;
-    }
-
-    if (privateDnsStatus.validatedServers().empty()) {
-        if (privateDnsStatus.mode == PrivateDnsMode::OPPORTUNISTIC) {
+    const bool enableDoH = isDoHEnabled();
+    ssize_t result = -1;
+    switch (privateDnsStatus.mode) {
+        case PrivateDnsMode::OFF: {
             *fallback = true;
             return -1;
-        } else {
-            // Sleep and iterate some small number of times checking for the
-            // arrival of resolved and validated server IP addresses, instead
-            // of returning an immediate error.
-            // This is needed because as soon as a network becomes the default network, apps will
-            // send DNS queries on that network. If no servers have yet validated, and we do not
-            // block those queries, they would immediately fail, causing application-visible errors.
-            // Note that this can happen even before the network validates, since an unvalidated
-            // network can become the default network if no validated networks are available.
-            //
-            // TODO: see if there is a better way to address this problem, such as buffering the
-            // queries in a queue or only blocking queries for the first few seconds after a default
-            // network change.
-            for (int i = 0; i < 42; i++) {
-                std::this_thread::sleep_for(std::chrono::milliseconds(100));
-                // Calling getStatus() to merely check if there's any validated server seems
-                // wasteful. Consider adding a new method in PrivateDnsConfiguration for speed ups.
-                if (!privateDnsConfiguration.getStatus(netId).validatedServers().empty()) {
-                    privateDnsStatus = privateDnsConfiguration.getStatus(netId);
-                    break;
-                }
+        }
+        case PrivateDnsMode::OPPORTUNISTIC: {
+            *fallback = true;
+            if (enableDoH) {
+                result = res_doh_send(statp, query, answer, rcode);
+                if (result != RESULT_CAN_NOT_SEND) return result;
+            }
+            return res_tls_send(privateDnsStatus.validatedServers(), statp, query, answer, rcode,
+                                privateDnsStatus.mode);
+        }
+        case PrivateDnsMode::STRICT: {
+            *fallback = false;
+            if (enableDoH) {
+                result = res_doh_send(statp, query, answer, rcode);
+                if (result != RESULT_CAN_NOT_SEND) return result;
             }
             if (privateDnsStatus.validatedServers().empty()) {
-                return -1;
+                // Sleep and iterate some small number of times checking for the
+                // arrival of resolved and validated server IP addresses, instead
+                // of returning an immediate error.
+                // This is needed because as soon as a network becomes the default network, apps
+                // will send DNS queries on that network. If no servers have yet validated, and we
+                // do not block those queries, they would immediately fail, causing
+                // application-visible errors. Note that this can happen even before the network
+                // validates, since an unvalidated network can become the default network if no
+                // validated networks are available.
+                //
+                // TODO: see if there is a better way to address this problem, such as buffering the
+                // queries in a queue or only blocking queries for the first few seconds after a
+                // default network change.
+                for (int i = 0; i < 42; i++) {
+                    std::this_thread::sleep_for(std::chrono::milliseconds(100));
+                    if (enableDoH) {
+                        result = res_doh_send(statp, query, answer, rcode);
+                        if (result != RESULT_CAN_NOT_SEND) return result;
+                    }
+                    // Calling getStatus() to merely check if there's any validated server seems
+                    // wasteful. Consider adding a new method in PrivateDnsConfiguration for speed
+                    // ups.
+                    if (!privateDnsConfiguration.getStatus(netId).validatedServers().empty()) {
+                        privateDnsStatus = privateDnsConfiguration.getStatus(netId);
+                        break;
+                    }
+                }
             }
+            return res_tls_send(privateDnsStatus.validatedServers(), statp, query, answer, rcode,
+                                privateDnsStatus.mode);
         }
     }
+    LOG(ERROR) << __func__ << ": unknown private DNS mode";
+    return -1;
+}
 
+ssize_t res_doh_send(ResState* statp, const Slice query, const Slice answer, int* rcode) {
+    auto& privateDnsConfiguration = PrivateDnsConfiguration::getInstance();
+    const unsigned netId = statp->netid;
+    LOG(INFO) << __func__ << ": performing query over Https";
+    Stopwatch queryStopwatch;
+    ssize_t result = privateDnsConfiguration.dohQuery(netId, query, answer, /*timeoutMs*/ 2000);
+    LOG(INFO) << __func__ << ": Https query result: " << result;
+
+    if (result == RESULT_CAN_NOT_SEND) return RESULT_CAN_NOT_SEND;
+
+    DnsQueryEvent* dnsQueryEvent = statp->event->mutable_dns_query_events()->add_dns_query_event();
+    dnsQueryEvent->set_latency_micros(saturate_cast<int32_t>(queryStopwatch.timeTakenUs()));
+    // TODO: Make this information available.
+    // dnsQueryEvent->set_ip_version(ipFamilyToIPVersion(?));
+    if (result > 0) {
+        *rcode = reinterpret_cast<HEADER*>(answer.base())->rcode;
+    } else {
+        *rcode = -result;
+    }
+    dnsQueryEvent->set_rcode(static_cast<NsRcode>(*rcode));
+    dnsQueryEvent->set_protocol(PROTO_DOH);
+    dnsQueryEvent->set_type(getQueryType(query.base(), query.size()));
+    return result;
+}
+
+int res_tls_send(const std::list<DnsTlsServer>& tlsServers, ResState* statp, const Slice query,
+                 const Slice answer, int* rcode, PrivateDnsMode mode) {
+    if (tlsServers.empty()) return -1;
     LOG(INFO) << __func__ << ": performing query over TLS";
-
-    const auto response = DnsTlsDispatcher::getInstance().query(privateDnsStatus.validatedServers(),
-                                                                statp, query, answer, &resplen);
+    int resplen = 0;
+    const auto response =
+            DnsTlsDispatcher::getInstance().query(tlsServers, statp, query, answer, &resplen);
 
     LOG(INFO) << __func__ << ": TLS query result: " << static_cast<int>(response);
-
-    if (privateDnsStatus.mode == PrivateDnsMode::OPPORTUNISTIC) {
+    if (mode == PrivateDnsMode::OPPORTUNISTIC) {
         // In opportunistic mode, handle falling back to cleartext in some
         // cases (DNS shouldn't fail if a validated opportunistic mode server
         // becomes unreachable for some reason).
@@ -1375,12 +1429,11 @@
             case DnsTlsTransport::Response::success:
                 *rcode = reinterpret_cast<HEADER*>(answer.base())->rcode;
                 return resplen;
+            // It's OPPORTUNISTIC mode,
+            // hence it's not required to do anything because it'll fallback to UDP.
             case DnsTlsTransport::Response::network_error:
-                // No need to set the error timeout here since it will fallback to UDP.
+                [[fallthrough]];
             case DnsTlsTransport::Response::internal_error:
-                // Note: this will cause cleartext queries to be emitted, with
-                // all of the EDNS0 goodness enabled. Fingers crossed.  :-/
-                *fallback = true;
                 [[fallthrough]];
             default:
                 return -1;
diff --git a/tests/Android.bp b/tests/Android.bp
index 351cea2..896f699 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -103,6 +103,7 @@
         "dnsresolver_aidl_interface-lateststable-ndk_platform",
         "golddata_proto",
         "libcrypto_static",
+        "libdoh_ffi_for_test",
         "libgmock",
         "libnetd_resolv",
         "libnetd_test_dnsresponder_ndk",
@@ -232,6 +233,7 @@
         "netd_event_listener_interface-lateststable-ndk_platform",
         "libcrypto_static",
         "libcutils",
+        "libdoh_ffi_for_test",
         "libgmock",
         "libnetd_resolv",
         "libnetd_test_dnsresponder_ndk",