blob: 323f851e2735f4049c277b5b079c239932715561 [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/dns_task_results_manager.h"
#include <algorithm>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <vector>
#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "net/base/connection_endpoint_metadata.h"
#include "net/base/ip_endpoint.h"
#include "net/base/net_errors.h"
#include "net/dns/host_resolver.h"
#include "net/dns/host_resolver_dns_task.h"
#include "net/dns/host_resolver_internal_result.h"
#include "net/dns/https_record_rdata.h"
#include "net/dns/public/dns_query_type.h"
#include "net/dns/public/host_resolver_results.h"
#include "net/log/net_log_event_type.h"
#include "net/log/net_log_with_source.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "url/scheme_host_port.h"
namespace net {
namespace {
// Prioritize with-ipv6 over ipv4-only.
bool CompareServiceEndpointAddresses(const ServiceEndpoint& a,
const ServiceEndpoint& b) {
const bool a_has_ipv6 = !a.ipv6_endpoints.empty();
const bool b_has_ipv6 = !b.ipv6_endpoints.empty();
if ((a_has_ipv6 && b_has_ipv6) || (!a_has_ipv6 && !b_has_ipv6)) {
return false;
}
if (b_has_ipv6) {
return false;
}
return true;
}
// Prioritize with-metadata, with-ipv6 over ipv4-only.
// TODO(crbug.com/41493696): Consider which fields should be prioritized. We
// may want to have different sorting algorithms and choose one via config.
bool CompareServiceEndpoint(const ServiceEndpoint& a,
const ServiceEndpoint& b) {
const bool a_has_metadata = a.metadata != ConnectionEndpointMetadata();
const bool b_has_metadata = b.metadata != ConnectionEndpointMetadata();
if (a_has_metadata && b_has_metadata) {
return CompareServiceEndpointAddresses(a, b);
}
if (a_has_metadata) {
return true;
}
if (b_has_metadata) {
return false;
}
return CompareServiceEndpointAddresses(a, b);
}
} // namespace
// Holds service endpoint results per domain name.
struct DnsTaskResultsManager::PerDomainResult {
PerDomainResult() = default;
~PerDomainResult() = default;
PerDomainResult(PerDomainResult&&) = default;
PerDomainResult& operator=(PerDomainResult&&) = default;
PerDomainResult(const PerDomainResult&) = delete;
PerDomainResult& operator=(const PerDomainResult&) = delete;
std::vector<IPEndPoint> ipv4_endpoints;
std::vector<IPEndPoint> ipv6_endpoints;
std::multimap<HttpsRecordPriority, ConnectionEndpointMetadata> metadatas;
};
DnsTaskResultsManager::DnsTaskResultsManager(Delegate* delegate,
HostResolver::Host host,
DnsQueryTypeSet query_types,
const NetLogWithSource& net_log)
: delegate_(delegate),
host_(std::move(host)),
query_types_(query_types),
net_log_(net_log) {
CHECK(delegate_);
}
DnsTaskResultsManager::~DnsTaskResultsManager() = default;
void DnsTaskResultsManager::ProcessDnsTransactionResults(
DnsQueryType query_type,
const std::set<std::unique_ptr<HostResolverInternalResult>>& results) {
CHECK(query_types_.Has(query_type));
bool should_update_endpoints = false;
bool should_notify = false;
if (query_type == DnsQueryType::HTTPS) {
// Chrome does not yet support HTTPS follow-up queries so metadata is
// considered ready when the HTTPS response is received.
CHECK(!is_metadata_ready_);
is_metadata_ready_ = true;
should_notify = true;
}
if (query_type == DnsQueryType::AAAA) {
aaaa_response_received_ = true;
if (resolution_delay_timer_.IsRunning()) {
resolution_delay_timer_.Stop();
RecordResolutionDelayResult(/*timedout=*/false);
// Need to update endpoints when there are IPv4 addresses.
if (HasIpv4Addresses()) {
should_update_endpoints = true;
}
}
}
for (auto& result : results) {
aliases_.insert(result->domain_name());
switch (result->type()) {
case HostResolverInternalResult::Type::kData: {
PerDomainResult& per_domain_result =
GetOrCreatePerDomainResult(result->domain_name());
if (query_type == DnsQueryType::A) {
for (const auto& ip_endpoint : result->AsData().endpoints()) {
CHECK(ip_endpoint.address().IsIPv4());
CHECK_EQ(ip_endpoint.port(), 0);
per_domain_result.ipv4_endpoints.emplace_back(ip_endpoint.address(),
host_.GetPort());
}
} else if (query_type == DnsQueryType::AAAA) {
for (const auto& ip_endpoint : result->AsData().endpoints()) {
CHECK(ip_endpoint.address().IsIPv6());
CHECK_EQ(ip_endpoint.port(), 0);
per_domain_result.ipv6_endpoints.emplace_back(ip_endpoint.address(),
host_.GetPort());
}
} else {
// TODO(crbug.com/41493696): This will eventually need to handle
// DnsQueryType::HTTPS to support getting ipv{4,6}hints.
NOTREACHED() << "Unexpected query type: "
<< kDnsQueryTypes.at(query_type);
}
should_update_endpoints |= !result->AsData().endpoints().empty();
break;
}
case HostResolverInternalResult::Type::kMetadata: {
CHECK_EQ(query_type, DnsQueryType::HTTPS);
for (auto [priority, metadata] : result->AsMetadata().metadatas()) {
// Associate the metadata with the target name instead of the domain
// name since the metadata is for the target name.
PerDomainResult& per_domain_result =
GetOrCreatePerDomainResult(metadata.target_name);
per_domain_result.metadatas.emplace(priority, metadata);
}
should_update_endpoints |= !result->AsMetadata().metadatas().empty();
break;
}
case net::HostResolverInternalResult::Type::kAlias:
aliases_.insert(result->AsAlias().alias_target());
break;
case net::HostResolverInternalResult::Type::kError:
// Need to update endpoints when AAAA response is NODATA but A response
// has at least one valid address.
// TODO(crbug.com/41493696): Revisit how to handle errors other than
// NODATA. Currently we just ignore errors here and defer
// HostResolverManager::Job to create an error result and notify the
// error to the corresponding requests. This means that if the
// connection layer has already attempted a connection using an
// intermediate endpoint, the error might not be treated as fatal. We
// may want to have a different semantics.
PerDomainResult& per_domain_result =
GetOrCreatePerDomainResult(result->domain_name());
if (query_type == DnsQueryType::AAAA &&
result->AsError().error() == ERR_NAME_NOT_RESOLVED &&
!per_domain_result.ipv4_endpoints.empty()) {
CHECK(per_domain_result.ipv6_endpoints.empty());
should_update_endpoints = true;
}
break;
}
}
const bool waiting_for_aaaa_response =
query_types_.Has(DnsQueryType::AAAA) && !aaaa_response_received_;
if (waiting_for_aaaa_response) {
if (query_type == DnsQueryType::A && should_update_endpoints) {
// A is responded, start the resolution delay timer.
CHECK(!resolution_delay_timer_.IsRunning());
resolution_delay_start_time_ = base::TimeTicks::Now();
net_log_.BeginEvent(
NetLogEventType::HOST_RESOLVER_SERVICE_ENDPOINTS_RESOLUTION_DELAY);
// Safe to unretain since `this` owns the timer.
resolution_delay_timer_.Start(
FROM_HERE, kResolutionDelay,
base::BindOnce(&DnsTaskResultsManager::OnAaaaResolutionTimedout,
base::Unretained(this)));
}
return;
}
if (should_update_endpoints) {
UpdateEndpoints();
return;
}
if (should_notify && !current_endpoints_.empty()) {
delegate_->OnServiceEndpointsUpdated();
}
}
const std::vector<ServiceEndpoint>& DnsTaskResultsManager::GetCurrentEndpoints()
const {
return current_endpoints_;
}
const std::set<std::string>& DnsTaskResultsManager::GetAliases() const {
return aliases_;
}
bool DnsTaskResultsManager::IsMetadataReady() const {
return !query_types_.Has(DnsQueryType::HTTPS) || is_metadata_ready_;
}
DnsTaskResultsManager::PerDomainResult&
DnsTaskResultsManager::GetOrCreatePerDomainResult(
const std::string& domain_name) {
auto it = per_domain_results_.find(domain_name);
if (it == per_domain_results_.end()) {
it = per_domain_results_.try_emplace(it, domain_name,
std::make_unique<PerDomainResult>());
}
return *it->second;
}
void DnsTaskResultsManager::OnAaaaResolutionTimedout() {
CHECK(!aaaa_response_received_);
RecordResolutionDelayResult(/*timedout=*/true);
UpdateEndpoints();
}
void DnsTaskResultsManager::UpdateEndpoints() {
std::vector<ServiceEndpoint> new_endpoints;
for (const auto& [domain_name, per_domain_result] : per_domain_results_) {
if (per_domain_result->ipv4_endpoints.empty() &&
per_domain_result->ipv6_endpoints.empty()) {
continue;
}
if (per_domain_result->metadatas.empty()) {
ServiceEndpoint endpoint;
endpoint.ipv4_endpoints = per_domain_result->ipv4_endpoints;
endpoint.ipv6_endpoints = per_domain_result->ipv6_endpoints;
new_endpoints.emplace_back(std::move(endpoint));
} else {
for (const auto& [_, metadata] : per_domain_result->metadatas) {
ServiceEndpoint endpoint;
endpoint.ipv4_endpoints = per_domain_result->ipv4_endpoints;
endpoint.ipv6_endpoints = per_domain_result->ipv6_endpoints;
// TODO(crbug.com/41493696): Just adding per-domain metadata does not
// work properly when the target name of HTTPS is an alias, e.g:
// example.com. 60 IN CNAME svc.example.com.
// svc.example.com. 60 IN AAAA 2001:db8::1
// svc.example.com. 60 IN HTTPS 1 example.com alpn="h2"
// In this case, svc.example.com should have metadata with alpn="h2" but
// the current logic doesn't do that. To handle it correctly we need to
// go though an alias tree for the domain name.
endpoint.metadata = metadata;
new_endpoints.emplace_back(std::move(endpoint));
}
}
}
// TODO(crbug.com/41493696): Determine how to handle non-SVCB connection
// fallback. See https://datatracker.ietf.org/doc/html/rfc9460#section-3-8
// HostCache::Entry::GetEndpoints() appends a final non-alternative endpoint
// at the end to ensure that the connection layer can fall back to non-SVCB
// connection. For ServiceEndpoint request API, the current plan is to handle
// non-SVCB connection fallback in the connection layer. The approach might
// not work when Chrome tries to support HTTPS follow-up queries and aliases.
// Stable sort preserves metadata priorities.
std::stable_sort(new_endpoints.begin(), new_endpoints.end(),
CompareServiceEndpoint);
current_endpoints_ = std::move(new_endpoints);
if (current_endpoints_.empty()) {
return;
}
net_log_.AddEvent(NetLogEventType::HOST_RESOLVER_SERVICE_ENDPOINTS_UPDATED,
[&] {
base::Value::Dict dict;
base::Value::List endpoints;
for (const auto& endpoint : current_endpoints_) {
endpoints.Append(endpoint.ToValue());
}
dict.Set("endpoints", std::move(endpoints));
return dict;
});
delegate_->OnServiceEndpointsUpdated();
}
bool DnsTaskResultsManager::HasIpv4Addresses() {
for (const auto& [_, per_domain_result] : per_domain_results_) {
if (!per_domain_result->ipv4_endpoints.empty()) {
return true;
}
}
return false;
}
void DnsTaskResultsManager::RecordResolutionDelayResult(bool timedout) {
net_log_.EndEvent(
NetLogEventType::HOST_RESOLVER_SERVICE_ENDPOINTS_RESOLUTION_DELAY, [&]() {
base::TimeDelta elapsed =
base::TimeTicks::Now() - resolution_delay_start_time_;
base::Value::Dict dict;
dict.Set("timedout", timedout);
dict.Set("elapsed", base::NumberToString(elapsed.InMilliseconds()));
return dict;
});
}
} // namespace net