// Copyright 2019 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/socks_connect_job.h" #include "base/callback.h" #include "base/containers/flat_set.h" #include "base/containers/span.h" #include "base/run_loop.h" #include "base/test/task_environment.h" #include "base/time/time.h" #include "build/build_config.h" #include "net/base/load_states.h" #include "net/base/load_timing_info.h" #include "net/base/load_timing_info_test_util.h" #include "net/base/net_errors.h" #include "net/base/network_isolation_key.h" #include "net/dns/mock_host_resolver.h" #include "net/dns/public/secure_dns_policy.h" #include "net/log/net_log.h" #include "net/socket/client_socket_factory.h" #include "net/socket/client_socket_handle.h" #include "net/socket/connect_job_test_util.h" #include "net/socket/socket_tag.h" #include "net/socket/socket_test_util.h" #include "net/socket/socks_connect_job.h" #include "net/socket/transport_client_socket_pool_test_util.h" #include "net/socket/transport_connect_job.h" #include "net/test/gtest_util.h" #include "net/test/test_with_task_environment.h" #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" namespace net { namespace { const char kProxyHostName[] = "proxy.test"; const int kProxyPort = 4321; constexpr base::TimeDelta kTinyTime = base::Microseconds(1); class SOCKSConnectJobTest : public testing::Test, public WithTaskEnvironment { public: enum class SOCKSVersion { V4, V5, }; SOCKSConnectJobTest() : WithTaskEnvironment(base::test::TaskEnvironment::TimeSource::MOCK_TIME), common_connect_job_params_( &client_socket_factory_, &host_resolver_, nullptr /* http_auth_cache */, nullptr /* http_auth_handler_factory */, nullptr /* spdy_session_pool */, nullptr /* quic_supported_versions */, nullptr /* quic_stream_factory */, nullptr /* proxy_delegate */, nullptr /* http_user_agent_settings */, nullptr /* ssl_client_context */, nullptr /* socket_performance_watcher_factory */, nullptr /* network_quality_estimator */, NetLog::Get(), nullptr /* websocket_endpoint_lock_manager */) {} ~SOCKSConnectJobTest() override = default; static scoped_refptr CreateSOCKSParams( SOCKSVersion socks_version, SecureDnsPolicy secure_dns_policy = SecureDnsPolicy::kAllow) { return base::MakeRefCounted( base::MakeRefCounted( HostPortPair(kProxyHostName, kProxyPort), NetworkIsolationKey(), secure_dns_policy, OnHostResolutionCallback(), /*supported_alpns=*/base::flat_set()), socks_version == SOCKSVersion::V5, socks_version == SOCKSVersion::V4 ? HostPortPair(kSOCKS4TestHost, kSOCKS4TestPort) : HostPortPair(kSOCKS5TestHost, kSOCKS5TestPort), NetworkIsolationKey(), TRAFFIC_ANNOTATION_FOR_TESTS); } protected: MockHostResolver host_resolver_{/*default_result=*/MockHostResolverBase:: RuleResolver::GetLocalhostResult()}; MockTaggingClientSocketFactory client_socket_factory_; const CommonConnectJobParams common_connect_job_params_; }; TEST_F(SOCKSConnectJobTest, HostResolutionFailure) { host_resolver_.rules()->AddSimulatedTimeoutFailure(kProxyHostName); for (bool failure_synchronous : {false, true}) { host_resolver_.set_synchronous_mode(failure_synchronous); TestConnectJobDelegate test_delegate; SOCKSConnectJob socks_connect_job(DEFAULT_PRIORITY, SocketTag(), &common_connect_job_params_, CreateSOCKSParams(SOCKSVersion::V5), &test_delegate, nullptr /* net_log */); test_delegate.StartJobExpectingResult( &socks_connect_job, ERR_PROXY_CONNECTION_FAILED, failure_synchronous); EXPECT_THAT(socks_connect_job.GetResolveErrorInfo().error, test::IsError(ERR_DNS_TIMED_OUT)); } } TEST_F(SOCKSConnectJobTest, HostResolutionFailureSOCKS4Endpoint) { const char hostname[] = "google.com"; host_resolver_.rules()->AddSimulatedTimeoutFailure(hostname); for (bool failure_synchronous : {false, true}) { host_resolver_.set_synchronous_mode(failure_synchronous); SequencedSocketData sequenced_socket_data{base::span(), base::span()}; sequenced_socket_data.set_connect_data(MockConnect(SYNCHRONOUS, OK)); client_socket_factory_.AddSocketDataProvider(&sequenced_socket_data); scoped_refptr socket_params = base::MakeRefCounted( base::MakeRefCounted( HostPortPair(kProxyHostName, kProxyPort), NetworkIsolationKey(), SecureDnsPolicy::kAllow, OnHostResolutionCallback(), /*supported_alpns=*/base::flat_set()), false /* socks_v5 */, HostPortPair(hostname, kSOCKS4TestPort), NetworkIsolationKey(), TRAFFIC_ANNOTATION_FOR_TESTS); TestConnectJobDelegate test_delegate; SOCKSConnectJob socks_connect_job( DEFAULT_PRIORITY, SocketTag(), &common_connect_job_params_, socket_params, &test_delegate, nullptr /* net_log */); test_delegate.StartJobExpectingResult( &socks_connect_job, ERR_NAME_NOT_RESOLVED, failure_synchronous); EXPECT_THAT(socks_connect_job.GetResolveErrorInfo().error, test::IsError(ERR_DNS_TIMED_OUT)); } } TEST_F(SOCKSConnectJobTest, HandshakeError) { for (bool host_resolution_synchronous : {false, true}) { for (bool write_failure_synchronous : {false, true}) { host_resolver_.set_synchronous_mode(host_resolution_synchronous); // No need to distinguish which part of the handshake fails. Those details // are all handled at the StreamSocket layer, not the SOCKSConnectJob. MockWrite writes[] = { MockWrite(write_failure_synchronous ? SYNCHRONOUS : ASYNC, ERR_UNEXPECTED, 0), }; SequencedSocketData sequenced_socket_data(base::span(), writes); // Host resolution is used to switch between sync and async connection // behavior. The SOCKS layer can't distinguish between sync and async host // resolution vs sync and async connection establishment, so just always // make connection establishment synchroonous. sequenced_socket_data.set_connect_data(MockConnect(SYNCHRONOUS, OK)); client_socket_factory_.AddSocketDataProvider(&sequenced_socket_data); TestConnectJobDelegate test_delegate; SOCKSConnectJob socks_connect_job(DEFAULT_PRIORITY, SocketTag(), &common_connect_job_params_, CreateSOCKSParams(SOCKSVersion::V5), &test_delegate, nullptr /* net_log */); test_delegate.StartJobExpectingResult( &socks_connect_job, ERR_UNEXPECTED, host_resolution_synchronous && write_failure_synchronous); } } } TEST_F(SOCKSConnectJobTest, SOCKS4) { for (bool host_resolution_synchronous : {false, true}) { for (bool read_and_writes_synchronous : {true}) { host_resolver_.set_synchronous_mode(host_resolution_synchronous); MockWrite writes[] = { MockWrite(SYNCHRONOUS, kSOCKS4OkRequestLocalHostPort80, kSOCKS4OkRequestLocalHostPort80Length, 0), }; MockRead reads[] = { MockRead(SYNCHRONOUS, kSOCKS4OkReply, kSOCKS4OkReplyLength, 1), }; SequencedSocketData sequenced_socket_data(reads, writes); // Host resolution is used to switch between sync and async connection // behavior. The SOCKS layer can't distinguish between sync and async host // resolution vs sync and async connection establishment, so just always // make connection establishment synchroonous. sequenced_socket_data.set_connect_data(MockConnect(SYNCHRONOUS, OK)); client_socket_factory_.AddSocketDataProvider(&sequenced_socket_data); TestConnectJobDelegate test_delegate; SOCKSConnectJob socks_connect_job(DEFAULT_PRIORITY, SocketTag(), &common_connect_job_params_, CreateSOCKSParams(SOCKSVersion::V4), &test_delegate, nullptr /* net_log */); test_delegate.StartJobExpectingResult( &socks_connect_job, OK, host_resolution_synchronous && read_and_writes_synchronous); // Proxies should not set any DNS aliases. EXPECT_TRUE(test_delegate.socket()->GetDnsAliases().empty()); } } } TEST_F(SOCKSConnectJobTest, SOCKS5) { for (bool host_resolution_synchronous : {false, true}) { for (bool read_and_writes_synchronous : {true}) { host_resolver_.set_synchronous_mode(host_resolution_synchronous); MockWrite writes[] = { MockWrite(SYNCHRONOUS, kSOCKS5GreetRequest, kSOCKS5GreetRequestLength, 0), MockWrite(SYNCHRONOUS, kSOCKS5OkRequest, kSOCKS5OkRequestLength, 2), }; MockRead reads[] = { MockRead(SYNCHRONOUS, kSOCKS5GreetResponse, kSOCKS5GreetResponseLength, 1), MockRead(SYNCHRONOUS, kSOCKS5OkResponse, kSOCKS5OkResponseLength, 3), }; SequencedSocketData sequenced_socket_data(reads, writes); // Host resolution is used to switch between sync and async connection // behavior. The SOCKS layer can't distinguish between sync and async host // resolution vs sync and async connection establishment, so just always // make connection establishment synchroonous. sequenced_socket_data.set_connect_data(MockConnect(SYNCHRONOUS, OK)); client_socket_factory_.AddSocketDataProvider(&sequenced_socket_data); TestConnectJobDelegate test_delegate; SOCKSConnectJob socks_connect_job(DEFAULT_PRIORITY, SocketTag(), &common_connect_job_params_, CreateSOCKSParams(SOCKSVersion::V5), &test_delegate, nullptr /* net_log */); test_delegate.StartJobExpectingResult( &socks_connect_job, OK, host_resolution_synchronous && read_and_writes_synchronous); // Proxies should not set any DNS aliases. EXPECT_TRUE(test_delegate.socket()->GetDnsAliases().empty()); } } } TEST_F(SOCKSConnectJobTest, HasEstablishedConnection) { host_resolver_.set_ondemand_mode(true); MockWrite writes[] = { MockWrite(ASYNC, kSOCKS4OkRequestLocalHostPort80, kSOCKS4OkRequestLocalHostPort80Length, 0), }; MockRead reads[] = { MockRead(ASYNC, ERR_IO_PENDING, 1), MockRead(ASYNC, kSOCKS4OkReply, kSOCKS4OkReplyLength, 2), }; SequencedSocketData sequenced_socket_data(reads, writes); sequenced_socket_data.set_connect_data(MockConnect(ASYNC, OK)); client_socket_factory_.AddSocketDataProvider(&sequenced_socket_data); TestConnectJobDelegate test_delegate; SOCKSConnectJob socks_connect_job(DEFAULT_PRIORITY, SocketTag(), &common_connect_job_params_, CreateSOCKSParams(SOCKSVersion::V4), &test_delegate, nullptr /* net_log */); socks_connect_job.Connect(); EXPECT_EQ(LOAD_STATE_RESOLVING_HOST, socks_connect_job.GetLoadState()); EXPECT_FALSE(socks_connect_job.HasEstablishedConnection()); host_resolver_.ResolveNow(1); EXPECT_EQ(LOAD_STATE_CONNECTING, socks_connect_job.GetLoadState()); EXPECT_FALSE(socks_connect_job.HasEstablishedConnection()); sequenced_socket_data.RunUntilPaused(); // "LOAD_STATE_CONNECTING" is also returned when negotiating a SOCKS // connection. EXPECT_EQ(LOAD_STATE_CONNECTING, socks_connect_job.GetLoadState()); EXPECT_TRUE(socks_connect_job.HasEstablishedConnection()); EXPECT_FALSE(test_delegate.has_result()); sequenced_socket_data.Resume(); EXPECT_THAT(test_delegate.WaitForResult(), test::IsOk()); EXPECT_TRUE(test_delegate.has_result()); } // Check that TransportConnectJob's timeout is respected for the nested // TransportConnectJob. TEST_F(SOCKSConnectJobTest, TimeoutDuringDnsResolution) { // Set HostResolver to hang. host_resolver_.set_ondemand_mode(true); TestConnectJobDelegate test_delegate; SOCKSConnectJob socks_connect_job(DEFAULT_PRIORITY, SocketTag(), &common_connect_job_params_, CreateSOCKSParams(SOCKSVersion::V5), &test_delegate, nullptr /* net_log */); socks_connect_job.Connect(); // Just before the TransportConnectJob's timeout, nothing should have // happened. FastForwardBy(TransportConnectJob::ConnectionTimeout() - kTinyTime); EXPECT_TRUE(host_resolver_.has_pending_requests()); EXPECT_FALSE(test_delegate.has_result()); // Wait for exactly the TransportConnectJob's timeout to have passed. The Job // should time out. FastForwardBy(kTinyTime); EXPECT_TRUE(test_delegate.has_result()); EXPECT_THAT(test_delegate.WaitForResult(), test::IsError(ERR_PROXY_CONNECTION_FAILED)); } // Check that SOCKSConnectJob's timeout is respected for the handshake phase. TEST_F(SOCKSConnectJobTest, TimeoutDuringHandshake) { host_resolver_.set_ondemand_mode(true); MockWrite writes[] = { MockWrite(SYNCHRONOUS, ERR_IO_PENDING, 0), }; SequencedSocketData sequenced_socket_data(base::span(), writes); sequenced_socket_data.set_connect_data(MockConnect(SYNCHRONOUS, OK)); client_socket_factory_.AddSocketDataProvider(&sequenced_socket_data); TestConnectJobDelegate test_delegate; SOCKSConnectJob socks_connect_job(DEFAULT_PRIORITY, SocketTag(), &common_connect_job_params_, CreateSOCKSParams(SOCKSVersion::V5), &test_delegate, nullptr /* net_log */); socks_connect_job.Connect(); // Just before the TransportConnectJob's timeout, nothing should have // happened. FastForwardBy(TransportConnectJob::ConnectionTimeout() - kTinyTime); EXPECT_FALSE(test_delegate.has_result()); EXPECT_TRUE(host_resolver_.has_pending_requests()); // DNS resolution completes, and the socket connects. The request should not // time out, even after the TransportConnectJob's timeout passes. The // SOCKSConnectJob's handshake timer should also be started. host_resolver_.ResolveAllPending(); // Waiting until just before the SOCKS handshake times out. There should cause // no observable change in the SOCKSConnectJob's status. FastForwardBy(SOCKSConnectJob::HandshakeTimeoutForTesting() - kTinyTime); EXPECT_FALSE(test_delegate.has_result()); // Wait for exactly the SOCKSConnectJob's handshake timeout has fully elapsed. // The Job should time out. FastForwardBy(kTinyTime); EXPECT_FALSE(host_resolver_.has_pending_requests()); EXPECT_TRUE(test_delegate.has_result()); EXPECT_THAT(test_delegate.WaitForResult(), test::IsError(ERR_TIMED_OUT)); } // Check initial priority is passed to the HostResolver, and priority can be // modified. TEST_F(SOCKSConnectJobTest, Priority) { host_resolver_.set_ondemand_mode(true); for (int initial_priority = MINIMUM_PRIORITY; initial_priority <= MAXIMUM_PRIORITY; ++initial_priority) { for (int new_priority = MINIMUM_PRIORITY; new_priority <= MAXIMUM_PRIORITY; ++new_priority) { // Don't try changing priority to itself, as APIs may not allow that. if (new_priority == initial_priority) continue; TestConnectJobDelegate test_delegate; SOCKSConnectJob socks_connect_job( static_cast(initial_priority), SocketTag(), &common_connect_job_params_, CreateSOCKSParams(SOCKSVersion::V4), &test_delegate, nullptr /* net_log */); ASSERT_THAT(socks_connect_job.Connect(), test::IsError(ERR_IO_PENDING)); ASSERT_TRUE(host_resolver_.has_pending_requests()); int request_id = host_resolver_.num_resolve(); EXPECT_EQ(initial_priority, host_resolver_.request_priority(request_id)); // Change priority. socks_connect_job.ChangePriority( static_cast(new_priority)); EXPECT_EQ(new_priority, host_resolver_.request_priority(request_id)); // Restore initial priority. socks_connect_job.ChangePriority( static_cast(initial_priority)); EXPECT_EQ(initial_priority, host_resolver_.request_priority(request_id)); } } } TEST_F(SOCKSConnectJobTest, SecureDnsPolicy) { for (auto secure_dns_policy : {SecureDnsPolicy::kAllow, SecureDnsPolicy::kDisable}) { TestConnectJobDelegate test_delegate; SOCKSConnectJob socks_connect_job( DEFAULT_PRIORITY, SocketTag(), &common_connect_job_params_, CreateSOCKSParams(SOCKSVersion::V4, secure_dns_policy), &test_delegate, nullptr /* net_log */); ASSERT_THAT(socks_connect_job.Connect(), test::IsError(ERR_IO_PENDING)); EXPECT_EQ(secure_dns_policy, host_resolver_.last_secure_dns_policy()); } } TEST_F(SOCKSConnectJobTest, ConnectTiming) { host_resolver_.set_ondemand_mode(true); MockWrite writes[] = { MockWrite(ASYNC, ERR_IO_PENDING, 0), MockWrite(ASYNC, kSOCKS5GreetRequest, kSOCKS5GreetRequestLength, 1), MockWrite(SYNCHRONOUS, kSOCKS5OkRequest, kSOCKS5OkRequestLength, 3), }; MockRead reads[] = { MockRead(SYNCHRONOUS, kSOCKS5GreetResponse, kSOCKS5GreetResponseLength, 2), MockRead(SYNCHRONOUS, kSOCKS5OkResponse, kSOCKS5OkResponseLength, 4), }; SequencedSocketData sequenced_socket_data(reads, writes); // Host resolution is used to switch between sync and async connection // behavior. The SOCKS layer can't distinguish between sync and async host // resolution vs sync and async connection establishment, so just always // make connection establishment synchroonous. sequenced_socket_data.set_connect_data(MockConnect(SYNCHRONOUS, OK)); client_socket_factory_.AddSocketDataProvider(&sequenced_socket_data); TestConnectJobDelegate test_delegate; SOCKSConnectJob socks_connect_job(DEFAULT_PRIORITY, SocketTag(), &common_connect_job_params_, CreateSOCKSParams(SOCKSVersion::V5), &test_delegate, nullptr /* net_log */); base::TimeTicks start = base::TimeTicks::Now(); socks_connect_job.Connect(); // DNS resolution completes after a short delay. The connection should be // immediately established as well. The first write to the socket stalls. FastForwardBy(kTinyTime); host_resolver_.ResolveAllPending(); RunUntilIdle(); // After another short delay, data is received from the server. FastForwardBy(kTinyTime); sequenced_socket_data.Resume(); EXPECT_THAT(test_delegate.WaitForResult(), test::IsOk()); // Proxy name resolution is not considered resolving the host name for // ConnectionInfo. For SOCKS4, where the host name is also looked up via DNS, // the resolution time is not currently reported. EXPECT_EQ(base::TimeTicks(), socks_connect_job.connect_timing().dns_start); EXPECT_EQ(base::TimeTicks(), socks_connect_job.connect_timing().dns_end); // The "connect" time for socks proxies includes DNS resolution time. EXPECT_EQ(start, socks_connect_job.connect_timing().connect_start); EXPECT_EQ(start + 2 * kTinyTime, socks_connect_job.connect_timing().connect_end); // Since SSL was not negotiated, SSL times are null. EXPECT_EQ(base::TimeTicks(), socks_connect_job.connect_timing().ssl_start); EXPECT_EQ(base::TimeTicks(), socks_connect_job.connect_timing().ssl_end); } TEST_F(SOCKSConnectJobTest, CancelDuringDnsResolution) { // Set HostResolver to hang. host_resolver_.set_ondemand_mode(true); TestConnectJobDelegate test_delegate; std::unique_ptr socks_connect_job = std::make_unique(DEFAULT_PRIORITY, SocketTag(), &common_connect_job_params_, CreateSOCKSParams(SOCKSVersion::V5), &test_delegate, nullptr /* net_log */); socks_connect_job->Connect(); EXPECT_TRUE(host_resolver_.has_pending_requests()); socks_connect_job.reset(); RunUntilIdle(); EXPECT_FALSE(host_resolver_.has_pending_requests()); EXPECT_FALSE(test_delegate.has_result()); } TEST_F(SOCKSConnectJobTest, CancelDuringConnect) { host_resolver_.set_synchronous_mode(true); SequencedSocketData sequenced_socket_data{base::span(), base::span()}; sequenced_socket_data.set_connect_data(MockConnect(ASYNC, OK)); client_socket_factory_.AddSocketDataProvider(&sequenced_socket_data); TestConnectJobDelegate test_delegate; std::unique_ptr socks_connect_job = std::make_unique(DEFAULT_PRIORITY, SocketTag(), &common_connect_job_params_, CreateSOCKSParams(SOCKSVersion::V5), &test_delegate, nullptr /* net_log */); socks_connect_job->Connect(); // Host resolution should resolve immediately. The ConnectJob should currently // be trying to connect. EXPECT_FALSE(host_resolver_.has_pending_requests()); socks_connect_job.reset(); RunUntilIdle(); EXPECT_FALSE(test_delegate.has_result()); // Socket should have been destroyed. EXPECT_FALSE(sequenced_socket_data.socket()); } TEST_F(SOCKSConnectJobTest, CancelDuringHandshake) { host_resolver_.set_synchronous_mode(true); // Hang at start of handshake. MockWrite writes[] = { MockWrite(SYNCHRONOUS, ERR_IO_PENDING, 0), }; SequencedSocketData sequenced_socket_data(base::span(), writes); sequenced_socket_data.set_connect_data(MockConnect(SYNCHRONOUS, OK)); client_socket_factory_.AddSocketDataProvider(&sequenced_socket_data); TestConnectJobDelegate test_delegate; std::unique_ptr socks_connect_job = std::make_unique(DEFAULT_PRIORITY, SocketTag(), &common_connect_job_params_, CreateSOCKSParams(SOCKSVersion::V5), &test_delegate, nullptr /* net_log */); socks_connect_job->Connect(); // Host resolution should resolve immediately. The socket connecting, and the // ConnectJob should currently be trying to send the SOCKS handshake. EXPECT_FALSE(host_resolver_.has_pending_requests()); socks_connect_job.reset(); RunUntilIdle(); EXPECT_FALSE(test_delegate.has_result()); // Socket should have been destroyed. EXPECT_FALSE(sequenced_socket_data.socket()); EXPECT_TRUE(sequenced_socket_data.AllWriteDataConsumed()); } } // namespace } // namespace net