| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "net/dns/resolve_context.h" |
| |
| #include <cstdlib> |
| #include <limits> |
| #include <utility> |
| |
| #include "base/check_op.h" |
| #include "base/containers/contains.h" |
| #include "base/metrics/bucket_ranges.h" |
| #include "base/metrics/histogram.h" |
| #include "base/metrics/histogram_base.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/sample_vector.h" |
| #include "base/no_destructor.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/observer_list.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/strings/stringprintf.h" |
| #include "net/base/features.h" |
| #include "net/base/ip_address.h" |
| #include "net/base/network_change_notifier.h" |
| #include "net/dns/dns_server_iterator.h" |
| #include "net/dns/dns_session.h" |
| #include "net/dns/dns_util.h" |
| #include "net/dns/host_cache.h" |
| #include "net/dns/public/dns_over_https_config.h" |
| #include "net/dns/public/doh_provider_entry.h" |
| #include "net/url_request/url_request_context.h" |
| |
| namespace net { |
| |
| namespace { |
| |
| // Min fallback period between queries, in case we are talking to a local DNS |
| // proxy. |
| const base::TimeDelta kMinFallbackPeriod = base::Milliseconds(10); |
| |
| // Default maximum fallback period between queries, even with exponential |
| // backoff. (Can be overridden by field trial.) |
| const base::TimeDelta kDefaultMaxFallbackPeriod = base::Seconds(5); |
| |
| // Maximum RTT that will fit in the RTT histograms. |
| const base::TimeDelta kRttMax = base::Seconds(30); |
| // Number of buckets in the histogram of observed RTTs. |
| const size_t kRttBucketCount = 350; |
| // Target percentile in the RTT histogram used for fallback period. |
| const int kRttPercentile = 99; |
| // Number of samples to seed the histogram with. |
| const base::HistogramBase::Count kNumSeeds = 2; |
| |
| DohProviderEntry::List FindDohProvidersMatchingServerConfig( |
| DnsOverHttpsServerConfig server_config) { |
| DohProviderEntry::List matching_entries; |
| for (const DohProviderEntry* entry : DohProviderEntry::GetList()) { |
| if (entry->doh_server_config == server_config) |
| matching_entries.push_back(entry); |
| } |
| |
| return matching_entries; |
| } |
| |
| DohProviderEntry::List FindDohProvidersAssociatedWithAddress( |
| IPAddress server_address) { |
| DohProviderEntry::List matching_entries; |
| for (const DohProviderEntry* entry : DohProviderEntry::GetList()) { |
| if (entry->ip_addresses.count(server_address) > 0) |
| matching_entries.push_back(entry); |
| } |
| |
| return matching_entries; |
| } |
| |
| base::TimeDelta GetDefaultFallbackPeriod(const DnsConfig& config) { |
| NetworkChangeNotifier::ConnectionType type = |
| NetworkChangeNotifier::GetConnectionType(); |
| return GetTimeDeltaForConnectionTypeFromFieldTrialOrDefault( |
| "AsyncDnsInitialTimeoutMsByConnectionType", config.fallback_period, type); |
| } |
| |
| base::TimeDelta GetMaxFallbackPeriod() { |
| NetworkChangeNotifier::ConnectionType type = |
| NetworkChangeNotifier::GetConnectionType(); |
| return GetTimeDeltaForConnectionTypeFromFieldTrialOrDefault( |
| "AsyncDnsMaxTimeoutMsByConnectionType", kDefaultMaxFallbackPeriod, type); |
| } |
| |
| class RttBuckets : public base::BucketRanges { |
| public: |
| RttBuckets() : base::BucketRanges(kRttBucketCount + 1) { |
| base::Histogram::InitializeBucketRanges( |
| 1, |
| base::checked_cast<base::HistogramBase::Sample>( |
| kRttMax.InMilliseconds()), |
| this); |
| } |
| }; |
| |
| static RttBuckets* GetRttBuckets() { |
| static base::NoDestructor<RttBuckets> buckets; |
| return buckets.get(); |
| } |
| |
| static std::unique_ptr<base::SampleVector> GetRttHistogram( |
| base::TimeDelta rtt_estimate) { |
| std::unique_ptr<base::SampleVector> histogram = |
| std::make_unique<base::SampleVector>(GetRttBuckets()); |
| // Seed histogram with 2 samples at |rtt_estimate|. |
| histogram->Accumulate(base::checked_cast<base::HistogramBase::Sample>( |
| rtt_estimate.InMilliseconds()), |
| kNumSeeds); |
| return histogram; |
| } |
| |
| } // namespace |
| |
| ResolveContext::ServerStats::ServerStats( |
| std::unique_ptr<base::SampleVector> buckets) |
| : rtt_histogram(std::move(buckets)) {} |
| |
| ResolveContext::ServerStats::ServerStats(ServerStats&&) = default; |
| |
| ResolveContext::ServerStats::~ServerStats() = default; |
| |
| ResolveContext::ResolveContext(URLRequestContext* url_request_context, |
| bool enable_caching) |
| : url_request_context_(url_request_context), |
| host_cache_(enable_caching ? HostCache::CreateDefaultCache() : nullptr), |
| isolation_info_(IsolationInfo::CreateTransient()) { |
| max_fallback_period_ = GetMaxFallbackPeriod(); |
| } |
| |
| ResolveContext::~ResolveContext() = default; |
| |
| std::unique_ptr<DnsServerIterator> ResolveContext::GetDohIterator( |
| const DnsConfig& config, |
| const SecureDnsMode& mode, |
| const DnsSession* session) { |
| // Make the iterator even if the session differs. The first call to the member |
| // functions will catch the out of date session. |
| |
| return std::make_unique<DohDnsServerIterator>( |
| doh_server_stats_.size(), FirstServerIndex(true, session), |
| config.doh_attempts, config.attempts, mode, this, session); |
| } |
| |
| std::unique_ptr<DnsServerIterator> ResolveContext::GetClassicDnsIterator( |
| const DnsConfig& config, |
| const DnsSession* session) { |
| // Make the iterator even if the session differs. The first call to the member |
| // functions will catch the out of date session. |
| |
| return std::make_unique<ClassicDnsServerIterator>( |
| config.nameservers.size(), FirstServerIndex(false, session), |
| config.attempts, config.attempts, this, session); |
| } |
| |
| bool ResolveContext::GetDohServerAvailability(size_t doh_server_index, |
| const DnsSession* session) const { |
| if (!IsCurrentSession(session)) |
| return false; |
| |
| CHECK_LT(doh_server_index, doh_server_stats_.size()); |
| return ServerStatsToDohAvailability(doh_server_stats_[doh_server_index]); |
| } |
| |
| size_t ResolveContext::NumAvailableDohServers(const DnsSession* session) const { |
| if (!IsCurrentSession(session)) |
| return 0; |
| |
| return base::ranges::count_if(doh_server_stats_, |
| &ServerStatsToDohAvailability); |
| } |
| |
| void ResolveContext::RecordServerFailure(size_t server_index, |
| bool is_doh_server, |
| int rv, |
| const DnsSession* session) { |
| DCHECK(rv != OK && rv != ERR_NAME_NOT_RESOLVED && rv != ERR_IO_PENDING); |
| |
| if (!IsCurrentSession(session)) |
| return; |
| |
| // "FailureError" metric is only recorded for secure queries. |
| if (is_doh_server) { |
| std::string query_type = |
| GetQueryTypeForUma(server_index, true /* is_doh_server */, session); |
| DCHECK_NE(query_type, "Insecure"); |
| std::string provider_id = |
| GetDohProviderIdForUma(server_index, true /* is_doh_server */, session); |
| |
| base::UmaHistogramSparse( |
| base::StringPrintf("Net.DNS.DnsTransaction.%s.%s.FailureError", |
| query_type.c_str(), provider_id.c_str()), |
| std::abs(rv)); |
| } |
| |
| size_t num_available_doh_servers_before = NumAvailableDohServers(session); |
| |
| ServerStats* stats = GetServerStats(server_index, is_doh_server); |
| ++(stats->last_failure_count); |
| stats->last_failure = base::TimeTicks::Now(); |
| |
| size_t num_available_doh_servers_now = NumAvailableDohServers(session); |
| if (num_available_doh_servers_now < num_available_doh_servers_before) { |
| NotifyDohStatusObserversOfUnavailable(false /* network_change */); |
| |
| // TODO(crbug.com/1022059): Consider figuring out some way to only for the |
| // first context enabling DoH or the last context disabling DoH. |
| if (num_available_doh_servers_now == 0) |
| NetworkChangeNotifier::TriggerNonSystemDnsChange(); |
| } |
| } |
| |
| void ResolveContext::RecordServerSuccess(size_t server_index, |
| bool is_doh_server, |
| const DnsSession* session) { |
| if (!IsCurrentSession(session)) |
| return; |
| |
| bool doh_available_before = NumAvailableDohServers(session) > 0; |
| |
| ServerStats* stats = GetServerStats(server_index, is_doh_server); |
| stats->last_failure_count = 0; |
| stats->current_connection_success = true; |
| stats->last_failure = base::TimeTicks(); |
| stats->last_success = base::TimeTicks::Now(); |
| |
| // TODO(crbug.com/1022059): Consider figuring out some way to only for the |
| // first context enabling DoH or the last context disabling DoH. |
| bool doh_available_now = NumAvailableDohServers(session) > 0; |
| if (doh_available_before != doh_available_now) |
| NetworkChangeNotifier::TriggerNonSystemDnsChange(); |
| } |
| |
| void ResolveContext::RecordRtt(size_t server_index, |
| bool is_doh_server, |
| base::TimeDelta rtt, |
| int rv, |
| const DnsSession* session) { |
| if (!IsCurrentSession(session)) |
| return; |
| |
| ServerStats* stats = GetServerStats(server_index, is_doh_server); |
| |
| base::TimeDelta base_fallback_period = |
| NextFallbackPeriodHelper(stats, 0 /* num_backoffs */); |
| RecordRttForUma(server_index, is_doh_server, rtt, rv, base_fallback_period, |
| session); |
| |
| // RTT values shouldn't be less than 0, but it shouldn't cause a crash if |
| // they are anyway, so clip to 0. See https://crbug.com/753568. |
| if (rtt.is_negative()) |
| rtt = base::TimeDelta(); |
| |
| // Histogram-based method. |
| stats->rtt_histogram->Accumulate( |
| base::saturated_cast<base::HistogramBase::Sample>(rtt.InMilliseconds()), |
| 1); |
| } |
| |
| base::TimeDelta ResolveContext::NextClassicFallbackPeriod( |
| size_t classic_server_index, |
| int attempt, |
| const DnsSession* session) { |
| if (!IsCurrentSession(session)) |
| return std::min(GetDefaultFallbackPeriod(session->config()), |
| max_fallback_period_); |
| |
| return NextFallbackPeriodHelper( |
| GetServerStats(classic_server_index, false /* is _doh_server */), |
| attempt / current_session_->config().nameservers.size()); |
| } |
| |
| base::TimeDelta ResolveContext::NextDohFallbackPeriod( |
| size_t doh_server_index, |
| const DnsSession* session) { |
| if (!IsCurrentSession(session)) |
| return std::min(GetDefaultFallbackPeriod(session->config()), |
| max_fallback_period_); |
| |
| return NextFallbackPeriodHelper( |
| GetServerStats(doh_server_index, true /* is _doh_server */), |
| 0 /* num_backoffs */); |
| } |
| |
| base::TimeDelta ResolveContext::ClassicTransactionTimeout( |
| const DnsSession* session) { |
| if (!IsCurrentSession(session)) |
| return features::kDnsMinTransactionTimeout.Get(); |
| |
| // Should not need to call if there are no classic servers configured. |
| DCHECK(!classic_server_stats_.empty()); |
| |
| return TransactionTimeoutHelper(classic_server_stats_.cbegin(), |
| classic_server_stats_.cend()); |
| } |
| |
| base::TimeDelta ResolveContext::SecureTransactionTimeout( |
| SecureDnsMode secure_dns_mode, |
| const DnsSession* session) { |
| // Currently only implemented for Secure mode as other modes are assumed to |
| // always use aggressive timeouts. If that ever changes, need to implement |
| // only accounting for available DoH servers when not Secure mode. |
| DCHECK_EQ(secure_dns_mode, SecureDnsMode::kSecure); |
| |
| if (!IsCurrentSession(session)) |
| return features::kDnsMinTransactionTimeout.Get(); |
| |
| // Should not need to call if there are no DoH servers configured. |
| DCHECK(!doh_server_stats_.empty()); |
| |
| return TransactionTimeoutHelper(doh_server_stats_.cbegin(), |
| doh_server_stats_.cend()); |
| } |
| |
| void ResolveContext::RegisterDohStatusObserver(DohStatusObserver* observer) { |
| DCHECK(observer); |
| doh_status_observers_.AddObserver(observer); |
| } |
| |
| void ResolveContext::UnregisterDohStatusObserver( |
| const DohStatusObserver* observer) { |
| DCHECK(observer); |
| doh_status_observers_.RemoveObserver(observer); |
| } |
| |
| void ResolveContext::InvalidateCachesAndPerSessionData( |
| const DnsSession* new_session, |
| bool network_change) { |
| // Network-bound ResolveContexts should never receive a cache invalidation due |
| // to a network change. |
| DCHECK(GetTargetNetwork() == handles::kInvalidNetworkHandle || |
| !network_change); |
| if (host_cache_) |
| host_cache_->Invalidate(); |
| |
| // DNS config is constant for any given session, so if the current session is |
| // unchanged, any per-session data is safe to keep, even if it's dependent on |
| // a specific config. |
| if (new_session && new_session == current_session_.get()) |
| return; |
| |
| current_session_.reset(); |
| classic_server_stats_.clear(); |
| doh_server_stats_.clear(); |
| initial_fallback_period_ = base::TimeDelta(); |
| max_fallback_period_ = GetMaxFallbackPeriod(); |
| |
| if (!new_session) { |
| NotifyDohStatusObserversOfSessionChanged(); |
| return; |
| } |
| |
| current_session_ = new_session->GetWeakPtr(); |
| |
| initial_fallback_period_ = |
| GetDefaultFallbackPeriod(current_session_->config()); |
| |
| for (size_t i = 0; i < new_session->config().nameservers.size(); ++i) { |
| classic_server_stats_.emplace_back( |
| GetRttHistogram(initial_fallback_period_)); |
| } |
| for (size_t i = 0; i < new_session->config().doh_config.servers().size(); |
| ++i) { |
| doh_server_stats_.emplace_back(GetRttHistogram(initial_fallback_period_)); |
| } |
| |
| CHECK_EQ(new_session->config().nameservers.size(), |
| classic_server_stats_.size()); |
| CHECK_EQ(new_session->config().doh_config.servers().size(), |
| doh_server_stats_.size()); |
| |
| NotifyDohStatusObserversOfSessionChanged(); |
| |
| if (!doh_server_stats_.empty()) |
| NotifyDohStatusObserversOfUnavailable(network_change); |
| } |
| |
| handles::NetworkHandle ResolveContext::GetTargetNetwork() const { |
| if (!url_request_context()) |
| return handles::kInvalidNetworkHandle; |
| |
| return url_request_context()->bound_network(); |
| } |
| |
| size_t ResolveContext::FirstServerIndex(bool doh_server, |
| const DnsSession* session) { |
| if (!IsCurrentSession(session)) |
| return 0u; |
| |
| // DoH first server doesn't rotate, so always return 0u. |
| if (doh_server) |
| return 0u; |
| |
| size_t index = classic_server_index_; |
| if (current_session_->config().rotate) { |
| classic_server_index_ = (classic_server_index_ + 1) % |
| current_session_->config().nameservers.size(); |
| } |
| return index; |
| } |
| |
| bool ResolveContext::IsCurrentSession(const DnsSession* session) const { |
| CHECK(session); |
| if (session == current_session_.get()) { |
| CHECK_EQ(current_session_->config().nameservers.size(), |
| classic_server_stats_.size()); |
| CHECK_EQ(current_session_->config().doh_config.servers().size(), |
| doh_server_stats_.size()); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| ResolveContext::ServerStats* ResolveContext::GetServerStats( |
| size_t server_index, |
| bool is_doh_server) { |
| if (!is_doh_server) { |
| CHECK_LT(server_index, classic_server_stats_.size()); |
| return &classic_server_stats_[server_index]; |
| } else { |
| CHECK_LT(server_index, doh_server_stats_.size()); |
| return &doh_server_stats_[server_index]; |
| } |
| } |
| |
| base::TimeDelta ResolveContext::NextFallbackPeriodHelper( |
| const ServerStats* server_stats, |
| int num_backoffs) { |
| // Respect initial fallback period (from config or field trial) if it exceeds |
| // max. |
| if (initial_fallback_period_ > max_fallback_period_) |
| return initial_fallback_period_; |
| |
| static_assert(std::numeric_limits<base::HistogramBase::Count>::is_signed, |
| "histogram base count assumed to be signed"); |
| |
| // Use fixed percentile of observed samples. |
| const base::SampleVector& samples = *server_stats->rtt_histogram; |
| |
| base::HistogramBase::Count total = samples.TotalCount(); |
| base::HistogramBase::Count remaining_count = kRttPercentile * total / 100; |
| size_t index = 0; |
| while (remaining_count > 0 && index < GetRttBuckets()->size()) { |
| remaining_count -= samples.GetCountAtIndex(index); |
| ++index; |
| } |
| |
| base::TimeDelta fallback_period = |
| base::Milliseconds(GetRttBuckets()->range(index)); |
| |
| fallback_period = std::max(fallback_period, kMinFallbackPeriod); |
| |
| return std::min(fallback_period * (1 << num_backoffs), max_fallback_period_); |
| } |
| |
| template <typename Iterator> |
| base::TimeDelta ResolveContext::TransactionTimeoutHelper( |
| Iterator server_stats_begin, |
| Iterator server_stats_end) { |
| DCHECK_GE(features::kDnsMinTransactionTimeout.Get(), base::TimeDelta()); |
| DCHECK_GE(features::kDnsTransactionTimeoutMultiplier.Get(), 0.0); |
| |
| // Expect at least one configured server. |
| DCHECK(server_stats_begin != server_stats_end); |
| |
| base::TimeDelta shortest_fallback_period = base::TimeDelta::Max(); |
| for (Iterator server_stats = server_stats_begin; |
| server_stats != server_stats_end; ++server_stats) { |
| shortest_fallback_period = std::min( |
| shortest_fallback_period, |
| NextFallbackPeriodHelper(&*server_stats, 0 /* num_backoffs */)); |
| } |
| |
| DCHECK_GE(shortest_fallback_period, base::TimeDelta()); |
| base::TimeDelta ratio_based_timeout = |
| shortest_fallback_period * |
| features::kDnsTransactionTimeoutMultiplier.Get(); |
| |
| return std::max(features::kDnsMinTransactionTimeout.Get(), |
| ratio_based_timeout); |
| } |
| |
| void ResolveContext::RecordRttForUma(size_t server_index, |
| bool is_doh_server, |
| base::TimeDelta rtt, |
| int rv, |
| base::TimeDelta base_fallback_period, |
| const DnsSession* session) { |
| DCHECK(IsCurrentSession(session)); |
| |
| std::string query_type = |
| GetQueryTypeForUma(server_index, is_doh_server, session); |
| std::string provider_id = |
| GetDohProviderIdForUma(server_index, is_doh_server, session); |
| |
| // Skip metrics for SecureNotValidated queries unless the provider is tagged |
| // for extra logging. |
| if (query_type == "SecureNotValidated" && |
| !GetProviderUseExtraLogging(server_index, is_doh_server, session)) { |
| return; |
| } |
| |
| if (rv == OK || rv == ERR_NAME_NOT_RESOLVED) { |
| base::UmaHistogramMediumTimes( |
| base::StringPrintf("Net.DNS.DnsTransaction.%s.%s.SuccessTime", |
| query_type.c_str(), provider_id.c_str()), |
| rtt); |
| } else { |
| base::UmaHistogramMediumTimes( |
| base::StringPrintf("Net.DNS.DnsTransaction.%s.%s.FailureTime", |
| query_type.c_str(), provider_id.c_str()), |
| rtt); |
| } |
| } |
| |
| std::string ResolveContext::GetQueryTypeForUma(size_t server_index, |
| bool is_doh_server, |
| const DnsSession* session) { |
| DCHECK(IsCurrentSession(session)); |
| |
| if (!is_doh_server) |
| return "Insecure"; |
| |
| // Secure queries are validated if the DoH server state is available. |
| if (GetDohServerAvailability(server_index, session)) |
| return "SecureValidated"; |
| |
| return "SecureNotValidated"; |
| } |
| |
| std::string ResolveContext::GetDohProviderIdForUma(size_t server_index, |
| bool is_doh_server, |
| const DnsSession* session) { |
| DCHECK(IsCurrentSession(session)); |
| |
| if (is_doh_server) { |
| return GetDohProviderIdForHistogramFromServerConfig( |
| session->config().doh_config.servers()[server_index]); |
| } |
| |
| return GetDohProviderIdForHistogramFromNameserver( |
| session->config().nameservers[server_index]); |
| } |
| |
| bool ResolveContext::GetProviderUseExtraLogging(size_t server_index, |
| bool is_doh_server, |
| const DnsSession* session) { |
| DCHECK(IsCurrentSession(session)); |
| |
| DohProviderEntry::List matching_entries; |
| if (is_doh_server) { |
| const DnsOverHttpsServerConfig& server_config = |
| session->config().doh_config.servers()[server_index]; |
| matching_entries = FindDohProvidersMatchingServerConfig(server_config); |
| } else { |
| IPAddress server_address = |
| session->config().nameservers[server_index].address(); |
| matching_entries = FindDohProvidersAssociatedWithAddress(server_address); |
| } |
| |
| // Use extra logging if any matching provider entries have |
| // `LoggingLevel::kExtra` set. |
| return base::Contains(matching_entries, |
| DohProviderEntry::LoggingLevel::kExtra, |
| &DohProviderEntry::logging_level); |
| } |
| |
| void ResolveContext::NotifyDohStatusObserversOfSessionChanged() { |
| for (auto& observer : doh_status_observers_) |
| observer.OnSessionChanged(); |
| } |
| |
| void ResolveContext::NotifyDohStatusObserversOfUnavailable( |
| bool network_change) { |
| for (auto& observer : doh_status_observers_) |
| observer.OnDohServerUnavailable(network_change); |
| } |
| |
| // static |
| bool ResolveContext::ServerStatsToDohAvailability( |
| const ResolveContext::ServerStats& stats) { |
| return stats.last_failure_count < kAutomaticModeFailureLimit && |
| stats.current_connection_success; |
| } |
| |
| } // namespace net |