| // Copyright 2024 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/host_resolver_manager_job.h" |
| |
| #include <deque> |
| #include <memory> |
| #include <optional> |
| #include <vector> |
| |
| #include "base/containers/linked_list.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/safe_ref.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/time/time.h" |
| #include "net/base/address_family.h" |
| #include "net/base/features.h" |
| #include "net/base/network_anonymization_key.h" |
| #include "net/base/network_handle.h" |
| #include "net/base/prioritized_dispatcher.h" |
| #include "net/base/url_util.h" |
| #include "net/dns/dns_client.h" |
| #include "net/dns/dns_task_results_manager.h" |
| #include "net/dns/host_cache.h" |
| #include "net/dns/host_resolver.h" |
| #include "net/dns/host_resolver_dns_task.h" |
| #include "net/dns/host_resolver_manager.h" |
| #include "net/dns/host_resolver_manager_request_impl.h" |
| #include "net/dns/host_resolver_manager_service_endpoint_request_impl.h" |
| #include "net/dns/host_resolver_mdns_task.h" |
| #include "net/dns/host_resolver_nat64_task.h" |
| #include "net/dns/public/dns_query_type.h" |
| #include "net/dns/public/secure_dns_mode.h" |
| #include "net/log/net_log_with_source.h" |
| #include "third_party/abseil-cpp/absl/types/variant.h" |
| #include "url/url_constants.h" |
| |
| namespace net { |
| |
| namespace { |
| |
| // Default TTL for successful resolutions with HostResolverSystemTask. |
| const unsigned kCacheEntryTTLSeconds = 60; |
| |
| // Default TTL for unsuccessful resolutions with HostResolverSystemTask. |
| const unsigned kNegativeCacheEntryTTLSeconds = 0; |
| |
| // Minimum TTL for successful resolutions with HostResolverDnsTask. |
| const unsigned kMinimumTTLSeconds = kCacheEntryTTLSeconds; |
| |
| // ICANN uses this localhost address to indicate a name collision. |
| // |
| // The policy in Chromium is to fail host resolving if it resolves to |
| // this special address. |
| // |
| // Not however that IP literals are exempt from this policy, so it is still |
| // possible to navigate to http://127.0.53.53/ directly. |
| // |
| // For more details: https://www.icann.org/news/announcement-2-2014-08-01-en |
| const uint8_t kIcanNameCollisionIp[] = {127, 0, 53, 53}; |
| |
| bool ContainsIcannNameCollisionIp(const std::vector<IPEndPoint>& endpoints) { |
| for (const auto& endpoint : endpoints) { |
| const IPAddress& addr = endpoint.address(); |
| if (addr.IsIPv4() && IPAddressStartsWith(addr, kIcanNameCollisionIp)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Creates NetLog parameters for HOST_RESOLVER_MANAGER_JOB_ATTACH/DETACH events. |
| base::Value::Dict NetLogJobAttachParams(const NetLogSource& source, |
| RequestPriority priority) { |
| base::Value::Dict dict; |
| source.AddToEventParameters(dict); |
| dict.Set("priority", RequestPriorityToString(priority)); |
| return dict; |
| } |
| |
| bool IsSchemeHttpsOrWss(const HostResolver::Host& host) { |
| if (!host.HasScheme()) { |
| return false; |
| } |
| const std::string& scheme = host.GetScheme(); |
| return scheme == url::kHttpsScheme || scheme == url::kWssScheme; |
| } |
| |
| } // namespace |
| |
| HostResolverManager::JobKey::JobKey(HostResolver::Host host, |
| ResolveContext* resolve_context) |
| : host(std::move(host)), resolve_context(resolve_context->GetWeakPtr()) {} |
| |
| HostResolverManager::JobKey::~JobKey() = default; |
| |
| HostResolverManager::JobKey::JobKey(const JobKey& other) = default; |
| HostResolverManager::JobKey& HostResolverManager::JobKey::operator=( |
| const JobKey& other) = default; |
| |
| bool HostResolverManager::JobKey::operator<(const JobKey& other) const { |
| return std::forward_as_tuple(query_types.ToEnumBitmask(), flags, source, |
| secure_dns_mode, &*resolve_context, host, |
| network_anonymization_key) < |
| std::forward_as_tuple(other.query_types.ToEnumBitmask(), other.flags, |
| other.source, other.secure_dns_mode, |
| &*other.resolve_context, other.host, |
| other.network_anonymization_key); |
| } |
| |
| bool HostResolverManager::JobKey::operator==(const JobKey& other) const { |
| return !(*this < other || other < *this); |
| } |
| |
| HostCache::Key HostResolverManager::JobKey::ToCacheKey(bool secure) const { |
| if (query_types.size() != 1) { |
| // This function will produce identical cache keys for `JobKey` structs |
| // that differ only in their (non-singleton) `query_types` fields. When we |
| // enable new query types, this behavior could lead to subtle bugs. That |
| // is why the following DCHECK restricts the allowable query types. |
| DCHECK(Difference(query_types, {DnsQueryType::A, DnsQueryType::AAAA, |
| DnsQueryType::HTTPS}) |
| .empty()); |
| } |
| const DnsQueryType query_type_for_key = query_types.size() == 1 |
| ? *query_types.begin() |
| : DnsQueryType::UNSPECIFIED; |
| absl::variant<url::SchemeHostPort, std::string> host_for_cache; |
| if (host.HasScheme()) { |
| host_for_cache = host.AsSchemeHostPort(); |
| } else { |
| host_for_cache = std::string(host.GetHostnameWithoutBrackets()); |
| } |
| HostCache::Key key(std::move(host_for_cache), query_type_for_key, flags, |
| source, network_anonymization_key); |
| key.secure = secure; |
| return key; |
| } |
| |
| handles::NetworkHandle HostResolverManager::JobKey::GetTargetNetwork() const { |
| return resolve_context ? resolve_context->GetTargetNetwork() |
| : handles::kInvalidNetworkHandle; |
| } |
| |
| HostResolverManager::Job::Job( |
| const base::WeakPtr<HostResolverManager>& resolver, |
| JobKey key, |
| ResolveHostParameters::CacheUsage cache_usage, |
| HostCache* host_cache, |
| std::deque<TaskType> tasks, |
| RequestPriority priority, |
| const NetLogWithSource& source_net_log, |
| const base::TickClock* tick_clock, |
| const HostResolver::HttpsSvcbOptions& https_svcb_options) |
| : resolver_(resolver), |
| key_(std::move(key)), |
| cache_usage_(cache_usage), |
| host_cache_(host_cache), |
| tasks_(tasks), |
| priority_tracker_(priority), |
| tick_clock_(tick_clock), |
| https_svcb_options_(https_svcb_options), |
| net_log_( |
| NetLogWithSource::Make(source_net_log.net_log(), |
| NetLogSourceType::HOST_RESOLVER_IMPL_JOB)) { |
| source_net_log.AddEvent(NetLogEventType::HOST_RESOLVER_MANAGER_CREATE_JOB); |
| |
| net_log_.BeginEvent(NetLogEventType::HOST_RESOLVER_MANAGER_JOB, [&] { |
| return NetLogJobCreationParams(source_net_log.source()); |
| }); |
| } |
| |
| HostResolverManager::Job::~Job() { |
| bool was_queued = is_queued(); |
| bool was_running = is_running(); |
| // Clean up now for nice NetLog. |
| Finish(); |
| if (was_running) { |
| // This Job was destroyed while still in flight. |
| net_log_.EndEventWithNetErrorCode( |
| NetLogEventType::HOST_RESOLVER_MANAGER_JOB, ERR_ABORTED); |
| } else if (was_queued) { |
| // Job was cancelled before it could run. |
| // TODO(szym): is there any benefit in having this distinction? |
| net_log_.AddEvent(NetLogEventType::CANCELLED); |
| net_log_.EndEvent(NetLogEventType::HOST_RESOLVER_MANAGER_JOB); |
| } |
| // else CompleteRequests logged EndEvent. |
| while (!requests_.empty()) { |
| // Log any remaining Requests as cancelled. |
| RequestImpl* req = requests_.head()->value(); |
| req->RemoveFromList(); |
| CHECK(key_ == req->GetJobKey()); |
| req->OnJobCancelled(key_); |
| } |
| |
| while (!service_endpoint_requests_.empty()) { |
| ServiceEndpointRequestImpl* request = |
| service_endpoint_requests_.head()->value(); |
| request->RemoveFromList(); |
| request->OnJobCancelled(); |
| } |
| } |
| |
| void HostResolverManager::Job::Schedule(bool at_head) { |
| DCHECK(!is_queued()); |
| PrioritizedDispatcher::Handle handle; |
| DCHECK(dispatched_); |
| if (!at_head) { |
| handle = resolver_->dispatcher_->Add(this, priority()); |
| } else { |
| handle = resolver_->dispatcher_->AddAtHead(this, priority()); |
| } |
| // The dispatcher could have started |this| in the above call to Add, which |
| // could have called Schedule again. In that case |handle| will be null, |
| // but |handle_| may have been set by the other nested call to Schedule. |
| if (!handle.is_null()) { |
| DCHECK(handle_.is_null()); |
| handle_ = handle; |
| } |
| } |
| |
| void HostResolverManager::Job::AddRequest(RequestImpl* request) { |
| // Job currently assumes a 1:1 correspondence between ResolveContext and |
| // HostCache. Since the ResolveContext is part of the JobKey, any request |
| // added to any existing Job should share the same HostCache. |
| DCHECK_EQ(host_cache_, request->host_cache()); |
| // TODO(crbug.com/40181080): Check equality of whole host once Jobs are |
| // separated by scheme/port. |
| DCHECK_EQ(key_.host.GetHostnameWithoutBrackets(), |
| request->request_host().GetHostnameWithoutBrackets()); |
| |
| request->AssignJob(weak_ptr_factory_.GetSafeRef()); |
| |
| AddRequestCommon(request->priority(), request->source_net_log(), |
| request->parameters().is_speculative); |
| |
| requests_.Append(request); |
| |
| UpdatePriority(); |
| } |
| |
| void HostResolverManager::Job::ChangeRequestPriority(RequestImpl* req, |
| RequestPriority priority) { |
| DCHECK_EQ(key_.host, req->request_host()); |
| |
| priority_tracker_.Remove(req->priority()); |
| req->set_priority(priority); |
| priority_tracker_.Add(req->priority()); |
| UpdatePriority(); |
| } |
| |
| void HostResolverManager::Job::CancelRequest(RequestImpl* request) { |
| DCHECK_EQ(key_.host, request->request_host()); |
| DCHECK(!requests_.empty()); |
| |
| CancelRequestCommon(request->priority(), request->source_net_log()); |
| |
| if (num_active_requests() > 0) { |
| UpdatePriority(); |
| request->RemoveFromList(); |
| } else { |
| // If we were called from a Request's callback within CompleteRequests, |
| // that Request could not have been cancelled, so num_active_requests() |
| // could not be 0. Therefore, we are not in CompleteRequests(). |
| CompleteRequestsWithError(ERR_DNS_REQUEST_CANCELLED, |
| /*task_type=*/std::nullopt); |
| } |
| } |
| |
| void HostResolverManager::Job::AddServiceEndpointRequest( |
| ServiceEndpointRequestImpl* request) { |
| CHECK_EQ(host_cache_, request->host_cache()); |
| |
| request->AssignJob(weak_ptr_factory_.GetSafeRef()); |
| |
| AddRequestCommon(request->priority(), request->net_log(), |
| request->parameters().is_speculative); |
| |
| service_endpoint_requests_.Append(request); |
| |
| UpdatePriority(); |
| } |
| |
| void HostResolverManager::Job::CancelServiceEndpointRequest( |
| ServiceEndpointRequestImpl* request) { |
| CancelRequestCommon(request->priority(), request->net_log()); |
| |
| if (num_active_requests() > 0) { |
| UpdatePriority(); |
| request->RemoveFromList(); |
| } else { |
| // See comments in CancelRequest(). |
| CompleteRequestsWithError(ERR_DNS_REQUEST_CANCELLED, |
| /*task_type=*/std::nullopt); |
| } |
| } |
| |
| void HostResolverManager::Job::Abort() { |
| CompleteRequestsWithError(ERR_NETWORK_CHANGED, /*task_type=*/std::nullopt); |
| } |
| |
| base::OnceClosure HostResolverManager::Job::GetAbortInsecureDnsTaskClosure( |
| int error, |
| bool fallback_only) { |
| return base::BindOnce(&Job::AbortInsecureDnsTask, |
| weak_ptr_factory_.GetWeakPtr(), error, fallback_only); |
| } |
| |
| void HostResolverManager::Job::AbortInsecureDnsTask(int error, |
| bool fallback_only) { |
| bool has_system_fallback = base::Contains(tasks_, TaskType::SYSTEM); |
| if (has_system_fallback) { |
| for (auto it = tasks_.begin(); it != tasks_.end();) { |
| if (*it == TaskType::DNS) { |
| it = tasks_.erase(it); |
| } else { |
| ++it; |
| } |
| } |
| } |
| |
| if (dns_task_ && !dns_task_->secure()) { |
| if (has_system_fallback) { |
| KillDnsTask(); |
| dns_task_error_ = OK; |
| RunNextTask(); |
| } else if (!fallback_only) { |
| CompleteRequestsWithError(error, /*task_type=*/std::nullopt); |
| } |
| } |
| } |
| |
| void HostResolverManager::Job::OnEvicted() { |
| DCHECK(!is_running()); |
| DCHECK(is_queued()); |
| handle_.Reset(); |
| |
| net_log_.AddEvent(NetLogEventType::HOST_RESOLVER_MANAGER_JOB_EVICTED); |
| |
| // This signals to CompleteRequests that parts of this job never ran. |
| // Job must be saved in |resolver_| to be completed asynchronously. |
| // Otherwise the job will be destroyed with requests silently cancelled |
| // before completion runs. |
| DCHECK(self_iterator_); |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(&Job::CompleteRequestsWithError, |
| weak_ptr_factory_.GetWeakPtr(), |
| ERR_HOST_RESOLVER_QUEUE_TOO_LARGE, |
| /*task_type=*/std::nullopt)); |
| } |
| |
| bool HostResolverManager::Job::ServeFromHosts() { |
| DCHECK_GT(num_active_requests(), 0u); |
| std::optional<HostCache::Entry> results = resolver_->ServeFromHosts( |
| key_.host.GetHostnameWithoutBrackets(), key_.query_types, |
| key_.flags & HOST_RESOLVER_DEFAULT_FAMILY_SET_DUE_TO_NO_IPV6, tasks_); |
| if (results) { |
| // This will destroy the Job. |
| CompleteRequests(results.value(), base::TimeDelta(), true /* allow_cache */, |
| true /* secure */, TaskType::HOSTS); |
| return true; |
| } |
| return false; |
| } |
| |
| void HostResolverManager::Job::OnAddedToJobMap(JobMap::iterator iterator) { |
| DCHECK(!self_iterator_); |
| DCHECK(iterator != resolver_->jobs_.end()); |
| self_iterator_ = iterator; |
| } |
| |
| void HostResolverManager::Job::OnRemovedFromJobMap() { |
| DCHECK(self_iterator_); |
| self_iterator_ = std::nullopt; |
| } |
| |
| void HostResolverManager::Job::RunNextTask() { |
| // If there are no tasks left to try, cache any stored results and complete |
| // the request with the last stored result. All stored results should be |
| // errors. |
| if (tasks_.empty()) { |
| // If there are no stored results, complete with an error. |
| if (completion_results_.size() == 0) { |
| CompleteRequestsWithError(ERR_NAME_NOT_RESOLVED, |
| /*task_type=*/std::nullopt); |
| return; |
| } |
| |
| // Cache all but the last result here. The last result will be cached |
| // as part of CompleteRequests. |
| for (size_t i = 0; i < completion_results_.size() - 1; ++i) { |
| const auto& result = completion_results_[i]; |
| DCHECK_NE(OK, result.entry.error()); |
| MaybeCacheResult(result.entry, result.ttl, result.secure); |
| } |
| const auto& last_result = completion_results_.back(); |
| DCHECK_NE(OK, last_result.entry.error()); |
| CompleteRequests(last_result.entry, last_result.ttl, true /* allow_cache */, |
| last_result.secure, |
| last_result.secure ? TaskType::SECURE_DNS : TaskType::DNS); |
| return; |
| } |
| |
| TaskType next_task = tasks_.front(); |
| |
| // Schedule insecure DnsTasks and HostResolverSystemTasks with the |
| // dispatcher. |
| if (!dispatched_ && |
| (next_task == TaskType::DNS || next_task == TaskType::SYSTEM || |
| next_task == TaskType::MDNS)) { |
| dispatched_ = true; |
| job_running_ = false; |
| Schedule(false); |
| DCHECK(is_running() || is_queued()); |
| |
| // Check for queue overflow. |
| PrioritizedDispatcher& dispatcher = *resolver_->dispatcher_; |
| if (dispatcher.num_queued_jobs() > resolver_->max_queued_jobs_) { |
| Job* evicted = static_cast<Job*>(dispatcher.EvictOldestLowest()); |
| DCHECK(evicted); |
| evicted->OnEvicted(); |
| } |
| return; |
| } |
| |
| if (start_time_ == base::TimeTicks()) { |
| net_log_.AddEvent(NetLogEventType::HOST_RESOLVER_MANAGER_JOB_STARTED); |
| start_time_ = tick_clock_->NowTicks(); |
| } |
| tasks_.pop_front(); |
| job_running_ = true; |
| |
| switch (next_task) { |
| case TaskType::SYSTEM: |
| StartSystemTask(); |
| break; |
| case TaskType::DNS: |
| StartDnsTask(false /* secure */); |
| break; |
| case TaskType::SECURE_DNS: |
| StartDnsTask(true /* secure */); |
| break; |
| case TaskType::MDNS: |
| StartMdnsTask(); |
| break; |
| case TaskType::INSECURE_CACHE_LOOKUP: |
| InsecureCacheLookup(); |
| break; |
| case TaskType::NAT64: |
| StartNat64Task(); |
| break; |
| case TaskType::SECURE_CACHE_LOOKUP: |
| case TaskType::CACHE_LOOKUP: |
| case TaskType::CONFIG_PRESET: |
| case TaskType::HOSTS: |
| // These task types should have been handled synchronously in |
| // ResolveLocally() prior to Job creation. |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| base::Value::Dict HostResolverManager::Job::NetLogJobCreationParams( |
| const NetLogSource& source) { |
| base::Value::Dict dict; |
| source.AddToEventParameters(dict); |
| dict.Set("host", key_.host.ToString()); |
| base::Value::List query_types_list; |
| for (DnsQueryType query_type : key_.query_types) { |
| query_types_list.Append(kDnsQueryTypes.at(query_type)); |
| } |
| dict.Set("dns_query_types", std::move(query_types_list)); |
| dict.Set("secure_dns_mode", base::strict_cast<int>(key_.secure_dns_mode)); |
| dict.Set("network_anonymization_key", |
| key_.network_anonymization_key.ToDebugString()); |
| return dict; |
| } |
| |
| void HostResolverManager::Job::Finish() { |
| if (is_running()) { |
| // Clean up but don't run any callbacks. |
| system_task_ = nullptr; |
| KillDnsTask(); |
| mdns_task_ = nullptr; |
| job_running_ = false; |
| |
| if (dispatched_) { |
| // Job should only ever occupy one slot after any tasks that may have |
| // required additional slots, e.g. DnsTask, have been killed, and |
| // additional slots are expected to be vacated as part of killing the |
| // task. |
| DCHECK_EQ(1, num_occupied_job_slots_); |
| if (resolver_) { |
| resolver_->dispatcher_->OnJobFinished(); |
| } |
| num_occupied_job_slots_ = 0; |
| } |
| } else if (is_queued()) { |
| DCHECK(dispatched_); |
| if (resolver_) { |
| resolver_->dispatcher_->Cancel(handle_); |
| } |
| handle_.Reset(); |
| } |
| } |
| |
| void HostResolverManager::Job::KillDnsTask() { |
| if (dns_task_) { |
| if (dispatched_) { |
| while (num_occupied_job_slots_ > 1 || is_queued()) { |
| ReduceByOneJobSlot(); |
| } |
| } |
| dns_task_.reset(); |
| } |
| dns_task_results_manager_.reset(); |
| } |
| |
| void HostResolverManager::Job::ReduceByOneJobSlot() { |
| DCHECK_GE(num_occupied_job_slots_, 1); |
| DCHECK(dispatched_); |
| if (is_queued()) { |
| if (resolver_) { |
| resolver_->dispatcher_->Cancel(handle_); |
| } |
| handle_.Reset(); |
| } else if (num_occupied_job_slots_ > 1) { |
| if (resolver_) { |
| resolver_->dispatcher_->OnJobFinished(); |
| } |
| --num_occupied_job_slots_; |
| } else { |
| NOTREACHED(); |
| } |
| } |
| |
| void HostResolverManager::Job::AddRequestCommon( |
| RequestPriority request_priority, |
| const NetLogWithSource& request_net_log, |
| bool is_speculative) { |
| priority_tracker_.Add(request_priority); |
| request_net_log.AddEventReferencingSource( |
| NetLogEventType::HOST_RESOLVER_MANAGER_JOB_ATTACH, net_log_.source()); |
| net_log_.AddEvent( |
| NetLogEventType::HOST_RESOLVER_MANAGER_JOB_REQUEST_ATTACH, [&] { |
| return NetLogJobAttachParams(request_net_log.source(), priority()); |
| }); |
| if (!is_speculative) { |
| had_non_speculative_request_ = true; |
| } |
| } |
| |
| void HostResolverManager::Job::CancelRequestCommon( |
| RequestPriority request_priority, |
| const NetLogWithSource& request_net_log) { |
| priority_tracker_.Remove(request_priority); |
| net_log_.AddEvent( |
| NetLogEventType::HOST_RESOLVER_MANAGER_JOB_REQUEST_DETACH, [&] { |
| return NetLogJobAttachParams(request_net_log.source(), priority()); |
| }); |
| } |
| |
| void HostResolverManager::Job::UpdatePriority() { |
| if (is_queued()) { |
| handle_ = resolver_->dispatcher_->ChangePriority(handle_, priority()); |
| } |
| } |
| |
| void HostResolverManager::Job::Start() { |
| handle_.Reset(); |
| ++num_occupied_job_slots_; |
| |
| if (num_occupied_job_slots_ >= 2) { |
| if (!dns_task_) { |
| resolver_->dispatcher_->OnJobFinished(); |
| return; |
| } |
| StartNextDnsTransaction(); |
| DCHECK_EQ(num_occupied_job_slots_, |
| dns_task_->num_transactions_in_progress()); |
| if (dns_task_->num_additional_transactions_needed() >= 1) { |
| Schedule(true); |
| } |
| return; |
| } |
| |
| DCHECK(!is_running()); |
| DCHECK(!tasks_.empty()); |
| RunNextTask(); |
| // Caution: Job::Start must not complete synchronously. |
| } |
| |
| void HostResolverManager::Job::StartSystemTask() { |
| DCHECK(dispatched_); |
| DCHECK_EQ(1, num_occupied_job_slots_); |
| DCHECK(HasAddressType(key_.query_types)); |
| |
| system_task_ = HostResolverSystemTask::Create( |
| std::string(key_.host.GetHostnameWithoutBrackets()), |
| HostResolver::DnsQueryTypeSetToAddressFamily(key_.query_types), |
| key_.flags, resolver_->host_resolver_system_params_, net_log_, |
| key_.GetTargetNetwork()); |
| |
| // Start() could be called from within Resolve(), hence it must NOT directly |
| // call OnSystemTaskComplete, for example, on synchronous failure. |
| system_task_->Start(base::BindOnce(&Job::OnSystemTaskComplete, |
| base::Unretained(this), |
| tick_clock_->NowTicks())); |
| } |
| |
| void HostResolverManager::Job::OnSystemTaskComplete( |
| base::TimeTicks start_time, |
| const AddressList& addr_list, |
| int /*os_error*/, |
| int net_error) { |
| DCHECK(system_task_); |
| |
| base::TimeDelta duration = tick_clock_->NowTicks() - start_time; |
| if (net_error == OK) { |
| UMA_HISTOGRAM_LONG_TIMES_100("Net.DNS.SystemTask.SuccessTime", duration); |
| } else { |
| UMA_HISTOGRAM_LONG_TIMES_100("Net.DNS.SystemTask.FailureTime", duration); |
| } |
| |
| if (dns_task_error_ != OK && net_error == OK) { |
| // This HostResolverSystemTask was a fallback resolution after a failed |
| // insecure DnsTask. |
| resolver_->OnFallbackResolve(dns_task_error_); |
| } |
| |
| if (ContainsIcannNameCollisionIp(addr_list.endpoints())) { |
| net_error = ERR_ICANN_NAME_COLLISION; |
| } |
| |
| base::TimeDelta ttl = base::Seconds(kNegativeCacheEntryTTLSeconds); |
| if (net_error == OK) { |
| ttl = base::Seconds(kCacheEntryTTLSeconds); |
| } |
| |
| auto aliases = std::set<std::string>(addr_list.dns_aliases().begin(), |
| addr_list.dns_aliases().end()); |
| |
| // Source unknown because the system resolver could have gotten it from a |
| // hosts file, its own cache, a DNS lookup or somewhere else. |
| // Don't store the |ttl| in cache since it's not obtained from the server. |
| CompleteRequests( |
| HostCache::Entry( |
| net_error, |
| net_error == OK ? addr_list.endpoints() : std::vector<IPEndPoint>(), |
| std::move(aliases), HostCache::Entry::SOURCE_UNKNOWN), |
| ttl, /*allow_cache=*/true, /*secure=*/false, TaskType::SYSTEM); |
| } |
| |
| void HostResolverManager::Job::InsecureCacheLookup() { |
| // Insecure cache lookups for requests allowing stale results should have |
| // occurred prior to Job creation. |
| DCHECK(cache_usage_ != ResolveHostParameters::CacheUsage::STALE_ALLOWED); |
| std::optional<HostCache::EntryStaleness> stale_info; |
| std::optional<HostCache::Entry> resolved = resolver_->MaybeServeFromCache( |
| host_cache_, key_.ToCacheKey(/*secure=*/false), cache_usage_, |
| false /* ignore_secure */, net_log_, &stale_info); |
| |
| if (resolved) { |
| DCHECK(stale_info); |
| DCHECK(!stale_info.value().is_stale()); |
| CompleteRequestsWithoutCache(resolved.value(), std::move(stale_info), |
| TaskType::INSECURE_CACHE_LOOKUP); |
| } else { |
| RunNextTask(); |
| } |
| } |
| |
| void HostResolverManager::Job::StartDnsTask(bool secure) { |
| DCHECK_EQ(secure, !dispatched_); |
| DCHECK_EQ(dispatched_ ? 1 : 0, num_occupied_job_slots_); |
| DCHECK(!resolver_->ShouldForceSystemResolverDueToTestOverride()); |
| |
| CHECK(!dns_task_results_manager_); |
| if (base::FeatureList::IsEnabled(features::kUseServiceEndpointRequest)) { |
| dns_task_results_manager_ = std::make_unique<DnsTaskResultsManager>( |
| this, key_.host, key_.query_types, net_log_); |
| } |
| |
| // Need to create the task even if we're going to post a failure instead of |
| // running it, as a "started" job needs a task to be properly cleaned up. |
| dns_task_ = std::make_unique<HostResolverDnsTask>( |
| resolver_->dns_client_.get(), key_.host, key_.network_anonymization_key, |
| key_.query_types, &*key_.resolve_context, secure, key_.secure_dns_mode, |
| this, net_log_, tick_clock_, !tasks_.empty() /* fallback_available */, |
| https_svcb_options_); |
| dns_task_->StartNextTransaction(); |
| // Schedule a second transaction, if needed. DoH queries can bypass the |
| // dispatcher and start all of their transactions immediately. |
| if (secure) { |
| while (dns_task_->num_additional_transactions_needed() >= 1) { |
| dns_task_->StartNextTransaction(); |
| } |
| DCHECK_EQ(dns_task_->num_additional_transactions_needed(), 0); |
| } else if (dns_task_->num_additional_transactions_needed() >= 1) { |
| Schedule(true); |
| } |
| } |
| |
| void HostResolverManager::Job::StartNextDnsTransaction() { |
| DCHECK(dns_task_); |
| DCHECK_EQ(dns_task_->secure(), !dispatched_); |
| DCHECK(!dispatched_ || num_occupied_job_slots_ == |
| dns_task_->num_transactions_in_progress() + 1); |
| DCHECK_GE(dns_task_->num_additional_transactions_needed(), 1); |
| dns_task_->StartNextTransaction(); |
| } |
| |
| void HostResolverManager::Job::OnDnsTaskFailure( |
| const base::WeakPtr<HostResolverDnsTask>& dns_task, |
| base::TimeDelta duration, |
| bool allow_fallback, |
| const HostCache::Entry& failure_results, |
| bool secure) { |
| DCHECK_NE(OK, failure_results.error()); |
| |
| if (!secure) { |
| DCHECK_NE(key_.secure_dns_mode, SecureDnsMode::kSecure); |
| UMA_HISTOGRAM_LONG_TIMES_100("Net.DNS.InsecureDnsTask.FailureTime", |
| duration); |
| } |
| |
| if (!dns_task) { |
| return; |
| } |
| |
| UMA_HISTOGRAM_LONG_TIMES_100("Net.DNS.JobQueueTime.Failure", |
| total_transaction_time_queued_); |
| |
| // If one of the fallback tasks doesn't complete the request, store a result |
| // to use during request completion. |
| base::TimeDelta ttl = |
| failure_results.has_ttl() ? failure_results.ttl() : base::Seconds(0); |
| completion_results_.push_back({failure_results, ttl, secure}); |
| |
| dns_task_error_ = failure_results.error(); |
| KillDnsTask(); |
| |
| if (!allow_fallback) { |
| tasks_.clear(); |
| } |
| |
| RunNextTask(); |
| } |
| |
| void HostResolverManager::Job::OnDnsTaskComplete(base::TimeTicks start_time, |
| bool allow_fallback, |
| HostCache::Entry results, |
| bool secure) { |
| DCHECK(dns_task_); |
| |
| // Tasks containing address queries are only considered successful overall |
| // if they find address results. However, DnsTask may claim success if any |
| // transaction, e.g. a supplemental HTTPS transaction, finds results. |
| DCHECK(!key_.query_types.Has(DnsQueryType::UNSPECIFIED)); |
| if (HasAddressType(key_.query_types) && results.error() == OK && |
| results.ip_endpoints().empty()) { |
| results.set_error(ERR_NAME_NOT_RESOLVED); |
| } |
| |
| base::TimeDelta duration = tick_clock_->NowTicks() - start_time; |
| if (results.error() != OK) { |
| OnDnsTaskFailure(dns_task_->AsWeakPtr(), duration, allow_fallback, results, |
| secure); |
| return; |
| } |
| |
| UMA_HISTOGRAM_LONG_TIMES_100("Net.DNS.DnsTask.SuccessTime", duration); |
| |
| UMA_HISTOGRAM_LONG_TIMES_100("Net.DNS.JobQueueTime.Success", |
| total_transaction_time_queued_); |
| |
| // Reset the insecure DNS failure counter if an insecure DnsTask completed |
| // successfully. |
| if (!secure) { |
| resolver_->dns_client_->ClearInsecureFallbackFailures(); |
| } |
| |
| base::TimeDelta bounded_ttl = |
| std::max(results.ttl(), base::Seconds(kMinimumTTLSeconds)); |
| |
| if (ContainsIcannNameCollisionIp(results.ip_endpoints())) { |
| CompleteRequestsWithError(ERR_ICANN_NAME_COLLISION, |
| secure ? TaskType::SECURE_DNS : TaskType::DNS); |
| return; |
| } |
| |
| CompleteRequests(results, bounded_ttl, true /* allow_cache */, secure, |
| secure ? TaskType::SECURE_DNS : TaskType::DNS); |
| } |
| |
| void HostResolverManager::Job::OnIntermediateTransactionsComplete( |
| std::optional<HostResolverDnsTask::SingleTransactionResults> |
| single_transaction_results) { |
| if (dispatched_) { |
| DCHECK_GE(num_occupied_job_slots_, |
| dns_task_->num_transactions_in_progress()); |
| int unused_slots = |
| num_occupied_job_slots_ - dns_task_->num_transactions_in_progress(); |
| |
| // Reuse vacated slots for any remaining transactions. |
| while (unused_slots > 0 && |
| dns_task_->num_additional_transactions_needed() > 0) { |
| dns_task_->StartNextTransaction(); |
| --unused_slots; |
| } |
| |
| // If all remaining transactions found a slot, no more needed from the |
| // dispatcher. |
| if (is_queued() && dns_task_->num_additional_transactions_needed() == 0) { |
| resolver_->dispatcher_->Cancel(handle_); |
| handle_.Reset(); |
| } |
| |
| // Relinquish any remaining extra slots. |
| while (unused_slots > 0) { |
| ReduceByOneJobSlot(); |
| --unused_slots; |
| } |
| } else if (dns_task_->num_additional_transactions_needed() >= 1) { |
| dns_task_->StartNextTransaction(); |
| } |
| |
| if (dns_task_results_manager_ && single_transaction_results.has_value()) { |
| dns_task_results_manager_->ProcessDnsTransactionResults( |
| single_transaction_results->query_type, |
| single_transaction_results->results); |
| // `this` may be deleted. Do not add code below. |
| } |
| } |
| |
| void HostResolverManager::Job::AddTransactionTimeQueued( |
| base::TimeDelta time_queued) { |
| total_transaction_time_queued_ += time_queued; |
| } |
| |
| void HostResolverManager::Job::OnServiceEndpointsUpdated() { |
| // Requests could be destroyed while executing callbacks. Post tasks |
| // instead of calling callbacks synchronously to prevent requests from being |
| // destroyed in the following for loop. |
| for (auto* request = service_endpoint_requests_.head(); |
| request != service_endpoint_requests_.end(); request = request->next()) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&ServiceEndpointRequestImpl::OnServiceEndpointsChanged, |
| request->value()->GetWeakPtr())); |
| } |
| } |
| |
| void HostResolverManager::Job::StartMdnsTask() { |
| // No flags are supported for MDNS except |
| // HOST_RESOLVER_DEFAULT_FAMILY_SET_DUE_TO_NO_IPV6 (which is not actually an |
| // input flag). |
| DCHECK_EQ(0, key_.flags & ~HOST_RESOLVER_DEFAULT_FAMILY_SET_DUE_TO_NO_IPV6); |
| |
| MDnsClient* client = nullptr; |
| int rv = resolver_->GetOrCreateMdnsClient(&client); |
| mdns_task_ = std::make_unique<HostResolverMdnsTask>( |
| client, std::string(key_.host.GetHostnameWithoutBrackets()), |
| key_.query_types); |
| |
| if (rv == OK) { |
| mdns_task_->Start( |
| base::BindOnce(&Job::OnMdnsTaskComplete, base::Unretained(this))); |
| } else { |
| // Could not create an mDNS client. Since we cannot complete synchronously |
| // from here, post a failure without starting the task. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(&Job::OnMdnsImmediateFailure, |
| weak_ptr_factory_.GetWeakPtr(), rv)); |
| } |
| } |
| |
| void HostResolverManager::Job::OnMdnsTaskComplete() { |
| DCHECK(mdns_task_); |
| // TODO(crbug.com/40577881): Consider adding MDNS-specific logging. |
| |
| HostCache::Entry results = mdns_task_->GetResults(); |
| |
| if (ContainsIcannNameCollisionIp(results.ip_endpoints())) { |
| CompleteRequestsWithError(ERR_ICANN_NAME_COLLISION, TaskType::MDNS); |
| return; |
| } |
| // MDNS uses a separate cache, so skip saving result to cache. |
| // TODO(crbug.com/40611558): Consider merging caches. |
| CompleteRequestsWithoutCache(results, std::nullopt /* stale_info */, |
| TaskType::MDNS); |
| } |
| |
| void HostResolverManager::Job::OnMdnsImmediateFailure(int rv) { |
| DCHECK(mdns_task_); |
| DCHECK_NE(OK, rv); |
| |
| CompleteRequestsWithError(rv, TaskType::MDNS); |
| } |
| |
| void HostResolverManager::Job::StartNat64Task() { |
| DCHECK(!nat64_task_); |
| nat64_task_ = std::make_unique<HostResolverNat64Task>( |
| key_.host.GetHostnameWithoutBrackets(), key_.network_anonymization_key, |
| net_log_, &*key_.resolve_context, resolver_); |
| nat64_task_->Start(base::BindOnce(&Job::OnNat64TaskComplete, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void HostResolverManager::Job::OnNat64TaskComplete() { |
| DCHECK(nat64_task_); |
| HostCache::Entry results = nat64_task_->GetResults(); |
| CompleteRequestsWithoutCache(results, std::nullopt /* stale_info */, |
| TaskType::NAT64); |
| } |
| |
| void HostResolverManager::Job::RecordJobHistograms( |
| const HostCache::Entry& results, |
| std::optional<TaskType> task_type) { |
| int error = results.error(); |
| // Used in UMA_HISTOGRAM_ENUMERATION. Do not renumber entries or reuse |
| // deprecated values. |
| enum Category { |
| RESOLVE_SUCCESS = 0, |
| RESOLVE_FAIL = 1, |
| RESOLVE_SPECULATIVE_SUCCESS = 2, |
| RESOLVE_SPECULATIVE_FAIL = 3, |
| RESOLVE_ABORT = 4, |
| RESOLVE_SPECULATIVE_ABORT = 5, |
| RESOLVE_MAX, // Bounding value. |
| }; |
| Category category = RESOLVE_MAX; // Illegal value for later DCHECK only. |
| |
| base::TimeDelta duration = tick_clock_->NowTicks() - start_time_; |
| if (error == OK) { |
| if (had_non_speculative_request_) { |
| category = RESOLVE_SUCCESS; |
| UMA_HISTOGRAM_LONG_TIMES_100("Net.DNS.ResolveSuccessTime", duration); |
| } else { |
| category = RESOLVE_SPECULATIVE_SUCCESS; |
| } |
| } else if (error == ERR_NETWORK_CHANGED || |
| error == ERR_HOST_RESOLVER_QUEUE_TOO_LARGE) { |
| category = had_non_speculative_request_ ? RESOLVE_ABORT |
| : RESOLVE_SPECULATIVE_ABORT; |
| } else { |
| if (had_non_speculative_request_) { |
| category = RESOLVE_FAIL; |
| UMA_HISTOGRAM_LONG_TIMES_100("Net.DNS.ResolveFailureTime", duration); |
| } else { |
| category = RESOLVE_SPECULATIVE_FAIL; |
| } |
| } |
| DCHECK_LT(static_cast<int>(category), |
| static_cast<int>(RESOLVE_MAX)); // Be sure it was set. |
| UMA_HISTOGRAM_ENUMERATION("Net.DNS.ResolveCategory", category, RESOLVE_MAX); |
| |
| if (category == RESOLVE_FAIL || |
| (start_time_ != base::TimeTicks() && category == RESOLVE_ABORT)) { |
| if (duration < base::Milliseconds(10)) { |
| base::UmaHistogramSparse("Net.DNS.ResolveError.Fast", std::abs(error)); |
| } else { |
| base::UmaHistogramSparse("Net.DNS.ResolveError.Slow", std::abs(error)); |
| } |
| } |
| |
| if (error == OK) { |
| DCHECK(task_type.has_value()); |
| // Record, for HTTPS-capable queries to a host known to serve HTTPS |
| // records, whether the HTTPS record was successfully received. |
| if (key_.query_types.Has(DnsQueryType::HTTPS) && |
| // Skip http- and ws-schemed hosts. Although they query HTTPS records, |
| // successful queries are reported as errors, which would skew the |
| // metrics. |
| IsSchemeHttpsOrWss(key_.host) && |
| IsGoogleHostWithAlpnH3(key_.host.GetHostnameWithoutBrackets())) { |
| bool has_metadata = !results.GetMetadatas().empty(); |
| base::UmaHistogramExactLinear( |
| "Net.DNS.H3SupportedGoogleHost.TaskTypeMetadataAvailability2", |
| static_cast<int>(task_type.value()) * 2 + (has_metadata ? 1 : 0), |
| (static_cast<int>(TaskType::kMaxValue) + 1) * 2); |
| } |
| } |
| } |
| |
| void HostResolverManager::Job::MaybeCacheResult(const HostCache::Entry& results, |
| base::TimeDelta ttl, |
| bool secure) { |
| // If the request did not complete, don't cache it. |
| if (!results.did_complete()) { |
| return; |
| } |
| resolver_->CacheResult(host_cache_, key_.ToCacheKey(secure), results, ttl); |
| } |
| |
| void HostResolverManager::Job::CompleteRequests( |
| const HostCache::Entry& results, |
| base::TimeDelta ttl, |
| bool allow_cache, |
| bool secure, |
| std::optional<TaskType> task_type) { |
| CHECK(resolver_.get()); |
| |
| // This job must be removed from resolver's |jobs_| now to make room for a |
| // new job with the same key in case one of the OnComplete callbacks decides |
| // to spawn one. Consequently, if the job was owned by |jobs_|, the job |
| // deletes itself when CompleteRequests is done. |
| std::unique_ptr<Job> self_deleter; |
| if (self_iterator_) { |
| self_deleter = resolver_->RemoveJob(self_iterator_.value()); |
| } |
| |
| Finish(); |
| |
| if (results.error() == ERR_DNS_REQUEST_CANCELLED) { |
| net_log_.AddEvent(NetLogEventType::CANCELLED); |
| net_log_.EndEventWithNetErrorCode( |
| NetLogEventType::HOST_RESOLVER_MANAGER_JOB, OK); |
| return; |
| } |
| |
| net_log_.EndEventWithNetErrorCode(NetLogEventType::HOST_RESOLVER_MANAGER_JOB, |
| results.error()); |
| |
| // Handle all caching before completing requests as completing requests may |
| // start new requests that rely on cached results. |
| if (allow_cache) { |
| MaybeCacheResult(results, ttl, secure); |
| } |
| |
| RecordJobHistograms(results, task_type); |
| |
| // Complete all of the requests that were attached to the job and |
| // detach them. |
| while (!requests_.empty()) { |
| RequestImpl* req = requests_.head()->value(); |
| req->RemoveFromList(); |
| CHECK(key_ == req->GetJobKey()); |
| |
| if (results.error() == OK && !req->parameters().is_speculative) { |
| req->set_results( |
| results.CopyWithDefaultPort(req->request_host().GetPort())); |
| } |
| req->OnJobCompleted( |
| key_, results.error(), |
| /*is_secure_network_error=*/secure && results.error() != OK); |
| |
| // Check if the resolver was destroyed as a result of running the |
| // callback. If it was, we could continue, but we choose to bail. |
| if (!resolver_.get()) { |
| return; |
| } |
| } |
| |
| while (!service_endpoint_requests_.empty()) { |
| ServiceEndpointRequestImpl* request = |
| service_endpoint_requests_.head()->value(); |
| request->RemoveFromList(); |
| request->OnJobCompleted(results, secure); |
| if (!resolver_.get()) { |
| return; |
| } |
| } |
| |
| // TODO(crbug.com/40178456): Call StartBootstrapFollowup() if any of the |
| // requests have the Bootstrap policy. Note: A naive implementation could |
| // cause an infinite loop if the bootstrap result has TTL=0. |
| } |
| |
| void HostResolverManager::Job::CompleteRequestsWithoutCache( |
| const HostCache::Entry& results, |
| std::optional<HostCache::EntryStaleness> stale_info, |
| TaskType task_type) { |
| // Record the stale_info for all non-speculative requests, if it exists. |
| if (stale_info) { |
| for (auto* node = requests_.head(); node != requests_.end(); |
| node = node->next()) { |
| if (!node->value()->parameters().is_speculative) { |
| node->value()->set_stale_info(stale_info.value()); |
| } |
| } |
| } |
| CompleteRequests(results, base::TimeDelta(), false /* allow_cache */, |
| false /* secure */, task_type); |
| } |
| |
| void HostResolverManager::Job::CompleteRequestsWithError( |
| int net_error, |
| std::optional<TaskType> task_type) { |
| DCHECK_NE(OK, net_error); |
| CompleteRequests( |
| HostCache::Entry(net_error, HostCache::Entry::SOURCE_UNKNOWN), |
| base::TimeDelta(), true /* allow_cache */, false /* secure */, task_type); |
| } |
| |
| RequestPriority HostResolverManager::Job::priority() const { |
| return priority_tracker_.highest_priority(); |
| } |
| |
| } // namespace net |