blob: d0701302f7dad4d5d372589dcfb22db58904bd85 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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 <iterator>
#include <map>
#include <memory>
#include <ostream>
#include <set>
#include <string>
#include <unordered_set>
#include <vector>
#include "base/check.h"
#include "base/containers/contains.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/ranges/algorithm.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.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_response.h"
#include "net/dns/dns_util.h"
#include "net/dns/host_cache.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"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace net {
namespace {
using AliasMap = std::map<std::string, std::string, DomainNameComparator>;
using ExtractionError = DnsResponseResultExtractor::ExtractionError;
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;
}
ExtractionError ValidateNamesAndAliases(
base::StringPiece query_name,
const AliasMap& aliases,
const std::vector<std::unique_ptr<const RecordParsed>>& results) {
// Validate that all aliases form a single non-looping chain, starting from
// `query_name`.
size_t aliases_in_chain = 0;
base::StringPiece final_chain_name = query_name;
auto alias = aliases.find(std::string(query_name));
while (alias != aliases.end() && aliases_in_chain <= aliases.size()) {
aliases_in_chain++;
final_chain_name = alias->second;
alias = aliases.find(alias->second);
}
if (aliases_in_chain != aliases.size())
return ExtractionError::kBadAliasChain;
// All results must match final alias name.
for (const auto& result : results) {
DCHECK_NE(result->type(), dns_protocol::kTypeCNAME);
if (!base::EqualsCaseInsensitiveASCII(final_chain_name, result->name())) {
return ExtractionError::kNameMismatch;
}
}
return ExtractionError::kOk;
}
ExtractionError ExtractResponseRecords(
const DnsResponse& response,
uint16_t result_qtype,
std::vector<std::unique_ptr<const RecordParsed>>* out_records,
absl::optional<base::TimeDelta>* out_response_ttl,
std::set<std::string>* out_aliases) {
DCHECK_EQ(response.question_count(), 1u);
DCHECK(out_records);
DCHECK(out_response_ttl);
std::vector<std::unique_ptr<const RecordParsed>> records;
absl::optional<base::TimeDelta> response_ttl;
DnsRecordParser parser = response.Parser();
// Expected to be validated by DnsTransaction.
DCHECK_EQ(result_qtype, response.GetSingleQType());
AliasMap aliases;
for (unsigned i = 0; i < response.answer_count(); ++i) {
std::unique_ptr<const RecordParsed> record =
RecordParsed::CreateFrom(&parser, base::Time::Now());
if (!record)
return ExtractionError::kMalformedRecord;
DCHECK_NE(result_qtype, dns_protocol::kTypeCNAME);
if (record->klass() == dns_protocol::kClassIN &&
record->type() == dns_protocol::kTypeCNAME) {
// Per RFC2181, multiple CNAME records are not allowed for the same name.
if (aliases.find(record->name()) != aliases.end())
return ExtractionError::kMultipleCnames;
const CnameRecordRdata* cname_data = record->rdata<CnameRecordRdata>();
if (!cname_data)
return ExtractionError::kMalformedCname;
base::TimeDelta ttl = base::Seconds(record->ttl());
response_ttl =
std::min(response_ttl.value_or(base::TimeDelta::Max()), ttl);
bool added = aliases.emplace(record->name(), cname_data->cname()).second;
DCHECK(added);
} else if (record->klass() == dns_protocol::kClassIN &&
record->type() == result_qtype) {
base::TimeDelta ttl = base::Seconds(record->ttl());
response_ttl =
std::min(response_ttl.value_or(base::TimeDelta::Max()), ttl);
records.push_back(std::move(record));
}
}
ExtractionError name_and_alias_validation_error =
ValidateNamesAndAliases(response.GetSingleDottedName(), aliases, records);
if (name_and_alias_validation_error != ExtractionError::kOk)
return name_and_alias_validation_error;
// For NXDOMAIN or NODATA (NOERROR with 0 answers), attempt to find a TTL
// via an SOA record.
if (response.rcode() == dns_protocol::kRcodeNXDOMAIN ||
(response.answer_count() == 0 &&
response.rcode() == dns_protocol::kRcodeNOERROR)) {
bool soa_found = false;
for (unsigned i = 0; i < response.authority_count(); ++i) {
DnsResourceRecord record;
if (parser.ReadRecord(&record) && record.type == dns_protocol::kTypeSOA) {
soa_found = true;
base::TimeDelta ttl = base::Seconds(record.ttl);
response_ttl =
std::min(response_ttl.value_or(base::TimeDelta::Max()), ttl);
}
}
// Per RFC2308, section 5, never cache negative results unless an SOA
// record is found.
if (!soa_found)
response_ttl.reset();
}
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 = result_qtype != dns_protocol::kTypeHttps;
SaveMetricsForAdditionalHttpsRecord(*record, is_unsolicited);
}
}
*out_records = std::move(records);
*out_response_ttl = response_ttl;
if (out_aliases) {
out_aliases->clear();
for (const auto& alias : aliases) {
std::string canonicalized_alias =
dns_alias_utility::ValidateAndCanonicalizeAlias(alias.second);
if (!canonicalized_alias.empty())
out_aliases->insert(std::move(canonicalized_alias));
}
std::string canonicalized_query =
dns_alias_utility::ValidateAndCanonicalizeAlias(
response.GetSingleDottedName());
if (!canonicalized_query.empty())
out_aliases->insert(std::move(canonicalized_query));
}
return ExtractionError::kOk;
}
ExtractionError ExtractAddressResults(const DnsResponse& response,
uint16_t address_qtype,
HostCache::Entry* out_results) {
DCHECK_EQ(response.question_count(), 1u);
DCHECK(address_qtype == dns_protocol::kTypeA ||
address_qtype == dns_protocol::kTypeAAAA);
DCHECK(out_results);
std::vector<std::unique_ptr<const RecordParsed>> records;
absl::optional<base::TimeDelta> response_ttl;
std::set<std::string> aliases;
ExtractionError extraction_error = ExtractResponseRecords(
response, address_qtype, &records, &response_ttl, &aliases);
if (extraction_error != ExtractionError::kOk) {
*out_results = HostCache::Entry(ERR_DNS_MALFORMED_RESPONSE,
HostCache::Entry::SOURCE_DNS);
return extraction_error;
}
std::vector<IPEndPoint> ip_endpoints;
std::string canonical_name;
for (const auto& record : records) {
if (ip_endpoints.empty())
canonical_name = record->name();
// Expect that ExtractResponseRecords validates that all results correctly
// have the same name.
DCHECK(base::EqualsCaseInsensitiveASCII(canonical_name, record->name()))
<< "canonical_name: " << canonical_name
<< "\nrecord->name(): " << record->name();
IPAddress address;
if (address_qtype == dns_protocol::kTypeA) {
const ARecordRdata* rdata = record->rdata<ARecordRdata>();
address = rdata->address();
DCHECK(address.IsIPv4());
} else {
DCHECK_EQ(address_qtype, dns_protocol::kTypeAAAA);
const AAAARecordRdata* rdata = record->rdata<AAAARecordRdata>();
address = rdata->address();
DCHECK(address.IsIPv6());
}
ip_endpoints.emplace_back(address, /*port=*/0);
}
HostCache::Entry results(ip_endpoints.empty() ? ERR_NAME_NOT_RESOLVED : OK,
std::move(ip_endpoints),
HostCache::Entry::SOURCE_DNS, response_ttl);
results.set_aliases(std::move(aliases));
*out_results = std::move(results);
return ExtractionError::kOk;
}
ExtractionError ExtractTxtResults(const DnsResponse& response,
HostCache::Entry* out_results) {
DCHECK(out_results);
std::vector<std::unique_ptr<const RecordParsed>> records;
absl::optional<base::TimeDelta> response_ttl;
ExtractionError extraction_error =
ExtractResponseRecords(response, dns_protocol::kTypeTXT, &records,
&response_ttl, nullptr /* out_aliases */);
if (extraction_error != ExtractionError::kOk) {
*out_results = HostCache::Entry(ERR_DNS_MALFORMED_RESPONSE,
HostCache::Entry::SOURCE_DNS);
return extraction_error;
}
std::vector<std::string> text_records;
for (const auto& record : records) {
const TxtRecordRdata* rdata = record->rdata<net::TxtRecordRdata>();
text_records.insert(text_records.end(), rdata->texts().begin(),
rdata->texts().end());
}
*out_results = HostCache::Entry(
text_records.empty() ? ERR_NAME_NOT_RESOLVED : OK,
std::move(text_records), HostCache::Entry::SOURCE_DNS, response_ttl);
return ExtractionError::kOk;
}
ExtractionError ExtractPointerResults(const DnsResponse& response,
HostCache::Entry* out_results) {
DCHECK(out_results);
std::vector<std::unique_ptr<const RecordParsed>> records;
absl::optional<base::TimeDelta> response_ttl;
ExtractionError extraction_error =
ExtractResponseRecords(response, dns_protocol::kTypePTR, &records,
&response_ttl, nullptr /* out_aliases */);
if (extraction_error != ExtractionError::kOk) {
*out_results = HostCache::Entry(ERR_DNS_MALFORMED_RESPONSE,
HostCache::Entry::SOURCE_DNS);
return extraction_error;
}
std::vector<HostPortPair> pointers;
for (const auto& record : records) {
const PtrRecordRdata* rdata = record->rdata<net::PtrRecordRdata>();
std::string pointer = rdata->ptrdomain();
// Skip pointers to the root domain.
if (!pointer.empty())
pointers.emplace_back(std::move(pointer), 0);
}
*out_results = HostCache::Entry(pointers.empty() ? ERR_NAME_NOT_RESOLVED : OK,
std::move(pointers),
HostCache::Entry::SOURCE_DNS, response_ttl);
return ExtractionError::kOk;
}
ExtractionError ExtractServiceResults(const DnsResponse& response,
HostCache::Entry* out_results) {
DCHECK(out_results);
std::vector<std::unique_ptr<const RecordParsed>> records;
absl::optional<base::TimeDelta> response_ttl;
ExtractionError extraction_error =
ExtractResponseRecords(response, dns_protocol::kTypeSRV, &records,
&response_ttl, nullptr /* out_aliases */);
if (extraction_error != ExtractionError::kOk) {
*out_results = HostCache::Entry(ERR_DNS_MALFORMED_RESPONSE,
HostCache::Entry::SOURCE_DNS);
return extraction_error;
}
std::vector<const SrvRecordRdata*> fitered_rdatas;
for (const auto& record : records) {
const SrvRecordRdata* rdata = record->rdata<net::SrvRecordRdata>();
// Skip pointers to the root domain.
if (!rdata->target().empty())
fitered_rdatas.push_back(rdata);
}
std::vector<HostPortPair> ordered_service_targets =
SortServiceTargets(fitered_rdatas);
*out_results = HostCache::Entry(
ordered_service_targets.empty() ? ERR_NAME_NOT_RESOLVED : OK,
std::move(ordered_service_targets), HostCache::Entry::SOURCE_DNS,
response_ttl);
return ExtractionError::kOk;
}
ExtractionError ExtractIntegrityResults(const DnsResponse& response,
HostCache::Entry* out_results) {
DCHECK(out_results);
absl::optional<base::TimeDelta> response_ttl;
std::vector<std::unique_ptr<const RecordParsed>> records;
ExtractionError extraction_error = ExtractResponseRecords(
response, dns_protocol::kExperimentalTypeIntegrity, &records,
&response_ttl, nullptr /* out_aliases */);
if (extraction_error != ExtractionError::kOk) {
*out_results = HostCache::Entry(ERR_DNS_MALFORMED_RESPONSE,
HostCache::Entry::SOURCE_DNS);
return extraction_error;
}
// Condense results into a list of booleans. We do not cache the results,
// but this enables us to write some unit tests.
std::vector<bool> condensed_results;
for (const auto& record : records) {
const IntegrityRecordRdata& rdata = *record->rdata<IntegrityRecordRdata>();
condensed_results.push_back(rdata.IsIntact());
}
*out_results = HostCache::Entry(
condensed_results.empty() ? ERR_NAME_NOT_RESOLVED : OK,
std::move(condensed_results), HostCache::Entry::SOURCE_DNS, response_ttl);
DCHECK_EQ(extraction_error, ExtractionError::kOk);
return extraction_error;
}
ExtractionError ExtractExperimentalHttpsResults(const DnsResponse& response,
HostCache::Entry* out_results) {
DCHECK(out_results);
absl::optional<base::TimeDelta> response_ttl;
std::vector<std::unique_ptr<const RecordParsed>> records;
ExtractionError extraction_error =
ExtractResponseRecords(response, dns_protocol::kTypeHttps, &records,
&response_ttl, nullptr /* out_aliases */);
if (extraction_error != ExtractionError::kOk) {
*out_results = HostCache::Entry(ERR_DNS_MALFORMED_RESPONSE,
HostCache::Entry::SOURCE_DNS);
return extraction_error;
}
// Record record compatibility (draft-ietf-dnsop-svcb-https-08#section-8) for
// each record.
std::vector<bool> record_compatibility;
for (const auto& record : records) {
const HttpsRecordRdata* rdata = record->rdata<HttpsRecordRdata>();
DCHECK(rdata);
record_compatibility.push_back(rdata->IsAlias() ||
rdata->AsServiceForm()->IsCompatible());
}
*out_results = HostCache::Entry(records.empty() ? ERR_NAME_NOT_RESOLVED : OK,
std::move(record_compatibility),
HostCache::Entry::SOURCE_DNS, response_ttl);
DCHECK_EQ(extraction_error, ExtractionError::kOk);
return extraction_error;
}
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();
}
ExtractionError ExtractHttpsResults(const DnsResponse& response,
base::StringPiece original_domain_name,
uint16_t request_port,
HostCache::Entry* out_results) {
DCHECK(!original_domain_name.empty());
DCHECK(out_results);
absl::optional<base::TimeDelta> response_ttl;
std::vector<std::unique_ptr<const RecordParsed>> records;
ExtractionError extraction_error =
ExtractResponseRecords(response, dns_protocol::kTypeHttps, &records,
&response_ttl, nullptr /* out_aliases */);
if (extraction_error != ExtractionError::kOk) {
*out_results = HostCache::Entry(ERR_DNS_MALFORMED_RESPONSE,
HostCache::Entry::SOURCE_DNS);
return extraction_error;
}
std::multimap<HttpsRecordPriority, ConnectionEndpointMetadata> results;
std::vector<bool> record_compatibility;
bool default_alpn_found = false;
#if DCHECK_IS_ON()
std::string canonical_name;
#endif // DCHECK_IS_ON()
for (const auto& record : records) {
#if DCHECK_IS_ON()
if (canonical_name.empty()) {
canonical_name = record->name();
} else {
DCHECK(record->name() == canonical_name);
}
#endif // DCHECK_IS_ON()
const HttpsRecordRdata* rdata = record->rdata<HttpsRecordRdata>();
DCHECK(rdata);
// Chrome does not yet support alias records.
if (rdata->IsAlias()) {
// Alias records are always considered compatible because they do not
// support "mandatory" params.
record_compatibility.push_back(true);
continue;
}
const ServiceFormHttpsRecordRdata* service = rdata->AsServiceForm();
record_compatibility.push_back(service->IsCompatible());
// Ignore services incompatible with Chrome's HTTPS record parser.
// draft-ietf-dnsop-svcb-https-08#section-8
if (!service->IsCompatible())
continue;
// Only support services at the original domain name, as that is the name at
// which Chrome queried A/AAAA. Chrome does not yet support followup queries
// or diverging addresses.
base::StringPiece target_name = service->service_name().empty()
? record->name()
: service->service_name();
if (target_name != original_domain_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-08#section-7.1.1 and
// draft-ietf-dnsop-svcb-https-08#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());
results.emplace(service->priority(), std::move(metadata));
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 (base::ranges::any_of(records, &RecordIsAlias, &UnwrapRecordPtr)) {
records.clear();
results.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-08#section-7.1.2
if (!default_alpn_found) {
records.clear();
results.clear();
}
*out_results = HostCache::Entry(results.empty() ? ERR_NAME_NOT_RESOLVED : OK,
std::move(results),
HostCache::Entry::SOURCE_DNS, response_ttl);
out_results->set_https_record_compatibility(std::move(record_compatibility));
DCHECK_EQ(extraction_error, ExtractionError::kOk);
return extraction_error;
}
} // namespace
DnsResponseResultExtractor::DnsResponseResultExtractor(
const DnsResponse* response)
: response_(response) {
DCHECK(response_);
}
DnsResponseResultExtractor::~DnsResponseResultExtractor() = default;
DnsResponseResultExtractor::ExtractionError
DnsResponseResultExtractor::ExtractDnsResults(
DnsQueryType query_type,
base::StringPiece original_domain_name,
uint16_t request_port,
HostCache::Entry* out_results) const {
DCHECK(!original_domain_name.empty());
DCHECK(out_results);
switch (query_type) {
case DnsQueryType::UNSPECIFIED:
// Should create multiple transactions with specified types.
NOTREACHED();
return ExtractionError::kUnexpected;
case DnsQueryType::A:
case DnsQueryType::AAAA:
return ExtractAddressResults(*response_, DnsQueryTypeToQtype(query_type),
out_results);
case DnsQueryType::TXT:
return ExtractTxtResults(*response_, out_results);
case DnsQueryType::PTR:
return ExtractPointerResults(*response_, out_results);
case DnsQueryType::SRV:
return ExtractServiceResults(*response_, out_results);
case DnsQueryType::INTEGRITY:
return ExtractIntegrityResults(*response_, out_results);
case DnsQueryType::HTTPS:
return ExtractHttpsResults(*response_, original_domain_name, request_port,
out_results);
case DnsQueryType::HTTPS_EXPERIMENTAL:
return ExtractExperimentalHttpsResults(*response_, out_results);
}
}
// static
HostCache::Entry DnsResponseResultExtractor::CreateEmptyResult(
DnsQueryType query_type) {
if (query_type != DnsQueryType::INTEGRITY &&
query_type != DnsQueryType::HTTPS &&
query_type != DnsQueryType::HTTPS_EXPERIMENTAL) {
// Currently only used for INTEGRITY/HTTPS.
NOTIMPLEMENTED();
return HostCache::Entry(ERR_FAILED, HostCache::Entry::SOURCE_UNKNOWN);
}
return HostCache::Entry(ERR_NAME_NOT_RESOLVED, std::vector<bool>(),
HostCache::Entry::SOURCE_DNS);
}
} // namespace net