| // 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 <cstdint> |
| #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/numbers.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/str_split.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/types/optional.h" |
| #include "absl/types/span.h" |
| #include "url/url_canon.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/masque/connect_udp_datagram_payload.h" |
| #include "quiche/common/platform/api/quiche_logging.h" |
| #include "quiche/common/platform/api/quiche_mem_slice.h" |
| #include "quiche/common/platform/api/quiche_url_utils.h" |
| #include "quiche/common/structured_headers.h" |
| #include "quiche/spdy/core/http2_header_block.h" |
| |
| namespace quic { |
| |
| namespace structured_headers = quiche::structured_headers; |
| |
| namespace { |
| |
| // Arbitrarily chosen. No effort has been made to figure out an optimal size. |
| constexpr size_t kReadSize = 4 * 1024; |
| |
| // Only support the default path |
| // ("/.well-known/masque/udp/{target_host}/{target_port}/") |
| absl::optional<QuicServerId> ValidateAndParseTargetFromPath( |
| absl::string_view path) { |
| std::string canonicalized_path_str; |
| url::StdStringCanonOutput canon_output(&canonicalized_path_str); |
| url::Component path_component; |
| url::CanonicalizePath(path.data(), url::Component(0, path.size()), |
| &canon_output, &path_component); |
| if (!path_component.is_nonempty()) { |
| QUICHE_DVLOG(1) << "CONNECT-UDP request with non-canonicalizable path: " |
| << path; |
| return absl::nullopt; |
| } |
| canon_output.Complete(); |
| absl::string_view canonicalized_path = |
| absl::string_view(canonicalized_path_str) |
| .substr(path_component.begin, path_component.len); |
| |
| std::vector<absl::string_view> path_split = |
| absl::StrSplit(canonicalized_path, '/'); |
| if (path_split.size() != 7 || !path_split[0].empty() || |
| path_split[1] != ".well-known" || path_split[2] != "masque" || |
| path_split[3] != "udp" || path_split[4].empty() || |
| path_split[5].empty() || !path_split[6].empty()) { |
| QUICHE_DVLOG(1) << "CONNECT-UDP request with bad path: " |
| << canonicalized_path; |
| return absl::nullopt; |
| } |
| |
| absl::optional<std::string> decoded_host = |
| quiche::AsciiUrlDecode(path_split[4]); |
| if (!decoded_host.has_value()) { |
| QUICHE_DVLOG(1) << "CONNECT-UDP request with undecodable host: " |
| << path_split[4]; |
| return absl::nullopt; |
| } |
| // Empty host checked above after path split. Expect decoding to never result |
| // in an empty decoded host from non-empty encoded host. |
| QUICHE_DCHECK(!decoded_host.value().empty()); |
| |
| absl::optional<std::string> decoded_port = |
| quiche::AsciiUrlDecode(path_split[5]); |
| if (!decoded_port.has_value()) { |
| QUICHE_DVLOG(1) << "CONNECT-UDP request with undecodable port: " |
| << path_split[5]; |
| return absl::nullopt; |
| } |
| // Empty port checked above after path split. Expect decoding to never result |
| // in an empty decoded port from non-empty encoded port. |
| QUICHE_DCHECK(!decoded_port.value().empty()); |
| |
| int parsed_port_number = |
| url::ParsePort(decoded_port.value().data(), |
| url::Component(0, decoded_port.value().size())); |
| // Negative result is either invalid or unspecified, either of which is |
| // disallowed for this parse. Port 0 is technically valid but reserved and not |
| // really usable in practice, so easiest to just disallow it here. |
| if (parsed_port_number <= 0) { |
| QUICHE_DVLOG(1) << "CONNECT-UDP request with bad port: " |
| << decoded_port.value(); |
| return absl::nullopt; |
| } |
| // Expect url::ParsePort() to validate port is uint16_t and otherwise return |
| // negative number checked for above. |
| QUICHE_DCHECK_LE(parsed_port_number, std::numeric_limits<uint16_t>::max()); |
| |
| return QuicServerId(decoded_host.value(), |
| static_cast<uint16_t>(parsed_port_number)); |
| } |
| |
| // Validate header expectations from RFC 9298, section 3.4. |
| absl::optional<QuicServerId> ValidateHeadersAndGetTarget( |
| 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")); |
| QUICHE_DCHECK(request_headers.find(":protocol")->second == "connect-udp"); |
| |
| auto authority_it = request_headers.find(":authority"); |
| if (authority_it == request_headers.end() || authority_it->second.empty()) { |
| QUICHE_DVLOG(1) << "CONNECT-UDP request missing authority"; |
| return absl::nullopt; |
| } |
| // For toy server simplicity, skip validating that the authority matches the |
| // current server. |
| |
| auto scheme_it = request_headers.find(":scheme"); |
| if (scheme_it == request_headers.end() || scheme_it->second.empty()) { |
| QUICHE_DVLOG(1) << "CONNECT-UDP request missing scheme"; |
| return absl::nullopt; |
| } else if (scheme_it->second != "https") { |
| QUICHE_DVLOG(1) << "CONNECT-UDP request contains unexpected scheme: " |
| << scheme_it->second; |
| return absl::nullopt; |
| } |
| |
| auto path_it = request_headers.find(":path"); |
| if (path_it == request_headers.end() || path_it->second.empty()) { |
| QUICHE_DVLOG(1) << "CONNECT-UDP request missing path"; |
| return absl::nullopt; |
| } |
| absl::optional<QuicServerId> target_server_id = |
| ValidateAndParseTargetFromPath(path_it->second); |
| |
| return target_server_id; |
| } |
| |
| bool ValidateTarget( |
| const QuicServerId& target, |
| const absl::flat_hash_set<QuicServerId>& acceptable_targets) { |
| if (acceptable_targets.contains(target)) { |
| return true; |
| } |
| |
| QUICHE_DVLOG(1) |
| << "CONNECT-UDP request target is not an acceptable allow-listed target: " |
| << target.ToHostPortString(); |
| return false; |
| } |
| |
| } // namespace |
| |
| ConnectUdpTunnel::ConnectUdpTunnel( |
| QuicSimpleServerBackend::RequestHandler* client_stream_request_handler, |
| SocketFactory* socket_factory, std::string server_label, |
| absl::flat_hash_set<QuicServerId> acceptable_targets) |
| : acceptable_targets_(std::move(acceptable_targets)), |
| socket_factory_(socket_factory), |
| server_label_(std::move(server_label)), |
| client_stream_request_handler_(client_stream_request_handler) { |
| QUICHE_DCHECK(client_stream_request_handler_); |
| QUICHE_DCHECK(socket_factory_); |
| QUICHE_DCHECK(!server_label_.empty()); |
| } |
| |
| ConnectUdpTunnel::~ConnectUdpTunnel() { |
| // Expect client and target sides of tunnel to both be closed before |
| // destruction. |
| QUICHE_DCHECK(!IsTunnelOpenToTarget()); |
| QUICHE_DCHECK(!receive_started_); |
| QUICHE_DCHECK(!datagram_visitor_registered_); |
| } |
| |
| void ConnectUdpTunnel::OpenTunnel( |
| const spdy::Http2HeaderBlock& request_headers) { |
| QUICHE_DCHECK(!IsTunnelOpenToTarget()); |
| |
| absl::optional<QuicServerId> target = |
| ValidateHeadersAndGetTarget(request_headers); |
| if (!target.has_value()) { |
| // Malformed request. |
| TerminateClientStream( |
| "invalid request headers", |
| QuicResetStreamError::FromIetf(QuicHttp3ErrorCode::MESSAGE_ERROR)); |
| return; |
| } |
| |
| if (!ValidateTarget(target.value(), acceptable_targets_)) { |
| SendErrorResponse("403", "destination_ip_prohibited", |
| "disallowed proxy target"); |
| return; |
| } |
| |
| // TODO(ericorth): Validate that the IP address doesn't fall into diallowed |
| // ranges per RFC 9298, Section 7. |
| QuicSocketAddress address = tools::LookupAddress(AF_UNSPEC, target.value()); |
| if (!address.IsInitialized()) { |
| SendErrorResponse("500", "dns_error", "host resolution error"); |
| return; |
| } |
| |
| target_socket_ = socket_factory_->CreateConnectingUdpClientSocket( |
| address, |
| /*receive_buffer_size=*/0, |
| /*send_buffer_size=*/0, |
| /*async_visitor=*/this); |
| QUICHE_DCHECK(target_socket_); |
| |
| absl::Status connect_result = target_socket_->ConnectBlocking(); |
| if (!connect_result.ok()) { |
| SendErrorResponse( |
| "502", "destination_ip_unroutable", |
| absl::StrCat("UDP socket error: ", connect_result.ToString())); |
| return; |
| } |
| |
| QUICHE_DVLOG(1) << "CONNECT-UDP tunnel opened from stream " |
| << client_stream_request_handler_->stream_id() << " to " |
| << target.value().ToHostPortString(); |
| |
| client_stream_request_handler_->GetStream()->RegisterHttp3DatagramVisitor( |
| this); |
| datagram_visitor_registered_ = true; |
| |
| SendConnectResponse(); |
| BeginAsyncReadFromTarget(); |
| } |
| |
| bool ConnectUdpTunnel::IsTunnelOpenToTarget() const { return !!target_socket_; } |
| |
| void ConnectUdpTunnel::OnClientStreamClose() { |
| QUICHE_CHECK(client_stream_request_handler_); |
| |
| QUICHE_DVLOG(1) << "CONNECT-UDP stream " |
| << client_stream_request_handler_->stream_id() << " closed"; |
| |
| if (datagram_visitor_registered_) { |
| client_stream_request_handler_->GetStream() |
| ->UnregisterHttp3DatagramVisitor(); |
| datagram_visitor_registered_ = false; |
| } |
| client_stream_request_handler_ = nullptr; |
| |
| if (IsTunnelOpenToTarget()) { |
| target_socket_->Disconnect(); |
| } |
| |
| // Clear socket pointer. |
| target_socket_.reset(); |
| } |
| |
| void ConnectUdpTunnel::ConnectComplete(absl::Status /*status*/) { |
| // Async connect not expected. |
| QUICHE_NOTREACHED(); |
| } |
| |
| void ConnectUdpTunnel::ReceiveComplete( |
| absl::StatusOr<quiche::QuicheMemSlice> data) { |
| QUICHE_DCHECK(IsTunnelOpenToTarget()); |
| QUICHE_DCHECK(receive_started_); |
| |
| receive_started_ = false; |
| |
| if (!data.ok()) { |
| if (client_stream_request_handler_) { |
| QUICHE_LOG(WARNING) << "Error receiving CONNECT-UDP data from target: " |
| << data.status(); |
| } else { |
| // This typically just means a receive operation was cancelled on calling |
| // target_socket_->Disconnect(). |
| QUICHE_DVLOG(1) << "Error receiving CONNECT-UDP data from target after " |
| "stream already closed."; |
| } |
| return; |
| } |
| |
| QUICHE_DCHECK(client_stream_request_handler_); |
| quiche::ConnectUdpDatagramUdpPacketPayload payload( |
| data.value().AsStringView()); |
| client_stream_request_handler_->GetStream()->SendHttp3Datagram( |
| payload.Serialize()); |
| |
| BeginAsyncReadFromTarget(); |
| } |
| |
| void ConnectUdpTunnel::SendComplete(absl::Status /*status*/) { |
| // Async send not expected. |
| QUICHE_NOTREACHED(); |
| } |
| |
| void ConnectUdpTunnel::OnHttp3Datagram(QuicStreamId stream_id, |
| absl::string_view payload) { |
| QUICHE_DCHECK(IsTunnelOpenToTarget()); |
| QUICHE_DCHECK_EQ(stream_id, client_stream_request_handler_->stream_id()); |
| QUICHE_DCHECK(!payload.empty()); |
| |
| std::unique_ptr<quiche::ConnectUdpDatagramPayload> parsed_payload = |
| quiche::ConnectUdpDatagramPayload::Parse(payload); |
| if (!parsed_payload) { |
| QUICHE_DVLOG(1) << "Ignoring HTTP Datagram payload, due to inability to " |
| "parse as CONNECT-UDP payload."; |
| return; |
| } |
| |
| switch (parsed_payload->GetType()) { |
| case quiche::ConnectUdpDatagramPayload::Type::kUdpPacket: |
| SendUdpPacketToTarget(parsed_payload->GetUdpProxyingPayload()); |
| break; |
| case quiche::ConnectUdpDatagramPayload::Type::kUnknown: |
| QUICHE_DVLOG(1) |
| << "Ignoring HTTP Datagram payload with unrecognized context ID."; |
| } |
| } |
| |
| void ConnectUdpTunnel::BeginAsyncReadFromTarget() { |
| QUICHE_DCHECK(IsTunnelOpenToTarget()); |
| QUICHE_DCHECK(client_stream_request_handler_); |
| QUICHE_DCHECK(!receive_started_); |
| |
| receive_started_ = true; |
| target_socket_->ReceiveAsync(kReadSize); |
| } |
| |
| void ConnectUdpTunnel::SendUdpPacketToTarget(absl::string_view packet) { |
| absl::Status send_result = target_socket_->SendBlocking(std::string(packet)); |
| if (!send_result.ok()) { |
| QUICHE_LOG(WARNING) << "Error sending CONNECT-UDP datagram to target: " |
| << send_result; |
| } |
| } |
| |
| void ConnectUdpTunnel::SendConnectResponse() { |
| QUICHE_DCHECK(IsTunnelOpenToTarget()); |
| QUICHE_DCHECK(client_stream_request_handler_); |
| |
| spdy::Http2HeaderBlock response_headers; |
| response_headers[":status"] = "200"; |
| |
| absl::optional<std::string> capsule_protocol_value = |
| structured_headers::SerializeItem(structured_headers::Item(true)); |
| QUICHE_CHECK(capsule_protocol_value.has_value()); |
| response_headers["Capsule-Protocol"] = capsule_protocol_value.value(); |
| |
| 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 ConnectUdpTunnel::SendErrorResponse(absl::string_view status, |
| absl::string_view proxy_status_error, |
| absl::string_view error_details) { |
| QUICHE_DCHECK(!status.empty()); |
| QUICHE_DCHECK(!proxy_status_error.empty()); |
| QUICHE_DCHECK(!error_details.empty()); |
| QUICHE_DCHECK(client_stream_request_handler_); |
| |
| #ifndef NDEBUG |
| // Expect a valid status code (number, 100 to 599 inclusive) and not a |
| // Successful code (200 to 299 inclusive). |
| int status_num = 0; |
| bool is_num = absl::SimpleAtoi(status, &status_num); |
| QUICHE_DCHECK(is_num); |
| QUICHE_DCHECK_GE(status_num, 100); |
| QUICHE_DCHECK_LT(status_num, 600); |
| QUICHE_DCHECK(status_num < 200 || status_num >= 300); |
| #endif // !NDEBUG |
| |
| spdy::Http2HeaderBlock headers; |
| headers[":status"] = status; |
| |
| structured_headers::Item proxy_status_item(server_label_); |
| structured_headers::Item proxy_status_error_item( |
| std::string{proxy_status_error}); |
| structured_headers::Item proxy_status_details_item( |
| std::string{error_details}); |
| structured_headers::ParameterizedMember proxy_status_member( |
| std::move(proxy_status_item), |
| {{"error", std::move(proxy_status_error_item)}, |
| {"details", std::move(proxy_status_details_item)}}); |
| absl::optional<std::string> proxy_status_value = |
| structured_headers::SerializeList({proxy_status_member}); |
| QUICHE_CHECK(proxy_status_value.has_value()); |
| headers["Proxy-Status"] = proxy_status_value.value(); |
| |
| QuicBackendResponse response; |
| response.set_headers(std::move(headers)); |
| |
| client_stream_request_handler_->OnResponseBackendComplete(&response); |
| } |
| |
| void ConnectUdpTunnel::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 |