• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2024 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "net/dns/dns_task_results_manager.h"
6 
7 #include <algorithm>
8 #include <map>
9 #include <memory>
10 #include <set>
11 #include <string>
12 #include <vector>
13 
14 #include "base/memory/raw_ptr.h"
15 #include "base/strings/string_number_conversions.h"
16 #include "base/time/time.h"
17 #include "base/timer/timer.h"
18 #include "net/base/connection_endpoint_metadata.h"
19 #include "net/base/ip_endpoint.h"
20 #include "net/base/net_errors.h"
21 #include "net/dns/host_resolver.h"
22 #include "net/dns/host_resolver_dns_task.h"
23 #include "net/dns/host_resolver_internal_result.h"
24 #include "net/dns/https_record_rdata.h"
25 #include "net/dns/public/dns_query_type.h"
26 #include "net/dns/public/host_resolver_results.h"
27 #include "net/log/net_log_event_type.h"
28 #include "net/log/net_log_with_source.h"
29 #include "third_party/abseil-cpp/absl/types/variant.h"
30 #include "url/scheme_host_port.h"
31 
32 namespace net {
33 
34 namespace {
35 
36 // Prioritize with-ipv6 over ipv4-only.
CompareServiceEndpointAddresses(const ServiceEndpoint & a,const ServiceEndpoint & b)37 bool CompareServiceEndpointAddresses(const ServiceEndpoint& a,
38                                      const ServiceEndpoint& b) {
39   const bool a_has_ipv6 = !a.ipv6_endpoints.empty();
40   const bool b_has_ipv6 = !b.ipv6_endpoints.empty();
41   if ((a_has_ipv6 && b_has_ipv6) || (!a_has_ipv6 && !b_has_ipv6)) {
42     return false;
43   }
44 
45   if (b_has_ipv6) {
46     return false;
47   }
48 
49   return true;
50 }
51 
52 // Prioritize with-metadata, with-ipv6 over ipv4-only.
53 // TODO(crbug.com/41493696): Consider which fields should be prioritized. We
54 // may want to have different sorting algorithms and choose one via config.
CompareServiceEndpoint(const ServiceEndpoint & a,const ServiceEndpoint & b)55 bool CompareServiceEndpoint(const ServiceEndpoint& a,
56                             const ServiceEndpoint& b) {
57   const bool a_has_metadata = a.metadata != ConnectionEndpointMetadata();
58   const bool b_has_metadata = b.metadata != ConnectionEndpointMetadata();
59   if (a_has_metadata && b_has_metadata) {
60     return CompareServiceEndpointAddresses(a, b);
61   }
62 
63   if (a_has_metadata) {
64     return true;
65   }
66 
67   if (b_has_metadata) {
68     return false;
69   }
70 
71   return CompareServiceEndpointAddresses(a, b);
72 }
73 
74 }  // namespace
75 
76 // Holds service endpoint results per domain name.
77 struct DnsTaskResultsManager::PerDomainResult {
78   PerDomainResult() = default;
79   ~PerDomainResult() = default;
80 
81   PerDomainResult(PerDomainResult&&) = default;
82   PerDomainResult& operator=(PerDomainResult&&) = default;
83   PerDomainResult(const PerDomainResult&) = delete;
84   PerDomainResult& operator=(const PerDomainResult&) = delete;
85 
86   std::vector<IPEndPoint> ipv4_endpoints;
87   std::vector<IPEndPoint> ipv6_endpoints;
88 
89   std::multimap<HttpsRecordPriority, ConnectionEndpointMetadata> metadatas;
90 };
91 
DnsTaskResultsManager(Delegate * delegate,HostResolver::Host host,DnsQueryTypeSet query_types,const NetLogWithSource & net_log)92 DnsTaskResultsManager::DnsTaskResultsManager(Delegate* delegate,
93                                              HostResolver::Host host,
94                                              DnsQueryTypeSet query_types,
95                                              const NetLogWithSource& net_log)
96     : delegate_(delegate),
97       host_(std::move(host)),
98       query_types_(query_types),
99       net_log_(net_log) {
100   CHECK(delegate_);
101 }
102 
103 DnsTaskResultsManager::~DnsTaskResultsManager() = default;
104 
ProcessDnsTransactionResults(DnsQueryType query_type,std::set<const HostResolverInternalResult * > results)105 void DnsTaskResultsManager::ProcessDnsTransactionResults(
106     DnsQueryType query_type,
107     std::set<const HostResolverInternalResult*> results) {
108   CHECK(query_types_.Has(query_type));
109 
110   bool should_update_endpoints = false;
111   bool should_notify = false;
112 
113   if (query_type == DnsQueryType::HTTPS) {
114     // Chrome does not yet support HTTPS follow-up queries so metadata is
115     // considered ready when the HTTPS response is received.
116     CHECK(!is_metadata_ready_);
117     is_metadata_ready_ = true;
118     should_notify = true;
119   }
120 
121   if (query_type == DnsQueryType::AAAA) {
122     aaaa_response_received_ = true;
123     if (resolution_delay_timer_.IsRunning()) {
124       resolution_delay_timer_.Stop();
125       RecordResolutionDelayResult(/*timedout=*/false);
126       // Need to update endpoints when there are IPv4 addresses.
127       if (HasIpv4Addresses()) {
128         should_update_endpoints = true;
129       }
130     }
131   }
132 
133   for (const auto& result : results) {
134     aliases_.insert(result->domain_name());
135 
136     switch (result->type()) {
137       case HostResolverInternalResult::Type::kData: {
138         PerDomainResult& per_domain_result =
139             GetOrCreatePerDomainResult(result->domain_name());
140         for (const auto& ip_endpoint : result->AsData().endpoints()) {
141           CHECK_EQ(ip_endpoint.port(), 0);
142           // TODO(crbug.com/41493696): This will eventually need to handle
143           // DnsQueryType::HTTPS to support getting ipv{4,6}hints.
144           if (ip_endpoint.address().IsIPv4()) {
145             per_domain_result.ipv4_endpoints.emplace_back(ip_endpoint.address(),
146                                                           host_.GetPort());
147           } else {
148             CHECK(ip_endpoint.address().IsIPv6());
149             per_domain_result.ipv6_endpoints.emplace_back(ip_endpoint.address(),
150                                                           host_.GetPort());
151           }
152         }
153 
154         should_update_endpoints |= !result->AsData().endpoints().empty();
155 
156         break;
157       }
158       case HostResolverInternalResult::Type::kMetadata: {
159         CHECK_EQ(query_type, DnsQueryType::HTTPS);
160         for (auto [priority, metadata] : result->AsMetadata().metadatas()) {
161           // Associate the metadata with the target name instead of the domain
162           // name since the metadata is for the target name.
163           PerDomainResult& per_domain_result =
164               GetOrCreatePerDomainResult(metadata.target_name);
165           per_domain_result.metadatas.emplace(priority, metadata);
166         }
167 
168         should_update_endpoints |= !result->AsMetadata().metadatas().empty();
169 
170         break;
171       }
172       case net::HostResolverInternalResult::Type::kAlias:
173         aliases_.insert(result->AsAlias().alias_target());
174 
175         break;
176       case net::HostResolverInternalResult::Type::kError:
177         // Need to update endpoints when AAAA response is NODATA but A response
178         // has at least one valid address.
179         // TODO(crbug.com/41493696): Revisit how to handle errors other than
180         // NODATA. Currently we just ignore errors here and defer
181         // HostResolverManager::Job to create an error result and notify the
182         // error to the corresponding requests. This means that if the
183         // connection layer has already attempted a connection using an
184         // intermediate endpoint, the error might not be treated as fatal. We
185         // may want to have a different semantics.
186         PerDomainResult& per_domain_result =
187             GetOrCreatePerDomainResult(result->domain_name());
188         if (query_type == DnsQueryType::AAAA &&
189             result->AsError().error() == ERR_NAME_NOT_RESOLVED &&
190             !per_domain_result.ipv4_endpoints.empty()) {
191           CHECK(per_domain_result.ipv6_endpoints.empty());
192           should_update_endpoints = true;
193         }
194 
195         break;
196     }
197   }
198 
199   const bool waiting_for_aaaa_response =
200       query_types_.Has(DnsQueryType::AAAA) && !aaaa_response_received_;
201   if (waiting_for_aaaa_response) {
202     if (query_type == DnsQueryType::A && should_update_endpoints) {
203       // A is responded, start the resolution delay timer.
204       CHECK(!resolution_delay_timer_.IsRunning());
205       resolution_delay_start_time_ = base::TimeTicks::Now();
206       net_log_.BeginEvent(
207           NetLogEventType::HOST_RESOLVER_SERVICE_ENDPOINTS_RESOLUTION_DELAY);
208       // Safe to unretain since `this` owns the timer.
209       resolution_delay_timer_.Start(
210           FROM_HERE, kResolutionDelay,
211           base::BindOnce(&DnsTaskResultsManager::OnAaaaResolutionTimedout,
212                          base::Unretained(this)));
213     }
214 
215     return;
216   }
217 
218   if (should_update_endpoints) {
219     UpdateEndpoints();
220     return;
221   }
222 
223   if (should_notify && !current_endpoints_.empty()) {
224     delegate_->OnServiceEndpointsUpdated();
225   }
226 }
227 
GetCurrentEndpoints() const228 const std::vector<ServiceEndpoint>& DnsTaskResultsManager::GetCurrentEndpoints()
229     const {
230   return current_endpoints_;
231 }
232 
GetAliases() const233 const std::set<std::string>& DnsTaskResultsManager::GetAliases() const {
234   return aliases_;
235 }
236 
IsMetadataReady() const237 bool DnsTaskResultsManager::IsMetadataReady() const {
238   return !query_types_.Has(DnsQueryType::HTTPS) || is_metadata_ready_;
239 }
240 
241 DnsTaskResultsManager::PerDomainResult&
GetOrCreatePerDomainResult(const std::string & domain_name)242 DnsTaskResultsManager::GetOrCreatePerDomainResult(
243     const std::string& domain_name) {
244   auto it = per_domain_results_.find(domain_name);
245   if (it == per_domain_results_.end()) {
246     it = per_domain_results_.try_emplace(it, domain_name,
247                                          std::make_unique<PerDomainResult>());
248   }
249   return *it->second;
250 }
251 
OnAaaaResolutionTimedout()252 void DnsTaskResultsManager::OnAaaaResolutionTimedout() {
253   CHECK(!aaaa_response_received_);
254   RecordResolutionDelayResult(/*timedout=*/true);
255   UpdateEndpoints();
256 }
257 
UpdateEndpoints()258 void DnsTaskResultsManager::UpdateEndpoints() {
259   std::vector<ServiceEndpoint> new_endpoints;
260 
261   for (const auto& [domain_name, per_domain_result] : per_domain_results_) {
262     if (per_domain_result->ipv4_endpoints.empty() &&
263         per_domain_result->ipv6_endpoints.empty()) {
264       continue;
265     }
266 
267     if (per_domain_result->metadatas.empty()) {
268       ServiceEndpoint endpoint;
269       endpoint.ipv4_endpoints = per_domain_result->ipv4_endpoints;
270       endpoint.ipv6_endpoints = per_domain_result->ipv6_endpoints;
271       new_endpoints.emplace_back(std::move(endpoint));
272     } else {
273       for (const auto& [_, metadata] : per_domain_result->metadatas) {
274         ServiceEndpoint endpoint;
275         endpoint.ipv4_endpoints = per_domain_result->ipv4_endpoints;
276         endpoint.ipv6_endpoints = per_domain_result->ipv6_endpoints;
277         // TODO(crbug.com/41493696): Just adding per-domain metadata does not
278         // work properly when the target name of HTTPS is an alias, e.g:
279         //   example.com.     60 IN CNAME svc.example.com.
280         //   svc.example.com. 60 IN AAAA  2001:db8::1
281         //   svc.example.com. 60 IN HTTPS 1 example.com alpn="h2"
282         // In this case, svc.example.com should have metadata with alpn="h2" but
283         // the current logic doesn't do that. To handle it correctly we need to
284         // go though an alias tree for the domain name.
285         endpoint.metadata = metadata;
286         new_endpoints.emplace_back(std::move(endpoint));
287       }
288     }
289   }
290 
291   // TODO(crbug.com/41493696): Determine how to handle non-SVCB connection
292   // fallback. See https://datatracker.ietf.org/doc/html/rfc9460#section-3-8
293   // HostCache::Entry::GetEndpoints() appends a final non-alternative endpoint
294   // at the end to ensure that the connection layer can fall back to non-SVCB
295   // connection. For ServiceEndpoint request API, the current plan is to handle
296   // non-SVCB connection fallback in the connection layer. The approach might
297   // not work when Chrome tries to support HTTPS follow-up queries and aliases.
298 
299   // Stable sort preserves metadata priorities.
300   std::stable_sort(new_endpoints.begin(), new_endpoints.end(),
301                    CompareServiceEndpoint);
302   current_endpoints_ = std::move(new_endpoints);
303 
304   if (current_endpoints_.empty()) {
305     return;
306   }
307 
308   net_log_.AddEvent(NetLogEventType::HOST_RESOLVER_SERVICE_ENDPOINTS_UPDATED,
309                     [&] {
310                       base::Value::Dict dict;
311                       base::Value::List endpoints;
312                       for (const auto& endpoint : current_endpoints_) {
313                         endpoints.Append(endpoint.ToValue());
314                       }
315                       dict.Set("endpoints", std::move(endpoints));
316                       return dict;
317                     });
318 
319   delegate_->OnServiceEndpointsUpdated();
320 }
321 
HasIpv4Addresses()322 bool DnsTaskResultsManager::HasIpv4Addresses() {
323   for (const auto& [_, per_domain_result] : per_domain_results_) {
324     if (!per_domain_result->ipv4_endpoints.empty()) {
325       return true;
326     }
327   }
328   return false;
329 }
330 
RecordResolutionDelayResult(bool timedout)331 void DnsTaskResultsManager::RecordResolutionDelayResult(bool timedout) {
332   net_log_.EndEvent(
333       NetLogEventType::HOST_RESOLVER_SERVICE_ENDPOINTS_RESOLUTION_DELAY, [&]() {
334         base::TimeDelta elapsed =
335             base::TimeTicks::Now() - resolution_delay_start_time_;
336         base::Value::Dict dict;
337         dict.Set("timedout", timedout);
338         dict.Set("elapsed", base::NumberToString(elapsed.InMilliseconds()));
339         return dict;
340       });
341 }
342 
343 }  // namespace net
344