1 // Copyright 2020 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "net/dns/resolve_context.h"
6
7 #include <cstdlib>
8 #include <limits>
9 #include <utility>
10
11 #include "base/check_op.h"
12 #include "base/containers/contains.h"
13 #include "base/metrics/bucket_ranges.h"
14 #include "base/metrics/histogram.h"
15 #include "base/metrics/histogram_base.h"
16 #include "base/metrics/histogram_functions.h"
17 #include "base/metrics/histogram_macros.h"
18 #include "base/metrics/sample_vector.h"
19 #include "base/no_destructor.h"
20 #include "base/numerics/safe_conversions.h"
21 #include "base/observer_list.h"
22 #include "base/ranges/algorithm.h"
23 #include "base/strings/stringprintf.h"
24 #include "net/base/features.h"
25 #include "net/base/ip_address.h"
26 #include "net/base/network_change_notifier.h"
27 #include "net/dns/dns_server_iterator.h"
28 #include "net/dns/dns_session.h"
29 #include "net/dns/dns_util.h"
30 #include "net/dns/host_cache.h"
31 #include "net/dns/public/dns_over_https_config.h"
32 #include "net/dns/public/doh_provider_entry.h"
33 #include "net/url_request/url_request_context.h"
34
35 namespace net {
36
37 namespace {
38
39 // Min fallback period between queries, in case we are talking to a local DNS
40 // proxy.
41 const base::TimeDelta kMinFallbackPeriod = base::Milliseconds(10);
42
43 // Default maximum fallback period between queries, even with exponential
44 // backoff. (Can be overridden by field trial.)
45 const base::TimeDelta kDefaultMaxFallbackPeriod = base::Seconds(5);
46
47 // Maximum RTT that will fit in the RTT histograms.
48 const base::TimeDelta kRttMax = base::Seconds(30);
49 // Number of buckets in the histogram of observed RTTs.
50 const size_t kRttBucketCount = 350;
51 // Target percentile in the RTT histogram used for fallback period.
52 const int kRttPercentile = 99;
53 // Number of samples to seed the histogram with.
54 const base::HistogramBase::Count kNumSeeds = 2;
55
FindDohProvidersMatchingServerConfig(DnsOverHttpsServerConfig server_config)56 DohProviderEntry::List FindDohProvidersMatchingServerConfig(
57 DnsOverHttpsServerConfig server_config) {
58 DohProviderEntry::List matching_entries;
59 for (const DohProviderEntry* entry : DohProviderEntry::GetList()) {
60 if (entry->doh_server_config == server_config)
61 matching_entries.push_back(entry);
62 }
63
64 return matching_entries;
65 }
66
FindDohProvidersAssociatedWithAddress(IPAddress server_address)67 DohProviderEntry::List FindDohProvidersAssociatedWithAddress(
68 IPAddress server_address) {
69 DohProviderEntry::List matching_entries;
70 for (const DohProviderEntry* entry : DohProviderEntry::GetList()) {
71 if (entry->ip_addresses.count(server_address) > 0)
72 matching_entries.push_back(entry);
73 }
74
75 return matching_entries;
76 }
77
GetDefaultFallbackPeriod(const DnsConfig & config)78 base::TimeDelta GetDefaultFallbackPeriod(const DnsConfig& config) {
79 NetworkChangeNotifier::ConnectionType type =
80 NetworkChangeNotifier::GetConnectionType();
81 return GetTimeDeltaForConnectionTypeFromFieldTrialOrDefault(
82 "AsyncDnsInitialTimeoutMsByConnectionType", config.fallback_period, type);
83 }
84
GetMaxFallbackPeriod()85 base::TimeDelta GetMaxFallbackPeriod() {
86 NetworkChangeNotifier::ConnectionType type =
87 NetworkChangeNotifier::GetConnectionType();
88 return GetTimeDeltaForConnectionTypeFromFieldTrialOrDefault(
89 "AsyncDnsMaxTimeoutMsByConnectionType", kDefaultMaxFallbackPeriod, type);
90 }
91
92 class RttBuckets : public base::BucketRanges {
93 public:
RttBuckets()94 RttBuckets() : base::BucketRanges(kRttBucketCount + 1) {
95 base::Histogram::InitializeBucketRanges(
96 1,
97 base::checked_cast<base::HistogramBase::Sample>(
98 kRttMax.InMilliseconds()),
99 this);
100 }
101 };
102
GetRttBuckets()103 static RttBuckets* GetRttBuckets() {
104 static base::NoDestructor<RttBuckets> buckets;
105 return buckets.get();
106 }
107
GetRttHistogram(base::TimeDelta rtt_estimate)108 static std::unique_ptr<base::SampleVector> GetRttHistogram(
109 base::TimeDelta rtt_estimate) {
110 std::unique_ptr<base::SampleVector> histogram =
111 std::make_unique<base::SampleVector>(GetRttBuckets());
112 // Seed histogram with 2 samples at |rtt_estimate|.
113 histogram->Accumulate(base::checked_cast<base::HistogramBase::Sample>(
114 rtt_estimate.InMilliseconds()),
115 kNumSeeds);
116 return histogram;
117 }
118
119 } // namespace
120
ServerStats(std::unique_ptr<base::SampleVector> buckets)121 ResolveContext::ServerStats::ServerStats(
122 std::unique_ptr<base::SampleVector> buckets)
123 : rtt_histogram(std::move(buckets)) {}
124
125 ResolveContext::ServerStats::ServerStats(ServerStats&&) = default;
126
127 ResolveContext::ServerStats::~ServerStats() = default;
128
ResolveContext(URLRequestContext * url_request_context,bool enable_caching)129 ResolveContext::ResolveContext(URLRequestContext* url_request_context,
130 bool enable_caching)
131 : url_request_context_(url_request_context),
132 host_cache_(enable_caching ? HostCache::CreateDefaultCache() : nullptr),
133 isolation_info_(IsolationInfo::CreateTransient()) {
134 max_fallback_period_ = GetMaxFallbackPeriod();
135 }
136
137 ResolveContext::~ResolveContext() = default;
138
GetDohIterator(const DnsConfig & config,const SecureDnsMode & mode,const DnsSession * session)139 std::unique_ptr<DnsServerIterator> ResolveContext::GetDohIterator(
140 const DnsConfig& config,
141 const SecureDnsMode& mode,
142 const DnsSession* session) {
143 // Make the iterator even if the session differs. The first call to the member
144 // functions will catch the out of date session.
145
146 return std::make_unique<DohDnsServerIterator>(
147 doh_server_stats_.size(), FirstServerIndex(true, session),
148 config.doh_attempts, config.attempts, mode, this, session);
149 }
150
GetClassicDnsIterator(const DnsConfig & config,const DnsSession * session)151 std::unique_ptr<DnsServerIterator> ResolveContext::GetClassicDnsIterator(
152 const DnsConfig& config,
153 const DnsSession* session) {
154 // Make the iterator even if the session differs. The first call to the member
155 // functions will catch the out of date session.
156
157 return std::make_unique<ClassicDnsServerIterator>(
158 config.nameservers.size(), FirstServerIndex(false, session),
159 config.attempts, config.attempts, this, session);
160 }
161
GetDohServerAvailability(size_t doh_server_index,const DnsSession * session) const162 bool ResolveContext::GetDohServerAvailability(size_t doh_server_index,
163 const DnsSession* session) const {
164 if (!IsCurrentSession(session))
165 return false;
166
167 CHECK_LT(doh_server_index, doh_server_stats_.size());
168 return ServerStatsToDohAvailability(doh_server_stats_[doh_server_index]);
169 }
170
NumAvailableDohServers(const DnsSession * session) const171 size_t ResolveContext::NumAvailableDohServers(const DnsSession* session) const {
172 if (!IsCurrentSession(session))
173 return 0;
174
175 return base::ranges::count_if(doh_server_stats_,
176 &ServerStatsToDohAvailability);
177 }
178
RecordServerFailure(size_t server_index,bool is_doh_server,int rv,const DnsSession * session)179 void ResolveContext::RecordServerFailure(size_t server_index,
180 bool is_doh_server,
181 int rv,
182 const DnsSession* session) {
183 DCHECK(rv != OK && rv != ERR_NAME_NOT_RESOLVED && rv != ERR_IO_PENDING);
184
185 if (!IsCurrentSession(session))
186 return;
187
188 // "FailureError" metric is only recorded for secure queries.
189 if (is_doh_server) {
190 std::string query_type =
191 GetQueryTypeForUma(server_index, true /* is_doh_server */, session);
192 DCHECK_NE(query_type, "Insecure");
193 std::string provider_id =
194 GetDohProviderIdForUma(server_index, true /* is_doh_server */, session);
195
196 base::UmaHistogramSparse(
197 base::StringPrintf("Net.DNS.DnsTransaction.%s.%s.FailureError",
198 query_type.c_str(), provider_id.c_str()),
199 std::abs(rv));
200 }
201
202 size_t num_available_doh_servers_before = NumAvailableDohServers(session);
203
204 ServerStats* stats = GetServerStats(server_index, is_doh_server);
205 ++(stats->last_failure_count);
206 stats->last_failure = base::TimeTicks::Now();
207
208 size_t num_available_doh_servers_now = NumAvailableDohServers(session);
209 if (num_available_doh_servers_now < num_available_doh_servers_before) {
210 NotifyDohStatusObserversOfUnavailable(false /* network_change */);
211
212 // TODO(crbug.com/1022059): Consider figuring out some way to only for the
213 // first context enabling DoH or the last context disabling DoH.
214 if (num_available_doh_servers_now == 0)
215 NetworkChangeNotifier::TriggerNonSystemDnsChange();
216 }
217 }
218
RecordServerSuccess(size_t server_index,bool is_doh_server,const DnsSession * session)219 void ResolveContext::RecordServerSuccess(size_t server_index,
220 bool is_doh_server,
221 const DnsSession* session) {
222 if (!IsCurrentSession(session))
223 return;
224
225 bool doh_available_before = NumAvailableDohServers(session) > 0;
226
227 ServerStats* stats = GetServerStats(server_index, is_doh_server);
228 stats->last_failure_count = 0;
229 stats->current_connection_success = true;
230 stats->last_failure = base::TimeTicks();
231 stats->last_success = base::TimeTicks::Now();
232
233 // TODO(crbug.com/1022059): Consider figuring out some way to only for the
234 // first context enabling DoH or the last context disabling DoH.
235 bool doh_available_now = NumAvailableDohServers(session) > 0;
236 if (doh_available_before != doh_available_now)
237 NetworkChangeNotifier::TriggerNonSystemDnsChange();
238 }
239
RecordRtt(size_t server_index,bool is_doh_server,base::TimeDelta rtt,int rv,const DnsSession * session)240 void ResolveContext::RecordRtt(size_t server_index,
241 bool is_doh_server,
242 base::TimeDelta rtt,
243 int rv,
244 const DnsSession* session) {
245 if (!IsCurrentSession(session))
246 return;
247
248 ServerStats* stats = GetServerStats(server_index, is_doh_server);
249
250 base::TimeDelta base_fallback_period =
251 NextFallbackPeriodHelper(stats, 0 /* num_backoffs */);
252 RecordRttForUma(server_index, is_doh_server, rtt, rv, base_fallback_period,
253 session);
254
255 // RTT values shouldn't be less than 0, but it shouldn't cause a crash if
256 // they are anyway, so clip to 0. See https://crbug.com/753568.
257 if (rtt.is_negative())
258 rtt = base::TimeDelta();
259
260 // Histogram-based method.
261 stats->rtt_histogram->Accumulate(
262 base::saturated_cast<base::HistogramBase::Sample>(rtt.InMilliseconds()),
263 1);
264 }
265
NextClassicFallbackPeriod(size_t classic_server_index,int attempt,const DnsSession * session)266 base::TimeDelta ResolveContext::NextClassicFallbackPeriod(
267 size_t classic_server_index,
268 int attempt,
269 const DnsSession* session) {
270 if (!IsCurrentSession(session))
271 return std::min(GetDefaultFallbackPeriod(session->config()),
272 max_fallback_period_);
273
274 return NextFallbackPeriodHelper(
275 GetServerStats(classic_server_index, false /* is _doh_server */),
276 attempt / current_session_->config().nameservers.size());
277 }
278
NextDohFallbackPeriod(size_t doh_server_index,const DnsSession * session)279 base::TimeDelta ResolveContext::NextDohFallbackPeriod(
280 size_t doh_server_index,
281 const DnsSession* session) {
282 if (!IsCurrentSession(session))
283 return std::min(GetDefaultFallbackPeriod(session->config()),
284 max_fallback_period_);
285
286 return NextFallbackPeriodHelper(
287 GetServerStats(doh_server_index, true /* is _doh_server */),
288 0 /* num_backoffs */);
289 }
290
ClassicTransactionTimeout(const DnsSession * session)291 base::TimeDelta ResolveContext::ClassicTransactionTimeout(
292 const DnsSession* session) {
293 if (!IsCurrentSession(session))
294 return features::kDnsMinTransactionTimeout.Get();
295
296 // Should not need to call if there are no classic servers configured.
297 DCHECK(!classic_server_stats_.empty());
298
299 return TransactionTimeoutHelper(classic_server_stats_.cbegin(),
300 classic_server_stats_.cend());
301 }
302
SecureTransactionTimeout(SecureDnsMode secure_dns_mode,const DnsSession * session)303 base::TimeDelta ResolveContext::SecureTransactionTimeout(
304 SecureDnsMode secure_dns_mode,
305 const DnsSession* session) {
306 // Currently only implemented for Secure mode as other modes are assumed to
307 // always use aggressive timeouts. If that ever changes, need to implement
308 // only accounting for available DoH servers when not Secure mode.
309 DCHECK_EQ(secure_dns_mode, SecureDnsMode::kSecure);
310
311 if (!IsCurrentSession(session))
312 return features::kDnsMinTransactionTimeout.Get();
313
314 // Should not need to call if there are no DoH servers configured.
315 DCHECK(!doh_server_stats_.empty());
316
317 return TransactionTimeoutHelper(doh_server_stats_.cbegin(),
318 doh_server_stats_.cend());
319 }
320
RegisterDohStatusObserver(DohStatusObserver * observer)321 void ResolveContext::RegisterDohStatusObserver(DohStatusObserver* observer) {
322 DCHECK(observer);
323 doh_status_observers_.AddObserver(observer);
324 }
325
UnregisterDohStatusObserver(const DohStatusObserver * observer)326 void ResolveContext::UnregisterDohStatusObserver(
327 const DohStatusObserver* observer) {
328 DCHECK(observer);
329 doh_status_observers_.RemoveObserver(observer);
330 }
331
InvalidateCachesAndPerSessionData(const DnsSession * new_session,bool network_change)332 void ResolveContext::InvalidateCachesAndPerSessionData(
333 const DnsSession* new_session,
334 bool network_change) {
335 // Network-bound ResolveContexts should never receive a cache invalidation due
336 // to a network change.
337 DCHECK(GetTargetNetwork() == handles::kInvalidNetworkHandle ||
338 !network_change);
339 if (host_cache_)
340 host_cache_->Invalidate();
341
342 // DNS config is constant for any given session, so if the current session is
343 // unchanged, any per-session data is safe to keep, even if it's dependent on
344 // a specific config.
345 if (new_session && new_session == current_session_.get())
346 return;
347
348 current_session_.reset();
349 classic_server_stats_.clear();
350 doh_server_stats_.clear();
351 initial_fallback_period_ = base::TimeDelta();
352 max_fallback_period_ = GetMaxFallbackPeriod();
353
354 if (!new_session) {
355 NotifyDohStatusObserversOfSessionChanged();
356 return;
357 }
358
359 current_session_ = new_session->GetWeakPtr();
360
361 initial_fallback_period_ =
362 GetDefaultFallbackPeriod(current_session_->config());
363
364 for (size_t i = 0; i < new_session->config().nameservers.size(); ++i) {
365 classic_server_stats_.emplace_back(
366 GetRttHistogram(initial_fallback_period_));
367 }
368 for (size_t i = 0; i < new_session->config().doh_config.servers().size();
369 ++i) {
370 doh_server_stats_.emplace_back(GetRttHistogram(initial_fallback_period_));
371 }
372
373 CHECK_EQ(new_session->config().nameservers.size(),
374 classic_server_stats_.size());
375 CHECK_EQ(new_session->config().doh_config.servers().size(),
376 doh_server_stats_.size());
377
378 NotifyDohStatusObserversOfSessionChanged();
379
380 if (!doh_server_stats_.empty())
381 NotifyDohStatusObserversOfUnavailable(network_change);
382 }
383
GetTargetNetwork() const384 handles::NetworkHandle ResolveContext::GetTargetNetwork() const {
385 if (!url_request_context())
386 return handles::kInvalidNetworkHandle;
387
388 return url_request_context()->bound_network();
389 }
390
FirstServerIndex(bool doh_server,const DnsSession * session)391 size_t ResolveContext::FirstServerIndex(bool doh_server,
392 const DnsSession* session) {
393 if (!IsCurrentSession(session))
394 return 0u;
395
396 // DoH first server doesn't rotate, so always return 0u.
397 if (doh_server)
398 return 0u;
399
400 size_t index = classic_server_index_;
401 if (current_session_->config().rotate) {
402 classic_server_index_ = (classic_server_index_ + 1) %
403 current_session_->config().nameservers.size();
404 }
405 return index;
406 }
407
IsCurrentSession(const DnsSession * session) const408 bool ResolveContext::IsCurrentSession(const DnsSession* session) const {
409 CHECK(session);
410 if (session == current_session_.get()) {
411 CHECK_EQ(current_session_->config().nameservers.size(),
412 classic_server_stats_.size());
413 CHECK_EQ(current_session_->config().doh_config.servers().size(),
414 doh_server_stats_.size());
415 return true;
416 }
417
418 return false;
419 }
420
GetServerStats(size_t server_index,bool is_doh_server)421 ResolveContext::ServerStats* ResolveContext::GetServerStats(
422 size_t server_index,
423 bool is_doh_server) {
424 if (!is_doh_server) {
425 CHECK_LT(server_index, classic_server_stats_.size());
426 return &classic_server_stats_[server_index];
427 } else {
428 CHECK_LT(server_index, doh_server_stats_.size());
429 return &doh_server_stats_[server_index];
430 }
431 }
432
NextFallbackPeriodHelper(const ServerStats * server_stats,int num_backoffs)433 base::TimeDelta ResolveContext::NextFallbackPeriodHelper(
434 const ServerStats* server_stats,
435 int num_backoffs) {
436 // Respect initial fallback period (from config or field trial) if it exceeds
437 // max.
438 if (initial_fallback_period_ > max_fallback_period_)
439 return initial_fallback_period_;
440
441 static_assert(std::numeric_limits<base::HistogramBase::Count>::is_signed,
442 "histogram base count assumed to be signed");
443
444 // Use fixed percentile of observed samples.
445 const base::SampleVector& samples = *server_stats->rtt_histogram;
446
447 base::HistogramBase::Count total = samples.TotalCount();
448 base::HistogramBase::Count remaining_count = kRttPercentile * total / 100;
449 size_t index = 0;
450 while (remaining_count > 0 && index < GetRttBuckets()->size()) {
451 remaining_count -= samples.GetCountAtIndex(index);
452 ++index;
453 }
454
455 base::TimeDelta fallback_period =
456 base::Milliseconds(GetRttBuckets()->range(index));
457
458 fallback_period = std::max(fallback_period, kMinFallbackPeriod);
459
460 return std::min(fallback_period * (1 << num_backoffs), max_fallback_period_);
461 }
462
463 template <typename Iterator>
TransactionTimeoutHelper(Iterator server_stats_begin,Iterator server_stats_end)464 base::TimeDelta ResolveContext::TransactionTimeoutHelper(
465 Iterator server_stats_begin,
466 Iterator server_stats_end) {
467 DCHECK_GE(features::kDnsMinTransactionTimeout.Get(), base::TimeDelta());
468 DCHECK_GE(features::kDnsTransactionTimeoutMultiplier.Get(), 0.0);
469
470 // Expect at least one configured server.
471 DCHECK(server_stats_begin != server_stats_end);
472
473 base::TimeDelta shortest_fallback_period = base::TimeDelta::Max();
474 for (Iterator server_stats = server_stats_begin;
475 server_stats != server_stats_end; ++server_stats) {
476 shortest_fallback_period = std::min(
477 shortest_fallback_period,
478 NextFallbackPeriodHelper(&*server_stats, 0 /* num_backoffs */));
479 }
480
481 DCHECK_GE(shortest_fallback_period, base::TimeDelta());
482 base::TimeDelta ratio_based_timeout =
483 shortest_fallback_period *
484 features::kDnsTransactionTimeoutMultiplier.Get();
485
486 return std::max(features::kDnsMinTransactionTimeout.Get(),
487 ratio_based_timeout);
488 }
489
RecordRttForUma(size_t server_index,bool is_doh_server,base::TimeDelta rtt,int rv,base::TimeDelta base_fallback_period,const DnsSession * session)490 void ResolveContext::RecordRttForUma(size_t server_index,
491 bool is_doh_server,
492 base::TimeDelta rtt,
493 int rv,
494 base::TimeDelta base_fallback_period,
495 const DnsSession* session) {
496 DCHECK(IsCurrentSession(session));
497
498 std::string query_type =
499 GetQueryTypeForUma(server_index, is_doh_server, session);
500 std::string provider_id =
501 GetDohProviderIdForUma(server_index, is_doh_server, session);
502
503 // Skip metrics for SecureNotValidated queries unless the provider is tagged
504 // for extra logging.
505 if (query_type == "SecureNotValidated" &&
506 !GetProviderUseExtraLogging(server_index, is_doh_server, session)) {
507 return;
508 }
509
510 if (rv == OK || rv == ERR_NAME_NOT_RESOLVED) {
511 base::UmaHistogramMediumTimes(
512 base::StringPrintf("Net.DNS.DnsTransaction.%s.%s.SuccessTime",
513 query_type.c_str(), provider_id.c_str()),
514 rtt);
515 } else {
516 base::UmaHistogramMediumTimes(
517 base::StringPrintf("Net.DNS.DnsTransaction.%s.%s.FailureTime",
518 query_type.c_str(), provider_id.c_str()),
519 rtt);
520 }
521 }
522
GetQueryTypeForUma(size_t server_index,bool is_doh_server,const DnsSession * session)523 std::string ResolveContext::GetQueryTypeForUma(size_t server_index,
524 bool is_doh_server,
525 const DnsSession* session) {
526 DCHECK(IsCurrentSession(session));
527
528 if (!is_doh_server)
529 return "Insecure";
530
531 // Secure queries are validated if the DoH server state is available.
532 if (GetDohServerAvailability(server_index, session))
533 return "SecureValidated";
534
535 return "SecureNotValidated";
536 }
537
GetDohProviderIdForUma(size_t server_index,bool is_doh_server,const DnsSession * session)538 std::string ResolveContext::GetDohProviderIdForUma(size_t server_index,
539 bool is_doh_server,
540 const DnsSession* session) {
541 DCHECK(IsCurrentSession(session));
542
543 if (is_doh_server) {
544 return GetDohProviderIdForHistogramFromServerConfig(
545 session->config().doh_config.servers()[server_index]);
546 }
547
548 return GetDohProviderIdForHistogramFromNameserver(
549 session->config().nameservers[server_index]);
550 }
551
GetProviderUseExtraLogging(size_t server_index,bool is_doh_server,const DnsSession * session)552 bool ResolveContext::GetProviderUseExtraLogging(size_t server_index,
553 bool is_doh_server,
554 const DnsSession* session) {
555 DCHECK(IsCurrentSession(session));
556
557 DohProviderEntry::List matching_entries;
558 if (is_doh_server) {
559 const DnsOverHttpsServerConfig& server_config =
560 session->config().doh_config.servers()[server_index];
561 matching_entries = FindDohProvidersMatchingServerConfig(server_config);
562 } else {
563 IPAddress server_address =
564 session->config().nameservers[server_index].address();
565 matching_entries = FindDohProvidersAssociatedWithAddress(server_address);
566 }
567
568 // Use extra logging if any matching provider entries have
569 // `LoggingLevel::kExtra` set.
570 return base::Contains(matching_entries,
571 DohProviderEntry::LoggingLevel::kExtra,
572 &DohProviderEntry::logging_level);
573 }
574
NotifyDohStatusObserversOfSessionChanged()575 void ResolveContext::NotifyDohStatusObserversOfSessionChanged() {
576 for (auto& observer : doh_status_observers_)
577 observer.OnSessionChanged();
578 }
579
NotifyDohStatusObserversOfUnavailable(bool network_change)580 void ResolveContext::NotifyDohStatusObserversOfUnavailable(
581 bool network_change) {
582 for (auto& observer : doh_status_observers_)
583 observer.OnDohServerUnavailable(network_change);
584 }
585
586 // static
ServerStatsToDohAvailability(const ResolveContext::ServerStats & stats)587 bool ResolveContext::ServerStatsToDohAvailability(
588 const ResolveContext::ServerStats& stats) {
589 return stats.last_failure_count < kAutomaticModeFailureLimit &&
590 stats.current_connection_success;
591 }
592
593 } // namespace net
594