chromium/chrome/browser/ash/net/secure_dns_manager.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/net/secure_dns_manager.h"

#include <map>
#include <string>
#include <string_view>

#include "ash/constants/ash_pref_names.h"
#include "base/check_is_test.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "chrome/browser/ash/net/ash_dns_over_https_config_source.h"
#include "chrome/browser/ash/net/dns_over_https/templates_uri_resolver_impl.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/net/secure_dns_config.h"
#include "chrome/browser/net/secure_dns_util.h"
#include "chrome/browser/net/stub_resolver_config_reader.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/network/device_state.h"
#include "chromeos/ash/components/network/network_configuration_handler.h"
#include "chromeos/ash/components/network/network_event_log.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_metadata_store.h"
#include "chromeos/ash/components/network/network_state.h"
#include "components/country_codes/country_codes.h"
#include "net/dns/public/doh_provider_entry.h"
#include "net/dns/public/secure_dns_mode.h"
#include "third_party/cros_system_api/dbus/shill/dbus-constants.h"

namespace ash {

SecureDnsManager::SecureDnsManager(PrefService* local_state,
                                   bool is_profile_managed)
    : local_state_(local_state) {
  doh_templates_uri_resolver_ =
      std::make_unique<dns_over_https::TemplatesUriResolverImpl>();

  MonitorPolicyPrefs();
  LoadProviders();
  OnPolicyPrefChanged();
  OnDoHIncludedDomainsPrefChanged();
  OnDoHExcludedDomainsPrefChanged();

  // The DNS-over-HTTPS config source is reset in the destructor of the
  // `SecureDnsManager`. This means the `SecureDnsManager` instance should
  // outlive the `AshDnsOverHttpsConfigSource` instance.
  g_browser_process->system_network_context_manager()
      ->GetStubResolverConfigReader()
      ->SetOverrideDnsOverHttpsConfigSource(
          std::make_unique<AshDnsOverHttpsConfigSource>(this, local_state));
}

void SecureDnsManager::MonitorPolicyPrefs() {
  local_state_registrar_.Init(local_state_);
  static constexpr std::array<const char*, 4> secure_dns_pref_names = {
      ::prefs::kDnsOverHttpsMode, ::prefs::kDnsOverHttpsTemplates,
      ::prefs::kDnsOverHttpsTemplatesWithIdentifiers,
      ::prefs::kDnsOverHttpsSalt};
  for (auto* const pref_name : secure_dns_pref_names) {
    local_state_registrar_.Add(
        pref_name, base::BindRepeating(&SecureDnsManager::OnPolicyPrefChanged,
                                       base::Unretained(this)));
  }
  local_state_registrar_.Add(
      prefs::kDnsOverHttpsIncludedDomains,
      base::BindRepeating(&SecureDnsManager::OnDoHIncludedDomainsPrefChanged,
                          base::Unretained(this)));
  local_state_registrar_.Add(
      prefs::kDnsOverHttpsExcludedDomains,
      base::BindRepeating(&SecureDnsManager::OnDoHExcludedDomainsPrefChanged,
                          base::Unretained(this)));
}

SecureDnsManager::~SecureDnsManager() {
  for (auto& observer : observers_) {
    observer.OnSecureDnsManagerShutdown();
  }

  g_browser_process->system_network_context_manager()
      ->GetStubResolverConfigReader()
      ->SetOverrideDnsOverHttpsConfigSource(nullptr);

  // `local_state_` outlives the SecureDnsManager instance. The value of
  // `::prefs::kDnsOverHttpsEffectiveTemplatesChromeOS` should not outlive the
  // current instance of SecureDnsManager.
  local_state_->ClearPref(::prefs::kDnsOverHttpsEffectiveTemplatesChromeOS);
}

void SecureDnsManager::SetDoHTemplatesUriResolverForTesting(
    std::unique_ptr<dns_over_https::TemplatesUriResolver>
        doh_templates_uri_resolver) {
  CHECK_IS_TEST();
  doh_templates_uri_resolver_ = std::move(doh_templates_uri_resolver);
}

void SecureDnsManager::LoadProviders() {
  // Note: Check whether each provider is enabled *after* filtering based on
  // country code so that if we are doing experimentation via Finch for a
  // regional provider, the experiment groups will be less likely to include
  // users from other regions unnecessarily (since a client will be included in
  // the experiment if the provider feature flag is checked).
  const net::DohProviderEntry::List local_providers =
      chrome_browser_net::secure_dns::SelectEnabledProviders(
          chrome_browser_net::secure_dns::ProvidersForCountry(
              net::DohProviderEntry::GetList(),
              country_codes::GetCurrentCountryID()));

  for (const net::DohProviderEntry* provider : local_providers) {
    std::vector<std::string> ip_addrs;
    base::ranges::transform(provider->ip_addresses,
                            std::back_inserter(ip_addrs),
                            &net::IPAddress::ToString);
    local_doh_providers_[provider->doh_server_config] =
        base::JoinString(ip_addrs, ",");
  }
}

base::Value::Dict SecureDnsManager::GetProviders(
    const std::string& mode,
    const std::string& templates) const {
  base::Value::Dict doh_providers;

  if (mode == SecureDnsConfig::kModeOff) {
    return doh_providers;
  }

  // If there are templates then use them. In secure mode, the values, which
  // hold the IP addresses of the name servers, are left empty. In secure DNS
  // mode with fallback to plain-text nameservers, the values are stored as a
  // wildcard character denoting that it matches any IP addresses. In automatic
  // upgrade mode, the corresponding name servers will be populated using the
  // applicable providers.
  const std::string_view addr =
      mode == SecureDnsConfig::kModeSecure
          ? ""
          : shill::kDNSProxyDOHProvidersMatchAnyIPAddress;
  for (const auto& doh_template : base::SplitString(
           templates, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY)) {
    doh_providers.Set(doh_template, addr);
  }
  if (mode == SecureDnsConfig::kModeSecure) {
    return doh_providers;
  }
  if (!doh_providers.empty()) {
    return doh_providers;
  }

  // No specified DoH providers, relay all DoH provider upgrade configuration
  // for dns-proxy to switch providers whenever the network or its settings
  // change.
  for (const auto& provider : local_doh_providers_) {
    doh_providers.Set(provider.first.server_template(), provider.second);
  }
  return doh_providers;
}

void SecureDnsManager::AddObserver(Observer* observer) {
  observer->OnModeChanged(cached_mode_);
  observer->OnTemplateUrisChanged(cached_template_uris_);
  observers_.AddObserver(observer);
}

void SecureDnsManager::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

void SecureDnsManager::DefaultNetworkChanged(const NetworkState* network) {
  const std::string& mode = local_state_->GetString(::prefs::kDnsOverHttpsMode);
  if (mode == SecureDnsConfig::kModeOff) {
    return;
  }

  // Network updates are only relevant for determining the effective DoH
  // template URI if the admin has configured the
  // DnsOverHttpsTemplatesWithIdentifiers policy to include the IP addresses.
  std::string templates_with_identifiers =
      local_state_->GetString(::prefs::kDnsOverHttpsTemplatesWithIdentifiers);
  if (!dns_over_https::TemplatesUriResolverImpl::
          IsDeviceIpAddressIncludedInUriTemplate(templates_with_identifiers)) {
    return;
  }
  UpdateTemplateUri();
}

void SecureDnsManager::OnPolicyPrefChanged() {
  UpdateTemplateUri();
  ToggleNetworkMonitoring();
}

void SecureDnsManager::ToggleNetworkMonitoring() {
  // If DoH with identifiers are active, verify if network changes need to be
  // observed for URI template placeholder replacement.
  std::string templates_with_identifiers =
      local_state_->GetString(::prefs::kDnsOverHttpsTemplatesWithIdentifiers);

  bool template_uri_includes_network_identifiers =
      doh_templates_uri_resolver_->GetDohWithIdentifiersActive() &&
      dns_over_https::TemplatesUriResolverImpl::
          IsDeviceIpAddressIncludedInUriTemplate(templates_with_identifiers);

  bool should_observe_default_network_changes =
      template_uri_includes_network_identifiers &&
      (local_state_->GetString(::prefs::kDnsOverHttpsMode) !=
       SecureDnsConfig::kModeOff);

  if (!should_observe_default_network_changes) {
    network_state_handler_observer_.Reset();
    return;
  }
  // Already observing default network changes.
  if (network_state_handler_observer_.IsObserving()) {
    return;
  }
  network_state_handler_observer_.Observe(
      NetworkHandler::Get()->network_state_handler());
}

void SecureDnsManager::OnDoHIncludedDomainsPrefChanged() {
  base::Value::List included_domains =
      local_state_->GetList(prefs::kDnsOverHttpsIncludedDomains).Clone();
  NetworkHandler::Get()->network_configuration_handler()->SetManagerProperty(
      shill::kDOHIncludedDomainsProperty,
      base::Value(std::move(included_domains)));

  // TODO(b/351091814): Proxy DoH packets from the browser using plain-text DNS
  // to DNS proxy. DNS proxy should be responsible for the DoH usage when domain
  // DoH config is set.
}

void SecureDnsManager::OnDoHExcludedDomainsPrefChanged() {
  base::Value::List excluded_domains =
      local_state_->GetList(prefs::kDnsOverHttpsExcludedDomains).Clone();
  NetworkHandler::Get()->network_configuration_handler()->SetManagerProperty(
      shill::kDOHExcludedDomainsProperty,
      base::Value(std::move(excluded_domains)));

  // TODO(b/351091814): Proxy DoH packets from the browser using plain-text DNS
  // to DNS proxy. DNS proxy should be responsible for the DoH usage when domain
  // DoH config is set.
}

void SecureDnsManager::BroadcastUpdates(bool template_uris_changed,
                                        bool mode_changed) const {
  bool force_update =
      local_state_->FindPreference(::prefs::kDnsOverHttpsMode)->IsManaged() !=
      cached_is_config_managed_;
  if (!force_update && !template_uris_changed && !mode_changed) {
    // The secure DNS configuration has not changed
    return;
  }

  for (auto& observer : observers_) {
    if (template_uris_changed || force_update) {
      observer.OnTemplateUrisChanged(cached_template_uris_);
    }
    if (mode_changed || force_update) {
      observer.OnModeChanged(cached_mode_);
    }
  }

  // Set the DoH URI template pref which is synced with Lacros and the
  // NetworkService.
  // TODO(acostinas, b/331903009): Storing the effective DoH providers in a
  // local_state pref on Chrome OS has downsides. Replace this pref with an
  // in-memory mechanism to sync effective DoH prefs.
  local_state_->SetString(::prefs::kDnsOverHttpsEffectiveTemplatesChromeOS,
                          cached_template_uris_);

  // Set the DoH URI template shill property which is synced with platform
  // daemons (shill, dns-proxy etc).

  NetworkHandler::Get()->network_configuration_handler()->SetManagerProperty(
      shill::kDNSProxyDOHProvidersProperty,
      base::Value(GetProviders(cached_mode_, cached_template_uris_)));
}

void SecureDnsManager::UpdateTemplateUri() {
  doh_templates_uri_resolver_->Update(local_state_);

  const std::string new_templates =
      doh_templates_uri_resolver_->GetEffectiveTemplates();
  const std::string new_mode =
      local_state_->GetString(::prefs::kDnsOverHttpsMode);

  bool template_uris_changed = new_templates != cached_template_uris_;
  bool mode_changed = new_mode != cached_mode_;

  cached_template_uris_ = new_templates;
  cached_mode_ = new_mode;

  BroadcastUpdates(template_uris_changed, mode_changed);

  cached_is_config_managed_ =
      local_state_->FindPreference(::prefs::kDnsOverHttpsMode)->IsManaged();

  // May be missing in tests.
  if (NetworkHandler::Get()->network_metadata_store()) {
    NetworkHandler::Get()
        ->network_metadata_store()
        ->SetSecureDnsTemplatesWithIdentifiersActive(
            doh_templates_uri_resolver_->GetDohWithIdentifiersActive());
  }
}

// static
void SecureDnsManager::RegisterLocalStatePrefs(PrefRegistrySimple* registry) {
  registry->RegisterStringPref(::prefs::kDnsOverHttpsSalt, std::string());
  registry->RegisterStringPref(::prefs::kDnsOverHttpsTemplatesWithIdentifiers,
                               std::string());
}

std::optional<std::string>
SecureDnsManager::GetDohWithIdentifiersDisplayServers() const {
  if (doh_templates_uri_resolver_->GetDohWithIdentifiersActive()) {
    return doh_templates_uri_resolver_->GetDisplayTemplates();
  }
  return std::nullopt;
}

}  // namespace ash