| // Copyright 2022 The Chromium Authors | 
 | // Use of this source code is governed by a BSD-style license that can be | 
 | // found in the LICENSE file. | 
 |  | 
 | #include "quiche/quic/tools/connect_udp_tunnel.h" | 
 |  | 
 | #include <memory> | 
 | #include <string> | 
 |  | 
 | #include "absl/status/status.h" | 
 | #include "absl/status/statusor.h" | 
 | #include "absl/strings/str_cat.h" | 
 | #include "absl/strings/string_view.h" | 
 | #include "url/url_canon_stdstring.h" | 
 | #include "url/url_util.h" | 
 | #include "quiche/quic/core/connecting_client_socket.h" | 
 | #include "quiche/quic/core/http/quic_spdy_stream.h" | 
 | #include "quiche/quic/core/quic_connection_id.h" | 
 | #include "quiche/quic/core/quic_error_codes.h" | 
 | #include "quiche/quic/core/quic_types.h" | 
 | #include "quiche/quic/core/socket_factory.h" | 
 | #include "quiche/quic/platform/api/quic_socket_address.h" | 
 | #include "quiche/quic/platform/api/quic_test_loopback.h" | 
 | #include "quiche/quic/test_tools/quic_test_utils.h" | 
 | #include "quiche/quic/tools/quic_simple_server_backend.h" | 
 | #include "quiche/common/masque/connect_udp_datagram_payload.h" | 
 | #include "quiche/common/platform/api/quiche_mem_slice.h" | 
 | #include "quiche/common/platform/api/quiche_test.h" | 
 |  | 
 | namespace quic::test { | 
 | namespace { | 
 |  | 
 | using ::testing::_; | 
 | using ::testing::AnyOf; | 
 | using ::testing::Eq; | 
 | using ::testing::Ge; | 
 | using ::testing::Gt; | 
 | using ::testing::HasSubstr; | 
 | using ::testing::InvokeWithoutArgs; | 
 | using ::testing::IsEmpty; | 
 | using ::testing::Matcher; | 
 | using ::testing::NiceMock; | 
 | using ::testing::Pair; | 
 | using ::testing::Property; | 
 | using ::testing::Return; | 
 | using ::testing::StrictMock; | 
 | using ::testing::UnorderedElementsAre; | 
 |  | 
 | constexpr QuicStreamId kStreamId = 100; | 
 |  | 
 | class MockStream : public QuicSpdyStream { | 
 |  public: | 
 |   explicit MockStream(QuicSpdySession* spdy_session) | 
 |       : QuicSpdyStream(kStreamId, spdy_session, BIDIRECTIONAL) {} | 
 |  | 
 |   void OnBodyAvailable() override {} | 
 |  | 
 |   MOCK_METHOD(MessageStatus, SendHttp3Datagram, (absl::string_view data), | 
 |               (override)); | 
 | }; | 
 |  | 
 | class MockRequestHandler : public QuicSimpleServerBackend::RequestHandler { | 
 |  public: | 
 |   QuicConnectionId connection_id() const override { | 
 |     return TestConnectionId(41212); | 
 |   } | 
 |   QuicStreamId stream_id() const override { return kStreamId; } | 
 |   std::string peer_host() const override { return "127.0.0.1"; } | 
 |  | 
 |   MOCK_METHOD(QuicSpdyStream*, GetStream, (), (override)); | 
 |   MOCK_METHOD(void, OnResponseBackendComplete, | 
 |               (const QuicBackendResponse* response), (override)); | 
 |   MOCK_METHOD(void, SendStreamData, (absl::string_view data, bool close_stream), | 
 |               (override)); | 
 |   MOCK_METHOD(void, TerminateStreamWithError, (QuicResetStreamError error), | 
 |               (override)); | 
 | }; | 
 |  | 
 | class MockSocketFactory : public SocketFactory { | 
 |  public: | 
 |   MOCK_METHOD(std::unique_ptr<ConnectingClientSocket>, CreateTcpClientSocket, | 
 |               (const QuicSocketAddress& peer_address, | 
 |                QuicByteCount receive_buffer_size, | 
 |                QuicByteCount send_buffer_size, | 
 |                ConnectingClientSocket::AsyncVisitor* async_visitor), | 
 |               (override)); | 
 |   MOCK_METHOD(std::unique_ptr<ConnectingClientSocket>, | 
 |               CreateConnectingUdpClientSocket, | 
 |               (const QuicSocketAddress& peer_address, | 
 |                QuicByteCount receive_buffer_size, | 
 |                QuicByteCount send_buffer_size, | 
 |                ConnectingClientSocket::AsyncVisitor* async_visitor), | 
 |               (override)); | 
 | }; | 
 |  | 
 | class MockSocket : public ConnectingClientSocket { | 
 |  public: | 
 |   MOCK_METHOD(absl::Status, ConnectBlocking, (), (override)); | 
 |   MOCK_METHOD(void, ConnectAsync, (), (override)); | 
 |   MOCK_METHOD(void, Disconnect, (), (override)); | 
 |   MOCK_METHOD(absl::StatusOr<QuicSocketAddress>, GetLocalAddress, (), | 
 |               (override)); | 
 |   MOCK_METHOD(absl::StatusOr<quiche::QuicheMemSlice>, ReceiveBlocking, | 
 |               (QuicByteCount max_size), (override)); | 
 |   MOCK_METHOD(void, ReceiveAsync, (QuicByteCount max_size), (override)); | 
 |   MOCK_METHOD(absl::Status, SendBlocking, (std::string data), (override)); | 
 |   MOCK_METHOD(absl::Status, SendBlocking, (quiche::QuicheMemSlice data), | 
 |               (override)); | 
 |   MOCK_METHOD(void, SendAsync, (std::string data), (override)); | 
 |   MOCK_METHOD(void, SendAsync, (quiche::QuicheMemSlice data), (override)); | 
 | }; | 
 |  | 
 | class ConnectUdpTunnelTest : public quiche::test::QuicheTest { | 
 |  public: | 
 |   void SetUp() override { | 
 |     auto socket = std::make_unique<StrictMock<MockSocket>>(); | 
 |     socket_ = socket.get(); | 
 |     ON_CALL(socket_factory_, | 
 |             CreateConnectingUdpClientSocket( | 
 |                 AnyOf(QuicSocketAddress(TestLoopback4(), kAcceptablePort), | 
 |                       QuicSocketAddress(TestLoopback6(), kAcceptablePort)), | 
 |                 _, _, &tunnel_)) | 
 |         .WillByDefault(Return(ByMove(std::move(socket)))); | 
 |  | 
 |     EXPECT_CALL(request_handler_, GetStream()).WillRepeatedly(Return(&stream_)); | 
 |   } | 
 |  | 
 |  protected: | 
 |   static constexpr absl::string_view kAcceptableTarget = "localhost"; | 
 |   static constexpr uint16_t kAcceptablePort = 977; | 
 |  | 
 |   NiceMock<MockQuicConnectionHelper> connection_helper_; | 
 |   NiceMock<MockAlarmFactory> alarm_factory_; | 
 |   NiceMock<MockQuicSpdySession> session_{new NiceMock<MockQuicConnection>( | 
 |       &connection_helper_, &alarm_factory_, Perspective::IS_SERVER)}; | 
 |   StrictMock<MockStream> stream_{&session_}; | 
 |  | 
 |   StrictMock<MockRequestHandler> request_handler_; | 
 |   NiceMock<MockSocketFactory> socket_factory_; | 
 |   StrictMock<MockSocket>* socket_; | 
 |  | 
 |   ConnectUdpTunnel tunnel_{ | 
 |       &request_handler_, | 
 |       &socket_factory_, | 
 |       "server_label", | 
 |       /*acceptable_targets=*/ | 
 |       {{std::string(kAcceptableTarget), kAcceptablePort}, | 
 |        {TestLoopback4().ToString(), kAcceptablePort}, | 
 |        {absl::StrCat("[", TestLoopback6().ToString(), "]"), kAcceptablePort}}}; | 
 | }; | 
 |  | 
 | TEST_F(ConnectUdpTunnelTest, OpenTunnel) { | 
 |   EXPECT_CALL(*socket_, ConnectBlocking()).WillOnce(Return(absl::OkStatus())); | 
 |   EXPECT_CALL(*socket_, ReceiveAsync(Gt(0))); | 
 |   EXPECT_CALL(*socket_, Disconnect()).WillOnce(InvokeWithoutArgs([this]() { | 
 |     tunnel_.ReceiveComplete(absl::CancelledError()); | 
 |   })); | 
 |  | 
 |   EXPECT_CALL( | 
 |       request_handler_, | 
 |       OnResponseBackendComplete( | 
 |           AllOf(Property(&QuicBackendResponse::response_type, | 
 |                          QuicBackendResponse::INCOMPLETE_RESPONSE), | 
 |                 Property(&QuicBackendResponse::headers, | 
 |                          UnorderedElementsAre(Pair(":status", "200"), | 
 |                                               Pair("Capsule-Protocol", "?1"))), | 
 |                 Property(&QuicBackendResponse::trailers, IsEmpty()), | 
 |                 Property(&QuicBackendResponse::body, IsEmpty())))); | 
 |  | 
 |   spdy::Http2HeaderBlock request_headers; | 
 |   request_headers[":method"] = "CONNECT"; | 
 |   request_headers[":protocol"] = "connect-udp"; | 
 |   request_headers[":authority"] = "proxy.test"; | 
 |   request_headers[":scheme"] = "https"; | 
 |   request_headers[":path"] = absl::StrCat( | 
 |       "/.well-known/masque/udp/", kAcceptableTarget, "/", kAcceptablePort, "/"); | 
 |  | 
 |   tunnel_.OpenTunnel(request_headers); | 
 |   EXPECT_TRUE(tunnel_.IsTunnelOpenToTarget()); | 
 |   tunnel_.OnClientStreamClose(); | 
 |   EXPECT_FALSE(tunnel_.IsTunnelOpenToTarget()); | 
 | } | 
 |  | 
 | TEST_F(ConnectUdpTunnelTest, OpenTunnelToIpv4LiteralTarget) { | 
 |   EXPECT_CALL(*socket_, ConnectBlocking()).WillOnce(Return(absl::OkStatus())); | 
 |   EXPECT_CALL(*socket_, ReceiveAsync(Gt(0))); | 
 |   EXPECT_CALL(*socket_, Disconnect()).WillOnce(InvokeWithoutArgs([this]() { | 
 |     tunnel_.ReceiveComplete(absl::CancelledError()); | 
 |   })); | 
 |  | 
 |   EXPECT_CALL( | 
 |       request_handler_, | 
 |       OnResponseBackendComplete( | 
 |           AllOf(Property(&QuicBackendResponse::response_type, | 
 |                          QuicBackendResponse::INCOMPLETE_RESPONSE), | 
 |                 Property(&QuicBackendResponse::headers, | 
 |                          UnorderedElementsAre(Pair(":status", "200"), | 
 |                                               Pair("Capsule-Protocol", "?1"))), | 
 |                 Property(&QuicBackendResponse::trailers, IsEmpty()), | 
 |                 Property(&QuicBackendResponse::body, IsEmpty())))); | 
 |  | 
 |   spdy::Http2HeaderBlock request_headers; | 
 |   request_headers[":method"] = "CONNECT"; | 
 |   request_headers[":protocol"] = "connect-udp"; | 
 |   request_headers[":authority"] = "proxy.test"; | 
 |   request_headers[":scheme"] = "https"; | 
 |   request_headers[":path"] = | 
 |       absl::StrCat("/.well-known/masque/udp/", TestLoopback4().ToString(), "/", | 
 |                    kAcceptablePort, "/"); | 
 |  | 
 |   tunnel_.OpenTunnel(request_headers); | 
 |   EXPECT_TRUE(tunnel_.IsTunnelOpenToTarget()); | 
 |   tunnel_.OnClientStreamClose(); | 
 |   EXPECT_FALSE(tunnel_.IsTunnelOpenToTarget()); | 
 | } | 
 |  | 
 | std::string PercentEncode(absl::string_view input) { | 
 |   std::string encoded; | 
 |   url::StdStringCanonOutput canon_output(&encoded); | 
 |   url::EncodeURIComponent(input.data(), input.size(), &canon_output); | 
 |   canon_output.Complete(); | 
 |   return encoded; | 
 | } | 
 |  | 
 | TEST_F(ConnectUdpTunnelTest, OpenTunnelToIpv6LiteralTarget) { | 
 |   EXPECT_CALL(*socket_, ConnectBlocking()).WillOnce(Return(absl::OkStatus())); | 
 |   EXPECT_CALL(*socket_, ReceiveAsync(Gt(0))); | 
 |   EXPECT_CALL(*socket_, Disconnect()).WillOnce(InvokeWithoutArgs([this]() { | 
 |     tunnel_.ReceiveComplete(absl::CancelledError()); | 
 |   })); | 
 |  | 
 |   EXPECT_CALL( | 
 |       request_handler_, | 
 |       OnResponseBackendComplete( | 
 |           AllOf(Property(&QuicBackendResponse::response_type, | 
 |                          QuicBackendResponse::INCOMPLETE_RESPONSE), | 
 |                 Property(&QuicBackendResponse::headers, | 
 |                          UnorderedElementsAre(Pair(":status", "200"), | 
 |                                               Pair("Capsule-Protocol", "?1"))), | 
 |                 Property(&QuicBackendResponse::trailers, IsEmpty()), | 
 |                 Property(&QuicBackendResponse::body, IsEmpty())))); | 
 |  | 
 |   spdy::Http2HeaderBlock request_headers; | 
 |   request_headers[":method"] = "CONNECT"; | 
 |   request_headers[":protocol"] = "connect-udp"; | 
 |   request_headers[":authority"] = "proxy.test"; | 
 |   request_headers[":scheme"] = "https"; | 
 |   request_headers[":path"] = absl::StrCat( | 
 |       "/.well-known/masque/udp/", | 
 |       PercentEncode(absl::StrCat("[", TestLoopback6().ToString(), "]")), "/", | 
 |       kAcceptablePort, "/"); | 
 |  | 
 |   tunnel_.OpenTunnel(request_headers); | 
 |   EXPECT_TRUE(tunnel_.IsTunnelOpenToTarget()); | 
 |   tunnel_.OnClientStreamClose(); | 
 |   EXPECT_FALSE(tunnel_.IsTunnelOpenToTarget()); | 
 | } | 
 |  | 
 | TEST_F(ConnectUdpTunnelTest, OpenTunnelWithMalformedRequest) { | 
 |   EXPECT_CALL(request_handler_, | 
 |               TerminateStreamWithError(Property( | 
 |                   &QuicResetStreamError::ietf_application_code, | 
 |                   static_cast<uint64_t>(QuicHttp3ErrorCode::MESSAGE_ERROR)))); | 
 |  | 
 |   spdy::Http2HeaderBlock request_headers; | 
 |   request_headers[":method"] = "CONNECT"; | 
 |   request_headers[":protocol"] = "connect-udp"; | 
 |   request_headers[":authority"] = "proxy.test"; | 
 |   request_headers[":scheme"] = "https"; | 
 |   // No ":path" header. | 
 |  | 
 |   tunnel_.OpenTunnel(request_headers); | 
 |   EXPECT_FALSE(tunnel_.IsTunnelOpenToTarget()); | 
 |   tunnel_.OnClientStreamClose(); | 
 | } | 
 |  | 
 | TEST_F(ConnectUdpTunnelTest, OpenTunnelWithUnacceptableTarget) { | 
 |   EXPECT_CALL(request_handler_, | 
 |               OnResponseBackendComplete(AllOf( | 
 |                   Property(&QuicBackendResponse::response_type, | 
 |                            QuicBackendResponse::REGULAR_RESPONSE), | 
 |                   Property(&QuicBackendResponse::headers, | 
 |                            UnorderedElementsAre( | 
 |                                Pair(":status", "403"), | 
 |                                Pair("Proxy-Status", | 
 |                                     HasSubstr("destination_ip_prohibited")))), | 
 |                   Property(&QuicBackendResponse::trailers, IsEmpty())))); | 
 |  | 
 |   spdy::Http2HeaderBlock request_headers; | 
 |   request_headers[":method"] = "CONNECT"; | 
 |   request_headers[":protocol"] = "connect-udp"; | 
 |   request_headers[":authority"] = "proxy.test"; | 
 |   request_headers[":scheme"] = "https"; | 
 |   request_headers[":path"] = "/.well-known/masque/udp/unacceptable.test/100/"; | 
 |  | 
 |   tunnel_.OpenTunnel(request_headers); | 
 |   EXPECT_FALSE(tunnel_.IsTunnelOpenToTarget()); | 
 |   tunnel_.OnClientStreamClose(); | 
 | } | 
 |  | 
 | TEST_F(ConnectUdpTunnelTest, ReceiveFromTarget) { | 
 |   static constexpr absl::string_view kData = "\x11\x22\x33\x44\x55"; | 
 |  | 
 |   EXPECT_CALL(*socket_, ConnectBlocking()).WillOnce(Return(absl::OkStatus())); | 
 |   EXPECT_CALL(*socket_, ReceiveAsync(Ge(kData.size()))).Times(2); | 
 |   EXPECT_CALL(*socket_, Disconnect()).WillOnce(InvokeWithoutArgs([this]() { | 
 |     tunnel_.ReceiveComplete(absl::CancelledError()); | 
 |   })); | 
 |  | 
 |   EXPECT_CALL(request_handler_, OnResponseBackendComplete(_)); | 
 |  | 
 |   EXPECT_CALL( | 
 |       stream_, | 
 |       SendHttp3Datagram( | 
 |           quiche::ConnectUdpDatagramUdpPacketPayload(kData).Serialize())) | 
 |       .WillOnce(Return(MESSAGE_STATUS_SUCCESS)); | 
 |  | 
 |   spdy::Http2HeaderBlock request_headers; | 
 |   request_headers[":method"] = "CONNECT"; | 
 |   request_headers[":protocol"] = "connect-udp"; | 
 |   request_headers[":authority"] = "proxy.test"; | 
 |   request_headers[":scheme"] = "https"; | 
 |   request_headers[":path"] = absl::StrCat( | 
 |       "/.well-known/masque/udp/", kAcceptableTarget, "/", kAcceptablePort, "/"); | 
 |  | 
 |   tunnel_.OpenTunnel(request_headers); | 
 |  | 
 |   // Simulate receiving `kData`. | 
 |   tunnel_.ReceiveComplete(MemSliceFromString(kData)); | 
 |  | 
 |   tunnel_.OnClientStreamClose(); | 
 | } | 
 |  | 
 | TEST_F(ConnectUdpTunnelTest, SendToTarget) { | 
 |   static constexpr absl::string_view kData = "\x11\x22\x33\x44\x55"; | 
 |  | 
 |   EXPECT_CALL(*socket_, ConnectBlocking()).WillOnce(Return(absl::OkStatus())); | 
 |   EXPECT_CALL(*socket_, ReceiveAsync(Gt(0))); | 
 |   EXPECT_CALL(*socket_, SendBlocking(Matcher<std::string>(Eq(kData)))) | 
 |       .WillOnce(Return(absl::OkStatus())); | 
 |   EXPECT_CALL(*socket_, Disconnect()).WillOnce(InvokeWithoutArgs([this]() { | 
 |     tunnel_.ReceiveComplete(absl::CancelledError()); | 
 |   })); | 
 |  | 
 |   EXPECT_CALL(request_handler_, OnResponseBackendComplete(_)); | 
 |  | 
 |   spdy::Http2HeaderBlock request_headers; | 
 |   request_headers[":method"] = "CONNECT"; | 
 |   request_headers[":protocol"] = "connect-udp"; | 
 |   request_headers[":authority"] = "proxy.test"; | 
 |   request_headers[":scheme"] = "https"; | 
 |   request_headers[":path"] = absl::StrCat( | 
 |       "/.well-known/masque/udp/", kAcceptableTarget, "/", kAcceptablePort, "/"); | 
 |  | 
 |   tunnel_.OpenTunnel(request_headers); | 
 |   tunnel_.OnHttp3Datagram( | 
 |       kStreamId, quiche::ConnectUdpDatagramUdpPacketPayload(kData).Serialize()); | 
 |   tunnel_.OnClientStreamClose(); | 
 | } | 
 |  | 
 | }  // namespace | 
 | }  // namespace quic::test |