blob: 091bdc7649198398702df35524422f00e9f0ee9d [file] [log] [blame] [edit]
// 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/dns_response_result_extractor.h"
#include <limits.h>
#include <stdint.h>
#include <algorithm>
#include <iterator>
#include <map>
#include <memory>
#include <optional>
#include <ostream>
#include <set>
#include <string>
#include <string_view>
#include <unordered_set>
#include <vector>
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/containers/to_vector.h"
#include "base/dcheck_is_on.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/numerics/checked_math.h"
#include "base/numerics/ostream_operators.h"
#include "base/rand_util.h"
#include "base/strings/string_util.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "net/base/address_list.h"
#include "net/base/connection_endpoint_metadata.h"
#include "net/base/host_port_pair.h"
#include "net/base/ip_address.h"
#include "net/base/ip_endpoint.h"
#include "net/base/net_errors.h"
#include "net/dns/dns_alias_utility.h"
#include "net/dns/dns_names_util.h"
#include "net/dns/dns_response.h"
#include "net/dns/dns_util.h"
#include "net/dns/host_cache.h"
#include "net/dns/host_resolver_internal_result.h"
#include "net/dns/https_record_rdata.h"
#include "net/dns/public/dns_protocol.h"
#include "net/dns/public/dns_query_type.h"
#include "net/dns/record_parsed.h"
#include "net/dns/record_rdata.h"
namespace net {
namespace {
using AliasMap = std::map<std::string,
std::unique_ptr<const RecordParsed>,
dns_names_util::DomainNameComparator>;
using ExtractionError = DnsResponseResultExtractor::ExtractionError;
using RecordsOrError =
base::expected<std::vector<std::unique_ptr<const RecordParsed>>,
ExtractionError>;
using ResultsOrError = DnsResponseResultExtractor::ResultsOrError;
using Source = HostResolverInternalResult::Source;
void SaveMetricsForAdditionalHttpsRecord(const RecordParsed& record,
bool is_unsolicited) {
const HttpsRecordRdata* rdata = record.rdata<HttpsRecordRdata>();
DCHECK(rdata);
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class UnsolicitedHttpsRecordStatus {
kMalformed = 0, // No longer recorded.
kAlias = 1,
kService = 2,
kMaxValue = kService
} status;
if (rdata->IsAlias()) {
status = UnsolicitedHttpsRecordStatus::kAlias;
} else {
status = UnsolicitedHttpsRecordStatus::kService;
}
if (is_unsolicited) {
UMA_HISTOGRAM_ENUMERATION("Net.DNS.DnsTask.AdditionalHttps.Unsolicited",
status);
} else {
UMA_HISTOGRAM_ENUMERATION("Net.DNS.DnsTask.AdditionalHttps.Requested",
status);
}
}
// Sort service targets per RFC2782. In summary, sort first by `priority`,
// lowest first. For targets with the same priority, secondary sort randomly
// using `weight` with higher weighted objects more likely to go first.
std::vector<HostPortPair> SortServiceTargets(
const std::vector<const SrvRecordRdata*>& rdatas) {
std::map<uint16_t, std::unordered_set<const SrvRecordRdata*>>
ordered_by_priority;
for (const SrvRecordRdata* rdata : rdatas) {
ordered_by_priority[rdata->priority()].insert(rdata);
}
std::vector<HostPortPair> sorted_targets;
for (auto& priority : ordered_by_priority) {
// With (num results) <= UINT16_MAX (and in practice, much less) and
// (weight per result) <= UINT16_MAX, then it should be the case that
// (total weight) <= UINT32_MAX, but use CheckedNumeric for extra safety.
auto total_weight = base::MakeCheckedNum<uint32_t>(0);
for (const SrvRecordRdata* rdata : priority.second) {
total_weight += rdata->weight();
}
// Add 1 to total weight because, to deal with 0-weight targets, we want
// our random selection to be inclusive [0, total].
total_weight++;
// Order by weighted random. Make such random selections, removing from
// |priority.second| until |priority.second| only contains 1 rdata.
while (priority.second.size() >= 2) {
uint32_t random_selection =
base::RandGenerator(total_weight.ValueOrDie());
const SrvRecordRdata* selected_rdata = nullptr;
for (const SrvRecordRdata* rdata : priority.second) {
// >= to always select the first target on |random_selection| == 0,
// even if its weight is 0.
if (rdata->weight() >= random_selection) {
selected_rdata = rdata;
break;
}
random_selection -= rdata->weight();
}
DCHECK(selected_rdata);
sorted_targets.emplace_back(selected_rdata->target(),
selected_rdata->port());
total_weight -= selected_rdata->weight();
size_t removed = priority.second.erase(selected_rdata);
DCHECK_EQ(1u, removed);
}
DCHECK_EQ(1u, priority.second.size());
DCHECK_EQ((total_weight - 1).ValueOrDie(),
(*priority.second.begin())->weight());
const SrvRecordRdata* rdata = *priority.second.begin();
sorted_targets.emplace_back(rdata->target(), rdata->port());
}
return sorted_targets;
}
// Validates that all `aliases` form a single non-looping chain, starting from
// `query_name` and that all alias records are valid. Also validates that all
// `data_records` are at the final name at the end of the alias chain.
// TODO(crbug.com/40245250): Consider altering chain TTLs so that each TTL is
// less than or equal to all previous links in the chain.
ExtractionError ValidateNamesAndAliases(
std::string_view query_name,
const AliasMap& aliases,
const std::vector<std::unique_ptr<const RecordParsed>>& data_records,
std::string& out_final_chain_name) {
// Validate that all aliases form a single non-looping chain, starting from
// `query_name`.
size_t aliases_in_chain = 0;
std::string target_name =
dns_names_util::UrlCanonicalizeNameIfAble(query_name);
for (auto alias = aliases.find(target_name);
alias != aliases.end() && aliases_in_chain <= aliases.size();
alias = aliases.find(target_name)) {
aliases_in_chain++;
const CnameRecordRdata* cname_data =
alias->second->rdata<CnameRecordRdata>();
if (!cname_data) {
return ExtractionError::kMalformedCname;
}
target_name =
dns_names_util::UrlCanonicalizeNameIfAble(cname_data->cname());
if (!dns_names_util::IsValidDnsRecordName(target_name)) {
return ExtractionError::kMalformedCname;
}
}
if (aliases_in_chain != aliases.size()) {
return ExtractionError::kBadAliasChain;
}
// All records must match final alias name.
for (const auto& record : data_records) {
DCHECK_NE(record->type(), dns_protocol::kTypeCNAME);
if (!base::EqualsCaseInsensitiveASCII(
target_name,
dns_names_util::UrlCanonicalizeNameIfAble(record->name()))) {
return ExtractionError::kNameMismatch;
}
}
out_final_chain_name = std::move(target_name);
return ExtractionError::kOk;
}
// Common results (aliases and errors) are extracted into
// `out_non_data_results`.
RecordsOrError ExtractResponseRecords(
const DnsResponse& response,
DnsQueryType query_type,
base::Time now,
base::TimeTicks now_ticks,
std::set<std::unique_ptr<HostResolverInternalResult>>&
out_non_data_results) {
DCHECK_EQ(response.question_count(), 1u);
std::vector<std::unique_ptr<const RecordParsed>> data_records;
std::optional<base::TimeDelta> response_ttl;
DnsRecordParser parser = response.Parser();
// Expected to be validated by DnsTransaction.
DCHECK_EQ(DnsQueryTypeToQtype(query_type), response.GetSingleQType());
AliasMap aliases;
for (unsigned i = 0; i < response.answer_count(); ++i) {
std::unique_ptr<const RecordParsed> record =
RecordParsed::CreateFrom(&parser, now);
if (!record || !dns_names_util::IsValidDnsRecordName(record->name())) {
return base::unexpected(ExtractionError::kMalformedRecord);
}
if (record->klass() == dns_protocol::kClassIN &&
record->type() == dns_protocol::kTypeCNAME) {
std::string canonicalized_name =
dns_names_util::UrlCanonicalizeNameIfAble(record->name());
DCHECK(dns_names_util::IsValidDnsRecordName(canonicalized_name));
bool added =
aliases.emplace(canonicalized_name, std::move(record)).second;
// Per RFC2181, multiple CNAME records are not allowed for the same name.
if (!added) {
return base::unexpected(ExtractionError::kMultipleCnames);
}
} else if (record->klass() == dns_protocol::kClassIN &&
record->type() == DnsQueryTypeToQtype(query_type)) {
base::TimeDelta ttl = base::Seconds(record->ttl());
response_ttl =
std::min(response_ttl.value_or(base::TimeDelta::Max()), ttl);
data_records.push_back(std::move(record));
}
}
std::string final_chain_name;
ExtractionError name_and_alias_validation_error = ValidateNamesAndAliases(
response.GetSingleDottedName(), aliases, data_records, final_chain_name);
bool has_extraction_error =
name_and_alias_validation_error != ExtractionError::kOk;
if (query_type == DnsQueryType::A || query_type == DnsQueryType::AAAA) {
UMA_HISTOGRAM_BOOLEAN(
DnsResponseResultExtractor::kHasValidCnameRecordsHistogram,
!has_extraction_error && !aliases.empty());
}
if (has_extraction_error) {
return base::unexpected(name_and_alias_validation_error);
}
std::set<std::unique_ptr<HostResolverInternalResult>> non_data_results;
for (const auto& alias : aliases) {
DCHECK(alias.second->rdata<CnameRecordRdata>());
non_data_results.insert(std::make_unique<HostResolverInternalAliasResult>(
alias.first, query_type, now_ticks + base::Seconds(alias.second->ttl()),
now + base::Seconds(alias.second->ttl()), Source::kDns,
alias.second->rdata<CnameRecordRdata>()->cname()));
}
std::optional<base::TimeDelta> error_ttl;
for (unsigned i = 0; i < response.authority_count(); ++i) {
DnsResourceRecord record;
if (!parser.ReadRecord(&record)) {
// Stop trying to process records if things get malformed in the authority
// section.
break;
}
if (record.type == dns_protocol::kTypeSOA) {
base::TimeDelta ttl = base::Seconds(record.ttl);
error_ttl = std::min(error_ttl.value_or(base::TimeDelta::Max()), ttl);
}
}
// For NXDOMAIN or NODATA (NOERROR with 0 answers matching the qtype), cache
// an error if an error TTL was found from SOA records. Also, ignore the error
// if we somehow have result records (most likely if the server incorrectly
// sends NXDOMAIN with results). Note that, per the weird QNAME definition in
// RFC2308, section 1, as well as the clarifications in RFC6604, section 3,
// and in RFC8020, section 2, the cached error is specific to the final chain
// name, not the query name.
//
// TODO(ericorth@chromium.org): Differentiate nxdomain errors by making it
// cacheable across any query type (per RFC2308, Section 5).
bool is_cachable_error = data_records.empty() &&
(response.rcode() == dns_protocol::kRcodeNXDOMAIN ||
response.rcode() == dns_protocol::kRcodeNOERROR);
if (is_cachable_error && error_ttl.has_value()) {
non_data_results.insert(std::make_unique<HostResolverInternalErrorResult>(
final_chain_name, query_type, now_ticks + error_ttl.value(),
now + error_ttl.value(), Source::kDns, ERR_NAME_NOT_RESOLVED));
}
for (unsigned i = 0; i < response.additional_answer_count(); ++i) {
std::unique_ptr<const RecordParsed> record =
RecordParsed::CreateFrom(&parser, base::Time::Now());
if (record && record->klass() == dns_protocol::kClassIN &&
record->type() == dns_protocol::kTypeHttps) {
bool is_unsolicited = query_type != DnsQueryType::HTTPS;
SaveMetricsForAdditionalHttpsRecord(*record, is_unsolicited);
}
}
out_non_data_results = std::move(non_data_results);
return data_records;
}
ResultsOrError ExtractAddressResults(const DnsResponse& response,
DnsQueryType query_type,
base::Time now,
base::TimeTicks now_ticks) {
DCHECK_EQ(response.question_count(), 1u);
DCHECK(query_type == DnsQueryType::A || query_type == DnsQueryType::AAAA);
std::set<std::unique_ptr<HostResolverInternalResult>> results;
RecordsOrError records =
ExtractResponseRecords(response, query_type, now, now_ticks, results);
if (!records.has_value()) {
return base::unexpected(records.error());
}
std::vector<IPEndPoint> ip_endpoints;
auto min_ttl = base::TimeDelta::Max();
for (const auto& record : records.value()) {
IPAddress address;
if (query_type == DnsQueryType::A) {
const ARecordRdata* rdata = record->rdata<ARecordRdata>();
DCHECK(rdata);
address = rdata->address();
DCHECK(address.IsIPv4());
} else {
DCHECK_EQ(query_type, DnsQueryType::AAAA);
const AAAARecordRdata* rdata = record->rdata<AAAARecordRdata>();
DCHECK(rdata);
address = rdata->address();
DCHECK(address.IsIPv6());
}
ip_endpoints.emplace_back(address, /*port=*/0);
base::TimeDelta ttl = base::Seconds(record->ttl());
min_ttl = std::min(ttl, min_ttl);
}
if (!ip_endpoints.empty()) {
results.insert(std::make_unique<HostResolverInternalDataResult>(
records->front()->name(), query_type, now_ticks + min_ttl,
now + min_ttl, Source::kDns, std::move(ip_endpoints),
std::vector<std::string>{}, std::vector<HostPortPair>{}));
}
return results;
}
ResultsOrError ExtractTxtResults(const DnsResponse& response,
base::Time now,
base::TimeTicks now_ticks) {
std::set<std::unique_ptr<HostResolverInternalResult>> results;
RecordsOrError txt_records = ExtractResponseRecords(
response, DnsQueryType::TXT, now, now_ticks, results);
if (!txt_records.has_value()) {
return base::unexpected(txt_records.error());
}
std::vector<std::string> strings;
base::TimeDelta min_ttl = base::TimeDelta::Max();
for (const auto& record : txt_records.value()) {
const TxtRecordRdata* rdata = record->rdata<net::TxtRecordRdata>();
DCHECK(rdata);
// TXT invalid without at least one string. If none, should be rejected by
// parser.
CHECK(!rdata->texts().empty());
strings.insert(strings.end(), rdata->texts().begin(), rdata->texts().end());
base::TimeDelta ttl = base::Seconds(record->ttl());
min_ttl = std::min(ttl, min_ttl);
}
if (!strings.empty()) {
results.insert(std::make_unique<HostResolverInternalDataResult>(
txt_records->front()->name(), DnsQueryType::TXT, now_ticks + min_ttl,
now + min_ttl, Source::kDns, std::vector<IPEndPoint>{},
std::move(strings), std::vector<HostPortPair>{}));
}
return results;
}
ResultsOrError ExtractPointerResults(const DnsResponse& response,
base::Time now,
base::TimeTicks now_ticks) {
std::set<std::unique_ptr<HostResolverInternalResult>> results;
RecordsOrError ptr_records = ExtractResponseRecords(
response, DnsQueryType::PTR, now, now_ticks, results);
if (!ptr_records.has_value()) {
return base::unexpected(ptr_records.error());
}
std::vector<HostPortPair> pointers;
auto min_ttl = base::TimeDelta::Max();
for (const auto& record : ptr_records.value()) {
const PtrRecordRdata* rdata = record->rdata<net::PtrRecordRdata>();
DCHECK(rdata);
std::string pointer = rdata->ptrdomain();
// Skip pointers to the root domain.
if (!pointer.empty()) {
pointers.emplace_back(std::move(pointer), 0);
base::TimeDelta ttl = base::Seconds(record->ttl());
min_ttl = std::min(ttl, min_ttl);
}
}
if (!pointers.empty()) {
results.insert(std::make_unique<HostResolverInternalDataResult>(
ptr_records->front()->name(), DnsQueryType::PTR, now_ticks + min_ttl,
now + min_ttl, Source::kDns, std::vector<IPEndPoint>{},
std::vector<std::string>{}, std::move(pointers)));
}
return results;
}
ResultsOrError ExtractServiceResults(const DnsResponse& response,
base::Time now,
base::TimeTicks now_ticks) {
std::set<std::unique_ptr<HostResolverInternalResult>> results;
RecordsOrError srv_records = ExtractResponseRecords(
response, DnsQueryType::SRV, now, now_ticks, results);
if (!srv_records.has_value()) {
return base::unexpected(srv_records.error());
}
std::vector<const SrvRecordRdata*> fitered_rdatas;
auto min_ttl = base::TimeDelta::Max();
for (const auto& record : srv_records.value()) {
const SrvRecordRdata* rdata = record->rdata<net::SrvRecordRdata>();
DCHECK(rdata);
// Skip pointers to the root domain.
if (!rdata->target().empty()) {
fitered_rdatas.push_back(rdata);
base::TimeDelta ttl = base::Seconds(record->ttl());
min_ttl = std::min(ttl, min_ttl);
}
}
std::vector<HostPortPair> ordered_service_targets =
SortServiceTargets(fitered_rdatas);
if (!ordered_service_targets.empty()) {
results.insert(std::make_unique<HostResolverInternalDataResult>(
srv_records->front()->name(), DnsQueryType::SRV, now_ticks + min_ttl,
now + min_ttl, Source::kDns, std::vector<IPEndPoint>{},
std::vector<std::string>{}, std::move(ordered_service_targets)));
}
return results;
}
const RecordParsed* UnwrapRecordPtr(
const std::unique_ptr<const RecordParsed>& ptr) {
return ptr.get();
}
bool RecordIsAlias(const RecordParsed* record) {
DCHECK(record->rdata<HttpsRecordRdata>());
return record->rdata<HttpsRecordRdata>()->IsAlias();
}
ResultsOrError ExtractHttpsResults(const DnsResponse& response,
std::string_view original_domain_name,
uint16_t request_port,
base::Time now,
base::TimeTicks now_ticks) {
DCHECK(!original_domain_name.empty());
std::set<std::unique_ptr<HostResolverInternalResult>> results;
RecordsOrError https_records = ExtractResponseRecords(
response, DnsQueryType::HTTPS, now, now_ticks, results);
if (!https_records.has_value()) {
return base::unexpected(https_records.error());
}
// Min TTL among records of full use to Chrome.
std::optional<base::TimeDelta> min_ttl;
// Min TTL among all records considered compatible with Chrome, per
// RFC9460#section-8.
std::optional<base::TimeDelta> min_compatible_ttl;
std::multimap<HttpsRecordPriority, ConnectionEndpointMetadata> metadatas;
bool compatible_record_found = false;
bool default_alpn_found = false;
for (const auto& record : https_records.value()) {
const HttpsRecordRdata* rdata = record->rdata<HttpsRecordRdata>();
DCHECK(rdata);
base::TimeDelta ttl = base::Seconds(record->ttl());
// Chrome does not yet support alias records.
if (rdata->IsAlias()) {
// Alias records are always considered compatible because they do not
// support "mandatory" params.
compatible_record_found = true;
min_compatible_ttl =
std::min(ttl, min_compatible_ttl.value_or(base::TimeDelta::Max()));
continue;
}
const ServiceFormHttpsRecordRdata* service = rdata->AsServiceForm();
if (service->IsCompatible()) {
compatible_record_found = true;
min_compatible_ttl =
std::min(ttl, min_compatible_ttl.value_or(base::TimeDelta::Max()));
} else {
// Ignore services incompatible with Chrome's HTTPS record parser.
// draft-ietf-dnsop-svcb-https-12#section-8
continue;
}
std::string target_name = dns_names_util::UrlCanonicalizeNameIfAble(
service->service_name().empty() ? record->name()
: service->service_name());
// Chrome does not yet support followup queries. So only support services at
// the original domain name or the canonical name (the record name).
// Note: HostCache::Entry::GetEndpoints() will not return metadatas which
// target name is different from the canonical name of A/AAAA query results.
if (!base::EqualsCaseInsensitiveASCII(
target_name,
dns_names_util::UrlCanonicalizeNameIfAble(original_domain_name)) &&
!base::EqualsCaseInsensitiveASCII(
target_name,
dns_names_util::UrlCanonicalizeNameIfAble(record->name()))) {
continue;
}
// Ignore services at a different port from the request port. Chrome does
// not yet support endpoints diverging by port. Note that before supporting
// port redirects, Chrome must ensure redirects to the "bad port list" are
// disallowed. Unclear if such logic would belong here or in socket
// connection logic.
if (service->port().has_value() &&
service->port().value() != request_port) {
continue;
}
ConnectionEndpointMetadata metadata;
metadata.supported_protocol_alpns = service->alpn_ids();
if (service->default_alpn() &&
!base::Contains(metadata.supported_protocol_alpns,
dns_protocol::kHttpsServiceDefaultAlpn)) {
metadata.supported_protocol_alpns.push_back(
dns_protocol::kHttpsServiceDefaultAlpn);
}
// Services with no supported ALPNs (those with "no-default-alpn" and no or
// empty "alpn") are not self-consistent and are rejected.
// draft-ietf-dnsop-svcb-https-12#section-7.1.1 and
// draft-ietf-dnsop-svcb-https-12#section-2.4.3.
if (metadata.supported_protocol_alpns.empty()) {
continue;
}
metadata.ech_config_list = ConnectionEndpointMetadata::EchConfigList(
service->ech_config().cbegin(), service->ech_config().cend());
metadata.target_name = std::move(target_name);
metadata.trust_anchor_ids = base::ToVector(service->trust_anchor_ids());
metadatas.emplace(service->priority(), std::move(metadata));
min_ttl = std::min(ttl, min_ttl.value_or(base::TimeDelta::Max()));
if (service->default_alpn()) {
default_alpn_found = true;
}
}
// Ignore all records if any are an alias record. Chrome does not yet support
// alias records, but aliases take precedence over any other records.
if (std::ranges::any_of(https_records.value(), &RecordIsAlias,
&UnwrapRecordPtr)) {
metadatas.clear();
}
// Ignore all records if they all mark "no-default-alpn". Domains should
// always provide at least one endpoint allowing default ALPN to ensure a
// reasonable expectation of connection success.
// draft-ietf-dnsop-svcb-https-12#section-7.1.2
if (!default_alpn_found) {
metadatas.clear();
}
if (metadatas.empty() && compatible_record_found) {
// Empty metadata result signifies that compatible HTTPS records were
// received but with no contained metadata of use to Chrome. Use the min TTL
// of all compatible records.
CHECK(min_compatible_ttl.has_value());
results.insert(std::make_unique<HostResolverInternalMetadataResult>(
https_records->front()->name(), DnsQueryType::HTTPS,
now_ticks + min_compatible_ttl.value(),
now + min_compatible_ttl.value(), Source::kDns,
/*metadatas=*/
std::multimap<HttpsRecordPriority, ConnectionEndpointMetadata>{}));
} else if (!metadatas.empty()) {
// Use min TTL only of those records contributing useful metadata.
CHECK(min_ttl.has_value());
results.insert(std::make_unique<HostResolverInternalMetadataResult>(
https_records->front()->name(), DnsQueryType::HTTPS,
now_ticks + min_ttl.value(), now + min_ttl.value(), Source::kDns,
std::move(metadatas)));
}
return results;
}
} // namespace
DnsResponseResultExtractor::DnsResponseResultExtractor(
const DnsResponse& response,
const base::Clock& clock,
const base::TickClock& tick_clock)
: response_(response), clock_(clock), tick_clock_(tick_clock) {}
DnsResponseResultExtractor::~DnsResponseResultExtractor() = default;
ResultsOrError DnsResponseResultExtractor::ExtractDnsResults(
DnsQueryType query_type,
std::string_view original_domain_name,
uint16_t request_port) const {
DCHECK(!original_domain_name.empty());
switch (query_type) {
case DnsQueryType::UNSPECIFIED:
// Should create multiple transactions with specified types.
NOTREACHED();
case DnsQueryType::A:
case DnsQueryType::AAAA:
return ExtractAddressResults(*response_, query_type, clock_->Now(),
tick_clock_->NowTicks());
case DnsQueryType::TXT:
return ExtractTxtResults(*response_, clock_->Now(),
tick_clock_->NowTicks());
case DnsQueryType::PTR:
return ExtractPointerResults(*response_, clock_->Now(),
tick_clock_->NowTicks());
case DnsQueryType::SRV:
return ExtractServiceResults(*response_, clock_->Now(),
tick_clock_->NowTicks());
case DnsQueryType::HTTPS:
return ExtractHttpsResults(*response_, original_domain_name, request_port,
clock_->Now(), tick_clock_->NowTicks());
}
}
} // namespace net