Allow upgrade to DoH during automatic mode.

Uses a hardcoded mapping to upgrade insecure DNS and DoT services to
associated DoH services. The mapping is preliminary and will be adjusted
before we run an experiment.

Whether upgrade is attempted is controlled by the new kDnsOverHttpsUpgrade
feature, which is enabled by default on ChromeOS, MacOS, Android, and
Windows. Problematic providers can be excluded from the mapping using the
kDnsOverHttpsUpgradeDisabledProvidersParam feature.

Example usage: --enable-features="DnsOverHttpsUpgrade<DoHTrial" \
--force-fieldtrials="DoHTrial/Group1" --force-fieldtrial-params=\
"DoHTrial.Group1:DisabledProviders/Google"

Bug: 985589
Change-Id: Ife81913bf5b7aade66cace1d3d8ad87d55b11ee5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1638928
Commit-Queue: Katharine Daly <dalyk@google.com>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: Eric Orth <ericorth@chromium.org>
Cr-Commit-Position: refs/heads/master@{#692989}
diff --git a/net/dns/dns_client.cc b/net/dns/dns_client.cc
index b0d235d..26bc7d0 100644
--- a/net/dns/dns_client.cc
+++ b/net/dns/dns_client.cc
@@ -15,6 +15,7 @@
 #include "net/dns/dns_session.h"
 #include "net/dns/dns_socket_pool.h"
 #include "net/dns/dns_transaction.h"
+#include "net/dns/dns_util.h"
 #include "net/log/net_log.h"
 #include "net/log/net_log_event_type.h"
 #include "net/socket/client_socket_factory.h"
@@ -41,6 +42,24 @@
   return c1.value() == *c2;
 }
 
+void UpdateConfigForDohUpgrade(DnsConfig* config) {
+  // TODO(crbug.com/878582): Reconsider whether the hardcoded mapping should
+  // also be applied in SECURE mode.
+  if (config->allow_dns_over_https_upgrade &&
+      config->dns_over_https_servers.empty() &&
+      config->secure_dns_mode == DnsConfig::SecureDnsMode::AUTOMATIC) {
+    // If we're in strict mode on Android, only attempt to upgrade the
+    // specified DoT hostname.
+    if (!config->dns_over_tls_hostname.empty()) {
+      config->dns_over_https_servers = GetDohUpgradeServersFromDotHostname(
+          config->dns_over_tls_hostname, config->disabled_upgrade_providers);
+    } else {
+      config->dns_over_https_servers = GetDohUpgradeServersFromNameservers(
+          config->nameservers, config->disabled_upgrade_providers);
+    }
+  }
+}
+
 constexpr base::TimeDelta kInitialDoHTimeout =
     base::TimeDelta::FromMilliseconds(5000);
 
@@ -168,6 +187,8 @@
       config = config_overrides_.ApplyOverrides(system_config_.value());
     }
 
+    UpdateConfigForDohUpgrade(&config);
+
     if (!config.IsValid() || config.unhandled_options)
       return base::nullopt;
 
diff --git a/net/dns/dns_config.cc b/net/dns/dns_config.cc
index 96b162d0..0a94c73 100644
--- a/net/dns/dns_config.cc
+++ b/net/dns/dns_config.cc
@@ -30,7 +30,8 @@
       attempts(2),
       rotate(false),
       use_local_ipv6(false),
-      secure_dns_mode(SecureDnsMode::OFF) {}
+      secure_dns_mode(SecureDnsMode::OFF),
+      allow_dns_over_https_upgrade(false) {}
 
 DnsConfig::~DnsConfig() = default;
 
@@ -60,7 +61,9 @@
          (attempts == d.attempts) && (rotate == d.rotate) &&
          (use_local_ipv6 == d.use_local_ipv6) &&
          (dns_over_https_servers == d.dns_over_https_servers) &&
-         (secure_dns_mode == d.secure_dns_mode);
+         (secure_dns_mode == d.secure_dns_mode) &&
+         (allow_dns_over_https_upgrade == d.allow_dns_over_https_upgrade) &&
+         (disabled_upgrade_providers == d.disabled_upgrade_providers);
 }
 
 void DnsConfig::CopyIgnoreHosts(const DnsConfig& d) {
@@ -77,6 +80,8 @@
   use_local_ipv6 = d.use_local_ipv6;
   dns_over_https_servers = d.dns_over_https_servers;
   secure_dns_mode = d.secure_dns_mode;
+  allow_dns_over_https_upgrade = d.allow_dns_over_https_upgrade;
+  disabled_upgrade_providers = d.disabled_upgrade_providers;
 }
 
 std::unique_ptr<base::Value> DnsConfig::ToValue() const {
@@ -114,6 +119,13 @@
   }
   dict->Set("doh_servers", std::move(list));
   dict->SetInteger("secure_dns_mode", static_cast<int>(secure_dns_mode));
+  dict->SetBoolean("allow_dns_over_https_upgrade",
+                   allow_dns_over_https_upgrade);
+
+  list = std::make_unique<base::ListValue>();
+  for (const auto& provider : disabled_upgrade_providers)
+    list->AppendString(provider);
+  dict->Set("disabled_upgrade_providers", std::move(list));
 
   return std::move(dict);
 }
diff --git a/net/dns/dns_config.h b/net/dns/dns_config.h
index 4d5a882..e1b01ae1 100644
--- a/net/dns/dns_config.h
+++ b/net/dns/dns_config.h
@@ -120,6 +120,15 @@
   // server hostname) using |HostResolver::ResolveHostParameters::
   // secure_dns_mode_override|.
   SecureDnsMode secure_dns_mode;
+
+  // If set to |true|, we will attempt to upgrade the user's DNS configuration
+  // to use DoH server(s) operated by the same provider(s) when the user is
+  // in AUTOMATIC mode and has not pre-specified DoH servers.
+  bool allow_dns_over_https_upgrade;
+
+  // List of providers to exclude from upgrade mapping. See the
+  // mapping in net/dns/dns_util.cc for provider ids.
+  std::vector<std::string> disabled_upgrade_providers;
 };
 
 }  // namespace net
diff --git a/net/dns/dns_config_overrides.cc b/net/dns/dns_config_overrides.cc
index 9f8f5e4..bf63483 100644
--- a/net/dns/dns_config_overrides.cc
+++ b/net/dns/dns_config_overrides.cc
@@ -29,7 +29,9 @@
          timeout == other.timeout && attempts == other.attempts &&
          rotate == other.rotate && use_local_ipv6 == other.use_local_ipv6 &&
          dns_over_https_servers == other.dns_over_https_servers &&
-         secure_dns_mode == other.secure_dns_mode;
+         secure_dns_mode == other.secure_dns_mode &&
+         allow_dns_over_https_upgrade == other.allow_dns_over_https_upgrade &&
+         disabled_upgrade_providers == other.disabled_upgrade_providers;
 }
 
 bool DnsConfigOverrides::operator!=(const DnsConfigOverrides& other) const {
@@ -54,6 +56,9 @@
   overrides.use_local_ipv6 = defaults.use_local_ipv6;
   overrides.dns_over_https_servers = defaults.dns_over_https_servers;
   overrides.secure_dns_mode = defaults.secure_dns_mode;
+  overrides.allow_dns_over_https_upgrade =
+      defaults.allow_dns_over_https_upgrade;
+  overrides.disabled_upgrade_providers = defaults.disabled_upgrade_providers;
 
   return overrides;
 }
@@ -61,7 +66,8 @@
 bool DnsConfigOverrides::OverridesEverything() const {
   return nameservers && search && hosts && append_to_multi_label_name &&
          randomize_ports && ndots && timeout && attempts && rotate &&
-         use_local_ipv6 && dns_over_https_servers && secure_dns_mode;
+         use_local_ipv6 && dns_over_https_servers && secure_dns_mode &&
+         allow_dns_over_https_upgrade && disabled_upgrade_providers;
 }
 
 DnsConfig DnsConfigOverrides::ApplyOverrides(const DnsConfig& config) const {
@@ -94,6 +100,12 @@
     overridden.dns_over_https_servers = dns_over_https_servers.value();
   if (secure_dns_mode)
     overridden.secure_dns_mode = secure_dns_mode.value();
+  if (allow_dns_over_https_upgrade) {
+    overridden.allow_dns_over_https_upgrade =
+        allow_dns_over_https_upgrade.value();
+  }
+  if (disabled_upgrade_providers)
+    overridden.disabled_upgrade_providers = disabled_upgrade_providers.value();
 
   return overridden;
 }
diff --git a/net/dns/dns_config_overrides.h b/net/dns/dns_config_overrides.h
index bd96265..34dc9a5 100644
--- a/net/dns/dns_config_overrides.h
+++ b/net/dns/dns_config_overrides.h
@@ -58,6 +58,8 @@
   base::Optional<std::vector<DnsConfig::DnsOverHttpsServerConfig>>
       dns_over_https_servers;
   base::Optional<DnsConfig::SecureDnsMode> secure_dns_mode;
+  base::Optional<bool> allow_dns_over_https_upgrade;
+  base::Optional<std::vector<std::string>> disabled_upgrade_providers;
 
   // Note no overriding value for |unhandled_options|. It is meta-configuration,
   // and there should be no reason to override it.
diff --git a/net/dns/dns_util.cc b/net/dns/dns_util.cc
index 8ae9bb6..3c8e070 100644
--- a/net/dns/dns_util.cc
+++ b/net/dns/dns_util.cc
@@ -14,6 +14,8 @@
 #include "base/big_endian.h"
 #include "base/metrics/field_trial.h"
 #include "base/metrics/histogram_macros.h"
+#include "base/no_destructor.h"
+#include "base/stl_util.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_split.h"
 #include "build/build_config.h"
@@ -110,6 +112,103 @@
   return true;
 }
 
+// Represents insecure DNS, DoT, and DoH services run by the same provider
+// and providing the same filtering behavior. These entries are used to
+// determine if insecure DNS or DoT services can be upgraded to associated
+// DoH services in automatic mode.
+struct DohUpgradeEntry {
+  DohUpgradeEntry(std::string provider,
+                  std::set<std::string> ip_strs,
+                  std::set<std::string> dns_over_tls_hostnames,
+                  DnsConfig::DnsOverHttpsServerConfig dns_over_https_config)
+      : provider(std::move(provider)),
+        dns_over_tls_hostnames(std::move(dns_over_tls_hostnames)),
+        dns_over_https_config(std::move(dns_over_https_config)) {
+    for (const std::string& ip_str : ip_strs) {
+      IPAddress ip_address;
+      bool success = ip_address.AssignFromIPLiteral(ip_str);
+      DCHECK(success);
+      ip_addresses.insert(ip_address);
+    }
+  }
+  DohUpgradeEntry(const DohUpgradeEntry& other) = default;
+  ~DohUpgradeEntry() = default;
+  const std::string provider;
+  std::set<IPAddress> ip_addresses;
+  const std::set<std::string> dns_over_tls_hostnames;
+  const DnsConfig::DnsOverHttpsServerConfig dns_over_https_config;
+};
+
+const std::vector<const DohUpgradeEntry>& GetDohUpgradeList() {
+  static const base::NoDestructor<std::vector<const DohUpgradeEntry>>
+      upgradable_servers({
+          DohUpgradeEntry("Cisco",
+                          {"208.67.222.222", "208.67.220.220",
+                           "2620:119:35::35", "2620:119:53::53"},
+                          {""} /* DoT hostname */,
+                          {"https://doh.opendns.com/dns-query{?dns}",
+                           false /* use_post */}),
+          DohUpgradeEntry(
+              "CleanBrowsingAdult",
+              {"185.228.168.10", "185.228.169.11", "2a0d:2a00:1::1",
+               "2a0d:2a00:2::1"},
+              {"adult-filter-dns.cleanbrowsing.org"} /* DoT hostname */,
+              {"https://doh.cleanbrowsing.org/doh/adult-filter{?dns}",
+               false /* use_post */}),
+          DohUpgradeEntry(
+              "CleanBrowsingFamily",
+              {"185.228.168.168", "185.228.169.168",
+               "2a0d:2a00:1::", "2a0d:2a00:2::"},
+              {"family-filter-dns.cleanbrowsing.org"} /* DoT hostname */,
+              {"https://doh.cleanbrowsing.org/doh/family-filter{?dns}",
+               false /* use_post */}),
+          DohUpgradeEntry(
+              "CleanBrowsingSecure",
+              {"185.228.168.9", "185.228.169.9", "2a0d:2a00:1::2",
+               "2a0d:2a00:2::2"},
+              {"security-filter-dns.cleanbrowsing.org"} /* DoT hostname */,
+              {"https://doh.cleanbrowsing.org/doh/security-filter{?dns}",
+               false /* use_post */}),
+          DohUpgradeEntry(
+              "Cloudflare",
+              {"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111",
+               "2606:4700:4700::1001"},
+              {"one.one.one.one",
+               "1dot1dot1dot1.cloudflare-dns.com"} /* DoT hostname */,
+              {"https://chrome.cloudflare-dns.com/dns-query",
+               true /* use-post */}),
+          DohUpgradeEntry(
+              "Dnssb",
+              {"185.222.222.222", "185.184.222.222", "2a09::", "2a09::1"},
+              {"dns.sb"} /* DoT hostname */,
+              {"https://doh.dns.sb/dns-query?no_ecs=true{&dns}",
+               false /* use_post */}),
+          DohUpgradeEntry(
+              "Google",
+              {"8.8.8.8", "8.8.4.4", "2001:4860:4860::8888",
+               "2001:4860:4860::8844"},
+              {"dns.google", "dns.google.com",
+               "8888.google"} /* DoT hostname */,
+              {"https://dns.google/dns-query{?dns}", false /* use_post */}),
+          DohUpgradeEntry(
+              "Quad9Cdn",
+              {"9.9.9.11", "149.112.112.11", "2620:fe::11", "2620:fe::fe:11"},
+              {"dns11.quad9.net"} /* DoT hostname */,
+              {"https://dns11.quad9.net/dns-query", true /* use_post */}),
+          DohUpgradeEntry(
+              "Quad9Insecure",
+              {"9.9.9.10", "149.112.112.10", "2620:fe::10", "2620:fe::fe:10"},
+              {"dns10.quad9.net"} /* DoT hostname */,
+              {"https://dns10.quad9.net/dns-query", true /* use_post */}),
+          DohUpgradeEntry(
+              "Quad9Secure",
+              {"9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9"},
+              {"dns.quad9.net", "dns9.quad9.net"} /* DoT hostname */,
+              {"https://dns.quad9.net/dns-query", true /* use_post */}),
+      });
+  return *upgradable_servers;
+}
+
 }  // namespace
 
 bool DNSDomainFromDot(const base::StringPiece& dotted, std::string* out) {
@@ -282,4 +381,50 @@
   }
 }
 
+std::vector<DnsConfig::DnsOverHttpsServerConfig>
+GetDohUpgradeServersFromDotHostname(
+    const std::string& dot_server,
+    const std::vector<std::string>& excluded_providers) {
+  const std::vector<const DohUpgradeEntry>& upgradable_servers =
+      GetDohUpgradeList();
+  std::vector<DnsConfig::DnsOverHttpsServerConfig> doh_servers;
+
+  if (dot_server.empty())
+    return doh_servers;
+
+  for (const auto& upgrade_entry : upgradable_servers) {
+    if (base::Contains(excluded_providers, upgrade_entry.provider))
+      continue;
+
+    if (base::Contains(upgrade_entry.dns_over_tls_hostnames, dot_server)) {
+      doh_servers.push_back(upgrade_entry.dns_over_https_config);
+      break;
+    }
+  }
+  return doh_servers;
+}
+
+std::vector<DnsConfig::DnsOverHttpsServerConfig>
+GetDohUpgradeServersFromNameservers(
+    const std::vector<IPEndPoint>& dns_servers,
+    const std::vector<std::string>& excluded_providers) {
+  const std::vector<const DohUpgradeEntry>& upgradable_servers =
+      GetDohUpgradeList();
+  std::vector<DnsConfig::DnsOverHttpsServerConfig> doh_servers;
+
+  for (const auto& server : dns_servers) {
+    for (const auto& upgrade_entry : upgradable_servers) {
+      if (base::Contains(excluded_providers, upgrade_entry.provider))
+        continue;
+
+      // DoH servers should only be added once.
+      if (base::Contains(upgrade_entry.ip_addresses, server.address()) &&
+          !base::Contains(doh_servers, upgrade_entry.dns_over_https_config)) {
+        doh_servers.push_back(upgrade_entry.dns_over_https_config);
+      }
+    }
+  }
+  return doh_servers;
+}
+
 }  // namespace net
diff --git a/net/dns/dns_util.h b/net/dns/dns_util.h
index c12815f..038a68b 100644
--- a/net/dns/dns_util.h
+++ b/net/dns/dns_util.h
@@ -6,12 +6,15 @@
 #define NET_DNS_DNS_UTIL_H_
 
 #include <string>
+#include <vector>
 
 #include "base/strings/string_piece.h"
 #include "base/time/time.h"
 #include "net/base/address_family.h"
+#include "net/base/ip_endpoint.h"
 #include "net/base/net_export.h"
 #include "net/base/network_change_notifier.h"
+#include "net/dns/dns_config.h"
 #include "net/dns/public/dns_query_type.h"
 
 namespace net {
@@ -108,6 +111,23 @@
 NET_EXPORT DnsQueryType
 AddressFamilyToDnsQueryType(AddressFamily address_family);
 
+// Uses the hardcoded upgrade mapping to discover DoH service(s) associated
+// with a DoT hostname. Providers listed in |excluded_providers| are not
+// eligible for upgrade.
+NET_EXPORT_PRIVATE std::vector<DnsConfig::DnsOverHttpsServerConfig>
+GetDohUpgradeServersFromDotHostname(
+    const std::string& dot_server,
+    const std::vector<std::string>& excluded_providers);
+
+// Uses the hardcoded upgrade mapping to discover DoH service(s) associated
+// with a list of insecure DNS servers. Server ordering is preserved across
+// the mapping. Providers listed in |excluded_providers| are not
+// eligible for upgrade.
+NET_EXPORT_PRIVATE std::vector<DnsConfig::DnsOverHttpsServerConfig>
+GetDohUpgradeServersFromNameservers(
+    const std::vector<IPEndPoint>& dns_servers,
+    const std::vector<std::string>& excluded_providers);
+
 }  // namespace net
 
 #endif  // NET_DNS_DNS_UTIL_H_
diff --git a/net/dns/dns_util_unittest.cc b/net/dns/dns_util_unittest.cc
index c88b673..1309b38 100644
--- a/net/dns/dns_util_unittest.cc
+++ b/net/dns/dns_util_unittest.cc
@@ -5,6 +5,7 @@
 #include "net/dns/dns_util.h"
 
 #include "base/stl_util.h"
+#include "net/dns/public/dns_protocol.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace net {
@@ -134,4 +135,68 @@
                 "https://dnsserver.example.net/dns-query{?dns}"));
 }
 
+TEST_F(DNSUtilTest, GetDohUpgradeServersFromDotHostname) {
+  std::vector<DnsConfig::DnsOverHttpsServerConfig> doh_servers =
+      GetDohUpgradeServersFromDotHostname("", std::vector<std::string>());
+  EXPECT_EQ(0u, doh_servers.size());
+
+  doh_servers = GetDohUpgradeServersFromDotHostname("unrecognized",
+                                                    std::vector<std::string>());
+  EXPECT_EQ(0u, doh_servers.size());
+
+  doh_servers = GetDohUpgradeServersFromDotHostname(
+      "family-filter-dns.cleanbrowsing.org", std::vector<std::string>());
+  EXPECT_EQ(1u, doh_servers.size());
+  EXPECT_EQ("https://doh.cleanbrowsing.org/doh/family-filter{?dns}",
+            doh_servers[0].server_template);
+
+  doh_servers = GetDohUpgradeServersFromDotHostname(
+      "family-filter-dns.cleanbrowsing.org",
+      std::vector<std::string>({"CleanBrowsingFamily"}));
+  EXPECT_EQ(0u, doh_servers.size());
+}
+
+TEST_F(DNSUtilTest, GetDohUpgradeServersFromNameservers) {
+  std::vector<IPEndPoint> nameservers;
+  // Cloudflare upgradeable IPs
+  IPAddress dns_ip0(1, 0, 0, 1);
+  IPAddress dns_ip1;
+  EXPECT_TRUE(dns_ip1.AssignFromIPLiteral("2606:4700:4700::1111"));
+  // SafeBrowsing family filter upgradeable IP
+  IPAddress dns_ip2;
+  EXPECT_TRUE(dns_ip2.AssignFromIPLiteral("2a0d:2a00:2::"));
+  // SafeBrowsing security filter upgradeable IP
+  IPAddress dns_ip3(185, 228, 169, 9);
+  // None-upgradeable IP
+  IPAddress dns_ip4(1, 2, 3, 4);
+
+  nameservers.push_back(IPEndPoint(dns_ip0, dns_protocol::kDefaultPort));
+  nameservers.push_back(IPEndPoint(dns_ip1, dns_protocol::kDefaultPort));
+  nameservers.push_back(IPEndPoint(dns_ip2, 54));
+  nameservers.push_back(IPEndPoint(dns_ip3, dns_protocol::kDefaultPort));
+  nameservers.push_back(IPEndPoint(dns_ip4, dns_protocol::kDefaultPort));
+
+  std::vector<DnsConfig::DnsOverHttpsServerConfig> doh_servers =
+      GetDohUpgradeServersFromNameservers(std::vector<IPEndPoint>(),
+                                          std::vector<std::string>());
+  EXPECT_EQ(0u, doh_servers.size());
+
+  doh_servers = GetDohUpgradeServersFromNameservers(nameservers,
+                                                    std::vector<std::string>());
+  EXPECT_EQ(3u, doh_servers.size());
+  EXPECT_EQ("https://chrome.cloudflare-dns.com/dns-query",
+            doh_servers[0].server_template);
+  EXPECT_EQ("https://doh.cleanbrowsing.org/doh/family-filter{?dns}",
+            doh_servers[1].server_template);
+  EXPECT_EQ("https://doh.cleanbrowsing.org/doh/security-filter{?dns}",
+            doh_servers[2].server_template);
+
+  doh_servers = GetDohUpgradeServersFromNameservers(
+      nameservers, std::vector<std::string>(
+                       {"CleanBrowsingSecure", "Cloudflare", "Unexpected"}));
+  EXPECT_EQ(1u, doh_servers.size());
+  EXPECT_EQ("https://doh.cleanbrowsing.org/doh/family-filter{?dns}",
+            doh_servers[0].server_template);
+}
+
 }  // namespace net
diff --git a/net/dns/host_resolver_manager_unittest.cc b/net/dns/host_resolver_manager_unittest.cc
index b8aaf44..b1cfeed 100644
--- a/net/dns/host_resolver_manager_unittest.cc
+++ b/net/dns/host_resolver_manager_unittest.cc
@@ -3359,6 +3359,31 @@
   return config;
 }
 
+DnsConfig CreateUpgradableDnsConfig() {
+  DnsConfig config;
+  config.secure_dns_mode = DnsConfig::SecureDnsMode::AUTOMATIC;
+  config.allow_dns_over_https_upgrade = true;
+  // Cloudflare upgradeable IPs
+  IPAddress dns_ip0(1, 0, 0, 1);
+  IPAddress dns_ip1;
+  EXPECT_TRUE(dns_ip1.AssignFromIPLiteral("2606:4700:4700::1111"));
+  // SafeBrowsing family filter upgradeable IP
+  IPAddress dns_ip2;
+  EXPECT_TRUE(dns_ip2.AssignFromIPLiteral("2a0d:2a00:2::"));
+  // SafeBrowsing security filter upgradeable IP
+  IPAddress dns_ip3(185, 228, 169, 9);
+  // Non-upgradeable IP
+  IPAddress dns_ip4(1, 2, 3, 4);
+
+  config.nameservers.push_back(IPEndPoint(dns_ip0, dns_protocol::kDefaultPort));
+  config.nameservers.push_back(IPEndPoint(dns_ip1, dns_protocol::kDefaultPort));
+  config.nameservers.push_back(IPEndPoint(dns_ip2, 54));
+  config.nameservers.push_back(IPEndPoint(dns_ip3, dns_protocol::kDefaultPort));
+  config.nameservers.push_back(IPEndPoint(dns_ip4, dns_protocol::kDefaultPort));
+  EXPECT_TRUE(config.IsValid());
+  return config;
+}
+
 // Specialized fixture for tests of DnsTask.
 class HostResolverManagerDnsTest : public HostResolverManagerTest {
  public:
@@ -6430,6 +6455,9 @@
   const DnsConfig::SecureDnsMode secure_dns_mode =
       DnsConfig::SecureDnsMode::SECURE;
   overrides.secure_dns_mode = secure_dns_mode;
+  overrides.allow_dns_over_https_upgrade = true;
+  const std::vector<std::string> disabled_upgrade_providers = {"provider_name"};
+  overrides.disabled_upgrade_providers = disabled_upgrade_providers;
 
   // This test is expected to test overriding all fields.
   EXPECT_TRUE(overrides.OverridesEverything());
@@ -6450,6 +6478,9 @@
   EXPECT_TRUE(overridden_config->use_local_ipv6);
   EXPECT_EQ(dns_over_https_servers, overridden_config->dns_over_https_servers);
   EXPECT_EQ(secure_dns_mode, overridden_config->secure_dns_mode);
+  EXPECT_TRUE(overridden_config->allow_dns_over_https_upgrade);
+  EXPECT_EQ(disabled_upgrade_providers,
+            overridden_config->disabled_upgrade_providers);
 }
 
 TEST_F(HostResolverManagerDnsTest,
@@ -6583,6 +6614,178 @@
   EXPECT_THAT(client_ptr->GetEffectiveConfig(),
               testing::Pointee(original_config));
 }
+
+TEST_F(HostResolverManagerDnsTest, DohMapping) {
+  // Use a real DnsClient to test config-handling behavior.
+  AlwaysFailSocketFactory socket_factory;
+  auto client = DnsClient::CreateClientForTesting(
+      nullptr /* net_log */, &socket_factory, base::Bind(&base::RandInt));
+  DnsClient* client_ptr = client.get();
+  resolver_->SetDnsClientForTesting(std::move(client));
+
+  // Create a DnsConfig containing IP addresses associated with Cloudflare,
+  // SafeBrowsing family filter, SafeBrowsing security filter, and other IPs
+  // not associated with hardcoded DoH services.
+  DnsConfig original_config = CreateUpgradableDnsConfig();
+  ChangeDnsConfig(original_config);
+
+  const DnsConfig* fetched_config = client_ptr->GetEffectiveConfig();
+  EXPECT_EQ(original_config.nameservers, fetched_config->nameservers);
+  std::vector<DnsConfig::DnsOverHttpsServerConfig> expected_doh_servers = {
+      {"https://chrome.cloudflare-dns.com/dns-query", true /* use-post */},
+      {"https://doh.cleanbrowsing.org/doh/family-filter{?dns}",
+       false /* use_post */},
+      {"https://doh.cleanbrowsing.org/doh/security-filter{?dns}",
+       false /* use_post */}};
+  EXPECT_EQ(expected_doh_servers, fetched_config->dns_over_https_servers);
+}
+
+TEST_F(HostResolverManagerDnsTest, DohMappingDisabled) {
+  // Use a real DnsClient to test config-handling behavior.
+  AlwaysFailSocketFactory socket_factory;
+  auto client = DnsClient::CreateClientForTesting(
+      nullptr /* net_log */, &socket_factory, base::Bind(&base::RandInt));
+  DnsClient* client_ptr = client.get();
+  resolver_->SetDnsClientForTesting(std::move(client));
+
+  // Create a DnsConfig containing IP addresses associated with Cloudflare,
+  // SafeBrowsing family filter, SafeBrowsing security filter, and other IPs
+  // not associated with hardcoded DoH services.
+  DnsConfig original_config = CreateUpgradableDnsConfig();
+  original_config.allow_dns_over_https_upgrade = false;
+  ChangeDnsConfig(original_config);
+
+  const DnsConfig* fetched_config = client_ptr->GetEffectiveConfig();
+  EXPECT_EQ(original_config.nameservers, fetched_config->nameservers);
+  std::vector<DnsConfig::DnsOverHttpsServerConfig> expected_doh_servers = {};
+  EXPECT_EQ(expected_doh_servers, fetched_config->dns_over_https_servers);
+}
+
+TEST_F(HostResolverManagerDnsTest, DohMappingModeIneligibleForUpgrade) {
+  // Use a real DnsClient to test config-handling behavior.
+  AlwaysFailSocketFactory socket_factory;
+  auto client = DnsClient::CreateClientForTesting(
+      nullptr /* net_log */, &socket_factory, base::Bind(&base::RandInt));
+  DnsClient* client_ptr = client.get();
+  resolver_->SetDnsClientForTesting(std::move(client));
+
+  // Create a DnsConfig containing IP addresses associated with Cloudflare,
+  // SafeBrowsing family filter, SafeBrowsing security filter, and other IPs
+  // not associated with hardcoded DoH services.
+  DnsConfig original_config = CreateUpgradableDnsConfig();
+  original_config.secure_dns_mode = DnsConfig::SecureDnsMode::SECURE;
+  ChangeDnsConfig(original_config);
+
+  const DnsConfig* fetched_config = client_ptr->GetEffectiveConfig();
+  EXPECT_EQ(original_config.nameservers, fetched_config->nameservers);
+  std::vector<DnsConfig::DnsOverHttpsServerConfig> expected_doh_servers = {};
+  EXPECT_EQ(expected_doh_servers, fetched_config->dns_over_https_servers);
+}
+
+TEST_F(HostResolverManagerDnsTest, DohMappingWithExclusion) {
+  // Use a real DnsClient to test config-handling behavior.
+  AlwaysFailSocketFactory socket_factory;
+  auto client = DnsClient::CreateClientForTesting(
+      nullptr /* net_log */, &socket_factory, base::Bind(&base::RandInt));
+  DnsClient* client_ptr = client.get();
+  resolver_->SetDnsClientForTesting(std::move(client));
+
+  // Create a DnsConfig containing IP addresses associated with Cloudflare,
+  // SafeBrowsing family filter, SafeBrowsing security filter, and other IPs
+  // not associated with hardcoded DoH services.
+  DnsConfig original_config = CreateUpgradableDnsConfig();
+  original_config.disabled_upgrade_providers = {"CleanBrowsingSecure",
+                                                "Cloudflare", "Unexpected"};
+  ChangeDnsConfig(original_config);
+
+  // A DoH upgrade should be attempted on the DNS servers in the config, but
+  // only for permitted providers.
+  const DnsConfig* fetched_config = client_ptr->GetEffectiveConfig();
+  EXPECT_EQ(original_config.nameservers, fetched_config->nameservers);
+  std::vector<DnsConfig::DnsOverHttpsServerConfig> expected_doh_servers = {
+      {"https://doh.cleanbrowsing.org/doh/family-filter{?dns}",
+       false /* use_post */}};
+  EXPECT_EQ(expected_doh_servers, fetched_config->dns_over_https_servers);
+}
+
+TEST_F(HostResolverManagerDnsTest, DohMappingIgnoredIfTemplateSpecified) {
+  // Use a real DnsClient to test config-handling behavior.
+  AlwaysFailSocketFactory socket_factory;
+  auto client = DnsClient::CreateClientForTesting(
+      nullptr /* net_log */, &socket_factory, base::Bind(&base::RandInt));
+  DnsClient* client_ptr = client.get();
+  resolver_->SetDnsClientForTesting(std::move(client));
+
+  // Create a DnsConfig containing IP addresses associated with Cloudflare,
+  // SafeBrowsing family filter, SafeBrowsing security filter, and other IPs
+  // not associated with hardcoded DoH services.
+  DnsConfig original_config = CreateUpgradableDnsConfig();
+  ChangeDnsConfig(original_config);
+
+  // If the overrides contains DoH servers, no DoH upgrade should be attempted.
+  DnsConfigOverrides overrides;
+  const std::vector<DnsConfig::DnsOverHttpsServerConfig>
+      dns_over_https_servers_overrides = {
+          DnsConfig::DnsOverHttpsServerConfig("doh.server.override.com", true)};
+  overrides.dns_over_https_servers = dns_over_https_servers_overrides;
+  resolver_->SetDnsConfigOverrides(overrides);
+  const DnsConfig* fetched_config = client_ptr->GetEffectiveConfig();
+  EXPECT_EQ(original_config.nameservers, fetched_config->nameservers);
+  EXPECT_EQ(dns_over_https_servers_overrides,
+            fetched_config->dns_over_https_servers);
+}
+
+TEST_F(HostResolverManagerDnsTest, DohMappingWithAutomaticDot) {
+  // Use a real DnsClient to test config-handling behavior.
+  AlwaysFailSocketFactory socket_factory;
+  auto client = DnsClient::CreateClientForTesting(
+      nullptr /* net_log */, &socket_factory, base::Bind(&base::RandInt));
+  DnsClient* client_ptr = client.get();
+  resolver_->SetDnsClientForTesting(std::move(client));
+
+  // Create a DnsConfig containing IP addresses associated with Cloudflare,
+  // SafeBrowsing family filter, SafeBrowsing security filter, and other IPs
+  // not associated with hardcoded DoH services.
+  DnsConfig original_config = CreateUpgradableDnsConfig();
+  original_config.dns_over_tls_active = true;
+  ChangeDnsConfig(original_config);
+
+  const DnsConfig* fetched_config = client_ptr->GetEffectiveConfig();
+  EXPECT_EQ(original_config.nameservers, fetched_config->nameservers);
+  std::vector<DnsConfig::DnsOverHttpsServerConfig> expected_doh_servers = {
+      {"https://chrome.cloudflare-dns.com/dns-query", true /* use-post */},
+      {"https://doh.cleanbrowsing.org/doh/family-filter{?dns}",
+       false /* use_post */},
+      {"https://doh.cleanbrowsing.org/doh/security-filter{?dns}",
+       false /* use_post */}};
+  EXPECT_EQ(expected_doh_servers, fetched_config->dns_over_https_servers);
+}
+
+TEST_F(HostResolverManagerDnsTest, DohMappingWithStrictDot) {
+  // Use a real DnsClient to test config-handling behavior.
+  AlwaysFailSocketFactory socket_factory;
+  auto client = DnsClient::CreateClientForTesting(
+      nullptr /* net_log */, &socket_factory, base::Bind(&base::RandInt));
+  DnsClient* client_ptr = client.get();
+  resolver_->SetDnsClientForTesting(std::move(client));
+
+  // Create a DnsConfig containing IP addresses associated with Cloudflare,
+  // SafeBrowsing family filter, SafeBrowsing security filter, and other IPs
+  // not associated with hardcoded DoH services.
+  DnsConfig original_config = CreateUpgradableDnsConfig();
+  original_config.secure_dns_mode = DnsConfig::SecureDnsMode::AUTOMATIC;
+  original_config.dns_over_tls_active = true;
+
+  // Google DoT hostname
+  original_config.dns_over_tls_hostname = "dns.google";
+  ChangeDnsConfig(original_config);
+  const DnsConfig* fetched_config = client_ptr->GetEffectiveConfig();
+  EXPECT_EQ(original_config.nameservers, fetched_config->nameservers);
+  std::vector<DnsConfig::DnsOverHttpsServerConfig> expected_doh_servers = {
+      {"https://dns.google/dns-query{?dns}", false /* use_post */}};
+  EXPECT_EQ(expected_doh_servers, fetched_config->dns_over_https_servers);
+}
+
 #endif  // !defined(OS_IOS)
 
 TEST_F(HostResolverManagerDnsTest, FlushCacheOnDnsConfigOverridesChange) {
diff --git a/services/network/network_service.cc b/services/network/network_service.cc
index d6898e2..34a87ba 100644
--- a/services/network/network_service.cc
+++ b/services/network/network_service.cc
@@ -21,6 +21,7 @@
 #include "base/task/post_task.h"
 #include "base/timer/timer.h"
 #include "base/values.h"
+#include "components/network_session_configurator/common/network_features.h"
 #include "components/os_crypt/os_crypt.h"
 #include "mojo/core/embedder/embedder.h"
 #include "mojo/public/cpp/bindings/strong_binding.h"
@@ -442,12 +443,6 @@
   host_resolver_manager_->SetInsecureDnsClientEnabled(
       insecure_dns_client_enabled);
 
-  // Configure DNS over HTTPS.
-  if (!dns_over_https_servers || dns_over_https_servers.value().empty()) {
-    host_resolver_manager_->SetDnsConfigOverrides(net::DnsConfigOverrides());
-    return;
-  }
-
   for (auto* network_context : network_contexts_) {
     if (!network_context->IsPrimaryNetworkContext())
       continue;
@@ -456,13 +451,21 @@
         network_context->url_request_context());
   }
 
+  // Configure DNS over HTTPS.
   net::DnsConfigOverrides overrides;
-  overrides.dns_over_https_servers.emplace();
-  for (const auto& doh_server : *dns_over_https_servers) {
-    overrides.dns_over_https_servers.value().emplace_back(
-        doh_server->server_template, doh_server->use_post);
+  if (dns_over_https_servers && !dns_over_https_servers.value().empty()) {
+    overrides.dns_over_https_servers.emplace();
+    for (const auto& doh_server : *dns_over_https_servers) {
+      overrides.dns_over_https_servers.value().emplace_back(
+          doh_server->server_template, doh_server->use_post);
+    }
   }
   overrides.secure_dns_mode = secure_dns_mode;
+  overrides.allow_dns_over_https_upgrade =
+      base::FeatureList::IsEnabled(features::kDnsOverHttpsUpgrade);
+  overrides.disabled_upgrade_providers =
+      SplitString(features::kDnsOverHttpsUpgradeDisabledProvidersParam.Get(),
+                  ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
   host_resolver_manager_->SetDnsConfigOverrides(overrides);
 }
 
@@ -712,16 +715,6 @@
 }
 
 void NetworkService::DestroyNetworkContexts() {
-  // If DNS over HTTPS is enabled, the HostResolver is currently using
-  // NetworkContexts to do DNS lookups, so need to tell the HostResolver
-  // to stop using DNS over HTTPS before destroying any NetworkContexts.
-  // The SetDnsConfigOverrides() call will will fail any in-progress DNS
-  // lookups, but only if there are current config overrides (which there will
-  // be if DNS over HTTPS is currently enabled).
-  if (host_resolver_manager_) {
-    host_resolver_manager_->SetDnsConfigOverrides(net::DnsConfigOverrides());
-  }
-
   // Delete NetworkContexts. If there's a primary NetworkContext, it must be
   // deleted after all other NetworkContexts, to avoid use-after-frees.
   for (auto it = owned_network_contexts_.begin();
diff --git a/services/network/network_service_unittest.cc b/services/network/network_service_unittest.cc
index 2db0a317..56a5a53 100644
--- a/services/network/network_service_unittest.cc
+++ b/services/network/network_service_unittest.cc
@@ -17,10 +17,12 @@
 #include "base/run_loop.h"
 #include "base/strings/string_util.h"
 #include "base/test/bind_test_util.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "base/threading/thread_task_runner_handle.h"
 #include "build/build_config.h"
 #include "net/base/escape.h"
+#include "net/base/ip_address.h"
 #include "net/base/ip_endpoint.h"
 #include "net/base/mock_network_change_notifier.h"
 #include "net/base/url_util.h"
@@ -30,6 +32,7 @@
 #include "net/dns/dns_test_util.h"
 #include "net/dns/host_resolver.h"
 #include "net/dns/host_resolver_manager.h"
+#include "net/dns/public/dns_protocol.h"
 #include "net/http/http_auth_handler_factory.h"
 #include "net/http/http_auth_scheme.h"
 #include "net/net_buildflags.h"
@@ -43,6 +46,7 @@
 #include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
 #include "net/url_request/url_request_context.h"
 #include "services/network/network_context.h"
+#include "services/network/public/cpp/features.h"
 #include "services/network/public/cpp/network_switches.h"
 #include "services/network/public/mojom/net_log.mojom.h"
 #include "services/network/public/mojom/network_change_manager.mojom.h"
@@ -475,7 +479,7 @@
       net::DnsConfig::SecureDnsMode::AUTOMATIC,
       base::nullopt /* dns_over_https_servers */);
   EXPECT_FALSE(dns_client_ptr->CanUseInsecureDnsTransactions());
-  EXPECT_EQ(net::DnsConfig::SecureDnsMode::OFF,
+  EXPECT_EQ(net::DnsConfig::SecureDnsMode::AUTOMATIC,
             dns_client_ptr->GetEffectiveConfig()->secure_dns_mode);
 
   std::vector<mojom::DnsOverHttpsServerPtr> dns_over_https_servers_ptr;
@@ -500,13 +504,6 @@
   const std::string kServer3 = "https://grapefruit/resolver/query{?dns}";
   const bool kServer3UsePost = false;
 
-  // Create the primary NetworkContext before enabling DNS over HTTPS.
-  mojom::NetworkContextPtr network_context;
-  mojom::NetworkContextParamsPtr context_params = CreateContextParams();
-  context_params->primary_network_context = true;
-  service()->CreateNetworkContext(mojo::MakeRequest(&network_context),
-                                  std::move(context_params));
-
   // Create valid DnsConfig.
   net::DnsConfig config;
   config.nameservers.push_back(net::IPEndPoint());
@@ -561,16 +558,54 @@
   EXPECT_EQ(kServer2UsePost, dns_over_https_servers[0].use_post);
   EXPECT_EQ(kServer3, dns_over_https_servers[1].server_template);
   EXPECT_EQ(kServer3UsePost, dns_over_https_servers[1].use_post);
+}
 
-  // Destroying the primary NetworkContext should disable DNS over HTTPS.
-  network_context.reset();
-  base::RunLoop().RunUntilIdle();
-  // DnsClient is still enabled.
-  EXPECT_TRUE(service()->host_resolver_manager()->GetDnsConfigAsValue());
-  // DNS over HTTPS is not.
-  dns_over_https_servers =
-      dns_client_ptr->GetEffectiveConfig()->dns_over_https_servers;
-  EXPECT_TRUE(dns_over_https_servers.empty());
+TEST_F(NetworkServiceTest, DisableDohUpgradeProviders) {
+  base::test::ScopedFeatureList scoped_features;
+  scoped_features.InitAndEnableFeatureWithParameters(
+      features::kDnsOverHttpsUpgrade,
+      {{"DisabledProviders", "CleanBrowsingSecure, , Cloudflare,Unexpected"}});
+  service()->ConfigureStubHostResolver(
+      true /* insecure_dns_client_enabled */,
+      net::DnsConfig::SecureDnsMode::AUTOMATIC,
+      base::nullopt /* dns_over_https_servers */);
+
+  // Set valid DnsConfig.
+  net::DnsConfig config;
+  // Cloudflare upgradeable IPs
+  net::IPAddress dns_ip0(1, 0, 0, 1);
+  net::IPAddress dns_ip1;
+  EXPECT_TRUE(dns_ip1.AssignFromIPLiteral("2606:4700:4700::1111"));
+  // CleanBrowsing family filter upgradeable IP
+  net::IPAddress dns_ip2;
+  EXPECT_TRUE(dns_ip2.AssignFromIPLiteral("2a0d:2a00:2::"));
+  // CleanBrowsing security filter upgradeable IP
+  net::IPAddress dns_ip3(185, 228, 169, 9);
+  // Non-upgradeable IP
+  net::IPAddress dns_ip4(1, 2, 3, 4);
+
+  config.nameservers.push_back(
+      net::IPEndPoint(dns_ip0, net::dns_protocol::kDefaultPort));
+  config.nameservers.push_back(
+      net::IPEndPoint(dns_ip1, net::dns_protocol::kDefaultPort));
+  config.nameservers.push_back(net::IPEndPoint(dns_ip2, 54));
+  config.nameservers.push_back(
+      net::IPEndPoint(dns_ip3, net::dns_protocol::kDefaultPort));
+  config.nameservers.push_back(
+      net::IPEndPoint(dns_ip4, net::dns_protocol::kDefaultPort));
+
+  auto dns_client = net::DnsClient::CreateClient(nullptr /* net_log */);
+  dns_client->SetSystemConfig(config);
+  net::DnsClient* dns_client_ptr = dns_client.get();
+  service()->host_resolver_manager()->SetDnsClientForTesting(
+      std::move(dns_client));
+
+  std::vector<net::DnsConfig::DnsOverHttpsServerConfig> expected_doh_servers = {
+      {"https://doh.cleanbrowsing.org/doh/family-filter{?dns}",
+       false /* use_post */}};
+  EXPECT_TRUE(dns_client_ptr->GetEffectiveConfig());
+  EXPECT_EQ(expected_doh_servers,
+            dns_client_ptr->GetEffectiveConfig()->dns_over_https_servers);
 }
 
 #endif  // !defined(OS_IOS)
diff --git a/services/network/public/cpp/features.cc b/services/network/public/cpp/features.cc
index 3f9668a..ea10768c 100644
--- a/services/network/public/cpp/features.cc
+++ b/services/network/public/cpp/features.cc
@@ -109,6 +109,24 @@
     "PrefetchMainResourceNetworkIsolationKey",
     base::FEATURE_DISABLED_BY_DEFAULT};
 
+// Enable usage of hardcoded DoH upgrade mapping for use in automatic mode.
+const base::Feature kDnsOverHttpsUpgrade {
+  "DnsOverHttpsUpgrade",
+#if defined(OS_CHROMEOS) || defined(OS_MACOSX) || defined(OS_ANDROID) || \
+    defined(OS_WIN)
+      base::FEATURE_ENABLED_BY_DEFAULT
+#else
+      base::FEATURE_DISABLED_BY_DEFAULT
+#endif
+};
+
+// Provides a mechanism to disable DoH upgrades for some subset of the hardcoded
+// upgrade mapping. Separate multiple provider ids with commas. See the
+// mapping in net/dns/dns_util.cc for provider ids.
+const base::FeatureParam<std::string>
+    kDnsOverHttpsUpgradeDisabledProvidersParam{&kDnsOverHttpsUpgrade,
+                                               "DisabledProviders", ""};
+
 bool ShouldEnableOutOfBlinkCors() {
   return base::FeatureList::IsEnabled(features::kOutOfBlinkCors);
 }
diff --git a/services/network/public/cpp/features.h b/services/network/public/cpp/features.h
index 7a13334..be538eb 100644
--- a/services/network/public/cpp/features.h
+++ b/services/network/public/cpp/features.h
@@ -7,6 +7,7 @@
 
 #include "base/component_export.h"
 #include "base/feature_list.h"
+#include "base/metrics/field_trial_params.h"
 
 namespace network {
 namespace features {
@@ -43,6 +44,11 @@
 extern const base::Feature kBlockNonSecureExternalRequests;
 COMPONENT_EXPORT(NETWORK_CPP)
 extern const base::Feature kPrefetchMainResourceNetworkIsolationKey;
+COMPONENT_EXPORT(NETWORK_CPP)
+extern const base::Feature kDnsOverHttpsUpgrade;
+COMPONENT_EXPORT(NETWORK_CPP)
+extern const base::FeatureParam<std::string>
+    kDnsOverHttpsUpgradeDisabledProvidersParam;
 
 COMPONENT_EXPORT(NETWORK_CPP) bool ShouldEnableOutOfBlinkCors();
 
diff --git a/services/network/public/cpp/host_resolver_mojom_traits.cc b/services/network/public/cpp/host_resolver_mojom_traits.cc
index 3796681..6dafe5a 100644
--- a/services/network/public/cpp/host_resolver_mojom_traits.cc
+++ b/services/network/public/cpp/host_resolver_mojom_traits.cc
@@ -207,6 +207,13 @@
 }
 
 // static
+DnsConfigOverrides::Tristate
+StructTraits<DnsConfigOverridesDataView, net::DnsConfigOverrides>::
+    allow_dns_over_https_upgrade(const net::DnsConfigOverrides& overrides) {
+  return ToTristate(overrides.allow_dns_over_https_upgrade);
+}
+
+// static
 bool StructTraits<DnsConfigOverridesDataView, net::DnsConfigOverrides>::Read(
     DnsConfigOverridesDataView data,
     net::DnsConfigOverrides* out) {
@@ -251,6 +258,11 @@
 
   out->secure_dns_mode = FromOptionalSecureDnsMode(data.secure_dns_mode());
 
+  out->allow_dns_over_https_upgrade =
+      FromTristate(data.allow_dns_over_https_upgrade());
+  if (!data.ReadDisabledUpgradeProviders(&out->disabled_upgrade_providers))
+    return false;
+
   return true;
 }
 
diff --git a/services/network/public/cpp/host_resolver_mojom_traits.h b/services/network/public/cpp/host_resolver_mojom_traits.h
index b7e175b..1f1f1b1 100644
--- a/services/network/public/cpp/host_resolver_mojom_traits.h
+++ b/services/network/public/cpp/host_resolver_mojom_traits.h
@@ -76,6 +76,14 @@
   static network::mojom::OptionalSecureDnsMode secure_dns_mode(
       const net::DnsConfigOverrides& overrides);
 
+  static network::mojom::DnsConfigOverrides::Tristate
+  allow_dns_over_https_upgrade(const net::DnsConfigOverrides& overrides);
+
+  static const base::Optional<std::vector<std::string>>&
+  disabled_upgrade_providers(const net::DnsConfigOverrides& overrides) {
+    return overrides.disabled_upgrade_providers;
+  }
+
   static bool Read(network::mojom::DnsConfigOverridesDataView data,
                    net::DnsConfigOverrides* out);
 };
diff --git a/services/network/public/cpp/host_resolver_mojom_traits_unittest.cc b/services/network/public/cpp/host_resolver_mojom_traits_unittest.cc
index 6a72b20..03f9f47 100644
--- a/services/network/public/cpp/host_resolver_mojom_traits_unittest.cc
+++ b/services/network/public/cpp/host_resolver_mojom_traits_unittest.cc
@@ -43,6 +43,8 @@
   original.dns_over_https_servers.emplace(
       {net::DnsConfig::DnsOverHttpsServerConfig("example.com", false)});
   original.secure_dns_mode = net::DnsConfig::SecureDnsMode::SECURE;
+  original.allow_dns_over_https_upgrade = true;
+  original.disabled_upgrade_providers.emplace({std::string("provider_name")});
 
   net::DnsConfigOverrides deserialized;
   EXPECT_TRUE(mojo::test::SerializeAndDeserialize<mojom::DnsConfigOverrides>(
diff --git a/services/network/public/mojom/host_resolver.mojom b/services/network/public/mojom/host_resolver.mojom
index fce8cc7..c7e8519 100644
--- a/services/network/public/mojom/host_resolver.mojom
+++ b/services/network/public/mojom/host_resolver.mojom
@@ -102,6 +102,13 @@
   // The default SecureDnsMode to use when resolving queries. It can be
   // for individual requests such as requests to resolve a DoH server hostname.
   OptionalSecureDnsMode secure_dns_mode = OptionalSecureDnsMode.NO_OVERRIDE;
+
+  // Whether automatic upgrade to DNS over HTTPS servers is permitted.
+  Tristate allow_dns_over_https_upgrade = Tristate.NO_OVERRIDE;
+
+  // List of providers to exclude from upgrade mapping. See the
+  // mapping in net/dns/dns_util.cc for provider ids.
+  array<string>? disabled_upgrade_providers;
 };
 
 // Control handle used to control outstanding NetworkContext::ResolveHost