123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571 |
- // 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 <memory>
- #include <utility>
- #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<url::SchemeHostPort>(endpoint)) {
- return HostPortPair::FromSchemeHostPort(
- absl::get<url::SchemeHostPort>(endpoint));
- }
- DCHECK(absl::holds_alternative<HostPortPair>(endpoint));
- return absl::get<HostPortPair>(endpoint);
- }
- } // namespace
- TransportSocketParams::TransportSocketParams(
- Endpoint destination,
- NetworkIsolationKey network_isolation_key,
- SecureDnsPolicy secure_dns_policy,
- OnHostResolutionCallback host_resolution_callback,
- base::flat_set<std::string> 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<url::SchemeHostPort>(&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> TransportConnectJob::Factory::Create(
- RequestPriority priority,
- const SocketTag& socket_tag,
- const CommonConnectJobParams* common_connect_job_params,
- const scoped_refptr<TransportSocketParams>& params,
- Delegate* delegate,
- const NetLogWithSource* net_log) {
- return std::make_unique<TransportConnectJob>(priority, socket_tag,
- common_connect_job_params,
- params, delegate, net_log);
- }
- TransportConnectJob::EndpointResultOverride::EndpointResultOverride(
- HostResolverEndpointResult result,
- std::set<std::string> 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<TransportSocketParams>& params,
- Delegate* delegate,
- const NetLogWithSource* net_log,
- absl::optional<EndpointResultOverride> 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<HostResolverEndpointResult>
- 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<url::SchemeHostPort>(params_->destination())) {
- request_ = host_resolver()->CreateRequest(
- absl::get<url::SchemeHostPort>(params_->destination()),
- params_->network_isolation_key(), net_log(), parameters);
- } else {
- request_ = host_resolver()->CreateRequest(
- absl::get<HostPortPair>(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<IPEndPoint> 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<IPEndPoint> 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<IPEndPoint> 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<TransportConnectSubJob>(
- std::move(ipv4_addresses), this, SUB_JOB_IPV4);
- }
- if (!ipv6_addresses.empty()) {
- ipv6_job_ = std::make_unique<TransportConnectSubJob>(
- 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<const HostResolverEndpointResult> 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<url::SchemeHostPort>(¶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
|