// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/socket/transport_connect_job.h" #include #include #include "base/bind.h" #include "base/check_op.h" #include "base/containers/contains.h" #include "base/feature_list.h" #include "base/location.h" #include "base/logging.h" #include "base/metrics/histogram_macros.h" #include "base/notreached.h" #include "base/stl_util.h" #include "base/threading/thread_task_runner_handle.h" #include "base/time/time.h" #include "base/trace_event/trace_event.h" #include "base/values.h" #include "net/base/features.h" #include "net/base/host_port_pair.h" #include "net/base/ip_endpoint.h" #include "net/base/net_errors.h" #include "net/base/trace_constants.h" #include "net/dns/host_resolver_results.h" #include "net/dns/public/secure_dns_policy.h" #include "net/log/net_log_event_type.h" #include "net/log/net_log_source.h" #include "net/socket/socket_tag.h" #include "net/socket/transport_connect_sub_job.h" #include "third_party/abseil-cpp/absl/types/variant.h" #include "url/scheme_host_port.h" #include "url/url_constants.h" namespace net { namespace { // TODO(crbug.com/1206799): Delete once endpoint usage is converted to using // url::SchemeHostPort when available. HostPortPair ToLegacyDestinationEndpoint( const TransportSocketParams::Endpoint& endpoint) { if (absl::holds_alternative(endpoint)) { return HostPortPair::FromSchemeHostPort( absl::get(endpoint)); } DCHECK(absl::holds_alternative(endpoint)); return absl::get(endpoint); } } // namespace TransportSocketParams::TransportSocketParams( Endpoint destination, NetworkIsolationKey network_isolation_key, SecureDnsPolicy secure_dns_policy, OnHostResolutionCallback host_resolution_callback, base::flat_set supported_alpns) : destination_(std::move(destination)), network_isolation_key_(std::move(network_isolation_key)), secure_dns_policy_(secure_dns_policy), host_resolution_callback_(std::move(host_resolution_callback)), supported_alpns_(std::move(supported_alpns)) { #if DCHECK_IS_ON() auto* scheme_host_port = absl::get_if(&destination_); if (scheme_host_port) { if (scheme_host_port->scheme() == url::kHttpsScheme) { // HTTPS destinations will, when passed to the DNS resolver, return // SVCB/HTTPS-based routes. Those routes require ALPN protocols to // evaluate. If there are none, `IsEndpointResultUsable` will correctly // skip each route, but it doesn't make sense to make a DNS query if we // can't handle the result. DCHECK(!supported_alpns_.empty()); } else if (scheme_host_port->scheme() == url::kHttpScheme) { // HTTP (not HTTPS) does not currently define ALPN protocols, so the list // should be empty. This means `IsEndpointResultUsable` will skip any // SVCB-based routes. HTTP also has no SVCB mapping, so `HostResolver` // will never return them anyway. // // `HostResolver` will still query SVCB (rather, HTTPS) records for the // corresponding HTTPS URL to implement an upgrade flow (section 9.5 of // draft-ietf-dnsop-svcb-https-08), but this will result in DNS resolution // failing with `ERR_DNS_NAME_HTTPS_ONLY`, not SVCB-based routes. DCHECK(supported_alpns_.empty()); } } #endif } TransportSocketParams::~TransportSocketParams() = default; std::unique_ptr TransportConnectJob::Factory::Create( RequestPriority priority, const SocketTag& socket_tag, const CommonConnectJobParams* common_connect_job_params, const scoped_refptr& params, Delegate* delegate, const NetLogWithSource* net_log) { return std::make_unique(priority, socket_tag, common_connect_job_params, params, delegate, net_log); } TransportConnectJob::EndpointResultOverride::EndpointResultOverride( HostResolverEndpointResult result, std::set dns_aliases) : result(std::move(result)), dns_aliases(std::move(dns_aliases)) {} TransportConnectJob::EndpointResultOverride::EndpointResultOverride( EndpointResultOverride&&) = default; TransportConnectJob::EndpointResultOverride::EndpointResultOverride( const EndpointResultOverride&) = default; TransportConnectJob::EndpointResultOverride::~EndpointResultOverride() = default; TransportConnectJob::TransportConnectJob( RequestPriority priority, const SocketTag& socket_tag, const CommonConnectJobParams* common_connect_job_params, const scoped_refptr& params, Delegate* delegate, const NetLogWithSource* net_log, absl::optional endpoint_result_override) : ConnectJob(priority, socket_tag, ConnectionTimeout(), common_connect_job_params, delegate, net_log, NetLogSourceType::TRANSPORT_CONNECT_JOB, NetLogEventType::TRANSPORT_CONNECT_JOB_CONNECT), params_(params) { if (endpoint_result_override) { has_dns_override_ = true; endpoint_results_ = {std::move(endpoint_result_override->result)}; dns_aliases_ = std::move(endpoint_result_override->dns_aliases); DCHECK(!endpoint_results_.front().ip_endpoints.empty()); DCHECK(IsEndpointResultUsable(endpoint_results_.front(), IsSvcbOptional(endpoint_results_))); } } // We don't worry about cancelling the host resolution and TCP connect, since // ~HostResolver::Request and ~TransportConnectSubJob will take care of it. TransportConnectJob::~TransportConnectJob() = default; LoadState TransportConnectJob::GetLoadState() const { switch (next_state_) { case STATE_RESOLVE_HOST: case STATE_RESOLVE_HOST_COMPLETE: case STATE_RESOLVE_HOST_CALLBACK_COMPLETE: return LOAD_STATE_RESOLVING_HOST; case STATE_TRANSPORT_CONNECT: case STATE_TRANSPORT_CONNECT_COMPLETE: { LoadState load_state = LOAD_STATE_IDLE; if (ipv6_job_ && ipv6_job_->started()) { load_state = ipv6_job_->GetLoadState(); } // This method should return LOAD_STATE_CONNECTING in preference to // LOAD_STATE_WAITING_FOR_AVAILABLE_SOCKET when possible because "waiting // for available socket" implies that nothing is happening. if (ipv4_job_ && ipv4_job_->started() && load_state != LOAD_STATE_CONNECTING) { load_state = ipv4_job_->GetLoadState(); } return load_state; } case STATE_NONE: return LOAD_STATE_IDLE; } } bool TransportConnectJob::HasEstablishedConnection() const { // No need to ever return true, since NotifyComplete() is called as soon as a // connection is established. return false; } ConnectionAttempts TransportConnectJob::GetConnectionAttempts() const { return connection_attempts_; } ResolveErrorInfo TransportConnectJob::GetResolveErrorInfo() const { return resolve_error_info_; } absl::optional TransportConnectJob::GetHostResolverEndpointResult() const { CHECK_LT(current_endpoint_result_, endpoint_results_.size()); return endpoint_results_[current_endpoint_result_]; } base::TimeDelta TransportConnectJob::ConnectionTimeout() { // TODO(eroman): The use of this constant needs to be re-evaluated. The time // needed for TCPClientSocketXXX::Connect() can be arbitrarily long, since // the address list may contain many alternatives, and most of those may // timeout. Even worse, the per-connect timeout threshold varies greatly // between systems (anywhere from 20 seconds to 190 seconds). // See comment #12 at http://crbug.com/23364 for specifics. return base::Minutes(4); } void TransportConnectJob::OnIOComplete(int result) { result = DoLoop(result); if (result != ERR_IO_PENDING) NotifyDelegateOfCompletion(result); // Deletes |this| } int TransportConnectJob::DoLoop(int result) { DCHECK_NE(next_state_, STATE_NONE); int rv = result; do { State state = next_state_; next_state_ = STATE_NONE; switch (state) { case STATE_RESOLVE_HOST: DCHECK_EQ(OK, rv); rv = DoResolveHost(); break; case STATE_RESOLVE_HOST_COMPLETE: rv = DoResolveHostComplete(rv); break; case STATE_RESOLVE_HOST_CALLBACK_COMPLETE: DCHECK_EQ(OK, rv); rv = DoResolveHostCallbackComplete(); break; case STATE_TRANSPORT_CONNECT: DCHECK_EQ(OK, rv); rv = DoTransportConnect(); break; case STATE_TRANSPORT_CONNECT_COMPLETE: rv = DoTransportConnectComplete(rv); break; default: NOTREACHED(); rv = ERR_FAILED; break; } } while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE); return rv; } int TransportConnectJob::DoResolveHost() { connect_timing_.dns_start = base::TimeTicks::Now(); if (has_dns_override_) { DCHECK_EQ(1u, endpoint_results_.size()); connect_timing_.dns_end = connect_timing_.dns_start; next_state_ = STATE_TRANSPORT_CONNECT; return OK; } next_state_ = STATE_RESOLVE_HOST_COMPLETE; HostResolver::ResolveHostParameters parameters; parameters.initial_priority = priority(); parameters.secure_dns_policy = params_->secure_dns_policy(); if (absl::holds_alternative(params_->destination())) { request_ = host_resolver()->CreateRequest( absl::get(params_->destination()), params_->network_isolation_key(), net_log(), parameters); } else { request_ = host_resolver()->CreateRequest( absl::get(params_->destination()), params_->network_isolation_key(), net_log(), parameters); } return request_->Start(base::BindOnce(&TransportConnectJob::OnIOComplete, base::Unretained(this))); } int TransportConnectJob::DoResolveHostComplete(int result) { TRACE_EVENT0(NetTracingCategory(), "TransportConnectJob::DoResolveHostComplete"); connect_timing_.dns_end = base::TimeTicks::Now(); // Overwrite connection start time, since for connections that do not go // through proxies, |connect_start| should not include dns lookup time. connect_timing_.connect_start = connect_timing_.dns_end; resolve_error_info_ = request_->GetResolveErrorInfo(); if (result != OK) { // If hostname resolution failed, record an empty endpoint and the result. connection_attempts_.push_back(ConnectionAttempt(IPEndPoint(), result)); return result; } DCHECK(request_->GetAddressResults()); DCHECK(request_->GetDnsAliasResults()); DCHECK(request_->GetEndpointResults()); // Invoke callback. If it indicates |this| may be slated for deletion, then // only continue after a PostTask. next_state_ = STATE_RESOLVE_HOST_CALLBACK_COMPLETE; if (!params_->host_resolution_callback().is_null()) { // TODO(https://crbug.com/1287240): Switch `OnHostResolutionCallbackResult` // to `request_->GetEndpointResults()` and `request_->GetDnsAliasResults()`. OnHostResolutionCallbackResult callback_result = params_->host_resolution_callback().Run( ToLegacyDestinationEndpoint(params_->destination()), *request_->GetAddressResults()); if (callback_result == OnHostResolutionCallbackResult::kMayBeDeletedAsync) { base::ThreadTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::BindOnce(&TransportConnectJob::OnIOComplete, weak_ptr_factory_.GetWeakPtr(), OK)); return ERR_IO_PENDING; } } return result; } int TransportConnectJob::DoResolveHostCallbackComplete() { const auto& unfiltered_results = *request_->GetEndpointResults(); bool svcb_optional = IsSvcbOptional(unfiltered_results); std::set ip_endpoints_seen; for (const auto& result : unfiltered_results) { if (!IsEndpointResultUsable(result, svcb_optional)) { continue; } // The TCP connect itself does not depend on any metadata, so we can dedup // by IP endpoint. In particular, the fallback A/AAAA route will often use // the same IP endpoints as the HTTPS route. If they do not work for one // route, there is no use in trying a second time. std::vector ip_endpoints; for (const auto& ip_endpoint : result.ip_endpoints) { auto [iter, inserted] = ip_endpoints_seen.insert(ip_endpoint); if (inserted) { ip_endpoints.push_back(ip_endpoint); } } if (!ip_endpoints.empty()) { HostResolverEndpointResult new_result; new_result.ip_endpoints = std::move(ip_endpoints); new_result.metadata = result.metadata; endpoint_results_.push_back(std::move(new_result)); } } dns_aliases_ = *request_->GetDnsAliasResults(); // No need to retain `request_` beyond this point. request_.reset(); if (endpoint_results_.empty()) { // In the general case, DNS may successfully return routes, but none are // compatible with this `ConnectJob`. This should not happen for HTTPS // because `HostResolver` will reject SVCB/HTTPS sets that do not cover the // default "http/1.1" ALPN. return ERR_NAME_NOT_RESOLVED; } next_state_ = STATE_TRANSPORT_CONNECT; return OK; } int TransportConnectJob::DoTransportConnect() { next_state_ = STATE_TRANSPORT_CONNECT_COMPLETE; const HostResolverEndpointResult& endpoint = GetEndpointResultForCurrentSubJobs(); std::vector ipv4_addresses, ipv6_addresses; for (const auto& ip_endpoint : endpoint.ip_endpoints) { switch (ip_endpoint.GetFamily()) { case ADDRESS_FAMILY_IPV4: ipv4_addresses.push_back(ip_endpoint); break; case ADDRESS_FAMILY_IPV6: ipv6_addresses.push_back(ip_endpoint); break; default: DVLOG(1) << "Unexpected ADDRESS_FAMILY: " << ip_endpoint.GetFamily(); break; } } if (!ipv4_addresses.empty()) { ipv4_job_ = std::make_unique( std::move(ipv4_addresses), this, SUB_JOB_IPV4); } if (!ipv6_addresses.empty()) { ipv6_job_ = std::make_unique( std::move(ipv6_addresses), this, SUB_JOB_IPV6); int result = ipv6_job_->Start(); if (result != ERR_IO_PENDING) return HandleSubJobComplete(result, ipv6_job_.get()); if (ipv4_job_) { // This use of base::Unretained is safe because |fallback_timer_| is // owned by this object. fallback_timer_.Start( FROM_HERE, kIPv6FallbackTime, base::BindOnce(&TransportConnectJob::StartIPv4JobAsync, base::Unretained(this))); } return ERR_IO_PENDING; } DCHECK(!ipv6_job_); DCHECK(ipv4_job_); int result = ipv4_job_->Start(); if (result != ERR_IO_PENDING) return HandleSubJobComplete(result, ipv4_job_.get()); return ERR_IO_PENDING; } int TransportConnectJob::DoTransportConnectComplete(int result) { // Make sure nothing else calls back into this object. ipv4_job_.reset(); ipv6_job_.reset(); fallback_timer_.Stop(); if (result == OK) { DCHECK(!connect_timing_.connect_start.is_null()); DCHECK(!connect_timing_.dns_start.is_null()); // `HandleSubJobComplete` should have called `SetSocket`. DCHECK(socket()); base::TimeTicks now = base::TimeTicks::Now(); base::TimeDelta total_duration = now - connect_timing_.dns_start; UMA_HISTOGRAM_CUSTOM_TIMES("Net.DNS_Resolution_And_TCP_Connection_Latency2", total_duration, base::Milliseconds(1), base::Minutes(10), 100); base::TimeDelta connect_duration = now - connect_timing_.connect_start; UMA_HISTOGRAM_CUSTOM_TIMES("Net.TCP_Connection_Latency", connect_duration, base::Milliseconds(1), base::Minutes(10), 100); } else { // Don't try the next route if entering suspend mode. if (result != ERR_NETWORK_IO_SUSPENDED) { // If there is another endpoint available, try it. current_endpoint_result_++; if (current_endpoint_result_ < endpoint_results_.size()) { next_state_ = STATE_TRANSPORT_CONNECT; result = OK; } } } return result; } int TransportConnectJob::HandleSubJobComplete(int result, TransportConnectSubJob* job) { DCHECK_NE(result, ERR_IO_PENDING); if (result == OK) { SetSocket(job->PassSocket(), dns_aliases_); return result; } if (result == ERR_NETWORK_IO_SUSPENDED) { // Don't try other jobs if entering suspend mode. return result; } switch (job->type()) { case SUB_JOB_IPV4: ipv4_job_.reset(); break; case SUB_JOB_IPV6: ipv6_job_.reset(); // Start the other job, rather than wait for the fallback timer. if (ipv4_job_ && !ipv4_job_->started()) { fallback_timer_.Stop(); result = ipv4_job_->Start(); if (result != ERR_IO_PENDING) { return HandleSubJobComplete(result, ipv4_job_.get()); } } break; } if (ipv4_job_ || ipv6_job_) { // Wait for the other job to complete, rather than reporting |result|. return ERR_IO_PENDING; } return result; } void TransportConnectJob::OnSubJobComplete(int result, TransportConnectSubJob* job) { result = HandleSubJobComplete(result, job); if (result != ERR_IO_PENDING) { OnIOComplete(result); } } void TransportConnectJob::StartIPv4JobAsync() { DCHECK(ipv4_job_); int result = ipv4_job_->Start(); if (result != ERR_IO_PENDING) OnSubJobComplete(result, ipv4_job_.get()); } int TransportConnectJob::ConnectInternal() { next_state_ = STATE_RESOLVE_HOST; return DoLoop(OK); } void TransportConnectJob::ChangePriorityInternal(RequestPriority priority) { if (next_state_ == STATE_RESOLVE_HOST_COMPLETE) { DCHECK(request_); // Change the request priority in the host resolver. request_->ChangeRequestPriority(priority); } } bool TransportConnectJob::IsSvcbOptional( base::span results) const { // If SVCB/HTTPS resolution succeeded, the client supports ECH, and all routes // support ECH, disable the A/AAAA fallback. See Section 10.1 of // draft-ietf-dnsop-svcb-https-08. auto* scheme_host_port = absl::get_if(¶ms_->destination()); if (!base::FeatureList::IsEnabled(features::kEncryptedClientHello) || !scheme_host_port || scheme_host_port->scheme() != url::kHttpsScheme) { return true; // ECH is not supported for this request. } bool has_svcb = false; for (const auto& result : results) { if (!result.metadata.supported_protocol_alpns.empty()) { has_svcb = true; if (result.metadata.ech_config_list.empty()) { return true; // There is a non-ECH SVCB/HTTPS route. } } } // Either there were no SVCB/HTTPS records (should be SVCB-optional), or there // were and all supported ECH (should be SVCB-reliant). return !has_svcb; } bool TransportConnectJob::IsEndpointResultUsable( const HostResolverEndpointResult& result, bool svcb_optional) const { // A `HostResolverEndpointResult` with no ALPN protocols is the fallback // A/AAAA route. This is always compatible. We assume the ALPN-less option is // TCP-based. if (result.metadata.supported_protocol_alpns.empty()) { // See draft-ietf-dnsop-svcb-https-08, Section 3. return svcb_optional; } // See draft-ietf-dnsop-svcb-https-08, Section 7.1.2. Routes are usable if // there is an overlap between the route's ALPN protocols and the configured // ones. This ensures we do not, e.g., connect to a QUIC-only route with TCP. // Note that, if `params_` did not specify any ALPN protocols, no // SVCB/HTTPS-based routes will match and we will effectively ignore all but // plain A/AAAA routes. for (const auto& alpn : result.metadata.supported_protocol_alpns) { if (params_->supported_alpns().contains(alpn)) { return true; } } return false; } const HostResolverEndpointResult& TransportConnectJob::GetEndpointResultForCurrentSubJobs() const { CHECK_LT(current_endpoint_result_, endpoint_results_.size()); return endpoint_results_[current_endpoint_result_]; } } // namespace net