blob: 22f6c3b233111e648e73b7bc3e0c6778f9051eb5 [file] [log] [blame]
// Copyright 2022 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 "quiche/quic/tools/connect_tunnel.h"
#include <cstdint>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_set.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/types/span.h"
#include "quiche/quic/core/quic_error_codes.h"
#include "quiche/quic/core/quic_server_id.h"
#include "quiche/quic/core/socket_factory.h"
#include "quiche/quic/platform/api/quic_socket_address.h"
#include "quiche/quic/tools/quic_backend_response.h"
#include "quiche/quic/tools/quic_name_lookup.h"
#include "quiche/quic/tools/quic_simple_server_backend.h"
#include "quiche/common/platform/api/quiche_logging.h"
#include "quiche/common/platform/api/quiche_mem_slice.h"
#include "quiche/spdy/core/http2_header_block.h"
namespace quic {
namespace {
// Arbitrarily chosen. No effort has been made to figure out an optimal size.
constexpr size_t kReadSize = 4 * 1024;
std::optional<QuicServerId> ValidateHeadersAndGetAuthority(
const spdy::Http2HeaderBlock& request_headers) {
QUICHE_DCHECK(request_headers.contains(":method"));
QUICHE_DCHECK(request_headers.find(":method")->second == "CONNECT");
QUICHE_DCHECK(!request_headers.contains(":protocol"));
auto scheme_it = request_headers.find(":scheme");
if (scheme_it != request_headers.end()) {
QUICHE_DVLOG(1) << "CONNECT request contains unexpected scheme: "
<< scheme_it->second;
return std::nullopt;
}
auto path_it = request_headers.find(":path");
if (path_it != request_headers.end()) {
QUICHE_DVLOG(1) << "CONNECT request contains unexpected path: "
<< path_it->second;
return std::nullopt;
}
auto authority_it = request_headers.find(":authority");
if (authority_it == request_headers.end() || authority_it->second.empty()) {
QUICHE_DVLOG(1) << "CONNECT request missing authority";
return std::nullopt;
}
// A valid CONNECT authority must contain host and port and nothing else, per
// https://www.rfc-editor.org/rfc/rfc9110.html#name-connect. This matches the
// host and port parsing rules for QuicServerId.
std::optional<QuicServerId> server_id =
QuicServerId::ParseFromHostPortString(authority_it->second);
if (!server_id.has_value()) {
QUICHE_DVLOG(1) << "CONNECT request authority is malformed: "
<< authority_it->second;
return std::nullopt;
}
return server_id;
}
bool ValidateAuthority(
const QuicServerId& authority,
const absl::flat_hash_set<QuicServerId>& acceptable_destinations) {
if (acceptable_destinations.contains(authority)) {
return true;
}
QUICHE_DVLOG(1) << "CONNECT request authority: "
<< authority.ToHostPortString()
<< " is not an acceptable allow-listed destiation ";
return false;
}
} // namespace
ConnectTunnel::ConnectTunnel(
QuicSimpleServerBackend::RequestHandler* client_stream_request_handler,
SocketFactory* socket_factory,
absl::flat_hash_set<QuicServerId> acceptable_destinations)
: acceptable_destinations_(std::move(acceptable_destinations)),
socket_factory_(socket_factory),
client_stream_request_handler_(client_stream_request_handler) {
QUICHE_DCHECK(client_stream_request_handler_);
QUICHE_DCHECK(socket_factory_);
}
ConnectTunnel::~ConnectTunnel() {
// Expect client and destination sides of tunnel to both be closed before
// destruction.
QUICHE_DCHECK_EQ(client_stream_request_handler_, nullptr);
QUICHE_DCHECK(!IsConnectedToDestination());
QUICHE_DCHECK(!receive_started_);
}
void ConnectTunnel::OpenTunnel(const spdy::Http2HeaderBlock& request_headers) {
QUICHE_DCHECK(!IsConnectedToDestination());
std::optional<QuicServerId> authority =
ValidateHeadersAndGetAuthority(request_headers);
if (!authority.has_value()) {
TerminateClientStream(
"invalid request headers",
QuicResetStreamError::FromIetf(QuicHttp3ErrorCode::MESSAGE_ERROR));
return;
}
if (!ValidateAuthority(authority.value(), acceptable_destinations_)) {
TerminateClientStream(
"disallowed request authority",
QuicResetStreamError::FromIetf(QuicHttp3ErrorCode::REQUEST_REJECTED));
return;
}
QuicSocketAddress address =
tools::LookupAddress(AF_UNSPEC, authority.value());
if (!address.IsInitialized()) {
TerminateClientStream("host resolution error");
return;
}
destination_socket_ =
socket_factory_->CreateTcpClientSocket(address,
/*receive_buffer_size=*/0,
/*send_buffer_size=*/0,
/*async_visitor=*/this);
QUICHE_DCHECK(destination_socket_);
absl::Status connect_result = destination_socket_->ConnectBlocking();
if (!connect_result.ok()) {
TerminateClientStream(
"error connecting TCP socket to destination server: " +
connect_result.ToString());
return;
}
QUICHE_DVLOG(1) << "CONNECT tunnel opened from stream "
<< client_stream_request_handler_->stream_id() << " to "
<< authority.value().ToHostPortString();
SendConnectResponse();
BeginAsyncReadFromDestination();
}
bool ConnectTunnel::IsConnectedToDestination() const {
return !!destination_socket_;
}
void ConnectTunnel::SendDataToDestination(absl::string_view data) {
QUICHE_DCHECK(IsConnectedToDestination());
QUICHE_DCHECK(!data.empty());
absl::Status send_result =
destination_socket_->SendBlocking(std::string(data));
if (!send_result.ok()) {
TerminateClientStream("TCP error sending data to destination server: " +
send_result.ToString());
}
}
void ConnectTunnel::OnClientStreamClose() {
QUICHE_DCHECK(client_stream_request_handler_);
QUICHE_DVLOG(1) << "CONNECT stream "
<< client_stream_request_handler_->stream_id() << " closed";
client_stream_request_handler_ = nullptr;
if (IsConnectedToDestination()) {
// TODO(ericorth): Consider just calling shutdown() on the socket rather
// than fully disconnecting in order to allow a graceful TCP FIN stream
// shutdown per
// https://www.rfc-editor.org/rfc/rfc9114.html#name-the-connect-method.
// Would require shutdown support in the socket library, and would need to
// deal with the tunnel/socket outliving the client stream.
destination_socket_->Disconnect();
}
// Clear socket pointer.
destination_socket_.reset();
}
void ConnectTunnel::ConnectComplete(absl::Status /*status*/) {
// Async connect not expected.
QUICHE_NOTREACHED();
}
void ConnectTunnel::ReceiveComplete(
absl::StatusOr<quiche::QuicheMemSlice> data) {
QUICHE_DCHECK(IsConnectedToDestination());
QUICHE_DCHECK(receive_started_);
receive_started_ = false;
if (!data.ok()) {
if (client_stream_request_handler_) {
TerminateClientStream("TCP error receiving data from destination server");
} else {
// This typically just means a receive operation was cancelled on calling
// destination_socket_->Disconnect().
QUICHE_DVLOG(1) << "TCP error receiving data from destination server "
"after stream already closed.";
}
return;
} else if (data.value().empty()) {
OnDestinationConnectionClosed();
return;
}
QUICHE_DCHECK(client_stream_request_handler_);
client_stream_request_handler_->SendStreamData(data.value().AsStringView(),
/*close_stream=*/false);
BeginAsyncReadFromDestination();
}
void ConnectTunnel::SendComplete(absl::Status /*status*/) {
// Async send not expected.
QUICHE_NOTREACHED();
}
void ConnectTunnel::BeginAsyncReadFromDestination() {
QUICHE_DCHECK(IsConnectedToDestination());
QUICHE_DCHECK(client_stream_request_handler_);
QUICHE_DCHECK(!receive_started_);
receive_started_ = true;
destination_socket_->ReceiveAsync(kReadSize);
}
void ConnectTunnel::OnDestinationConnectionClosed() {
QUICHE_DCHECK(IsConnectedToDestination());
QUICHE_DCHECK(client_stream_request_handler_);
QUICHE_DVLOG(1) << "CONNECT stream "
<< client_stream_request_handler_->stream_id()
<< " destination connection closed";
destination_socket_->Disconnect();
// Clear socket pointer.
destination_socket_.reset();
// Extra check that nothing in the Disconnect could lead to terminating the
// stream.
QUICHE_DCHECK(client_stream_request_handler_);
client_stream_request_handler_->SendStreamData("", /*close_stream=*/true);
}
void ConnectTunnel::SendConnectResponse() {
QUICHE_DCHECK(IsConnectedToDestination());
QUICHE_DCHECK(client_stream_request_handler_);
spdy::Http2HeaderBlock response_headers;
response_headers[":status"] = "200";
QuicBackendResponse response;
response.set_headers(std::move(response_headers));
// Need to leave the stream open after sending the CONNECT response.
response.set_response_type(QuicBackendResponse::INCOMPLETE_RESPONSE);
client_stream_request_handler_->OnResponseBackendComplete(&response);
}
void ConnectTunnel::TerminateClientStream(absl::string_view error_description,
QuicResetStreamError error_code) {
QUICHE_DCHECK(client_stream_request_handler_);
std::string error_description_str =
error_description.empty() ? ""
: absl::StrCat(" due to ", error_description);
QUICHE_DVLOG(1) << "Terminating CONNECT stream "
<< client_stream_request_handler_->stream_id()
<< " with error code " << error_code.ietf_application_code()
<< error_description_str;
client_stream_request_handler_->TerminateStreamWithError(error_code);
}
} // namespace quic