1 // Copyright 2012 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/socket/transport_connect_job.h"
6
7 #include <memory>
8 #include <utility>
9
10 #include "base/check_op.h"
11 #include "base/feature_list.h"
12 #include "base/functional/bind.h"
13 #include "base/location.h"
14 #include "base/logging.h"
15 #include "base/metrics/histogram_macros.h"
16 #include "base/notreached.h"
17 #include "base/task/single_thread_task_runner.h"
18 #include "base/time/time.h"
19 #include "net/base/features.h"
20 #include "net/base/host_port_pair.h"
21 #include "net/base/ip_endpoint.h"
22 #include "net/base/net_errors.h"
23 #include "net/base/trace_constants.h"
24 #include "net/base/tracing.h"
25 #include "net/dns/public/host_resolver_results.h"
26 #include "net/dns/public/secure_dns_policy.h"
27 #include "net/log/net_log_event_type.h"
28 #include "net/socket/socket_tag.h"
29 #include "net/socket/transport_connect_sub_job.h"
30 #include "third_party/abseil-cpp/absl/types/variant.h"
31 #include "url/scheme_host_port.h"
32 #include "url/url_constants.h"
33
34 namespace net {
35
36 namespace {
37
38 // TODO(crbug.com/40181080): Delete once endpoint usage is converted to using
39 // url::SchemeHostPort when available.
ToLegacyDestinationEndpoint(const TransportSocketParams::Endpoint & endpoint)40 HostPortPair ToLegacyDestinationEndpoint(
41 const TransportSocketParams::Endpoint& endpoint) {
42 if (absl::holds_alternative<url::SchemeHostPort>(endpoint)) {
43 return HostPortPair::FromSchemeHostPort(
44 absl::get<url::SchemeHostPort>(endpoint));
45 }
46
47 DCHECK(absl::holds_alternative<HostPortPair>(endpoint));
48 return absl::get<HostPortPair>(endpoint);
49 }
50
51 } // namespace
52
TransportSocketParams(Endpoint destination,NetworkAnonymizationKey network_anonymization_key,SecureDnsPolicy secure_dns_policy,OnHostResolutionCallback host_resolution_callback,base::flat_set<std::string> supported_alpns)53 TransportSocketParams::TransportSocketParams(
54 Endpoint destination,
55 NetworkAnonymizationKey network_anonymization_key,
56 SecureDnsPolicy secure_dns_policy,
57 OnHostResolutionCallback host_resolution_callback,
58 base::flat_set<std::string> supported_alpns)
59 : destination_(std::move(destination)),
60 network_anonymization_key_(std::move(network_anonymization_key)),
61 secure_dns_policy_(secure_dns_policy),
62 host_resolution_callback_(std::move(host_resolution_callback)),
63 supported_alpns_(std::move(supported_alpns)) {
64 #if DCHECK_IS_ON()
65 auto* scheme_host_port = absl::get_if<url::SchemeHostPort>(&destination_);
66 if (scheme_host_port) {
67 if (scheme_host_port->scheme() == url::kHttpsScheme) {
68 // HTTPS destinations will, when passed to the DNS resolver, return
69 // SVCB/HTTPS-based routes. Those routes require ALPN protocols to
70 // evaluate. If there are none, `IsEndpointResultUsable` will correctly
71 // skip each route, but it doesn't make sense to make a DNS query if we
72 // can't handle the result.
73 DCHECK(!supported_alpns_.empty());
74 } else if (scheme_host_port->scheme() == url::kHttpScheme) {
75 // HTTP (not HTTPS) does not currently define ALPN protocols, so the list
76 // should be empty. This means `IsEndpointResultUsable` will skip any
77 // SVCB-based routes. HTTP also has no SVCB mapping, so `HostResolver`
78 // will never return them anyway.
79 //
80 // `HostResolver` will still query SVCB (rather, HTTPS) records for the
81 // corresponding HTTPS URL to implement an upgrade flow (section 9.5 of
82 // draft-ietf-dnsop-svcb-https-08), but this will result in DNS resolution
83 // failing with `ERR_DNS_NAME_HTTPS_ONLY`, not SVCB-based routes.
84 DCHECK(supported_alpns_.empty());
85 }
86 }
87 #endif
88 }
89
90 TransportSocketParams::~TransportSocketParams() = default;
91
Create(RequestPriority priority,const SocketTag & socket_tag,const CommonConnectJobParams * common_connect_job_params,const scoped_refptr<TransportSocketParams> & params,Delegate * delegate,const NetLogWithSource * net_log)92 std::unique_ptr<TransportConnectJob> TransportConnectJob::Factory::Create(
93 RequestPriority priority,
94 const SocketTag& socket_tag,
95 const CommonConnectJobParams* common_connect_job_params,
96 const scoped_refptr<TransportSocketParams>& params,
97 Delegate* delegate,
98 const NetLogWithSource* net_log) {
99 return std::make_unique<TransportConnectJob>(priority, socket_tag,
100 common_connect_job_params,
101 params, delegate, net_log);
102 }
103
EndpointResultOverride(HostResolverEndpointResult result,std::set<std::string> dns_aliases)104 TransportConnectJob::EndpointResultOverride::EndpointResultOverride(
105 HostResolverEndpointResult result,
106 std::set<std::string> dns_aliases)
107 : result(std::move(result)), dns_aliases(std::move(dns_aliases)) {}
108 TransportConnectJob::EndpointResultOverride::EndpointResultOverride(
109 EndpointResultOverride&&) = default;
110 TransportConnectJob::EndpointResultOverride::EndpointResultOverride(
111 const EndpointResultOverride&) = default;
112 TransportConnectJob::EndpointResultOverride::~EndpointResultOverride() =
113 default;
114
TransportConnectJob(RequestPriority priority,const SocketTag & socket_tag,const CommonConnectJobParams * common_connect_job_params,const scoped_refptr<TransportSocketParams> & params,Delegate * delegate,const NetLogWithSource * net_log,std::optional<EndpointResultOverride> endpoint_result_override)115 TransportConnectJob::TransportConnectJob(
116 RequestPriority priority,
117 const SocketTag& socket_tag,
118 const CommonConnectJobParams* common_connect_job_params,
119 const scoped_refptr<TransportSocketParams>& params,
120 Delegate* delegate,
121 const NetLogWithSource* net_log,
122 std::optional<EndpointResultOverride> endpoint_result_override)
123 : ConnectJob(priority,
124 socket_tag,
125 ConnectionTimeout(),
126 common_connect_job_params,
127 delegate,
128 net_log,
129 NetLogSourceType::TRANSPORT_CONNECT_JOB,
130 NetLogEventType::TRANSPORT_CONNECT_JOB_CONNECT),
131 params_(params) {
132 if (endpoint_result_override) {
133 has_dns_override_ = true;
134 endpoint_results_ = {std::move(endpoint_result_override->result)};
135 dns_aliases_ = std::move(endpoint_result_override->dns_aliases);
136 DCHECK(!endpoint_results_.front().ip_endpoints.empty());
137 DCHECK(IsEndpointResultUsable(endpoint_results_.front(),
138 IsSvcbOptional(endpoint_results_)));
139 }
140 }
141
142 // We don't worry about cancelling the host resolution and TCP connect, since
143 // ~HostResolver::Request and ~TransportConnectSubJob will take care of it.
144 TransportConnectJob::~TransportConnectJob() = default;
145
GetLoadState() const146 LoadState TransportConnectJob::GetLoadState() const {
147 switch (next_state_) {
148 case STATE_RESOLVE_HOST:
149 case STATE_RESOLVE_HOST_COMPLETE:
150 case STATE_RESOLVE_HOST_CALLBACK_COMPLETE:
151 return LOAD_STATE_RESOLVING_HOST;
152 case STATE_TRANSPORT_CONNECT:
153 case STATE_TRANSPORT_CONNECT_COMPLETE: {
154 LoadState load_state = LOAD_STATE_IDLE;
155 if (ipv6_job_ && ipv6_job_->started()) {
156 load_state = ipv6_job_->GetLoadState();
157 }
158 // This method should return LOAD_STATE_CONNECTING in preference to
159 // LOAD_STATE_WAITING_FOR_AVAILABLE_SOCKET when possible because "waiting
160 // for available socket" implies that nothing is happening.
161 if (ipv4_job_ && ipv4_job_->started() &&
162 load_state != LOAD_STATE_CONNECTING) {
163 load_state = ipv4_job_->GetLoadState();
164 }
165 return load_state;
166 }
167 case STATE_NONE:
168 return LOAD_STATE_IDLE;
169 }
170 }
171
HasEstablishedConnection() const172 bool TransportConnectJob::HasEstablishedConnection() const {
173 // No need to ever return true, since NotifyComplete() is called as soon as a
174 // connection is established.
175 return false;
176 }
177
GetConnectionAttempts() const178 ConnectionAttempts TransportConnectJob::GetConnectionAttempts() const {
179 return connection_attempts_;
180 }
181
GetResolveErrorInfo() const182 ResolveErrorInfo TransportConnectJob::GetResolveErrorInfo() const {
183 return resolve_error_info_;
184 }
185
186 std::optional<HostResolverEndpointResult>
GetHostResolverEndpointResult() const187 TransportConnectJob::GetHostResolverEndpointResult() const {
188 CHECK_LT(current_endpoint_result_, endpoint_results_.size());
189 return endpoint_results_[current_endpoint_result_];
190 }
191
ConnectionTimeout()192 base::TimeDelta TransportConnectJob::ConnectionTimeout() {
193 // TODO(eroman): The use of this constant needs to be re-evaluated. The time
194 // needed for TCPClientSocketXXX::Connect() can be arbitrarily long, since
195 // the address list may contain many alternatives, and most of those may
196 // timeout. Even worse, the per-connect timeout threshold varies greatly
197 // between systems (anywhere from 20 seconds to 190 seconds).
198 // See comment #12 at http://crbug.com/23364 for specifics.
199 return base::Minutes(4);
200 }
201
OnIOComplete(int result)202 void TransportConnectJob::OnIOComplete(int result) {
203 result = DoLoop(result);
204 if (result != ERR_IO_PENDING)
205 NotifyDelegateOfCompletion(result); // Deletes |this|
206 }
207
DoLoop(int result)208 int TransportConnectJob::DoLoop(int result) {
209 DCHECK_NE(next_state_, STATE_NONE);
210
211 int rv = result;
212 do {
213 State state = next_state_;
214 next_state_ = STATE_NONE;
215 switch (state) {
216 case STATE_RESOLVE_HOST:
217 DCHECK_EQ(OK, rv);
218 rv = DoResolveHost();
219 break;
220 case STATE_RESOLVE_HOST_COMPLETE:
221 rv = DoResolveHostComplete(rv);
222 break;
223 case STATE_RESOLVE_HOST_CALLBACK_COMPLETE:
224 DCHECK_EQ(OK, rv);
225 rv = DoResolveHostCallbackComplete();
226 break;
227 case STATE_TRANSPORT_CONNECT:
228 DCHECK_EQ(OK, rv);
229 rv = DoTransportConnect();
230 break;
231 case STATE_TRANSPORT_CONNECT_COMPLETE:
232 rv = DoTransportConnectComplete(rv);
233 break;
234 default:
235 NOTREACHED();
236 }
237 } while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE);
238
239 return rv;
240 }
241
DoResolveHost()242 int TransportConnectJob::DoResolveHost() {
243 connect_timing_.domain_lookup_start = base::TimeTicks::Now();
244
245 if (has_dns_override_) {
246 DCHECK_EQ(1u, endpoint_results_.size());
247 connect_timing_.domain_lookup_end = connect_timing_.domain_lookup_start;
248 next_state_ = STATE_TRANSPORT_CONNECT;
249 return OK;
250 }
251
252 next_state_ = STATE_RESOLVE_HOST_COMPLETE;
253
254 HostResolver::ResolveHostParameters parameters;
255 parameters.initial_priority = priority();
256 parameters.secure_dns_policy = params_->secure_dns_policy();
257 if (absl::holds_alternative<url::SchemeHostPort>(params_->destination())) {
258 request_ = host_resolver()->CreateRequest(
259 absl::get<url::SchemeHostPort>(params_->destination()),
260 params_->network_anonymization_key(), net_log(), parameters);
261 } else {
262 request_ = host_resolver()->CreateRequest(
263 absl::get<HostPortPair>(params_->destination()),
264 params_->network_anonymization_key(), net_log(), parameters);
265 }
266
267 return request_->Start(base::BindOnce(&TransportConnectJob::OnIOComplete,
268 base::Unretained(this)));
269 }
270
DoResolveHostComplete(int result)271 int TransportConnectJob::DoResolveHostComplete(int result) {
272 TRACE_EVENT0(NetTracingCategory(),
273 "TransportConnectJob::DoResolveHostComplete");
274 connect_timing_.domain_lookup_end = base::TimeTicks::Now();
275 // Overwrite connection start time, since for connections that do not go
276 // through proxies, |connect_start| should not include dns lookup time.
277 connect_timing_.connect_start = connect_timing_.domain_lookup_end;
278 resolve_error_info_ = request_->GetResolveErrorInfo();
279
280 if (result != OK) {
281 // If hostname resolution failed, record an empty endpoint and the result.
282 connection_attempts_.push_back(ConnectionAttempt(IPEndPoint(), result));
283 return result;
284 }
285
286 DCHECK(request_->GetAddressResults());
287 DCHECK(request_->GetDnsAliasResults());
288 DCHECK(request_->GetEndpointResults());
289
290 // Invoke callback. If it indicates |this| may be slated for deletion, then
291 // only continue after a PostTask.
292 next_state_ = STATE_RESOLVE_HOST_CALLBACK_COMPLETE;
293 if (!params_->host_resolution_callback().is_null()) {
294 OnHostResolutionCallbackResult callback_result =
295 params_->host_resolution_callback().Run(
296 ToLegacyDestinationEndpoint(params_->destination()),
297 *request_->GetEndpointResults(), *request_->GetDnsAliasResults());
298 if (callback_result == OnHostResolutionCallbackResult::kMayBeDeletedAsync) {
299 base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
300 FROM_HERE, base::BindOnce(&TransportConnectJob::OnIOComplete,
301 weak_ptr_factory_.GetWeakPtr(), OK));
302 return ERR_IO_PENDING;
303 }
304 }
305
306 return result;
307 }
308
DoResolveHostCallbackComplete()309 int TransportConnectJob::DoResolveHostCallbackComplete() {
310 const auto& unfiltered_results = *request_->GetEndpointResults();
311 bool svcb_optional = IsSvcbOptional(unfiltered_results);
312 std::set<IPEndPoint> ip_endpoints_seen;
313 for (const auto& result : unfiltered_results) {
314 if (!IsEndpointResultUsable(result, svcb_optional)) {
315 continue;
316 }
317 // The TCP connect itself does not depend on any metadata, so we can dedup
318 // by IP endpoint. In particular, the fallback A/AAAA route will often use
319 // the same IP endpoints as the HTTPS route. If they do not work for one
320 // route, there is no use in trying a second time.
321 std::vector<IPEndPoint> ip_endpoints;
322 for (const auto& ip_endpoint : result.ip_endpoints) {
323 auto [iter, inserted] = ip_endpoints_seen.insert(ip_endpoint);
324 if (inserted) {
325 ip_endpoints.push_back(ip_endpoint);
326 }
327 }
328 if (!ip_endpoints.empty()) {
329 HostResolverEndpointResult new_result;
330 new_result.ip_endpoints = std::move(ip_endpoints);
331 new_result.metadata = result.metadata;
332 endpoint_results_.push_back(std::move(new_result));
333 }
334 }
335 dns_aliases_ = *request_->GetDnsAliasResults();
336
337 // No need to retain `request_` beyond this point.
338 request_.reset();
339
340 if (endpoint_results_.empty()) {
341 // In the general case, DNS may successfully return routes, but none are
342 // compatible with this `ConnectJob`. This should not happen for HTTPS
343 // because `HostResolver` will reject SVCB/HTTPS sets that do not cover the
344 // default "http/1.1" ALPN.
345 return ERR_NAME_NOT_RESOLVED;
346 }
347
348 next_state_ = STATE_TRANSPORT_CONNECT;
349 return OK;
350 }
351
DoTransportConnect()352 int TransportConnectJob::DoTransportConnect() {
353 next_state_ = STATE_TRANSPORT_CONNECT_COMPLETE;
354
355 const HostResolverEndpointResult& endpoint =
356 GetEndpointResultForCurrentSubJobs();
357 std::vector<IPEndPoint> ipv4_addresses, ipv6_addresses;
358 for (const auto& ip_endpoint : endpoint.ip_endpoints) {
359 switch (ip_endpoint.GetFamily()) {
360 case ADDRESS_FAMILY_IPV4:
361 ipv4_addresses.push_back(ip_endpoint);
362 break;
363
364 case ADDRESS_FAMILY_IPV6:
365 ipv6_addresses.push_back(ip_endpoint);
366 break;
367
368 default:
369 DVLOG(1) << "Unexpected ADDRESS_FAMILY: " << ip_endpoint.GetFamily();
370 break;
371 }
372 }
373
374 if (!ipv4_addresses.empty()) {
375 ipv4_job_ = std::make_unique<TransportConnectSubJob>(
376 std::move(ipv4_addresses), this, SUB_JOB_IPV4);
377 }
378
379 if (!ipv6_addresses.empty()) {
380 ipv6_job_ = std::make_unique<TransportConnectSubJob>(
381 std::move(ipv6_addresses), this, SUB_JOB_IPV6);
382 int result = ipv6_job_->Start();
383 if (result != ERR_IO_PENDING)
384 return HandleSubJobComplete(result, ipv6_job_.get());
385 if (ipv4_job_) {
386 // This use of base::Unretained is safe because |fallback_timer_| is
387 // owned by this object.
388 fallback_timer_.Start(
389 FROM_HERE, kIPv6FallbackTime,
390 base::BindOnce(&TransportConnectJob::StartIPv4JobAsync,
391 base::Unretained(this)));
392 }
393 return ERR_IO_PENDING;
394 }
395
396 DCHECK(!ipv6_job_);
397 DCHECK(ipv4_job_);
398 int result = ipv4_job_->Start();
399 if (result != ERR_IO_PENDING)
400 return HandleSubJobComplete(result, ipv4_job_.get());
401 return ERR_IO_PENDING;
402 }
403
DoTransportConnectComplete(int result)404 int TransportConnectJob::DoTransportConnectComplete(int result) {
405 // Make sure nothing else calls back into this object.
406 ipv4_job_.reset();
407 ipv6_job_.reset();
408 fallback_timer_.Stop();
409
410 if (result == OK) {
411 DCHECK(!connect_timing_.connect_start.is_null());
412 DCHECK(!connect_timing_.domain_lookup_start.is_null());
413 // `HandleSubJobComplete` should have called `SetSocket`.
414 DCHECK(socket());
415 base::TimeTicks now = base::TimeTicks::Now();
416 base::TimeDelta total_duration = now - connect_timing_.domain_lookup_start;
417 UMA_HISTOGRAM_CUSTOM_TIMES("Net.DNS_Resolution_And_TCP_Connection_Latency2",
418 total_duration, base::Milliseconds(1),
419 base::Minutes(10), 100);
420
421 base::TimeDelta connect_duration = now - connect_timing_.connect_start;
422 UMA_HISTOGRAM_CUSTOM_TIMES("Net.TCP_Connection_Latency", connect_duration,
423 base::Milliseconds(1), base::Minutes(10), 100);
424 } else {
425 // Don't try the next route if entering suspend mode.
426 if (result != ERR_NETWORK_IO_SUSPENDED) {
427 // If there is another endpoint available, try it.
428 current_endpoint_result_++;
429 if (current_endpoint_result_ < endpoint_results_.size()) {
430 next_state_ = STATE_TRANSPORT_CONNECT;
431 result = OK;
432 }
433 }
434 }
435
436 return result;
437 }
438
HandleSubJobComplete(int result,TransportConnectSubJob * job)439 int TransportConnectJob::HandleSubJobComplete(int result,
440 TransportConnectSubJob* job) {
441 DCHECK_NE(result, ERR_IO_PENDING);
442 if (result == OK) {
443 SetSocket(job->PassSocket(), dns_aliases_);
444 return result;
445 }
446
447 if (result == ERR_NETWORK_IO_SUSPENDED) {
448 // Don't try other jobs if entering suspend mode.
449 return result;
450 }
451
452 switch (job->type()) {
453 case SUB_JOB_IPV4:
454 ipv4_job_.reset();
455 break;
456
457 case SUB_JOB_IPV6:
458 ipv6_job_.reset();
459 // Start the other job, rather than wait for the fallback timer.
460 if (ipv4_job_ && !ipv4_job_->started()) {
461 fallback_timer_.Stop();
462 result = ipv4_job_->Start();
463 if (result != ERR_IO_PENDING) {
464 return HandleSubJobComplete(result, ipv4_job_.get());
465 }
466 }
467 break;
468 }
469
470 if (ipv4_job_ || ipv6_job_) {
471 // Wait for the other job to complete, rather than reporting |result|.
472 return ERR_IO_PENDING;
473 }
474
475 return result;
476 }
477
OnSubJobComplete(int result,TransportConnectSubJob * job)478 void TransportConnectJob::OnSubJobComplete(int result,
479 TransportConnectSubJob* job) {
480 result = HandleSubJobComplete(result, job);
481 if (result != ERR_IO_PENDING) {
482 OnIOComplete(result);
483 }
484 }
485
StartIPv4JobAsync()486 void TransportConnectJob::StartIPv4JobAsync() {
487 DCHECK(ipv4_job_);
488 net_log().AddEvent(NetLogEventType::TRANSPORT_CONNECT_JOB_IPV6_FALLBACK);
489 int result = ipv4_job_->Start();
490 if (result != ERR_IO_PENDING)
491 OnSubJobComplete(result, ipv4_job_.get());
492 }
493
ConnectInternal()494 int TransportConnectJob::ConnectInternal() {
495 next_state_ = STATE_RESOLVE_HOST;
496 return DoLoop(OK);
497 }
498
ChangePriorityInternal(RequestPriority priority)499 void TransportConnectJob::ChangePriorityInternal(RequestPriority priority) {
500 if (next_state_ == STATE_RESOLVE_HOST_COMPLETE) {
501 DCHECK(request_);
502 // Change the request priority in the host resolver.
503 request_->ChangeRequestPriority(priority);
504 }
505 }
506
IsSvcbOptional(base::span<const HostResolverEndpointResult> results) const507 bool TransportConnectJob::IsSvcbOptional(
508 base::span<const HostResolverEndpointResult> results) const {
509 // If SVCB/HTTPS resolution succeeded, the client supports ECH, and all routes
510 // support ECH, disable the A/AAAA fallback. See Section 10.1 of
511 // draft-ietf-dnsop-svcb-https-08.
512
513 auto* scheme_host_port =
514 absl::get_if<url::SchemeHostPort>(¶ms_->destination());
515 if (!scheme_host_port || scheme_host_port->scheme() != url::kHttpsScheme) {
516 return true; // This is not a SVCB-capable request at all.
517 }
518
519 if (!common_connect_job_params()->ssl_client_context ||
520 !common_connect_job_params()->ssl_client_context->config().ech_enabled) {
521 return true; // ECH is not supported for this request.
522 }
523
524 return !HostResolver::AllProtocolEndpointsHaveEch(results);
525 }
526
IsEndpointResultUsable(const HostResolverEndpointResult & result,bool svcb_optional) const527 bool TransportConnectJob::IsEndpointResultUsable(
528 const HostResolverEndpointResult& result,
529 bool svcb_optional) const {
530 // A `HostResolverEndpointResult` with no ALPN protocols is the fallback
531 // A/AAAA route. This is always compatible. We assume the ALPN-less option is
532 // TCP-based.
533 if (result.metadata.supported_protocol_alpns.empty()) {
534 // See draft-ietf-dnsop-svcb-https-08, Section 3.
535 return svcb_optional;
536 }
537
538 // See draft-ietf-dnsop-svcb-https-08, Section 7.1.2. Routes are usable if
539 // there is an overlap between the route's ALPN protocols and the configured
540 // ones. This ensures we do not, e.g., connect to a QUIC-only route with TCP.
541 // Note that, if `params_` did not specify any ALPN protocols, no
542 // SVCB/HTTPS-based routes will match and we will effectively ignore all but
543 // plain A/AAAA routes.
544 for (const auto& alpn : result.metadata.supported_protocol_alpns) {
545 if (params_->supported_alpns().contains(alpn)) {
546 return true;
547 }
548 }
549 return false;
550 }
551
552 const HostResolverEndpointResult&
GetEndpointResultForCurrentSubJobs() const553 TransportConnectJob::GetEndpointResultForCurrentSubJobs() const {
554 CHECK_LT(current_endpoint_result_, endpoint_results_.size());
555 return endpoint_results_[current_endpoint_result_];
556 }
557
558 } // namespace net
559