1 // Copyright 2022 The Chromium Authors. All rights reserved.
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 "quiche/quic/tools/connect_tunnel.h"
6
7 #include <cstdint>
8 #include <optional>
9 #include <string>
10 #include <utility>
11 #include <vector>
12
13 #include "absl/container/flat_hash_set.h"
14 #include "absl/status/status.h"
15 #include "absl/status/statusor.h"
16 #include "absl/strings/str_cat.h"
17 #include "absl/strings/string_view.h"
18 #include "absl/types/span.h"
19 #include "quiche/quic/core/quic_error_codes.h"
20 #include "quiche/quic/core/quic_server_id.h"
21 #include "quiche/quic/core/socket_factory.h"
22 #include "quiche/quic/platform/api/quic_socket_address.h"
23 #include "quiche/quic/tools/quic_backend_response.h"
24 #include "quiche/quic/tools/quic_name_lookup.h"
25 #include "quiche/quic/tools/quic_simple_server_backend.h"
26 #include "quiche/common/platform/api/quiche_logging.h"
27 #include "quiche/common/platform/api/quiche_mem_slice.h"
28 #include "quiche/spdy/core/http2_header_block.h"
29
30 namespace quic {
31
32 namespace {
33
34 // Arbitrarily chosen. No effort has been made to figure out an optimal size.
35 constexpr size_t kReadSize = 4 * 1024;
36
ValidateHeadersAndGetAuthority(const spdy::Http2HeaderBlock & request_headers)37 std::optional<QuicServerId> ValidateHeadersAndGetAuthority(
38 const spdy::Http2HeaderBlock& request_headers) {
39 QUICHE_DCHECK(request_headers.contains(":method"));
40 QUICHE_DCHECK(request_headers.find(":method")->second == "CONNECT");
41 QUICHE_DCHECK(!request_headers.contains(":protocol"));
42
43 auto scheme_it = request_headers.find(":scheme");
44 if (scheme_it != request_headers.end()) {
45 QUICHE_DVLOG(1) << "CONNECT request contains unexpected scheme: "
46 << scheme_it->second;
47 return std::nullopt;
48 }
49
50 auto path_it = request_headers.find(":path");
51 if (path_it != request_headers.end()) {
52 QUICHE_DVLOG(1) << "CONNECT request contains unexpected path: "
53 << path_it->second;
54 return std::nullopt;
55 }
56
57 auto authority_it = request_headers.find(":authority");
58 if (authority_it == request_headers.end() || authority_it->second.empty()) {
59 QUICHE_DVLOG(1) << "CONNECT request missing authority";
60 return std::nullopt;
61 }
62
63 // A valid CONNECT authority must contain host and port and nothing else, per
64 // https://www.rfc-editor.org/rfc/rfc9110.html#name-connect. This matches the
65 // host and port parsing rules for QuicServerId.
66 std::optional<QuicServerId> server_id =
67 QuicServerId::ParseFromHostPortString(authority_it->second);
68 if (!server_id.has_value()) {
69 QUICHE_DVLOG(1) << "CONNECT request authority is malformed: "
70 << authority_it->second;
71 return std::nullopt;
72 }
73
74 return server_id;
75 }
76
ValidateAuthority(const QuicServerId & authority,const absl::flat_hash_set<QuicServerId> & acceptable_destinations)77 bool ValidateAuthority(
78 const QuicServerId& authority,
79 const absl::flat_hash_set<QuicServerId>& acceptable_destinations) {
80 if (acceptable_destinations.contains(authority)) {
81 return true;
82 }
83
84 QUICHE_DVLOG(1) << "CONNECT request authority: "
85 << authority.ToHostPortString()
86 << " is not an acceptable allow-listed destiation ";
87 return false;
88 }
89
90 } // namespace
91
ConnectTunnel(QuicSimpleServerBackend::RequestHandler * client_stream_request_handler,SocketFactory * socket_factory,absl::flat_hash_set<QuicServerId> acceptable_destinations)92 ConnectTunnel::ConnectTunnel(
93 QuicSimpleServerBackend::RequestHandler* client_stream_request_handler,
94 SocketFactory* socket_factory,
95 absl::flat_hash_set<QuicServerId> acceptable_destinations)
96 : acceptable_destinations_(std::move(acceptable_destinations)),
97 socket_factory_(socket_factory),
98 client_stream_request_handler_(client_stream_request_handler) {
99 QUICHE_DCHECK(client_stream_request_handler_);
100 QUICHE_DCHECK(socket_factory_);
101 }
102
~ConnectTunnel()103 ConnectTunnel::~ConnectTunnel() {
104 // Expect client and destination sides of tunnel to both be closed before
105 // destruction.
106 QUICHE_DCHECK_EQ(client_stream_request_handler_, nullptr);
107 QUICHE_DCHECK(!IsConnectedToDestination());
108 QUICHE_DCHECK(!receive_started_);
109 }
110
OpenTunnel(const spdy::Http2HeaderBlock & request_headers)111 void ConnectTunnel::OpenTunnel(const spdy::Http2HeaderBlock& request_headers) {
112 QUICHE_DCHECK(!IsConnectedToDestination());
113
114 std::optional<QuicServerId> authority =
115 ValidateHeadersAndGetAuthority(request_headers);
116 if (!authority.has_value()) {
117 TerminateClientStream(
118 "invalid request headers",
119 QuicResetStreamError::FromIetf(QuicHttp3ErrorCode::MESSAGE_ERROR));
120 return;
121 }
122
123 if (!ValidateAuthority(authority.value(), acceptable_destinations_)) {
124 TerminateClientStream(
125 "disallowed request authority",
126 QuicResetStreamError::FromIetf(QuicHttp3ErrorCode::REQUEST_REJECTED));
127 return;
128 }
129
130 QuicSocketAddress address =
131 tools::LookupAddress(AF_UNSPEC, authority.value());
132 if (!address.IsInitialized()) {
133 TerminateClientStream("host resolution error");
134 return;
135 }
136
137 destination_socket_ =
138 socket_factory_->CreateTcpClientSocket(address,
139 /*receive_buffer_size=*/0,
140 /*send_buffer_size=*/0,
141 /*async_visitor=*/this);
142 QUICHE_DCHECK(destination_socket_);
143
144 absl::Status connect_result = destination_socket_->ConnectBlocking();
145 if (!connect_result.ok()) {
146 TerminateClientStream(
147 "error connecting TCP socket to destination server: " +
148 connect_result.ToString());
149 return;
150 }
151
152 QUICHE_DVLOG(1) << "CONNECT tunnel opened from stream "
153 << client_stream_request_handler_->stream_id() << " to "
154 << authority.value().ToHostPortString();
155
156 SendConnectResponse();
157 BeginAsyncReadFromDestination();
158 }
159
IsConnectedToDestination() const160 bool ConnectTunnel::IsConnectedToDestination() const {
161 return !!destination_socket_;
162 }
163
SendDataToDestination(absl::string_view data)164 void ConnectTunnel::SendDataToDestination(absl::string_view data) {
165 QUICHE_DCHECK(IsConnectedToDestination());
166 QUICHE_DCHECK(!data.empty());
167
168 absl::Status send_result =
169 destination_socket_->SendBlocking(std::string(data));
170 if (!send_result.ok()) {
171 TerminateClientStream("TCP error sending data to destination server: " +
172 send_result.ToString());
173 }
174 }
175
OnClientStreamClose()176 void ConnectTunnel::OnClientStreamClose() {
177 QUICHE_DCHECK(client_stream_request_handler_);
178
179 QUICHE_DVLOG(1) << "CONNECT stream "
180 << client_stream_request_handler_->stream_id() << " closed";
181
182 client_stream_request_handler_ = nullptr;
183
184 if (IsConnectedToDestination()) {
185 // TODO(ericorth): Consider just calling shutdown() on the socket rather
186 // than fully disconnecting in order to allow a graceful TCP FIN stream
187 // shutdown per
188 // https://www.rfc-editor.org/rfc/rfc9114.html#name-the-connect-method.
189 // Would require shutdown support in the socket library, and would need to
190 // deal with the tunnel/socket outliving the client stream.
191 destination_socket_->Disconnect();
192 }
193
194 // Clear socket pointer.
195 destination_socket_.reset();
196 }
197
ConnectComplete(absl::Status)198 void ConnectTunnel::ConnectComplete(absl::Status /*status*/) {
199 // Async connect not expected.
200 QUICHE_NOTREACHED();
201 }
202
ReceiveComplete(absl::StatusOr<quiche::QuicheMemSlice> data)203 void ConnectTunnel::ReceiveComplete(
204 absl::StatusOr<quiche::QuicheMemSlice> data) {
205 QUICHE_DCHECK(IsConnectedToDestination());
206 QUICHE_DCHECK(receive_started_);
207
208 receive_started_ = false;
209
210 if (!data.ok()) {
211 if (client_stream_request_handler_) {
212 TerminateClientStream("TCP error receiving data from destination server");
213 } else {
214 // This typically just means a receive operation was cancelled on calling
215 // destination_socket_->Disconnect().
216 QUICHE_DVLOG(1) << "TCP error receiving data from destination server "
217 "after stream already closed.";
218 }
219 return;
220 } else if (data.value().empty()) {
221 OnDestinationConnectionClosed();
222 return;
223 }
224
225 QUICHE_DCHECK(client_stream_request_handler_);
226 client_stream_request_handler_->SendStreamData(data.value().AsStringView(),
227 /*close_stream=*/false);
228
229 BeginAsyncReadFromDestination();
230 }
231
SendComplete(absl::Status)232 void ConnectTunnel::SendComplete(absl::Status /*status*/) {
233 // Async send not expected.
234 QUICHE_NOTREACHED();
235 }
236
BeginAsyncReadFromDestination()237 void ConnectTunnel::BeginAsyncReadFromDestination() {
238 QUICHE_DCHECK(IsConnectedToDestination());
239 QUICHE_DCHECK(client_stream_request_handler_);
240 QUICHE_DCHECK(!receive_started_);
241
242 receive_started_ = true;
243 destination_socket_->ReceiveAsync(kReadSize);
244 }
245
OnDestinationConnectionClosed()246 void ConnectTunnel::OnDestinationConnectionClosed() {
247 QUICHE_DCHECK(IsConnectedToDestination());
248 QUICHE_DCHECK(client_stream_request_handler_);
249
250 QUICHE_DVLOG(1) << "CONNECT stream "
251 << client_stream_request_handler_->stream_id()
252 << " destination connection closed";
253 destination_socket_->Disconnect();
254
255 // Clear socket pointer.
256 destination_socket_.reset();
257
258 // Extra check that nothing in the Disconnect could lead to terminating the
259 // stream.
260 QUICHE_DCHECK(client_stream_request_handler_);
261
262 client_stream_request_handler_->SendStreamData("", /*close_stream=*/true);
263 }
264
SendConnectResponse()265 void ConnectTunnel::SendConnectResponse() {
266 QUICHE_DCHECK(IsConnectedToDestination());
267 QUICHE_DCHECK(client_stream_request_handler_);
268
269 spdy::Http2HeaderBlock response_headers;
270 response_headers[":status"] = "200";
271
272 QuicBackendResponse response;
273 response.set_headers(std::move(response_headers));
274 // Need to leave the stream open after sending the CONNECT response.
275 response.set_response_type(QuicBackendResponse::INCOMPLETE_RESPONSE);
276
277 client_stream_request_handler_->OnResponseBackendComplete(&response);
278 }
279
TerminateClientStream(absl::string_view error_description,QuicResetStreamError error_code)280 void ConnectTunnel::TerminateClientStream(absl::string_view error_description,
281 QuicResetStreamError error_code) {
282 QUICHE_DCHECK(client_stream_request_handler_);
283
284 std::string error_description_str =
285 error_description.empty() ? ""
286 : absl::StrCat(" due to ", error_description);
287 QUICHE_DVLOG(1) << "Terminating CONNECT stream "
288 << client_stream_request_handler_->stream_id()
289 << " with error code " << error_code.ietf_application_code()
290 << error_description_str;
291
292 client_stream_request_handler_->TerminateStreamWithError(error_code);
293 }
294
295 } // namespace quic
296