blob: fe84b89a3e3909515de56c7ab7d275a28862fcf8 [file] [log] [blame]
// Copyright 2024 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/host_resolver_dns_task.h"
#include <string_view>
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/ranges/algorithm.h"
#include "base/time/tick_clock.h"
#include "net/base/features.h"
#include "net/dns/address_sorter.h"
#include "net/dns/dns_client.h"
#include "net/dns/dns_names_util.h"
#include "net/dns/dns_response.h"
#include "net/dns/dns_transaction.h"
#include "net/dns/dns_util.h"
#include "net/dns/host_resolver.h"
#include "net/dns/host_resolver_cache.h"
#include "net/dns/host_resolver_internal_result.h"
#include "net/dns/public/util.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
namespace net {
namespace {
DnsResponse CreateFakeEmptyResponse(std::string_view hostname,
DnsQueryType query_type) {
std::optional<std::vector<uint8_t>> qname =
dns_names_util::DottedNameToNetwork(
hostname, /*require_valid_internet_hostname=*/true);
CHECK(qname.has_value());
return DnsResponse::CreateEmptyNoDataResponse(
/*id=*/0u, /*is_authoritative=*/true, qname.value(),
DnsQueryTypeToQtype(query_type));
}
base::Value::Dict NetLogDnsTaskExtractionFailureParams(
DnsResponseResultExtractor::ExtractionError extraction_error,
DnsQueryType dns_query_type) {
base::Value::Dict dict;
dict.Set("extraction_error", base::strict_cast<int>(extraction_error));
dict.Set("dns_query_type", kDnsQueryTypes.at(dns_query_type));
return dict;
}
// Creates NetLog parameters when the DnsTask failed.
base::Value::Dict NetLogDnsTaskFailedParams(
int net_error,
std::optional<DnsQueryType> failed_transaction_type,
std::optional<base::TimeDelta> ttl,
const HostCache::Entry* saved_results) {
base::Value::Dict dict;
if (failed_transaction_type) {
dict.Set("dns_query_type", kDnsQueryTypes.at(*failed_transaction_type));
}
if (ttl) {
dict.Set("error_ttl_sec",
base::saturated_cast<int>(ttl.value().InSeconds()));
}
dict.Set("net_error", net_error);
if (saved_results) {
dict.Set("saved_results", saved_results->NetLogParams());
}
return dict;
}
base::Value::Dict NetLogResults(const HostCache::Entry& results) {
base::Value::Dict dict;
dict.Set("results", results.NetLogParams());
return dict;
}
void RecordResolveTimeDiffForBucket(const char* histogram_variant,
const char* histogram_bucket,
base::TimeDelta diff) {
base::UmaHistogramTimes(
base::StrCat({"Net.Dns.ResolveTimeDiff.", histogram_variant,
".FirstRecord", histogram_bucket}),
diff);
}
void RecordResolveTimeDiff(const char* histogram_variant,
base::TimeTicks start_time,
base::TimeTicks first_record_end_time,
base::TimeTicks second_record_end_time) {
CHECK_LE(start_time, first_record_end_time);
CHECK_LE(first_record_end_time, second_record_end_time);
base::TimeDelta first_elapsed = first_record_end_time - start_time;
base::TimeDelta diff = second_record_end_time - first_record_end_time;
if (first_elapsed < base::Milliseconds(10)) {
RecordResolveTimeDiffForBucket(histogram_variant, "FasterThan10ms", diff);
} else if (first_elapsed < base::Milliseconds(25)) {
RecordResolveTimeDiffForBucket(histogram_variant, "10msTo25ms", diff);
} else if (first_elapsed < base::Milliseconds(50)) {
RecordResolveTimeDiffForBucket(histogram_variant, "25msTo50ms", diff);
} else if (first_elapsed < base::Milliseconds(100)) {
RecordResolveTimeDiffForBucket(histogram_variant, "50msTo100ms", diff);
} else if (first_elapsed < base::Milliseconds(250)) {
RecordResolveTimeDiffForBucket(histogram_variant, "100msTo250ms", diff);
} else if (first_elapsed < base::Milliseconds(500)) {
RecordResolveTimeDiffForBucket(histogram_variant, "250msTo500ms", diff);
} else if (first_elapsed < base::Seconds(1)) {
RecordResolveTimeDiffForBucket(histogram_variant, "500msTo1s", diff);
} else {
RecordResolveTimeDiffForBucket(histogram_variant, "SlowerThan1s", diff);
}
}
} // namespace
HostResolverDnsTask::SingleTransactionResults::SingleTransactionResults(
DnsQueryType query_type,
Results results)
: query_type(query_type), results(std::move(results)) {}
HostResolverDnsTask::SingleTransactionResults::~SingleTransactionResults() =
default;
HostResolverDnsTask::SingleTransactionResults::SingleTransactionResults(
SingleTransactionResults&&) = default;
HostResolverDnsTask::SingleTransactionResults&
HostResolverDnsTask::SingleTransactionResults::operator=(
SingleTransactionResults&&) = default;
HostResolverDnsTask::TransactionInfo::TransactionInfo(
DnsQueryType type,
TransactionErrorBehavior error_behavior)
: type(type), error_behavior(error_behavior) {}
HostResolverDnsTask::TransactionInfo::~TransactionInfo() = default;
HostResolverDnsTask::TransactionInfo::TransactionInfo(
HostResolverDnsTask::TransactionInfo&& other) = default;
HostResolverDnsTask::TransactionInfo&
HostResolverDnsTask::TransactionInfo::operator=(
HostResolverDnsTask::TransactionInfo&& other) = default;
bool HostResolverDnsTask::TransactionInfo::operator<(
const HostResolverDnsTask::TransactionInfo& other) const {
return std::tie(type, error_behavior, transaction) <
std::tie(other.type, other.error_behavior, other.transaction);
}
HostResolverDnsTask::HostResolverDnsTask(
DnsClient* client,
HostResolver::Host host,
NetworkAnonymizationKey anonymization_key,
DnsQueryTypeSet query_types,
ResolveContext* resolve_context,
bool secure,
SecureDnsMode secure_dns_mode,
Delegate* delegate,
const NetLogWithSource& job_net_log,
const base::TickClock* tick_clock,
bool fallback_available,
const HostResolver::HttpsSvcbOptions& https_svcb_options)
: client_(client),
host_(std::move(host)),
anonymization_key_(std::move(anonymization_key)),
resolve_context_(resolve_context->AsSafeRef()),
secure_(secure),
secure_dns_mode_(secure_dns_mode),
delegate_(delegate),
net_log_(job_net_log),
tick_clock_(tick_clock),
task_start_time_(tick_clock_->NowTicks()),
fallback_available_(fallback_available),
https_svcb_options_(https_svcb_options) {
DCHECK(client_);
DCHECK(delegate_);
if (!secure_) {
DCHECK(client_->CanUseInsecureDnsTransactions());
}
PushTransactionsNeeded(MaybeDisableAdditionalQueries(query_types));
}
HostResolverDnsTask::~HostResolverDnsTask() = default;
void HostResolverDnsTask::StartNextTransaction() {
DCHECK_GE(num_additional_transactions_needed(), 1);
if (!any_transaction_started_) {
net_log_.BeginEvent(NetLogEventType::HOST_RESOLVER_DNS_TASK,
[&] { return NetLogDnsTaskCreationParams(); });
}
any_transaction_started_ = true;
TransactionInfo transaction_info = std::move(transactions_needed_.front());
transactions_needed_.pop_front();
DCHECK(IsAddressType(transaction_info.type) || secure_ ||
client_->CanQueryAdditionalTypesViaInsecureDns());
// Record how long this transaction has been waiting to be created.
base::TimeDelta time_queued = tick_clock_->NowTicks() - task_start_time_;
UMA_HISTOGRAM_LONG_TIMES_100("Net.DNS.JobQueueTime.PerTransaction",
time_queued);
delegate_->AddTransactionTimeQueued(time_queued);
CreateAndStartTransaction(std::move(transaction_info));
}
base::Value::Dict HostResolverDnsTask::NetLogDnsTaskCreationParams() {
base::Value::Dict dict;
dict.Set("secure", secure());
base::Value::List transactions_needed_value;
for (const TransactionInfo& info : transactions_needed_) {
base::Value::Dict transaction_dict;
transaction_dict.Set("dns_query_type", kDnsQueryTypes.at(info.type));
transactions_needed_value.Append(std::move(transaction_dict));
}
dict.Set("transactions_needed", std::move(transactions_needed_value));
return dict;
}
base::Value::Dict HostResolverDnsTask::NetLogDnsTaskTimeoutParams() {
base::Value::Dict dict;
if (!transactions_in_progress_.empty()) {
base::Value::List list;
for (const TransactionInfo& info : transactions_in_progress_) {
base::Value::Dict transaction_dict;
transaction_dict.Set("dns_query_type", kDnsQueryTypes.at(info.type));
list.Append(std::move(transaction_dict));
}
dict.Set("started_transactions", std::move(list));
}
if (!transactions_needed_.empty()) {
base::Value::List list;
for (const TransactionInfo& info : transactions_needed_) {
base::Value::Dict transaction_dict;
transaction_dict.Set("dns_query_type", kDnsQueryTypes.at(info.type));
list.Append(std::move(transaction_dict));
}
dict.Set("queued_transactions", std::move(list));
}
return dict;
}
DnsQueryTypeSet HostResolverDnsTask::MaybeDisableAdditionalQueries(
DnsQueryTypeSet types) {
DCHECK(!types.empty());
DCHECK(!types.Has(DnsQueryType::UNSPECIFIED));
// No-op if the caller explicitly requested this one query type.
if (types.size() == 1) {
return types;
}
if (types.Has(DnsQueryType::HTTPS)) {
if (!secure_ && !client_->CanQueryAdditionalTypesViaInsecureDns()) {
types.Remove(DnsQueryType::HTTPS);
} else {
DCHECK(!httpssvc_metrics_);
httpssvc_metrics_.emplace(secure_);
}
}
DCHECK(!types.empty());
return types;
}
void HostResolverDnsTask::PushTransactionsNeeded(DnsQueryTypeSet query_types) {
DCHECK(transactions_needed_.empty());
if (query_types.Has(DnsQueryType::HTTPS) &&
features::kUseDnsHttpsSvcbEnforceSecureResponse.Get() && secure_) {
query_types.Remove(DnsQueryType::HTTPS);
transactions_needed_.emplace_back(DnsQueryType::HTTPS,
TransactionErrorBehavior::kFatalOrEmpty);
}
// Give AAAA/A queries a head start by pushing them to the queue first.
constexpr DnsQueryType kHighPriorityQueries[] = {DnsQueryType::AAAA,
DnsQueryType::A};
for (DnsQueryType high_priority_query : kHighPriorityQueries) {
if (query_types.Has(high_priority_query)) {
query_types.Remove(high_priority_query);
transactions_needed_.emplace_back(high_priority_query);
}
}
for (DnsQueryType remaining_query : query_types) {
if (remaining_query == DnsQueryType::HTTPS) {
// Ignore errors for these types. In most cases treating them normally
// would only result in fallback to resolution without querying the
// type. Instead, synthesize empty results.
transactions_needed_.emplace_back(
remaining_query, TransactionErrorBehavior::kSynthesizeEmpty);
} else {
transactions_needed_.emplace_back(remaining_query);
}
}
}
void HostResolverDnsTask::CreateAndStartTransaction(
TransactionInfo transaction_info) {
DCHECK(!transaction_info.transaction);
DCHECK_NE(DnsQueryType::UNSPECIFIED, transaction_info.type);
std::string transaction_hostname(host_.GetHostnameWithoutBrackets());
// For HTTPS, prepend "_<port>._https." for any non-default port.
uint16_t request_port = 0;
if (transaction_info.type == DnsQueryType::HTTPS && host_.HasScheme()) {
const auto& scheme_host_port = host_.AsSchemeHostPort();
transaction_hostname =
dns_util::GetNameForHttpsQuery(scheme_host_port, &request_port);
}
transaction_info.transaction =
client_->GetTransactionFactory()->CreateTransaction(
std::move(transaction_hostname),
DnsQueryTypeToQtype(transaction_info.type), net_log_, secure_,
secure_dns_mode_, &*resolve_context_,
fallback_available_ /* fast_timeout */);
transaction_info.transaction->SetRequestPriority(delegate_->priority());
auto transaction_info_it =
transactions_in_progress_.insert(std::move(transaction_info)).first;
// Safe to pass `transaction_info_it` because it is only modified/removed
// after async completion of this call or by destruction (which cancels the
// transaction and prevents callback because it owns the `DnsTransaction`
// object).
transaction_info_it->transaction->Start(base::BindOnce(
&HostResolverDnsTask::OnDnsTransactionComplete, base::Unretained(this),
transaction_info_it, request_port));
}
void HostResolverDnsTask::OnTimeout() {
net_log_.AddEvent(NetLogEventType::HOST_RESOLVER_DNS_TASK_TIMEOUT,
[&] { return NetLogDnsTaskTimeoutParams(); });
for (const TransactionInfo& transaction : transactions_in_progress_) {
base::TimeDelta elapsed_time = tick_clock_->NowTicks() - task_start_time_;
switch (transaction.type) {
case DnsQueryType::HTTPS:
DCHECK(!secure_ ||
!features::kUseDnsHttpsSvcbEnforceSecureResponse.Get());
if (httpssvc_metrics_) {
// Don't record provider ID for timeouts. It is not precisely known
// at this level which provider is actually to blame for the
// timeout, and breaking metrics out by provider is no longer
// important for current experimentation goals.
httpssvc_metrics_->SaveForHttps(HttpssvcDnsRcode::kTimedOut,
/*condensed_records=*/{},
elapsed_time);
}
break;
default:
// The timeout timer is only started when all other transactions have
// completed.
NOTREACHED();
}
}
// Clear in-progress and scheduled transactions so that
// OnTransactionsFinished() doesn't call delegate's
// OnIntermediateTransactionComplete().
transactions_needed_.clear();
transactions_in_progress_.clear();
OnTransactionsFinished(/*single_transaction_results=*/std::nullopt);
}
void HostResolverDnsTask::OnDnsTransactionComplete(
std::set<TransactionInfo>::iterator transaction_info_it,
uint16_t request_port,
int net_error,
const DnsResponse* response) {
DCHECK(transaction_info_it != transactions_in_progress_.end());
DCHECK(base::Contains(transactions_in_progress_, *transaction_info_it));
// Pull the TransactionInfo out of `transactions_in_progress_` now, so it
// and its underlying DnsTransaction will be deleted on completion of
// OnTransactionComplete. Note: Once control leaves OnTransactionComplete,
// there's no further need for the transaction object. On the other hand,
// since it owns `*response`, it should stay around while
// OnTransactionComplete executes.
TransactionInfo transaction_info =
std::move(transactions_in_progress_.extract(transaction_info_it).value());
const base::TimeTicks now = tick_clock_->NowTicks();
base::TimeDelta elapsed_time = now - task_start_time_;
enum HttpssvcDnsRcode rcode_for_httpssvc = HttpssvcDnsRcode::kNoError;
if (httpssvc_metrics_) {
if (net_error == ERR_DNS_TIMED_OUT) {
rcode_for_httpssvc = HttpssvcDnsRcode::kTimedOut;
} else if (net_error == ERR_NAME_NOT_RESOLVED) {
rcode_for_httpssvc = HttpssvcDnsRcode::kNoError;
} else if (response == nullptr) {
rcode_for_httpssvc = HttpssvcDnsRcode::kMissingDnsResponse;
} else {
rcode_for_httpssvc =
TranslateDnsRcodeForHttpssvcExperiment(response->rcode());
}
}
// Handle network errors. Note that for NXDOMAIN, DnsTransaction returns
// ERR_NAME_NOT_RESOLVED, so that is not a network error if received with a
// valid response.
bool fatal_error =
IsFatalTransactionFailure(net_error, transaction_info, response);
std::optional<DnsResponse> fake_response;
if (net_error != OK && !(net_error == ERR_NAME_NOT_RESOLVED && response &&
response->IsValid())) {
if (transaction_info.error_behavior ==
TransactionErrorBehavior::kFallback ||
fatal_error) {
// Fail task (or maybe Job) completely on network failure.
OnFailure(net_error, /*allow_fallback=*/!fatal_error,
/*ttl=*/std::nullopt, transaction_info.type);
return;
} else {
DCHECK((transaction_info.error_behavior ==
TransactionErrorBehavior::kFatalOrEmpty &&
!fatal_error) ||
transaction_info.error_behavior ==
TransactionErrorBehavior::kSynthesizeEmpty);
// For non-fatal failures, synthesize an empty response.
fake_response = CreateFakeEmptyResponse(
host_.GetHostnameWithoutBrackets(), transaction_info.type);
response = &fake_response.value();
}
}
DCHECK(response);
DnsResponseResultExtractor::ResultsOrError results;
{
// Scope the extractor to ensure it is destroyed before `response`.
DnsResponseResultExtractor extractor(*response);
results = extractor.ExtractDnsResults(
transaction_info.type,
/*original_domain_name=*/host_.GetHostnameWithoutBrackets(),
request_port);
}
DCHECK_NE(results.error_or(DnsResponseResultExtractor::ExtractionError::kOk),
DnsResponseResultExtractor::ExtractionError::kUnexpected);
if (!results.has_value()) {
net_log_.AddEvent(
NetLogEventType::HOST_RESOLVER_DNS_TASK_EXTRACTION_FAILURE, [&] {
return NetLogDnsTaskExtractionFailureParams(results.error(),
transaction_info.type);
});
if (transaction_info.error_behavior ==
TransactionErrorBehavior::kFatalOrEmpty ||
transaction_info.error_behavior ==
TransactionErrorBehavior::kSynthesizeEmpty) {
// No extraction errors are currently considered fatal, otherwise, there
// would need to be a call to some sort of
// IsFatalTransactionExtractionError() function.
DCHECK(!fatal_error);
DCHECK_EQ(transaction_info.type, DnsQueryType::HTTPS);
results = Results();
} else {
OnFailure(ERR_DNS_MALFORMED_RESPONSE, /*allow_fallback=*/true,
/*ttl=*/std::nullopt, transaction_info.type);
return;
}
}
CHECK(results.has_value());
net_log_.AddEvent(NetLogEventType::HOST_RESOLVER_DNS_TASK_EXTRACTION_RESULTS,
[&] {
base::Value::List list;
list.reserve(results.value().size());
for (const auto& result : results.value()) {
list.Append(result->ToValue());
}
base::Value::Dict dict;
dict.Set("results", std::move(list));
return dict;
});
if (httpssvc_metrics_) {
if (transaction_info.type == DnsQueryType::HTTPS) {
bool has_compatible_https = base::ranges::any_of(
results.value(),
[](const std::unique_ptr<HostResolverInternalResult>& result) {
return result->type() ==
HostResolverInternalResult::Type::kMetadata;
});
if (has_compatible_https) {
httpssvc_metrics_->SaveForHttps(rcode_for_httpssvc,
std::vector<bool>{true}, elapsed_time);
} else {
httpssvc_metrics_->SaveForHttps(rcode_for_httpssvc, std::vector<bool>(),
elapsed_time);
}
} else {
httpssvc_metrics_->SaveForAddressQuery(elapsed_time, rcode_for_httpssvc);
}
}
switch (transaction_info.type) {
case DnsQueryType::A:
a_record_end_time_ = now;
if (!aaaa_record_end_time_.is_null()) {
RecordResolveTimeDiff("AAAABeforeA", task_start_time_,
aaaa_record_end_time_, a_record_end_time_);
}
break;
case DnsQueryType::AAAA:
aaaa_record_end_time_ = now;
if (!a_record_end_time_.is_null()) {
RecordResolveTimeDiff("ABeforeAAAA", task_start_time_,
a_record_end_time_, aaaa_record_end_time_);
}
break;
case DnsQueryType::HTTPS: {
base::TimeTicks first_address_end_time =
std::min(a_record_end_time_, aaaa_record_end_time_);
if (!first_address_end_time.is_null()) {
RecordResolveTimeDiff("AddressRecordBeforeHTTPS", task_start_time_,
first_address_end_time, now);
}
break;
}
default:
break;
}
if (base::FeatureList::IsEnabled(features::kUseHostResolverCache) ||
base::FeatureList::IsEnabled(features::kUseServiceEndpointRequest)) {
SortTransactionAndHandleResults(std::move(transaction_info),
std::move(results).value());
} else {
HandleTransactionResults(std::move(transaction_info),
std::move(results).value());
}
}
bool HostResolverDnsTask::IsFatalTransactionFailure(
int transaction_error,
const TransactionInfo& transaction_info,
const DnsResponse* response) {
if (transaction_info.type != DnsQueryType::HTTPS) {
DCHECK(transaction_info.error_behavior !=
TransactionErrorBehavior::kFatalOrEmpty);
return false;
}
// These values are logged to UMA. Entries should not be renumbered and
// numeric values should never be reused. Please keep in sync with
// "DNS.SvcbHttpsTransactionError" in
// src/tools/metrics/histograms/enums.xml.
enum class HttpsTransactionError {
kNoError = 0,
kInsecureError = 1,
kNonFatalError = 2,
kFatalErrorDisabled = 3,
kFatalErrorEnabled = 4,
kMaxValue = kFatalErrorEnabled
} error;
if (transaction_error == OK || (transaction_error == ERR_NAME_NOT_RESOLVED &&
response && response->IsValid())) {
error = HttpsTransactionError::kNoError;
} else if (!secure_) {
// HTTPS failures are never fatal via insecure DNS.
DCHECK(transaction_info.error_behavior !=
TransactionErrorBehavior::kFatalOrEmpty);
error = HttpsTransactionError::kInsecureError;
} else if (transaction_error == ERR_DNS_SERVER_FAILED && response &&
response->rcode() != dns_protocol::kRcodeSERVFAIL) {
// For server failures, only SERVFAIL is fatal.
error = HttpsTransactionError::kNonFatalError;
} else if (features::kUseDnsHttpsSvcbEnforceSecureResponse.Get()) {
DCHECK(transaction_info.error_behavior ==
TransactionErrorBehavior::kFatalOrEmpty);
error = HttpsTransactionError::kFatalErrorEnabled;
} else {
DCHECK(transaction_info.error_behavior !=
TransactionErrorBehavior::kFatalOrEmpty);
error = HttpsTransactionError::kFatalErrorDisabled;
}
UMA_HISTOGRAM_ENUMERATION("Net.DNS.DnsTask.SvcbHttpsTransactionError", error);
return error == HttpsTransactionError::kFatalErrorEnabled;
}
void HostResolverDnsTask::SortTransactionAndHandleResults(
TransactionInfo transaction_info,
Results transaction_results) {
// Expect at most 1 data result in an individual transaction.
CHECK_LE(base::ranges::count_if(
transaction_results,
[](const std::unique_ptr<HostResolverInternalResult>& result) {
return result->type() ==
HostResolverInternalResult::Type::kData;
}),
1);
auto data_result_it = base::ranges::find_if(
transaction_results,
[](const std::unique_ptr<HostResolverInternalResult>& result) {
return result->type() == HostResolverInternalResult::Type::kData;
});
std::vector<IPEndPoint> endpoints_to_sort;
if (data_result_it != transaction_results.end()) {
const HostResolverInternalDataResult& data_result =
(*data_result_it)->AsData();
endpoints_to_sort.insert(endpoints_to_sort.end(),
data_result.endpoints().begin(),
data_result.endpoints().end());
}
if (!endpoints_to_sort.empty()) {
// More async work to do, so insert `transaction_info` back onto
// `transactions_in_progress_`.
auto insertion_result =
transactions_in_progress_.insert(std::move(transaction_info));
CHECK(insertion_result.second);
// Sort() potentially calls OnTransactionSorted() synchronously.
client_->GetAddressSorter()->Sort(
endpoints_to_sort,
base::BindOnce(&HostResolverDnsTask::OnTransactionSorted, AsWeakPtr(),
insertion_result.first, std::move(transaction_results)));
} else {
HandleTransactionResults(std::move(transaction_info),
std::move(transaction_results));
}
}
void HostResolverDnsTask::OnTransactionSorted(
std::set<TransactionInfo>::iterator transaction_info_it,
Results transaction_results,
bool success,
std::vector<IPEndPoint> sorted) {
CHECK(transaction_info_it != transactions_in_progress_.end());
if (transactions_in_progress_.find(*transaction_info_it) ==
transactions_in_progress_.end()) {
// If no longer in `transactions_in_progress_`, transaction was cancelled.
// Do nothing.
return;
}
TransactionInfo transaction_info =
std::move(transactions_in_progress_.extract(transaction_info_it).value());
// Expect exactly one data result.
auto data_result_it = base::ranges::find_if(
transaction_results,
[](const std::unique_ptr<HostResolverInternalResult>& result) {
return result->type() == HostResolverInternalResult::Type::kData;
});
CHECK(data_result_it != transaction_results.end());
DCHECK_EQ(base::ranges::count_if(
transaction_results,
[](const std::unique_ptr<HostResolverInternalResult>& result) {
return result->type() ==
HostResolverInternalResult::Type::kData;
}),
1);
if (!success) {
// If sort failed, replace data result with a TTL-containing error result.
auto error_replacement = std::make_unique<HostResolverInternalErrorResult>(
(*data_result_it)->domain_name(), (*data_result_it)->query_type(),
(*data_result_it)->expiration(), (*data_result_it)->timed_expiration(),
HostResolverInternalResult::Source::kUnknown, ERR_DNS_SORT_ERROR);
CHECK(error_replacement->expiration().has_value());
CHECK(error_replacement->timed_expiration().has_value());
transaction_results.erase(data_result_it);
transaction_results.insert(std::move(error_replacement));
} else if (sorted.empty()) {
// Sorter prunes unusable destinations. If all addresses are pruned,
// remove the data result and replace with TTL-containing error result.
auto error_replacement = std::make_unique<HostResolverInternalErrorResult>(
(*data_result_it)->domain_name(), (*data_result_it)->query_type(),
(*data_result_it)->expiration(), (*data_result_it)->timed_expiration(),
(*data_result_it)->source(), ERR_NAME_NOT_RESOLVED);
CHECK(error_replacement->expiration().has_value());
CHECK(error_replacement->timed_expiration().has_value());
transaction_results.erase(data_result_it);
transaction_results.insert(std::move(error_replacement));
} else {
(*data_result_it)->AsData().set_endpoints(std::move(sorted));
}
HandleTransactionResults(std::move(transaction_info),
std::move(transaction_results));
}
void HostResolverDnsTask::HandleTransactionResults(
TransactionInfo transaction_info,
Results transaction_results) {
CHECK(transactions_in_progress_.find(transaction_info) ==
transactions_in_progress_.end());
if (base::FeatureList::IsEnabled(features::kUseHostResolverCache) &&
resolve_context_->host_resolver_cache() != nullptr) {
for (const std::unique_ptr<HostResolverInternalResult>& result :
transaction_results) {
resolve_context_->host_resolver_cache()->Set(
result->Clone(), anonymization_key_, HostResolverSource::DNS,
secure_);
}
}
// Trigger HTTP->HTTPS upgrade if an HTTPS record is received for an "http"
// or "ws" request.
if (transaction_info.type == DnsQueryType::HTTPS &&
ShouldTriggerHttpToHttpsUpgrade(transaction_results)) {
// Disallow fallback. Otherwise DNS could be reattempted without HTTPS
// queries, and that would hide this error instead of triggering upgrade.
OnFailure(
ERR_DNS_NAME_HTTPS_ONLY, /*allow_fallback=*/false,
HostCache::Entry::TtlFromInternalResults(
transaction_results, base::Time::Now(), tick_clock_->NowTicks()),
transaction_info.type);
return;
}
// Failures other than ERR_NAME_NOT_RESOLVED cannot be merged with other
// transactions.
auto failure_result_it = base::ranges::find_if(
transaction_results,
[](const std::unique_ptr<HostResolverInternalResult>& result) {
return result->type() == HostResolverInternalResult::Type::kError;
});
DCHECK_LE(base::ranges::count_if(
transaction_results,
[](const std::unique_ptr<HostResolverInternalResult>& result) {
return result->type() ==
HostResolverInternalResult::Type::kError;
}),
1);
if (failure_result_it != transaction_results.end() &&
(*failure_result_it)->AsError().error() != ERR_NAME_NOT_RESOLVED) {
OnFailure(
(*failure_result_it)->AsError().error(), /*allow_fallback=*/true,
HostCache::Entry::TtlFromInternalResults(
transaction_results, base::Time::Now(), tick_clock_->NowTicks()),
transaction_info.type);
return;
}
// TODO(crbug.com/40245250): Use new results type directly instead of
// converting to HostCache::Entry.
HostCache::Entry legacy_results(transaction_results, base::Time::Now(),
tick_clock_->NowTicks(),
HostCache::Entry::SOURCE_DNS);
// Merge results with saved results from previous transactions.
if (saved_results_) {
// If saved result is a deferred failure, try again to complete with that
// failure.
if (saved_results_is_failure_) {
OnFailure(saved_results_.value().error(), /*allow_fallback=*/true,
saved_results_.value().GetOptionalTtl());
return;
}
switch (transaction_info.type) {
case DnsQueryType::A:
// Canonical names from A results have lower priority than those
// from AAAA results, so merge to the back.
legacy_results = HostCache::Entry::MergeEntries(
std::move(saved_results_).value(), std::move(legacy_results));
break;
case DnsQueryType::AAAA:
// Canonical names from AAAA results take priority over those
// from A results, so merge to the front.
legacy_results = HostCache::Entry::MergeEntries(
std::move(legacy_results), std::move(saved_results_).value());
break;
case DnsQueryType::HTTPS:
// No particular importance to order.
legacy_results = HostCache::Entry::MergeEntries(
std::move(legacy_results), std::move(saved_results_).value());
break;
default:
// Only expect address query types with multiple transactions.
NOTREACHED();
}
}
saved_results_ = std::move(legacy_results);
OnTransactionsFinished(SingleTransactionResults(
transaction_info.type, std::move(transaction_results)));
}
void HostResolverDnsTask::OnTransactionsFinished(
std::optional<SingleTransactionResults> single_transaction_results) {
if (!transactions_in_progress_.empty() || !transactions_needed_.empty()) {
MaybeStartTimeoutTimer();
delegate_->OnIntermediateTransactionsComplete(
std::move(single_transaction_results));
// `this` may be deleted by `delegate_`. Do not add code below.
return;
}
DCHECK(saved_results_.has_value());
HostCache::Entry results = std::move(*saved_results_);
timeout_timer_.Stop();
// If using HostResolverCache, transactions are already invidvidually sorted
// on completion.
if (!base::FeatureList::IsEnabled(features::kUseHostResolverCache)) {
std::vector<IPEndPoint> ip_endpoints = results.ip_endpoints();
// If there are multiple addresses, and at least one is IPv6, need to
// sort them.
bool at_least_one_ipv6_address = base::ranges::any_of(
ip_endpoints,
[](auto& e) { return e.GetFamily() == ADDRESS_FAMILY_IPV6; });
if (at_least_one_ipv6_address) {
// Sort addresses if needed. Sort could complete synchronously.
client_->GetAddressSorter()->Sort(
ip_endpoints,
base::BindOnce(&HostResolverDnsTask::OnSortComplete, AsWeakPtr(),
tick_clock_->NowTicks(), std::move(results), secure_));
return;
}
}
OnSuccess(std::move(results));
}
void HostResolverDnsTask::OnSortComplete(base::TimeTicks sort_start_time,
HostCache::Entry results,
bool secure,
bool success,
std::vector<IPEndPoint> sorted) {
results.set_ip_endpoints(std::move(sorted));
if (!success) {
OnFailure(ERR_DNS_SORT_ERROR, /*allow_fallback=*/true,
results.GetOptionalTtl());
return;
}
// AddressSorter prunes unusable destinations.
if (results.ip_endpoints().empty() && results.text_records().empty() &&
results.hostnames().empty()) {
LOG(WARNING) << "Address list empty after RFC3484 sort";
OnFailure(ERR_NAME_NOT_RESOLVED, /*allow_fallback=*/true,
results.GetOptionalTtl());
return;
}
OnSuccess(std::move(results));
}
bool HostResolverDnsTask::AnyPotentiallyFatalTransactionsRemain() {
auto is_fatal_or_empty_error = [](TransactionErrorBehavior behavior) {
return behavior == TransactionErrorBehavior::kFatalOrEmpty;
};
return base::ranges::any_of(transactions_needed_, is_fatal_or_empty_error,
&TransactionInfo::error_behavior) ||
base::ranges::any_of(transactions_in_progress_,
is_fatal_or_empty_error,
&TransactionInfo::error_behavior);
}
void HostResolverDnsTask::CancelNonFatalTransactions() {
auto has_non_fatal_or_empty_error = [](const TransactionInfo& info) {
return info.error_behavior != TransactionErrorBehavior::kFatalOrEmpty;
};
base::EraseIf(transactions_needed_, has_non_fatal_or_empty_error);
std::erase_if(transactions_in_progress_, has_non_fatal_or_empty_error);
}
void HostResolverDnsTask::OnFailure(
int net_error,
bool allow_fallback,
std::optional<base::TimeDelta> ttl,
std::optional<DnsQueryType> failed_transaction_type) {
if (httpssvc_metrics_ && failed_transaction_type.has_value() &&
IsAddressType(failed_transaction_type.value())) {
httpssvc_metrics_->SaveAddressQueryFailure();
}
DCHECK_NE(OK, net_error);
HostCache::Entry results(net_error, HostCache::Entry::SOURCE_UNKNOWN, ttl);
// On non-fatal errors, if any potentially fatal transactions remain, need
// to defer ending the task in case any of those remaining transactions end
// with a fatal failure.
if (allow_fallback && AnyPotentiallyFatalTransactionsRemain()) {
saved_results_ = std::move(results);
saved_results_is_failure_ = true;
CancelNonFatalTransactions();
OnTransactionsFinished(/*single_transaction_results=*/std::nullopt);
return;
}
net_log_.EndEvent(NetLogEventType::HOST_RESOLVER_DNS_TASK, [&] {
return NetLogDnsTaskFailedParams(net_error, failed_transaction_type, ttl,
base::OptionalToPtr(saved_results_));
});
// Expect this to result in destroying `this` and thus cancelling any
// remaining transactions.
delegate_->OnDnsTaskComplete(task_start_time_, allow_fallback,
std::move(results), secure_);
}
void HostResolverDnsTask::OnSuccess(HostCache::Entry results) {
net_log_.EndEvent(NetLogEventType::HOST_RESOLVER_DNS_TASK,
[&] { return NetLogResults(results); });
delegate_->OnDnsTaskComplete(task_start_time_, /*allow_fallback=*/true,
std::move(results), secure_);
}
bool HostResolverDnsTask::AnyOfTypeTransactionsRemain(
std::initializer_list<DnsQueryType> types) const {
// Should only be called if some transactions are still running or waiting
// to run.
DCHECK(!transactions_needed_.empty() || !transactions_in_progress_.empty());
// Check running transactions.
if (base::ranges::find_first_of(transactions_in_progress_, types,
/*pred=*/{},
/*proj1=*/&TransactionInfo::type) !=
transactions_in_progress_.end()) {
return true;
}
// Check queued transactions, in case it ever becomes possible to get here
// without the transactions being started first.
return base::ranges::find_first_of(transactions_needed_, types, /*pred=*/{},
/*proj1=*/&TransactionInfo::type) !=
transactions_needed_.end();
}
void HostResolverDnsTask::MaybeStartTimeoutTimer() {
// Should only be called if some transactions are still running or waiting
// to run.
DCHECK(!transactions_in_progress_.empty() || !transactions_needed_.empty());
// Timer already running.
if (timeout_timer_.IsRunning()) {
return;
}
// Always wait for address transactions.
if (AnyOfTypeTransactionsRemain({DnsQueryType::A, DnsQueryType::AAAA})) {
return;
}
base::TimeDelta timeout_max;
int extra_time_percent = 0;
base::TimeDelta timeout_min;
if (AnyOfTypeTransactionsRemain({DnsQueryType::HTTPS})) {
DCHECK(https_svcb_options_.enable);
if (secure_) {
timeout_max = https_svcb_options_.secure_extra_time_max;
extra_time_percent = https_svcb_options_.secure_extra_time_percent;
timeout_min = https_svcb_options_.secure_extra_time_min;
} else {
timeout_max = https_svcb_options_.insecure_extra_time_max;
extra_time_percent = https_svcb_options_.insecure_extra_time_percent;
timeout_min = https_svcb_options_.insecure_extra_time_min;
}
// Skip timeout for secure requests if the timeout would be a fatal
// failure.
if (secure_ && features::kUseDnsHttpsSvcbEnforceSecureResponse.Get()) {
timeout_max = base::TimeDelta();
extra_time_percent = 0;
timeout_min = base::TimeDelta();
}
} else {
// Unhandled supplemental type.
NOTREACHED();
}
base::TimeDelta timeout;
if (extra_time_percent > 0) {
base::TimeDelta total_time_for_other_transactions =
tick_clock_->NowTicks() - task_start_time_;
timeout = total_time_for_other_transactions * extra_time_percent / 100;
// Use at least 1ms to ensure timeout doesn't occur immediately in tests.
timeout = std::max(timeout, base::Milliseconds(1));
if (!timeout_max.is_zero()) {
timeout = std::min(timeout, timeout_max);
}
if (!timeout_min.is_zero()) {
timeout = std::max(timeout, timeout_min);
}
} else {
// If no relative timeout, use a non-zero min/max as timeout. If both are
// non-zero, that's not very sensible, but arbitrarily take the higher
// timeout.
timeout = std::max(timeout_min, timeout_max);
}
if (!timeout.is_zero()) {
timeout_timer_.Start(FROM_HERE, timeout,
base::BindOnce(&HostResolverDnsTask::OnTimeout,
base::Unretained(this)));
}
}
bool HostResolverDnsTask::ShouldTriggerHttpToHttpsUpgrade(
const Results& results) {
// Upgrade if at least one HTTPS record was compatible, and the host uses an
// upgradable scheme.
if (!host_.HasScheme()) {
return false;
}
const std::string& scheme = host_.GetScheme();
if (scheme != url::kHttpScheme && scheme != url::kWsScheme) {
return false;
}
return base::ranges::any_of(
results, [](const std::unique_ptr<HostResolverInternalResult>& result) {
return result->type() == HostResolverInternalResult::Type::kMetadata;
});
}
} // namespace net