// 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 #include #include #include #include "base/ranges/algorithm.h" #include "base/test/simple_test_clock.h" #include "base/test/simple_test_tick_clock.h" #include "base/time/time.h" #include "net/base/connection_endpoint_metadata_test_util.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_query.h" #include "net/dns/dns_response.h" #include "net/dns/dns_test_util.h" #include "net/dns/host_cache.h" #include "net/dns/host_resolver_internal_result.h" #include "net/dns/host_resolver_internal_result_test_util.h" #include "net/dns/host_resolver_results_test_util.h" #include "net/dns/public/dns_protocol.h" #include "net/dns/public/dns_query_type.h" #include "net/test/gtest_util.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" namespace net { namespace { using ::testing::AllOf; using ::testing::ElementsAre; using ::testing::ElementsAreArray; using ::testing::Eq; using ::testing::IsEmpty; using ::testing::Ne; using ::testing::Optional; using ::testing::Pair; using ::testing::Pointee; using ::testing::ResultOf; using ::testing::SizeIs; using ::testing::UnorderedElementsAre; using ExtractionError = DnsResponseResultExtractor::ExtractionError; using ResultsOrError = DnsResponseResultExtractor::ResultsOrError; constexpr HostResolverInternalResult::Source kDnsSource = HostResolverInternalResult::Source::kDns; class DnsResponseResultExtractorTest : public ::testing::Test { protected: base::SimpleTestClock clock_; base::SimpleTestTickClock tick_clock_; }; TEST_F(DnsResponseResultExtractorTest, ExtractsSingleARecord) { constexpr char kName[] = "address.test"; const IPAddress kExpected(192, 168, 0, 1); DnsResponse response = BuildTestDnsAddressResponse(kName, kExpected); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalDataResult( kName, DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(IPEndPoint(kExpected, /*port=*/0)))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsSingleAAAARecord) { constexpr char kName[] = "address.test"; IPAddress expected; CHECK(expected.AssignFromIPLiteral("2001:4860:4860::8888")); DnsResponse response = BuildTestDnsAddressResponse(kName, expected); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::AAAA, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalDataResult( kName, DnsQueryType::AAAA, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(IPEndPoint(expected, /*port=*/0)))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsSingleARecordWithCname) { const IPAddress kExpected(192, 168, 0, 1); constexpr char kName[] = "address.test"; constexpr char kCanonicalName[] = "alias.test"; DnsResponse response = BuildTestDnsAddressResponseWithCname(kName, kExpected, kCanonicalName); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalDataResult( kCanonicalName, DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(IPEndPoint(kExpected, /*port=*/0)))), Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), kCanonicalName)))); } TEST_F(DnsResponseResultExtractorTest, ExtractsARecordsWithCname) { constexpr char kName[] = "addresses.test"; DnsResponse response = BuildTestDnsResponse( "addresses.test", dns_protocol::kTypeA, { BuildTestAddressRecord("alias.test", IPAddress(74, 125, 226, 179)), BuildTestAddressRecord("alias.test", IPAddress(74, 125, 226, 180)), BuildTestCnameRecord(kName, "alias.test"), BuildTestAddressRecord("alias.test", IPAddress(74, 125, 226, 176)), BuildTestAddressRecord("alias.test", IPAddress(74, 125, 226, 177)), BuildTestAddressRecord("alias.test", IPAddress(74, 125, 226, 178)), }); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalDataResult( "alias.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), UnorderedElementsAre( IPEndPoint(IPAddress(74, 125, 226, 179), /*port=*/0), IPEndPoint(IPAddress(74, 125, 226, 180), /*port=*/0), IPEndPoint(IPAddress(74, 125, 226, 176), /*port=*/0), IPEndPoint(IPAddress(74, 125, 226, 177), /*port=*/0), IPEndPoint(IPAddress(74, 125, 226, 178), /*port=*/0)))), Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "alias.test")))); } TEST_F(DnsResponseResultExtractorTest, ExtractsNxdomainAResponses) { constexpr char kName[] = "address.test"; constexpr auto kTtl = base::Hours(2); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeA, /*answers=*/{}, /*authority=*/ {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)}, /*additional=*/{}, dns_protocol::kRcodeNXDOMAIN); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalErrorResult( kName, DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl), ERR_NAME_NOT_RESOLVED)))); } TEST_F(DnsResponseResultExtractorTest, ExtractsNodataAResponses) { constexpr char kName[] = "address.test"; constexpr auto kTtl = base::Minutes(15); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeA, /*answers=*/{}, /*authority=*/ {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalErrorResult( kName, DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl), ERR_NAME_NOT_RESOLVED)))); } TEST_F(DnsResponseResultExtractorTest, ExtractsNodataAResponsesWithoutTtl) { constexpr char kName[] = "address.test"; // Response without a TTL-containing SOA record. DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeA, /*answers=*/{}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); // Expect empty result because not cacheable. ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), IsEmpty()); } TEST_F(DnsResponseResultExtractorTest, RejectsMalformedARecord) { constexpr char kName[] = "address.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeA, {BuildTestDnsRecord(kName, dns_protocol::kTypeA, "malformed rdata")} /* answers */); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kMalformedRecord); } TEST_F(DnsResponseResultExtractorTest, RejectsWrongNameARecord) { constexpr char kName[] = "address.test"; DnsResponse response = BuildTestDnsAddressResponse( kName, IPAddress(1, 2, 3, 4), "different.test"); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kNameMismatch); } TEST_F(DnsResponseResultExtractorTest, IgnoresWrongTypeRecordsInAResponse) { constexpr char kName[] = "address.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeA, {BuildTestTextRecord("address.test", {"foo"} /* text_strings */)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); // Expect empty results because NODATA is not cacheable (due to no TTL). ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), IsEmpty()); } TEST_F(DnsResponseResultExtractorTest, IgnoresWrongTypeRecordsMixedWithARecords) { constexpr char kName[] = "address.test"; const IPAddress kExpected(8, 8, 8, 8); constexpr auto kTtl = base::Days(3); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeA, {BuildTestTextRecord(kName, /*text_strings=*/{"foo"}, base::Hours(2)), BuildTestAddressRecord(kName, kExpected, kTtl)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalDataResult( kName, DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl), ElementsAre(IPEndPoint(kExpected, /*port=*/0)))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsMinATtl) { constexpr char kName[] = "name.test"; constexpr base::TimeDelta kMinTtl = base::Minutes(4); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeA, {BuildTestAddressRecord(kName, IPAddress(1, 2, 3, 4), base::Hours(3)), BuildTestAddressRecord(kName, IPAddress(2, 3, 4, 5), kMinTtl), BuildTestAddressRecord(kName, IPAddress(3, 4, 5, 6), base::Minutes(15))}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalDataResult( kName, DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kMinTtl), /*timed_expiration_matcher=*/Eq(clock_.Now() + kMinTtl), /*endpoints_matcher=*/SizeIs(3))))); } MATCHER_P(ContainsContiguousElements, elements, "") { return base::ranges::search(arg, elements) != arg.end(); } TEST_F(DnsResponseResultExtractorTest, ExtractsTxtResponses) { constexpr char kName[] = "name.test"; // Simulate two separate DNS records, each with multiple strings. std::vector foo_records = {"foo1", "foo2", "foo3"}; std::vector bar_records = {"bar1", "bar2"}; std::vector> text_records = {foo_records, bar_records}; DnsResponse response = BuildTestDnsTextResponse(kName, std::move(text_records)); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); // Order between separate DNS records is undefined, but each record should // stay in order as that order may be meaningful. EXPECT_THAT( results.value(), ElementsAre(Pointee(ExpectHostResolverInternalDataResult( kName, DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), /*endpoints_matcher=*/IsEmpty(), /*strings_matcher=*/ AllOf(UnorderedElementsAre("foo1", "foo2", "foo3", "bar1", "bar2"), ContainsContiguousElements(foo_records), ContainsContiguousElements(bar_records)))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsNxdomainTxtResponses) { constexpr char kName[] = "name.test"; constexpr auto kTtl = base::Days(4); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeTXT, /*answers=*/{}, /*authority=*/ {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)}, /*additional=*/{}, dns_protocol::kRcodeNXDOMAIN); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalErrorResult( kName, DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl), ERR_NAME_NOT_RESOLVED)))); } TEST_F(DnsResponseResultExtractorTest, ExtractsNodataTxtResponses) { constexpr char kName[] = "name.test"; constexpr auto kTtl = base::Minutes(42); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeTXT, /*answers=*/{}, /*authority=*/ {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalErrorResult( kName, DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl), ERR_NAME_NOT_RESOLVED)))); } TEST_F(DnsResponseResultExtractorTest, RejectsMalformedTxtRecord) { constexpr char kName[] = "name.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeTXT, {BuildTestDnsRecord(kName, dns_protocol::kTypeTXT, "malformed rdata")} /* answers */); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kMalformedRecord); } TEST_F(DnsResponseResultExtractorTest, RejectsWrongNameTxtRecord) { constexpr char kName[] = "name.test"; DnsResponse response = BuildTestDnsTextResponse(kName, {{"foo"}}, "different.test"); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kNameMismatch); } TEST_F(DnsResponseResultExtractorTest, IgnoresWrongTypeTxtResponses) { constexpr char kName[] = "name.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeTXT, {BuildTestAddressRecord(kName, IPAddress(1, 2, 3, 4))}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0); // Expect empty results because NODATA is not cacheable (due to no TTL). ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), IsEmpty()); } TEST_F(DnsResponseResultExtractorTest, ExtractsMinTxtTtl) { constexpr char kName[] = "name.test"; constexpr base::TimeDelta kMinTtl = base::Minutes(4); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeTXT, {BuildTestTextRecord(kName, {"foo"}, base::Hours(3)), BuildTestTextRecord(kName, {"bar"}, kMinTtl), BuildTestTextRecord(kName, {"baz"}, base::Minutes(15))}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalDataResult( kName, DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kMinTtl), /*timed_expiration_matcher=*/Eq(clock_.Now() + kMinTtl), /*endpoints_matcher=*/IsEmpty(), /*strings_matcher=*/SizeIs(3))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsPtrResponses) { constexpr char kName[] = "name.test"; DnsResponse response = BuildTestDnsPointerResponse(kName, {"foo.com", "bar.com"}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::PTR, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalDataResult( kName, DnsQueryType::PTR, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), /*endpoints_matcher=*/IsEmpty(), /*strings_matcher=*/IsEmpty(), /*hosts_matcher=*/ UnorderedElementsAre(HostPortPair("foo.com", 0), HostPortPair("bar.com", 0)))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsNxdomainPtrResponses) { constexpr char kName[] = "name.test"; constexpr auto kTtl = base::Hours(5); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypePTR, /*answers=*/{}, /*authority=*/ {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)}, /*additional=*/{}, dns_protocol::kRcodeNXDOMAIN); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::PTR, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalErrorResult( kName, DnsQueryType::PTR, kDnsSource, /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl), ERR_NAME_NOT_RESOLVED)))); } TEST_F(DnsResponseResultExtractorTest, ExtractsNodataPtrResponses) { constexpr char kName[] = "name.test"; constexpr auto kTtl = base::Minutes(50); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypePTR, /*answers=*/{}, /*authority=*/ {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::PTR, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalErrorResult( kName, DnsQueryType::PTR, kDnsSource, /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl), ERR_NAME_NOT_RESOLVED)))); } TEST_F(DnsResponseResultExtractorTest, RejectsMalformedPtrRecord) { constexpr char kName[] = "name.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypePTR, {BuildTestDnsRecord(kName, dns_protocol::kTypePTR, "malformed rdata")} /* answers */); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::PTR, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kMalformedRecord); } TEST_F(DnsResponseResultExtractorTest, RejectsWrongNamePtrRecord) { constexpr char kName[] = "name.test"; DnsResponse response = BuildTestDnsPointerResponse( kName, {"foo.com", "bar.com"}, "different.test"); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::PTR, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kNameMismatch); } TEST_F(DnsResponseResultExtractorTest, IgnoresWrongTypePtrResponses) { constexpr char kName[] = "name.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypePTR, {BuildTestAddressRecord(kName, IPAddress(1, 2, 3, 4))}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::PTR, /*original_domain_name=*/kName, /*request_port=*/0); // Expect empty results because NODATA is not cacheable (due to no TTL). ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), IsEmpty()); } TEST_F(DnsResponseResultExtractorTest, ExtractsSrvResponses) { constexpr char kName[] = "name.test"; const TestServiceRecord kRecord1 = {2, 3, 1223, "foo.com"}; const TestServiceRecord kRecord2 = {5, 10, 80, "bar.com"}; const TestServiceRecord kRecord3 = {5, 1, 5, "google.com"}; const TestServiceRecord kRecord4 = {2, 100, 12345, "chromium.org"}; DnsResponse response = BuildTestDnsServiceResponse( kName, {kRecord1, kRecord2, kRecord3, kRecord4}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::SRV, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalDataResult( kName, DnsQueryType::SRV, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), /*endpoints_matcher=*/IsEmpty(), /*strings_matcher=*/IsEmpty(), /*hosts_matcher=*/ UnorderedElementsAre(HostPortPair("foo.com", 1223), HostPortPair("bar.com", 80), HostPortPair("google.com", 5), HostPortPair("chromium.org", 12345)))))); // Expect ordered by priority, and random within a priority. std::vector result_hosts = (*results.value().begin())->AsData().hosts(); auto priority2 = std::vector(result_hosts.begin(), result_hosts.begin() + 2); EXPECT_THAT(priority2, testing::UnorderedElementsAre( HostPortPair("foo.com", 1223), HostPortPair("chromium.org", 12345))); auto priority5 = std::vector(result_hosts.begin() + 2, result_hosts.end()); EXPECT_THAT(priority5, testing::UnorderedElementsAre(HostPortPair("bar.com", 80), HostPortPair("google.com", 5))); } // 0-weight services are allowed. Ensure that we can handle such records, // especially the case where all entries have weight 0. TEST_F(DnsResponseResultExtractorTest, ExtractsZeroWeightSrvResponses) { constexpr char kName[] = "name.test"; const TestServiceRecord kRecord1 = {5, 0, 80, "bar.com"}; const TestServiceRecord kRecord2 = {5, 0, 5, "google.com"}; DnsResponse response = BuildTestDnsServiceResponse(kName, {kRecord1, kRecord2}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::SRV, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalDataResult( kName, DnsQueryType::SRV, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), /*endpoints_matcher=*/IsEmpty(), /*strings_matcher=*/IsEmpty(), /*hosts_matcher=*/ UnorderedElementsAre(HostPortPair("bar.com", 80), HostPortPair("google.com", 5)))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsNxdomainSrvResponses) { constexpr char kName[] = "name.test"; constexpr auto kTtl = base::Days(7); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeSRV, /*answers=*/{}, /*authority=*/ {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)}, /*additional=*/{}, dns_protocol::kRcodeNXDOMAIN); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::SRV, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalErrorResult( kName, DnsQueryType::SRV, kDnsSource, /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl), ERR_NAME_NOT_RESOLVED)))); } TEST_F(DnsResponseResultExtractorTest, ExtractsNodataSrvResponses) { constexpr char kName[] = "name.test"; constexpr auto kTtl = base::Hours(12); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeSRV, /*answers=*/{}, /*authority=*/ {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::SRV, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalErrorResult( kName, DnsQueryType::SRV, kDnsSource, /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl), ERR_NAME_NOT_RESOLVED)))); } TEST_F(DnsResponseResultExtractorTest, RejectsMalformedSrvRecord) { constexpr char kName[] = "name.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeSRV, {BuildTestDnsRecord(kName, dns_protocol::kTypeSRV, "malformed rdata")} /* answers */); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::SRV, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kMalformedRecord); } TEST_F(DnsResponseResultExtractorTest, RejectsWrongNameSrvRecord) { constexpr char kName[] = "name.test"; const TestServiceRecord kRecord = {2, 3, 1223, "foo.com"}; DnsResponse response = BuildTestDnsServiceResponse(kName, {kRecord}, "different.test"); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::SRV, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kNameMismatch); } TEST_F(DnsResponseResultExtractorTest, IgnoresWrongTypeSrvResponses) { constexpr char kName[] = "name.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeSRV, {BuildTestAddressRecord(kName, IPAddress(1, 2, 3, 4))}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::SRV, /*original_domain_name=*/kName, /*request_port=*/0); // Expect empty results because NODATA is not cacheable (due to no TTL). ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), IsEmpty()); } TEST_F(DnsResponseResultExtractorTest, ExtractsBasicHttpsResponses) { constexpr char kName[] = "https.test"; constexpr auto kTtl = base::Hours(12); DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord(kName, /*priority=*/4, /*service_name=*/".", /*params=*/{}, kTtl)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, Eq(tick_clock_.NowTicks() + kTtl), Eq(clock_.Now() + kTtl), ElementsAre( Pair(4, ExpectConnectionEndpointMetadata( ElementsAre(dns_protocol::kHttpsServiceDefaultAlpn), /*ech_config_list_matcher=*/IsEmpty(), kName))))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsComprehensiveHttpsResponses) { constexpr char kName[] = "https.test"; constexpr char kAlpn[] = "foo"; constexpr uint8_t kEchConfig[] = "EEEEEEEEECH!"; constexpr auto kTtl = base::Hours(12); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord( kName, /*priority=*/4, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({kAlpn}), BuildTestHttpsServiceEchConfigParam(kEchConfig)}, kTtl), BuildTestHttpsServiceRecord( kName, /*priority=*/3, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({kAlpn}), {dns_protocol::kHttpsServiceParamKeyNoDefaultAlpn, ""}}, /*ttl=*/base::Days(3))}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, Eq(tick_clock_.NowTicks() + kTtl), Eq(clock_.Now() + kTtl), ElementsAre( Pair(3, ExpectConnectionEndpointMetadata( ElementsAre(kAlpn), /*ech_config_list_matcher=*/IsEmpty(), kName)), Pair(4, ExpectConnectionEndpointMetadata( ElementsAre(kAlpn, dns_protocol::kHttpsServiceDefaultAlpn), ElementsAreArray(kEchConfig), kName))))))); } TEST_F(DnsResponseResultExtractorTest, IgnoresHttpsResponseWithJustAlias) { constexpr char kName[] = "https.test"; constexpr base::TimeDelta kTtl = base::Days(5); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestHttpsAliasRecord(kName, "alias.test", kTtl)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); // Expect empty metadata result to signify compatible HTTPS records with no // data of use to Chrome. Still expect expiration from record, so the empty // response can be cached. ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Optional(clock_.Now() + kTtl), /*metadatas_matcher=*/IsEmpty())))); } TEST_F(DnsResponseResultExtractorTest, IgnoresHttpsResponseWithAlias) { constexpr char kName[] = "https.test"; constexpr base::TimeDelta kLowestTtl = base::Minutes(32); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord(kName, /*priority=*/4, /*service_name=*/".", /*params=*/{}, base::Days(1)), BuildTestHttpsAliasRecord(kName, "alias.test", kLowestTtl)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); // Expect empty metadata result to signify compatible HTTPS records with no // data of use to Chrome. Expiration should match lowest TTL from all // compatible records. ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kLowestTtl), /*timed_expiration_matcher=*/Optional(clock_.Now() + kLowestTtl), /*metadatas_matcher=*/IsEmpty())))); } // Expect the entire response to be ignored if all HTTPS records have the // "no-default-alpn" param. TEST_F(DnsResponseResultExtractorTest, IgnoresHttpsResponseWithNoDefaultAlpn) { constexpr char kName[] = "https.test"; constexpr base::TimeDelta kLowestTtl = base::Hours(3); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord( kName, /*priority=*/4, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo1"}), {dns_protocol::kHttpsServiceParamKeyNoDefaultAlpn, ""}}, kLowestTtl), BuildTestHttpsServiceRecord( kName, /*priority=*/5, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo2"}), {dns_protocol::kHttpsServiceParamKeyNoDefaultAlpn, ""}}, base::Days(3))}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); // Expect empty metadata result to signify compatible HTTPS records with no // data of use to Chrome. ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kLowestTtl), /*timed_expiration_matcher=*/Optional(clock_.Now() + kLowestTtl), /*metadatas_matcher=*/IsEmpty())))); } // Unsupported/unknown HTTPS params are simply ignored if not marked mandatory. TEST_F(DnsResponseResultExtractorTest, IgnoresUnsupportedParamsInHttpsRecord) { constexpr char kName[] = "https.test"; constexpr uint16_t kMadeUpParamKey = 65500; // From the private-use block. DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord(kName, /*priority=*/4, /*service_name=*/".", /*params=*/ {{kMadeUpParamKey, "foo"}})}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre( Pair(4, ExpectConnectionEndpointMetadata( ElementsAre(dns_protocol::kHttpsServiceDefaultAlpn), /*ech_config_list_matcher=*/IsEmpty(), kName))))))); } // Entire record is dropped if an unsupported/unknown HTTPS param is marked // mandatory. TEST_F(DnsResponseResultExtractorTest, IgnoresHttpsRecordWithUnsupportedMandatoryParam) { constexpr char kName[] = "https.test"; constexpr uint16_t kMadeUpParamKey = 65500; // From the private-use block. constexpr base::TimeDelta kTtl = base::Days(5); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord( kName, /*priority=*/4, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"ignored_alpn"}), BuildTestHttpsServiceMandatoryParam({kMadeUpParamKey}), {kMadeUpParamKey, "foo"}}, base::Hours(2)), BuildTestHttpsServiceRecord( kName, /*priority=*/5, /*service_name=*/".", /*params=*/{BuildTestHttpsServiceAlpnParam({"foo"})}, kTtl)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); // Expect expiration to be derived only from non-ignored records. EXPECT_THAT( results.value(), ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Optional(clock_.Now() + kTtl), ElementsAre(Pair( 5, ExpectConnectionEndpointMetadata( ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn), /*ech_config_list_matcher=*/IsEmpty(), kName))))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsHttpsRecordWithMatchingServiceName) { constexpr char kName[] = "https.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord(kName, /*priority=*/4, /*service_name=*/kName, /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo"})})}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(Pair( 4, ExpectConnectionEndpointMetadata( ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn), /*ech_config_list_matcher=*/IsEmpty(), kName))))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsHttpsRecordWithMatchingDefaultServiceName) { constexpr char kName[] = "https.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord(kName, /*priority=*/4, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo"})})}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(Pair( 4, ExpectConnectionEndpointMetadata( ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn), /*ech_config_list_matcher=*/IsEmpty(), kName))))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsHttpsRecordWithPrefixedNameAndMatchingServiceName) { constexpr char kName[] = "https.test"; constexpr char kPrefixedName[] = "_444._https.https.test"; DnsResponse response = BuildTestDnsResponse( kPrefixedName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord(kPrefixedName, /*priority=*/4, /*service_name=*/kName, /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo"})})}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kPrefixedName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(Pair( 4, ExpectConnectionEndpointMetadata( ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn), /*ech_config_list_matcher=*/IsEmpty(), kName))))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsHttpsRecordWithAliasingAndMatchingServiceName) { constexpr char kName[] = "https.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestCnameRecord(kName, "alias.test"), BuildTestHttpsServiceRecord("alias.test", /*priority=*/4, /*service_name=*/kName, /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo"})})}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "alias.test")), Pointee(ExpectHostResolverInternalMetadataResult( "alias.test", DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(Pair( 4, ExpectConnectionEndpointMetadata( ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn), /*ech_config_list_matcher=*/IsEmpty(), kName))))))); } TEST_F(DnsResponseResultExtractorTest, IgnoreHttpsRecordWithNonMatchingServiceName) { constexpr char kName[] = "https.test"; constexpr base::TimeDelta kTtl = base::Hours(14); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord( kName, /*priority=*/4, /*service_name=*/"other.service.test", /*params=*/ {BuildTestHttpsServiceAlpnParam({"ignored"})}, base::Hours(3)), BuildTestHttpsServiceRecord("https.test", /*priority=*/5, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo"})}, kTtl)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); // Expect expiration to be derived only from non-ignored records. EXPECT_THAT( results.value(), ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Optional(clock_.Now() + kTtl), ElementsAre(Pair( 5, ExpectConnectionEndpointMetadata( ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn), /*ech_config_list_matcher=*/IsEmpty(), kName))))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsHttpsRecordWithPrefixedNameAndDefaultServiceName) { constexpr char kPrefixedName[] = "_445._https.https.test"; DnsResponse response = BuildTestDnsResponse( kPrefixedName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord(kPrefixedName, /*priority=*/4, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo"})})}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/"https.test", /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kPrefixedName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(Pair( 4, ExpectConnectionEndpointMetadata( ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn), /*ech_config_list_matcher=*/IsEmpty(), kPrefixedName))))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsHttpsRecordWithAliasingAndDefaultServiceName) { constexpr char kName[] = "https.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestCnameRecord(kName, "alias.test"), BuildTestHttpsServiceRecord("alias.test", /*priority=*/4, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo"})})}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "alias.test")), Pointee(ExpectHostResolverInternalMetadataResult( "alias.test", DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(Pair( 4, ExpectConnectionEndpointMetadata( ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn), /*ech_config_list_matcher=*/IsEmpty(), "alias.test"))))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsHttpsRecordWithMatchingPort) { constexpr char kName[] = "https.test"; constexpr uint16_t kPort = 4567; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord(kName, /*priority=*/4, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo"}), BuildTestHttpsServicePortParam(kPort)})}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/kPort); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(Pair( 4, ExpectConnectionEndpointMetadata( ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn), /*ech_config_list_matcher=*/IsEmpty(), kName))))))); } TEST_F(DnsResponseResultExtractorTest, IgnoresHttpsRecordWithMismatchingPort) { constexpr char kName[] = "https.test"; constexpr base::TimeDelta kTtl = base::Days(14); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord(kName, /*priority=*/4, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"ignored"}), BuildTestHttpsServicePortParam(1003)}, base::Hours(12)), BuildTestHttpsServiceRecord(kName, /*priority=*/4, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo"})}, kTtl)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/55); ASSERT_TRUE(results.has_value()); // Expect expiration to be derived only from non-ignored records. EXPECT_THAT( results.value(), UnorderedElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Optional(clock_.Now() + kTtl), ElementsAre(Pair( 4, ExpectConnectionEndpointMetadata( ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn), /*ech_config_list_matcher=*/IsEmpty(), kName))))))); } // HTTPS records with "no-default-alpn" but also no "alpn" are not // "self-consistent" and should be ignored. TEST_F(DnsResponseResultExtractorTest, IgnoresHttpsRecordWithNoAlpn) { constexpr char kName[] = "https.test"; constexpr base::TimeDelta kTtl = base::Minutes(150); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord( kName, /*priority=*/4, /*service_name=*/".", /*params=*/ {{dns_protocol::kHttpsServiceParamKeyNoDefaultAlpn, ""}}, base::Minutes(10)), BuildTestHttpsServiceRecord(kName, /*priority=*/4, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo"})}, kTtl)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/55); ASSERT_TRUE(results.has_value()); // Expect expiration to be derived only from non-ignored records. EXPECT_THAT( results.value(), UnorderedElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Optional(clock_.Now() + kTtl), ElementsAre(Pair( 4, ExpectConnectionEndpointMetadata( ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn), /*ech_config_list_matcher=*/IsEmpty(), kName))))))); } // Expect the entire response to be ignored if all HTTPS records have the // "no-default-alpn" param. TEST_F(DnsResponseResultExtractorTest, IgnoresHttpsResponseWithNoCompatibleDefaultAlpn) { constexpr char kName[] = "https.test"; constexpr uint16_t kMadeUpParamKey = 65500; // From the private-use block. constexpr base::TimeDelta kLowestTtl = base::Days(2); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestHttpsServiceRecord( kName, /*priority=*/4, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo1"}), {dns_protocol::kHttpsServiceParamKeyNoDefaultAlpn, ""}}, base::Days(3)), BuildTestHttpsServiceRecord( kName, /*priority=*/5, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo2"}), {dns_protocol::kHttpsServiceParamKeyNoDefaultAlpn, ""}}, base::Days(4)), // Allows default ALPN, but ignored due to non-matching service name. BuildTestHttpsServiceRecord(kName, /*priority=*/3, /*service_name=*/"other.test", /*params=*/{}, kLowestTtl), // Allows default ALPN, but ignored due to incompatible param. BuildTestHttpsServiceRecord( kName, /*priority=*/6, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceMandatoryParam({kMadeUpParamKey}), {kMadeUpParamKey, "foo"}}, base::Hours(1)), // Allows default ALPN, but ignored due to mismatching port. BuildTestHttpsServiceRecord( kName, /*priority=*/10, /*service_name=*/".", /*params=*/{BuildTestHttpsServicePortParam(1005)}, base::Days(5))}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); // Expect expiration to be from the lowest TTL from the "compatible" records // that don't have incompatible params. EXPECT_THAT( results.value(), UnorderedElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kLowestTtl), /*timed_expiration_matcher=*/Optional(clock_.Now() + kLowestTtl), /*metadatas_matcher=*/IsEmpty())))); } TEST_F(DnsResponseResultExtractorTest, ExtractsNxdomainHttpsResponses) { constexpr char kName[] = "https.test"; constexpr auto kTtl = base::Minutes(45); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, /*answers=*/{}, /*authority=*/ {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)}, /*additional=*/{}, dns_protocol::kRcodeNXDOMAIN); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalErrorResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl), ERR_NAME_NOT_RESOLVED)))); } TEST_F(DnsResponseResultExtractorTest, ExtractsNodataHttpsResponses) { constexpr char kName[] = "https.test"; constexpr auto kTtl = base::Hours(36); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, /*answers=*/{}, /*authority=*/ {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalErrorResult( kName, DnsQueryType::HTTPS, kDnsSource, /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl), ERR_NAME_NOT_RESOLVED)))); } TEST_F(DnsResponseResultExtractorTest, ExtractsNodataHttpsResponsesWithoutTtl) { constexpr char kName[] = "https.test"; // Response without a TTL-containing SOA record. DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeHttps, /*answers=*/{}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); // Expect empty result because not cacheable. ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), IsEmpty()); } TEST_F(DnsResponseResultExtractorTest, RejectsMalformedHttpsRecord) { constexpr char kName[] = "https.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestDnsRecord(kName, dns_protocol::kTypeHttps, "malformed rdata")} /* answers */); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kMalformedRecord); } TEST_F(DnsResponseResultExtractorTest, RejectsWrongNameHttpsRecord) { constexpr char kName[] = "https.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestHttpsAliasRecord("different.test", "alias.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kNameMismatch); } TEST_F(DnsResponseResultExtractorTest, IgnoresWrongTypeHttpsResponses) { constexpr char kName[] = "https.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, {BuildTestAddressRecord(kName, IPAddress(1, 2, 3, 4))}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT(results.value(), IsEmpty()); } TEST_F(DnsResponseResultExtractorTest, IgnoresAdditionalHttpsRecords) { constexpr char kName[] = "https.test"; constexpr auto kTtl = base::Days(5); // Give all records an "alpn" value to help validate that only the correct // record is used. DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeHttps, /*answers=*/ {BuildTestHttpsServiceRecord(kName, /*priority=*/5u, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo1"})}, kTtl)}, /*authority=*/{}, /*additional=*/ {BuildTestHttpsServiceRecord(kName, /*priority=*/3u, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo2"})}, base::Minutes(44)), BuildTestHttpsServiceRecord(kName, /*priority=*/2u, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo3"})}, base::Minutes(30))}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::HTTPS, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre(Pointee(ExpectHostResolverInternalMetadataResult( kName, DnsQueryType::HTTPS, kDnsSource, Eq(tick_clock_.NowTicks() + kTtl), Eq(clock_.Now() + kTtl), ElementsAre(Pair( 5, ExpectConnectionEndpointMetadata( ElementsAre("foo1", dns_protocol::kHttpsServiceDefaultAlpn), /*ech_config_list_matcher=*/IsEmpty(), kName))))))); } TEST_F(DnsResponseResultExtractorTest, IgnoresUnsolicitedHttpsRecords) { constexpr char kName[] = "name.test"; constexpr auto kTtl = base::Minutes(45); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeTXT, /*answers=*/ {BuildTestDnsRecord(kName, dns_protocol::kTypeTXT, "\003foo", kTtl)}, /*authority=*/{}, /*additional=*/ {BuildTestHttpsServiceRecord( "https.test", /*priority=*/3u, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo2"})}, base::Minutes(44)), BuildTestHttpsServiceRecord("https.test", /*priority=*/2u, /*service_name=*/".", /*params=*/ {BuildTestHttpsServiceAlpnParam({"foo3"})}, base::Minutes(30))}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); // Expect expiration to be derived only from the non-ignored answer record. EXPECT_THAT(results.value(), ElementsAre(Pointee(ExpectHostResolverInternalDataResult( kName, DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl), /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl), /*endpoints_matcher=*/IsEmpty(), ElementsAre("foo"))))); } TEST_F(DnsResponseResultExtractorTest, HandlesInOrderCnameChain) { constexpr char kName[] = "first.test"; DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeTXT, {BuildTestCnameRecord(kName, "second.test"), BuildTestCnameRecord("second.test", "third.test"), BuildTestCnameRecord("third.test", "fourth.test"), BuildTestTextRecord("fourth.test", {"foo"}), BuildTestTextRecord("fourth.test", {"bar"})}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")), Pointee(ExpectHostResolverInternalAliasResult( "second.test", DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")), Pointee(ExpectHostResolverInternalAliasResult( "third.test", DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")), Pointee(ExpectHostResolverInternalDataResult( "fourth.test", DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), /*endpoints_matcher=*/IsEmpty(), UnorderedElementsAre("foo", "bar"))))); } TEST_F(DnsResponseResultExtractorTest, HandlesInOrderCnameChainTypeA) { constexpr char kName[] = "first.test"; const IPAddress kExpected(192, 168, 0, 1); IPEndPoint expected_endpoint(kExpected, 0 /* port */); DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeA, {BuildTestCnameRecord(kName, "second.test"), BuildTestCnameRecord("second.test", "third.test"), BuildTestCnameRecord("third.test", "fourth.test"), BuildTestAddressRecord("fourth.test", kExpected)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")), Pointee(ExpectHostResolverInternalAliasResult( "second.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")), Pointee(ExpectHostResolverInternalAliasResult( "third.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")), Pointee(ExpectHostResolverInternalDataResult( "fourth.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(expected_endpoint))))); } TEST_F(DnsResponseResultExtractorTest, HandlesReverseOrderCnameChain) { constexpr char kName[] = "first.test"; DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeTXT, {BuildTestTextRecord("fourth.test", {"foo"}), BuildTestCnameRecord("third.test", "fourth.test"), BuildTestCnameRecord("second.test", "third.test"), BuildTestCnameRecord(kName, "second.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")), Pointee(ExpectHostResolverInternalAliasResult( "second.test", DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")), Pointee(ExpectHostResolverInternalAliasResult( "third.test", DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")), Pointee(ExpectHostResolverInternalDataResult( "fourth.test", DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), /*endpoints_matcher=*/IsEmpty(), ElementsAre("foo"))))); } TEST_F(DnsResponseResultExtractorTest, HandlesReverseOrderCnameChainTypeA) { constexpr char kName[] = "first.test"; const IPAddress kExpected(192, 168, 0, 1); IPEndPoint expected_endpoint(kExpected, 0 /* port */); DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeA, {BuildTestAddressRecord("fourth.test", kExpected), BuildTestCnameRecord("third.test", "fourth.test"), BuildTestCnameRecord("second.test", "third.test"), BuildTestCnameRecord(kName, "second.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")), Pointee(ExpectHostResolverInternalAliasResult( "second.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")), Pointee(ExpectHostResolverInternalAliasResult( "third.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")), Pointee(ExpectHostResolverInternalDataResult( "fourth.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(expected_endpoint))))); } TEST_F(DnsResponseResultExtractorTest, HandlesArbitraryOrderCnameChain) { constexpr char kName[] = "first.test"; DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeTXT, {BuildTestCnameRecord("second.test", "third.test"), BuildTestTextRecord("fourth.test", {"foo"}), BuildTestCnameRecord("third.test", "fourth.test"), BuildTestCnameRecord(kName, "second.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")), Pointee(ExpectHostResolverInternalAliasResult( "second.test", DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")), Pointee(ExpectHostResolverInternalAliasResult( "third.test", DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")), Pointee(ExpectHostResolverInternalDataResult( "fourth.test", DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), /*endpoints_matcher=*/IsEmpty(), ElementsAre("foo"))))); } TEST_F(DnsResponseResultExtractorTest, HandlesArbitraryOrderCnameChainTypeA) { constexpr char kName[] = "first.test"; const IPAddress kExpected(192, 168, 0, 1); IPEndPoint expected_endpoint(kExpected, 0 /* port */); // Alias names are chosen so that the chain order is not in alphabetical // order. DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeA, {BuildTestCnameRecord("qsecond.test", "athird.test"), BuildTestAddressRecord("zfourth.test", kExpected), BuildTestCnameRecord("athird.test", "zfourth.test"), BuildTestCnameRecord(kName, "qsecond.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "qsecond.test")), Pointee(ExpectHostResolverInternalAliasResult( "qsecond.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "athird.test")), Pointee(ExpectHostResolverInternalAliasResult( "athird.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "zfourth.test")), Pointee(ExpectHostResolverInternalDataResult( "zfourth.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(expected_endpoint))))); } TEST_F(DnsResponseResultExtractorTest, IgnoresNonResultTypesMixedWithCnameChain) { constexpr char kName[] = "first.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeTXT, {BuildTestCnameRecord("second.test", "third.test"), BuildTestTextRecord("fourth.test", {"foo"}), BuildTestCnameRecord("third.test", "fourth.test"), BuildTestAddressRecord("third.test", IPAddress(1, 2, 3, 4)), BuildTestCnameRecord(kName, "second.test"), BuildTestAddressRecord("fourth.test", IPAddress(2, 3, 4, 5))}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")), Pointee(ExpectHostResolverInternalAliasResult( "second.test", DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")), Pointee(ExpectHostResolverInternalAliasResult( "third.test", DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")), Pointee(ExpectHostResolverInternalDataResult( "fourth.test", DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), /*endpoints_matcher=*/IsEmpty(), ElementsAre("foo"))))); } TEST_F(DnsResponseResultExtractorTest, IgnoresNonResultTypesMixedWithCnameChainTypeA) { constexpr char kName[] = "first.test"; const IPAddress kExpected(192, 168, 0, 1); IPEndPoint expected_endpoint(kExpected, 0 /* port */); DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeA, {BuildTestCnameRecord("second.test", "third.test"), BuildTestTextRecord("fourth.test", {"foo"}), BuildTestCnameRecord("third.test", "fourth.test"), BuildTestCnameRecord(kName, "second.test"), BuildTestAddressRecord("fourth.test", kExpected)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")), Pointee(ExpectHostResolverInternalAliasResult( "second.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")), Pointee(ExpectHostResolverInternalAliasResult( "third.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")), Pointee(ExpectHostResolverInternalDataResult( "fourth.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(expected_endpoint))))); } TEST_F(DnsResponseResultExtractorTest, HandlesCnameChainWithoutResult) { constexpr char kName[] = "first.test"; DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeTXT, {BuildTestCnameRecord("second.test", "third.test"), BuildTestCnameRecord("third.test", "fourth.test"), BuildTestCnameRecord(kName, "second.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")), Pointee(ExpectHostResolverInternalAliasResult( "second.test", DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")), Pointee(ExpectHostResolverInternalAliasResult( "third.test", DnsQueryType::TXT, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")))); } TEST_F(DnsResponseResultExtractorTest, HandlesCnameChainWithoutResultTypeA) { constexpr char kName[] = "first.test"; DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeA, {BuildTestCnameRecord("second.test", "third.test"), BuildTestCnameRecord("third.test", "fourth.test"), BuildTestCnameRecord(kName, "second.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")), Pointee(ExpectHostResolverInternalAliasResult( "second.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")), Pointee(ExpectHostResolverInternalAliasResult( "third.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")))); } TEST_F(DnsResponseResultExtractorTest, RejectsCnameChainWithLoop) { constexpr char kName[] = "first.test"; DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeTXT, {BuildTestCnameRecord("second.test", "third.test"), BuildTestTextRecord("third.test", {"foo"}), BuildTestCnameRecord("third.test", "second.test"), BuildTestCnameRecord(kName, "second.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kBadAliasChain); } TEST_F(DnsResponseResultExtractorTest, RejectsCnameChainWithLoopToBeginning) { constexpr char kName[] = "first.test"; DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeTXT, {BuildTestCnameRecord("second.test", "third.test"), BuildTestTextRecord("third.test", {"foo"}), BuildTestCnameRecord("third.test", "first.test"), BuildTestCnameRecord(kName, "second.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kBadAliasChain); } TEST_F(DnsResponseResultExtractorTest, RejectsCnameChainWithLoopToBeginningWithoutResult) { constexpr char kName[] = "first.test"; DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeTXT, {BuildTestCnameRecord("second.test", "third.test"), BuildTestCnameRecord("third.test", "first.test"), BuildTestCnameRecord(kName, "second.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kBadAliasChain); } TEST_F(DnsResponseResultExtractorTest, RejectsCnameChainWithWrongStart) { constexpr char kName[] = "test.test"; DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeTXT, {BuildTestCnameRecord("second.test", "third.test"), BuildTestTextRecord("fourth.test", {"foo"}), BuildTestCnameRecord("third.test", "fourth.test"), BuildTestCnameRecord("first.test", "second.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kBadAliasChain); } TEST_F(DnsResponseResultExtractorTest, RejectsCnameChainWithWrongResultName) { constexpr char kName[] = "first.test"; DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeTXT, {BuildTestCnameRecord("second.test", "third.test"), BuildTestTextRecord("third.test", {"foo"}), BuildTestCnameRecord("third.test", "fourth.test"), BuildTestCnameRecord(kName, "second.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kNameMismatch); } TEST_F(DnsResponseResultExtractorTest, RejectsCnameSharedWithResult) { constexpr char kName[] = "first.test"; DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeTXT, {BuildTestCnameRecord("second.test", "third.test"), BuildTestTextRecord(kName, {"foo"}), BuildTestCnameRecord("third.test", "fourth.test"), BuildTestCnameRecord(kName, "second.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kNameMismatch); } TEST_F(DnsResponseResultExtractorTest, RejectsDisjointCnameChain) { constexpr char kName[] = "first.test"; DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeTXT, {BuildTestCnameRecord("second.test", "third.test"), BuildTestTextRecord("fourth.test", {"foo"}), BuildTestCnameRecord("third.test", "fourth.test"), BuildTestCnameRecord("other1.test", "other2.test"), BuildTestCnameRecord(kName, "second.test"), BuildTestCnameRecord("other2.test", "other3.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kBadAliasChain); } TEST_F(DnsResponseResultExtractorTest, RejectsDoubledCnames) { constexpr char kName[] = "first.test"; DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeTXT, {BuildTestCnameRecord("second.test", "third.test"), BuildTestTextRecord("fourth.test", {"foo"}), BuildTestCnameRecord("third.test", "fourth.test"), BuildTestCnameRecord("third.test", "fifth.test"), BuildTestCnameRecord(kName, "second.test")}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kMultipleCnames); } TEST_F(DnsResponseResultExtractorTest, IgnoresTtlFromNonResultType) { constexpr char kName[] = "name.test"; constexpr base::TimeDelta kMinTtl = base::Minutes(4); DnsResponse response = BuildTestDnsResponse( kName, dns_protocol::kTypeTXT, {BuildTestTextRecord(kName, {"foo"}, base::Hours(3)), BuildTestTextRecord(kName, {"bar"}, kMinTtl), BuildTestAddressRecord(kName, IPAddress(1, 2, 3, 4), base::Seconds(2)), BuildTestTextRecord(kName, {"baz"}, base::Minutes(15))}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), ElementsAre(Pointee(ExpectHostResolverInternalDataResult( kName, DnsQueryType::TXT, kDnsSource, Eq(tick_clock_.NowTicks() + kMinTtl), Eq(clock_.Now() + kMinTtl), /*endpoints_matcher=*/IsEmpty(), UnorderedElementsAre("foo", "bar", "baz"))))); } TEST_F(DnsResponseResultExtractorTest, ExtractsTtlFromCname) { constexpr char kName[] = "name.test"; constexpr char kAlias[] = "alias.test"; constexpr base::TimeDelta kTtl = base::Minutes(4); DnsResponse response = BuildTestDnsResponse("name.test", dns_protocol::kTypeTXT, {BuildTestCnameRecord(kName, kAlias, kTtl)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::TXT, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre(Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::TXT, kDnsSource, Eq(tick_clock_.NowTicks() + kTtl), Eq(clock_.Now() + kTtl), kAlias)))); } TEST_F(DnsResponseResultExtractorTest, ValidatesAliasNames) { constexpr char kName[] = "first.test"; const IPAddress kExpected(192, 168, 0, 1); IPEndPoint expected_endpoint(kExpected, 0 /* port */); DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeA, {BuildTestCnameRecord(kName, "second.test"), BuildTestCnameRecord("second.test", "localhost"), BuildTestCnameRecord("localhost", "fourth.test"), BuildTestAddressRecord("fourth.test", kExpected)}); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); EXPECT_EQ(extractor .ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0) .error_or(ExtractionError::kOk), ExtractionError::kMalformedRecord); } TEST_F(DnsResponseResultExtractorTest, CanonicalizesAliasNames) { const IPAddress kExpected(192, 168, 0, 1); constexpr char kName[] = "address.test"; constexpr char kCname[] = "\005ALIAS\004test\000"; // Need to build records directly in order to manually encode alias target // name because BuildTestDnsAddressResponseWithCname() uses // DNSDomainFromDot() which does not support non-URL-canonicalized names. std::vector answers = { BuildTestDnsRecord(kName, dns_protocol::kTypeCNAME, std::string(kCname, sizeof(kCname) - 1)), BuildTestAddressRecord("alias.test", kExpected)}; DnsResponse response = BuildTestDnsResponse(kName, dns_protocol::kTypeA, answers); DnsResponseResultExtractor extractor(response, clock_, tick_clock_); ResultsOrError results = extractor.ExtractDnsResults(DnsQueryType::A, /*original_domain_name=*/kName, /*request_port=*/0); ASSERT_TRUE(results.has_value()); EXPECT_THAT( results.value(), UnorderedElementsAre( Pointee(ExpectHostResolverInternalAliasResult( kName, DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), "alias.test")), Pointee(ExpectHostResolverInternalDataResult( "alias.test", DnsQueryType::A, kDnsSource, /*expiration_matcher=*/Ne(std::nullopt), /*timed_expiration_matcher=*/Ne(std::nullopt), ElementsAre(IPEndPoint(kExpected, /*port=*/0)))))); } } // namespace } // namespace net