blob: 364bf8165e119b8b684033b7836b42e7f0e05d47 [file]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/quic/quic_proxy_datagram_client_socket.h"
#include "base/compiler_specific.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "base/values.h"
#include "net/base/completion_once_callback.h"
#include "net/base/features.h"
#include "net/base/net_errors.h"
#include "net/base/proxy_chain.h"
#include "net/base/proxy_delegate.h"
#include "net/http/http_log_util.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/log/net_log_source.h"
#include "net/log/net_log_source_type.h"
#include "net/spdy/spdy_http_utils.h"
namespace net {
QuicProxyDatagramClientSocket::QuicProxyDatagramClientSocket(
const GURL& url,
const ProxyChain& proxy_chain,
const std::string& user_agent,
const NetLogWithSource& source_net_log,
ProxyDelegate* proxy_delegate)
: url_(url),
proxy_chain_(proxy_chain),
proxy_delegate_(proxy_delegate),
user_agent_(user_agent),
net_log_(NetLogWithSource::Make(
source_net_log.net_log(),
NetLogSourceType::QUIC_PROXY_DATAGRAM_CLIENT_SOCKET)) {
CHECK_GE(proxy_chain.length(), 1u);
request_.method = "CONNECT";
request_.url = url_;
net_log_.BeginEventReferencingSource(NetLogEventType::SOCKET_ALIVE,
source_net_log.source());
}
QuicProxyDatagramClientSocket::~QuicProxyDatagramClientSocket() {
Close();
net_log_.EndEvent(NetLogEventType::SOCKET_ALIVE);
}
const HttpResponseInfo* QuicProxyDatagramClientSocket::GetConnectResponseInfo()
const {
return response_.headers.get() ? &response_ : nullptr;
}
bool QuicProxyDatagramClientSocket::IsConnectedForTesting() const {
return next_state_ == STATE_CONNECT_COMPLETE && stream_handle_->IsOpen();
}
int QuicProxyDatagramClientSocket::ConnectViaStream(
const IPEndPoint& local_address,
const IPEndPoint& proxy_peer_address,
std::unique_ptr<QuicChromiumClientStream::Handle> stream,
CompletionOnceCallback callback) {
DCHECK(connect_callback_.is_null());
local_address_ = local_address;
proxy_peer_address_ = proxy_peer_address;
stream_handle_ = std::move(stream);
if (!stream_handle_->IsOpen()) {
return ERR_CONNECTION_CLOSED;
}
// Register stream to receive HTTP/3 datagrams.
stream_handle_->RegisterHttp3DatagramVisitor(this);
DCHECK_EQ(STATE_DISCONNECTED, next_state_);
next_state_ = STATE_CALCULATE_HEADERS;
int rv = DoLoop(OK);
if (rv == ERR_IO_PENDING) {
connect_callback_ = std::move(callback);
}
return rv;
}
int QuicProxyDatagramClientSocket::Connect(const IPEndPoint& address) {
NOTREACHED();
}
int QuicProxyDatagramClientSocket::ConnectAsync(
const IPEndPoint& address,
CompletionOnceCallback callback) {
NOTREACHED();
}
int QuicProxyDatagramClientSocket::ConnectUsingDefaultNetworkAsync(
const IPEndPoint& address,
CompletionOnceCallback callback) {
NOTREACHED();
}
int QuicProxyDatagramClientSocket::ConnectUsingNetwork(
handles::NetworkHandle network,
const IPEndPoint& address) {
NOTREACHED();
}
int QuicProxyDatagramClientSocket::ConnectUsingDefaultNetwork(
const IPEndPoint& address) {
NOTREACHED();
}
int QuicProxyDatagramClientSocket::ConnectUsingNetworkAsync(
handles::NetworkHandle network,
const IPEndPoint& address,
CompletionOnceCallback callback) {
NOTREACHED();
}
void QuicProxyDatagramClientSocket::Close() {
connect_callback_.Reset();
read_callback_.Reset();
read_buf_len_ = 0;
read_buf_ = nullptr;
next_state_ = STATE_DISCONNECTED;
connect_request_sent_ = false;
awaiting_connect_response_ = false;
stream_handle_->Reset(quic::QUIC_STREAM_CANCELLED);
}
int QuicProxyDatagramClientSocket::SetReceiveBufferSize(int32_t size) {
return OK;
}
int QuicProxyDatagramClientSocket::SetSendBufferSize(int32_t size) {
return OK;
}
void QuicProxyDatagramClientSocket::OnHttp3Datagram(
quic::QuicStreamId stream_id,
std::string_view payload) {
DCHECK_EQ(stream_id, stream_handle_->id())
<< "Received datagram for unexpected stream.";
quic::QuicDataReader reader(payload);
uint64_t context_id;
if (!reader.ReadVarInt62(&context_id)) {
DLOG(WARNING)
<< "Ignoring HTTP Datagram payload. Failed to read context ID";
return;
}
if (context_id != 0) {
DLOG(WARNING) << "Ignoring HTTP Datagram with unrecognized context ID "
<< context_id;
return;
}
std::string_view http_payload = reader.ReadRemainingPayload();
// If there's a read callback, process the payload immediately.
if (read_callback_) {
int result;
int bytes_read = http_payload.size();
if (http_payload.size() > static_cast<std::size_t>(read_buf_len_)) {
result = ERR_MSG_TOO_BIG;
} else {
CHECK(read_buf_ != nullptr);
CHECK(read_buf_len_ > 0);
read_buf_->span().copy_prefix_from(base::as_byte_span(http_payload));
result = bytes_read;
}
read_buf_ = nullptr;
read_buf_len_ = 0;
std::move(read_callback_).Run(result);
} else {
base::UmaHistogramBoolean(kMaxQueueSizeHistogram,
datagrams_.size() >= kMaxDatagramQueueSize);
if (datagrams_.size() >= kMaxDatagramQueueSize) {
DLOG(WARNING) << "Dropping datagram because queue is full";
return;
}
// If no read callback, store the payload in the queue.
datagrams_.emplace(http_payload.data(), http_payload.size());
}
}
// Silently ignore unknown capsules.
void QuicProxyDatagramClientSocket::OnUnknownCapsule(
quic::QuicStreamId stream_id,
const quiche::UnknownCapsule& capsule) {}
// Proxied connections are not on any specific network.
handles::NetworkHandle QuicProxyDatagramClientSocket::GetBoundNetwork() const {
return handles::kInvalidNetworkHandle;
}
// TODO(crbug.com/41497362): Implement method.
void QuicProxyDatagramClientSocket::ApplySocketTag(const SocketTag& tag) {}
int QuicProxyDatagramClientSocket::SetMulticastInterface(
uint32_t interface_index) {
NOTREACHED();
}
void QuicProxyDatagramClientSocket::SetIOSNetworkServiceType(
int ios_network_service_type) {}
int QuicProxyDatagramClientSocket::GetPeerAddress(IPEndPoint* address) const {
*address = proxy_peer_address_;
return OK;
}
int QuicProxyDatagramClientSocket::GetLocalAddress(IPEndPoint* address) const {
*address = local_address_;
return OK;
}
void QuicProxyDatagramClientSocket::UseNonBlockingIO() {
NOTREACHED();
}
int QuicProxyDatagramClientSocket::SetDoNotFragment() {
NOTREACHED();
}
int QuicProxyDatagramClientSocket::SetRecvTos() {
NOTREACHED();
}
int QuicProxyDatagramClientSocket::SetTos(net::DiffServCodePoint dscp,
net::EcnCodePoint ecn) {
return OK;
}
void QuicProxyDatagramClientSocket::SetMsgConfirm(bool confirm) {
NOTREACHED();
}
const NetLogWithSource& QuicProxyDatagramClientSocket::NetLog() const {
return net_log_;
}
net::DscpAndEcn QuicProxyDatagramClientSocket::GetLastTos() const {
return {net::DSCP_DEFAULT, net::ECN_DEFAULT};
}
int QuicProxyDatagramClientSocket::Read(IOBuffer* buf,
int buf_len,
CompletionOnceCallback callback) {
CHECK(connect_callback_.is_null());
CHECK(read_callback_.is_null());
CHECK(!read_buf_);
CHECK(read_buf_len_ == 0);
if (next_state_ == STATE_DISCONNECTED) {
return ERR_SOCKET_NOT_CONNECTED;
}
// Return 0 if stream closed, signaling end-of-file or no more data.
if (!stream_handle_->IsOpen()) {
return 0;
}
// If there are datagrams available, attempt to read the first one into the
// buffer.
if (!datagrams_.empty()) {
auto& datagram = datagrams_.front();
int result;
int bytes_read = datagram.size();
if (datagram.size() > static_cast<std::size_t>(buf_len)) {
result = ERR_MSG_TOO_BIG;
} else {
buf->span().copy_prefix_from(base::as_byte_span(datagram));
result = bytes_read;
}
datagrams_.pop();
return result;
}
// Save read callback so we can call it next time we receive a datagram.
read_callback_ = std::move(callback);
read_buf_ = buf;
read_buf_len_ = buf_len;
return ERR_IO_PENDING;
}
base::expected<DatagramsMetadata, Error>
QuicProxyDatagramClientSocket::ReadMultiple(
IOBuffer* buf,
size_t buf_len,
size_t maximum_packet_size,
base::OnceCallback<void(base::expected<DatagramsMetadata, Error>)>
callback) {
// TODO(crbug.com/515333601): This is a temporary delegation to Read() to
// avoid crashes when QuicUseReadMultiple is enabled. Implement a proper
// ReadMultiple() in a follow-up CL.
return read_multiple_emulator_.ReadMultiple(buf, buf_len, maximum_packet_size,
std::move(callback));
}
int QuicProxyDatagramClientSocket::Write(
IOBuffer* buf,
int buf_len,
CompletionOnceCallback callback,
const NetworkTrafficAnnotationTag& traffic_annotation) {
DCHECK(connect_callback_.is_null());
if (!connect_request_sent_) {
return ERR_SOCKET_NOT_CONNECTED;
}
net_log_.AddByteTransferEvent(NetLogEventType::SOCKET_BYTES_SENT, buf_len,
buf->data());
std::string_view packet(buf->data(), buf_len);
int rv = stream_handle_->WriteConnectUdpPayload(packet);
if (rv == OK) {
return buf_len;
}
return rv;
}
void QuicProxyDatagramClientSocket::OnIOComplete(int result) {
DCHECK_NE(STATE_DISCONNECTED, next_state_);
// If the client didn't wait for a connect response so that it could
// immediately start writing, get it ready to resume the full process of
// tunnel establishment.
if (awaiting_connect_response_) {
next_state_ = STATE_READ_REPLY;
awaiting_connect_response_ = false;
}
int rv = DoLoop(result);
if (rv != ERR_IO_PENDING) {
// Connect() finished (successfully or unsuccessfully) but we may not have
// a connect_callback if we didn't wait for the response before considering
// the connection established.
if (!connect_callback_.is_null()) {
std::move(connect_callback_).Run(rv);
}
}
}
int QuicProxyDatagramClientSocket::DoLoop(int last_io_result) {
DCHECK_NE(next_state_, STATE_DISCONNECTED);
int rv = last_io_result;
do {
State state = next_state_;
next_state_ = STATE_DISCONNECTED;
// TODO(crbug.com/326437102): Add support for generate auth token request
// and complete states.
switch (state) {
case STATE_CALCULATE_HEADERS:
DCHECK_EQ(OK, rv);
rv = DoCalculateHeaders();
break;
case STATE_CALCULATE_HEADERS_COMPLETE:
rv = DoCalculateHeadersComplete(rv);
break;
case STATE_SEND_REQUEST:
DCHECK_EQ(OK, rv);
net_log_.BeginEvent(
NetLogEventType::HTTP_TRANSACTION_TUNNEL_SEND_REQUEST);
rv = DoSendRequest();
break;
case STATE_SEND_REQUEST_COMPLETE:
net_log_.EndEventWithNetErrorCode(
NetLogEventType::HTTP_TRANSACTION_TUNNEL_SEND_REQUEST, rv);
rv = DoSendRequestComplete(rv);
break;
case STATE_READ_REPLY:
rv = DoReadReply();
break;
case STATE_READ_REPLY_COMPLETE:
rv = DoReadReplyComplete(rv);
net_log_.EndEventWithNetErrorCode(
NetLogEventType::HTTP_TRANSACTION_TUNNEL_READ_HEADERS, rv);
break;
case STATE_PROCESS_RESPONSE_HEADERS:
DCHECK_EQ(OK, rv);
rv = DoProcessResponseHeaders();
break;
case STATE_PROCESS_RESPONSE_HEADERS_COMPLETE:
rv = DoProcessResponseHeadersComplete(rv);
break;
case STATE_PROCESS_RESPONSE_CODE:
DCHECK_EQ(OK, rv);
rv = DoProcessResponseCode();
break;
default:
NOTREACHED() << "bad state";
}
} while (rv != ERR_IO_PENDING && next_state_ != STATE_DISCONNECTED &&
next_state_ != STATE_CONNECT_COMPLETE);
return rv;
}
int QuicProxyDatagramClientSocket::DoCalculateHeaders() {
next_state_ = STATE_CALCULATE_HEADERS_COMPLETE;
proxy_delegate_headers_.Clear();
if (proxy_delegate_) {
ASSIGN_OR_RETURN(
proxy_delegate_headers_,
proxy_delegate_->OnBeforeTunnelRequest(
proxy_chain_, proxy_chain_index(),
base::BindOnce(
&QuicProxyDatagramClientSocket::OnBeforeTunnelRequestComplete,
weak_factory_.GetWeakPtr())),
[](const auto& e) {
// Success should always be reported via a base::expected containing
// an HttpRequestHeaders, see ProxyDelegate::OnBeforeTunnelRequest.
CHECK_NE(OK, e);
return e;
});
}
return OK;
}
int QuicProxyDatagramClientSocket::DoCalculateHeadersComplete(int result) {
DCHECK_NE(ERR_IO_PENDING, result);
if (result != OK) {
return result;
}
next_state_ = STATE_SEND_REQUEST;
// TODO(crbug.com/326437102): Add Proxy-Authentication headers.
request_.extra_headers.MergeFrom(proxy_delegate_headers_);
return result;
}
int QuicProxyDatagramClientSocket::DoSendRequest() {
next_state_ = STATE_SEND_REQUEST_COMPLETE;
if (!url_.has_host()) {
return ERR_ADDRESS_INVALID;
}
std::string host = url_.GetHost();
int port = url_.IntPort();
std::string host_and_port =
url_.has_port() ? base::StrCat({host, ":", base::NumberToString(port)})
: std::move(host);
request_.extra_headers.SetHeader(HttpRequestHeaders::kHost, host_and_port);
if (!user_agent_.empty()) {
request_.extra_headers.SetHeader(HttpRequestHeaders::kUserAgent,
user_agent_);
}
request_.extra_headers.SetHeader("capsule-protocol", "?1");
// Generate a fake request line for logging purposes.
std::string request_line =
base::StringPrintf("CONNECT-UDP %s HTTP/3\r\n", url_.GetPath().c_str());
NetLogRequestHeaders(net_log_,
NetLogEventType::HTTP_TRANSACTION_SEND_TUNNEL_HEADERS,
request_line, &request_.extra_headers);
quiche::HttpHeaderBlock headers;
CreateSpdyHeadersFromHttpRequestForExtendedConnect(
request_, /*priority=*/std::nullopt, "connect-udp",
request_.extra_headers, &headers);
int result = stream_handle_->WriteHeaders(std::move(headers), /*fin=*/false,
/*ack_notifier_delegate=*/nullptr);
connect_request_sent_ = true;
return result;
}
int QuicProxyDatagramClientSocket::DoSendRequestComplete(int result) {
if (result >= 0) {
// Wait for HEADERS frame from the server
next_state_ = STATE_READ_REPLY; // STATE_READ_REPLY_COMPLETE;
result = OK;
}
if (result >= 0 || result == ERR_IO_PENDING) {
// Emit extra event so can use the same events as HttpProxyClientSocket.
net_log_.BeginEvent(NetLogEventType::HTTP_TRANSACTION_TUNNEL_READ_HEADERS);
}
return result;
}
int QuicProxyDatagramClientSocket::DoReadReply() {
next_state_ = STATE_READ_REPLY_COMPLETE;
int rv = stream_handle_->ReadInitialHeaders(
&response_header_block_,
base::BindOnce(
&QuicProxyDatagramClientSocket::OnReadResponseHeadersComplete,
weak_factory_.GetWeakPtr()));
if (rv == ERR_IO_PENDING) {
// If the feature is enabled, the stream supports H3 datagrams and we
// haven't received a response to the CONNECT-UDP request yet, bypass
// processing response headers and consider tunnel "established" so
// datagrams can be sent and traffic is not blocked.
if (base::FeatureList::IsEnabled(
net::features::kUseQuicProxiesWithoutWaitingForConnectResponse) &&
stream_handle_->SupportsH3Datagram()) {
next_state_ = STATE_CONNECT_COMPLETE;
awaiting_connect_response_ = true;
return OK;
}
return ERR_IO_PENDING;
}
if (rv < 0) {
return rv;
}
return ProcessResponseHeaders(response_header_block_);
}
int QuicProxyDatagramClientSocket::DoReadReplyComplete(int result) {
if (result < 0) {
return result;
}
next_state_ = STATE_PROCESS_RESPONSE_HEADERS;
NetLogResponseHeaders(
net_log_, NetLogEventType::HTTP_TRANSACTION_READ_TUNNEL_RESPONSE_HEADERS,
response_.headers.get());
return OK;
}
int QuicProxyDatagramClientSocket::DoProcessResponseHeaders() {
next_state_ = STATE_PROCESS_RESPONSE_HEADERS_COMPLETE;
// TODO(crbug.com/326437102): Add case for Proxy Authentication.
if (proxy_delegate_) {
return proxy_delegate_->OnTunnelHeadersReceived(
proxy_chain(), proxy_chain_index(), *response_.headers,
base::BindOnce(&QuicProxyDatagramClientSocket::OnIOComplete,
weak_factory_.GetWeakPtr()));
}
return OK;
}
int QuicProxyDatagramClientSocket::DoProcessResponseHeadersComplete(
int result) {
DCHECK_NE(ERR_IO_PENDING, result);
if (result != OK) {
return result;
}
next_state_ = STATE_PROCESS_RESPONSE_CODE;
return OK;
}
int QuicProxyDatagramClientSocket::DoProcessResponseCode() {
switch (response_.headers->response_code()) {
case 200: // OK
next_state_ = STATE_CONNECT_COMPLETE;
return OK;
default:
// Ignore response to avoid letting the proxy impersonate the target
// server. (See http://crbug.com/137891.)
return ERR_TUNNEL_CONNECTION_FAILED;
}
}
void QuicProxyDatagramClientSocket::OnReadResponseHeadersComplete(int result) {
// Convert the now-populated quiche::HttpHeaderBlock to HttpResponseInfo
if (result > 0) {
result = ProcessResponseHeaders(response_header_block_);
}
if (result != ERR_IO_PENDING) {
OnIOComplete(result);
}
}
int QuicProxyDatagramClientSocket::ProcessResponseHeaders(
const quiche::HttpHeaderBlock& headers) {
if (SpdyHeadersToHttpResponse(headers, &response_) != OK) {
DLOG(WARNING) << "Invalid headers";
return ERR_QUIC_PROTOCOL_ERROR;
}
return OK;
}
void QuicProxyDatagramClientSocket::OnBeforeTunnelRequestComplete(
base::expected<HttpRequestHeaders, Error> result) {
if (result.has_value()) {
proxy_delegate_headers_ = std::move(result.value());
OnIOComplete(OK);
} else {
// OnBeforeTunnelRequestComplete should never report ERR_IO_PENDING since
// it's used to signal that IO has completed.
CHECK_NE(ERR_IO_PENDING, result.error());
// Success should always be reported via a base::expected containing an
// HttpRequestHeaders, see ProxyDelegate::OnBeforeTunnelRequest.
CHECK_NE(OK, result.error());
OnIOComplete(result.error());
}
}
} // namespace net