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